1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use std::marker::PhantomData;

use ahash::AHashMap;
pub use anyhow::Result;
use bevy::prelude::*;
use bevy::tasks::{futures_lite::future, Task};

use crate::{
    client::AuthenticatedClient,
    requestable::{LobbyRequest, LobbyRequestCreator},
};

pub(super) struct EndpointPlugin<T: LobbyRequestCreator> {
    _marker: PhantomData<fn() -> T>,
}

impl<T: LobbyRequestCreator> Plugin for EndpointPlugin<T> {
    fn build(&self, app: &mut App) {
        app.add_event::<RequestEvent<T>>()
            .add_event::<ResponseEvent<T>>()
            .init_resource::<PendingTasks<T>>()
            .add_systems(PostUpdate, fire::<T>)
            .add_systems(PreUpdate, poll::<T>);
    }
}

impl<T: LobbyRequestCreator> Default for EndpointPlugin<T> {
    fn default() -> Self {
        Self {
            _marker: PhantomData,
        }
    }
}

/// Use this event to make a request the Lobby API. Response the request will
/// delivered as [`ResponseEvent`].
#[derive(Event)]
pub struct RequestEvent<T> {
    id: String,
    request: T,
}

impl<T> RequestEvent<T> {
    /// # Arguments
    ///
    /// * `id` - ID of the request. The response event to this request will
    ///   have the same ID. Any pending request with the same ID and request
    ///   type will get dropped & canceled.
    ///
    /// * `request` - the request to be made itself.
    pub fn new<S: ToString>(id: S, request: T) -> Self {
        Self {
            id: id.to_string(),
            request,
        }
    }

    fn id(&self) -> &str {
        self.id.as_str()
    }

    fn request(&self) -> &T {
        &self.request
    }
}

/// Event corresponding to a finished Lobby API request which might have failed
/// or succeeded.
#[derive(Event)]
pub struct ResponseEvent<T>
where
    T: LobbyRequest,
{
    id: String,
    result: Result<T::Response>,
}

impl<T> ResponseEvent<T>
where
    T: LobbyRequest,
{
    fn new(id: String, result: Result<T::Response>) -> Self {
        Self { id, result }
    }

    pub fn id(&self) -> &str {
        self.id.as_str()
    }

    pub fn result(&self) -> &Result<T::Response> {
        &self.result
    }
}

#[derive(Resource)]
struct PendingTasks<T: LobbyRequest>(AHashMap<String, Task<Result<T::Response>>>);

impl<T: LobbyRequest> PendingTasks<T> {
    fn register(&mut self, id: String, task: Task<Result<T::Response>>) {
        self.0.insert(id, task);
    }
}

impl<T: LobbyRequest> Default for PendingTasks<T> {
    fn default() -> Self {
        Self(AHashMap::new())
    }
}

fn fire<T: LobbyRequestCreator>(
    client: AuthenticatedClient,
    mut pending: ResMut<PendingTasks<T>>,
    mut requests: EventReader<RequestEvent<T>>,
    mut responses: EventWriter<ResponseEvent<T>>,
) {
    for event in requests.read() {
        let result = client.fire(event.request());

        match result {
            Ok(task) => pending.register(event.id().to_owned(), task),
            Err(error) => {
                responses.send(ResponseEvent::new(event.id().to_owned(), Err(error)));
            }
        }
    }
}

fn poll<T: LobbyRequest>(
    mut pending: ResMut<PendingTasks<T>>,
    mut events: EventWriter<ResponseEvent<T>>,
) {
    let mut results = Vec::new();

    for (id, task) in pending.0.iter_mut() {
        if task.is_finished() {
            match future::block_on(future::poll_once(task)) {
                Some(result) => results.push((id.to_owned(), result)),
                None => unreachable!("The task is finished."),
            }
        }
    }

    for result in results.drain(..) {
        pending.0.remove(result.0.as_str());
        events.send(ResponseEvent::new(result.0, result.1));
    }
}