Skip to main content

ferroui_browser/interop/
completion_helper.rs

1//! Completion of the asynchronous calls into the page.
2//!
3//! Not in the original, where the runtime turns a promise of the page into a
4//! task. Here a function of the script module that answers with a promise is
5//! imported as returning the promise object; [`await_promise`] gives the
6//! promise a request id, hands both to `CompletionHelper.track` and returns a
7//! future. When the promise settles, the script module calls
8//! `CompletionHelper_OnResolved` or `CompletionHelper_OnRejected` with the id,
9//! which completes the future and wakes its task. The task is polled again by
10//! whoever drives it (on the UI thread, the dispatcher), never from inside the
11//! callback.
12//!
13//! This is the one way an asynchronous answer crosses the boundary: no Rust
14//! closure is passed to the page.
15
16use super::JsObject;
17use std::cell::{Cell, RefCell};
18use std::collections::HashMap;
19use std::fmt;
20use std::future::Future;
21use std::pin::Pin;
22use std::rc::Rc;
23use std::task::{Context, Poll, Waker};
24use wasm_bindgen::prelude::*;
25
26#[wasm_bindgen(raw_module = "./ferroui.js")]
27extern "C" {
28    #[wasm_bindgen(js_namespace = CompletionHelper, js_name = track)]
29    fn js_track(promise: &JsObject, request_id: u32);
30}
31
32/// Why a promise of the page was rejected: the name and the message of the
33/// error it was rejected with (for a reason that is not an error, an empty
34/// name and the reason as text).
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct PromiseError {
37    name: String,
38    message: String,
39}
40
41impl PromiseError {
42    /// Creates the error of a rejection.
43    pub fn new(name: impl Into<String>, message: impl Into<String>) -> Self {
44        Self { name: name.into(), message: message.into() }
45    }
46
47    /// The name of the error, such as `NotAllowedError`.
48    pub fn name(&self) -> &str {
49        &self.name
50    }
51
52    /// The message of the error.
53    pub fn message(&self) -> &str {
54        &self.message
55    }
56}
57
58impl fmt::Display for PromiseError {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        if self.name.is_empty() {
61            f.write_str(&self.message)
62        } else {
63            write!(f, "{}: {}", self.name, self.message)
64        }
65    }
66}
67
68impl std::error::Error for PromiseError {}
69
70impl From<PromiseError> for std::io::Error {
71    /// The error as an I/O error whose payload is the rejection (read it back
72    /// with `get_ref` and `downcast_ref::<PromiseError>` for its name and
73    /// message). The kind follows the name of the DOM exception, so that a
74    /// caller can tell a missing item, a refused permission and a cancelled
75    /// request apart; any other name is [`std::io::ErrorKind::Other`].
76    fn from(error: PromiseError) -> Self {
77        let kind = match error.name() {
78            "NotFoundError" => std::io::ErrorKind::NotFound,
79            "NotAllowedError" | "SecurityError" => std::io::ErrorKind::PermissionDenied,
80            "AbortError" => std::io::ErrorKind::Interrupted,
81            _ => std::io::ErrorKind::Other,
82        };
83        std::io::Error::new(kind, error)
84    }
85}
86
87/// The outcome of a promise: its value or its rejection.
88pub type PromiseOutcome<V> = Result<V, PromiseError>;
89
90/// The call that hands a promise and its request id to the page; replaced
91/// by a recorder in the tests.
92trait IPromiseTracker<V> {
93    fn track(&self, promise: &V, request_id: u32);
94}
95
96struct PagePromiseTracker;
97
98impl IPromiseTracker<JsObject> for PagePromiseTracker {
99    fn track(&self, promise: &JsObject, request_id: u32) {
100        js_track(promise, request_id);
101    }
102}
103
104struct PendingRequest<V> {
105    outcome: Option<PromiseOutcome<V>>,
106    waker: Option<Waker>,
107}
108
109/// The requests waiting for the page, by id.
110struct Completions<V> {
111    last_request_id: Cell<u32>,
112    pending: RefCell<HashMap<u32, PendingRequest<V>>>,
113}
114
115impl<V> Completions<V> {
116    fn new() -> Rc<Self> {
117        Rc::new(Self { last_request_id: Cell::new(0), pending: RefCell::new(HashMap::new()) })
118    }
119
120    /// Registers a request for `promise`, hands it to the page and returns
121    /// the future of its outcome.
122    fn await_promise(self: &Rc<Self>, tracker: &dyn IPromiseTracker<V>, promise: &V) -> PromiseFuture<V> {
123        let mut request_id = self.last_request_id.get();
124        loop {
125            // Ids are reused after 2^32 requests; one still waiting is skipped.
126            request_id = request_id.wrapping_add(1);
127            if !self.pending.borrow().contains_key(&request_id) {
128                break;
129            }
130        }
131        self.last_request_id.set(request_id);
132        self.pending.borrow_mut().insert(request_id, PendingRequest { outcome: None, waker: None });
133
134        tracker.track(promise, request_id);
135
136        PromiseFuture { completions: self.clone(), request_id }
137    }
138
139    /// Completes a request. Returns `false` when no request has the id: it
140    /// was completed before, or its future was dropped.
141    fn complete(&self, request_id: u32, outcome: PromiseOutcome<V>) -> bool {
142        let waker = {
143            let mut pending = self.pending.borrow_mut();
144            let Some(request) = pending.get_mut(&request_id) else {
145                return false;
146            };
147            if request.outcome.is_some() {
148                return false;
149            }
150            request.outcome = Some(outcome);
151            request.waker.take()
152        };
153
154        // Outside the borrow: waking may poll the future right away.
155        if let Some(waker) = waker {
156            waker.wake();
157        }
158        true
159    }
160
161    fn pending_count(&self) -> usize {
162        self.pending.borrow().len()
163    }
164}
165
166/// The outcome of a promise of the page, as a future. Dropping the future
167/// forgets the request: an answer that comes later is ignored.
168///
169/// # Panics
170/// Polling the future again after it returned its outcome panics: the
171/// request is gone once its outcome has been taken.
172pub struct PromiseFuture<V> {
173    completions: Rc<Completions<V>>,
174    request_id: u32,
175}
176
177impl<V> PromiseFuture<V> {
178    /// The id the page answers the request with.
179    pub fn request_id(&self) -> u32 {
180        self.request_id
181    }
182}
183
184impl<V> Future for PromiseFuture<V> {
185    type Output = PromiseOutcome<V>;
186
187    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
188        let mut pending = self.completions.pending.borrow_mut();
189        let Some(request) = pending.get_mut(&self.request_id) else {
190            panic!("The request {} was polled after it completed.", self.request_id);
191        };
192
193        if request.outcome.is_some() {
194            let request = pending.remove(&self.request_id);
195            return Poll::Ready(request.and_then(|request| request.outcome).expect("the outcome of the request"));
196        }
197
198        request.waker = Some(cx.waker().clone());
199        Poll::Pending
200    }
201}
202
203impl<V> Drop for PromiseFuture<V> {
204    fn drop(&mut self) {
205        let removed = self.completions.pending.borrow_mut().remove(&self.request_id);
206        drop(removed);
207    }
208}
209
210ferroui_base::ferro_thread_local! {
211    static PAGE_COMPLETIONS: Rc<Completions<JsObject>> = Completions::new();
212}
213
214/// The future of the outcome of a promise of the page.
215pub fn await_promise(promise: &JsObject) -> PromiseFuture<JsObject> {
216    PAGE_COMPLETIONS.with(|completions| completions.await_promise(&PagePromiseTracker, promise))
217}
218
219/// The number of requests waiting for the page.
220pub fn pending_request_count() -> usize {
221    PAGE_COMPLETIONS.with(|completions| completions.pending_count())
222}
223
224/// A promise handed to the page by [`await_promise`] was fulfilled with
225/// `value`.
226#[wasm_bindgen(js_name = CompletionHelper_OnResolved)]
227pub fn on_resolved(request_id: u32, value: JsObject) {
228    PAGE_COMPLETIONS.with(|completions| completions.complete(request_id, Ok(value)));
229}
230
231/// A promise handed to the page by [`await_promise`] was rejected with an
232/// error of the given name and message.
233#[wasm_bindgen(js_name = CompletionHelper_OnRejected)]
234pub fn on_rejected(request_id: u32, name: Option<String>, message: Option<String>) {
235    let error = PromiseError::new(name.unwrap_or_default(), message.unwrap_or_default());
236    PAGE_COMPLETIONS.with(|completions| completions.complete(request_id, Err(error)));
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::sync::atomic::{AtomicUsize, Ordering};
243    use std::sync::Arc;
244    use std::task::Wake;
245
246    /// Records the requests handed to the page.
247    #[derive(Default)]
248    struct RecordingTracker {
249        tracked: RefCell<Vec<(String, u32)>>,
250    }
251
252    impl IPromiseTracker<String> for RecordingTracker {
253        fn track(&self, promise: &String, request_id: u32) {
254            self.tracked.borrow_mut().push((promise.clone(), request_id));
255        }
256    }
257
258    #[derive(Default)]
259    struct CountingWaker {
260        wakes: AtomicUsize,
261    }
262
263    impl Wake for CountingWaker {
264        fn wake(self: Arc<Self>) {
265            self.wakes.fetch_add(1, Ordering::SeqCst);
266        }
267    }
268
269    fn poll<V>(future: &mut PromiseFuture<V>, waker: &Arc<CountingWaker>) -> Poll<PromiseOutcome<V>> {
270        let waker = Waker::from(waker.clone());
271        let mut context = Context::from_waker(&waker);
272        Pin::new(future).poll(&mut context)
273    }
274
275    #[test]
276    fn the_promise_is_handed_to_the_page_with_its_request_id() {
277        let completions = Completions::new();
278        let tracker = RecordingTracker::default();
279
280        let first = completions.await_promise(&tracker, &"first".to_string());
281        let second = completions.await_promise(&tracker, &"second".to_string());
282
283        assert_eq!(
284            vec![("first".to_string(), first.request_id()), ("second".to_string(), second.request_id())],
285            *tracker.tracked.borrow()
286        );
287        assert_ne!(first.request_id(), second.request_id());
288        assert_eq!(2, completions.pending_count());
289    }
290
291    #[test]
292    fn the_future_waits_until_the_page_answers_and_then_has_the_value() {
293        let completions = Completions::new();
294        let tracker = RecordingTracker::default();
295        let waker = Arc::new(CountingWaker::default());
296
297        let mut future = completions.await_promise(&tracker, &"promise".to_string());
298        assert_eq!(Poll::Pending, poll(&mut future, &waker));
299        assert_eq!(0, waker.wakes.load(Ordering::SeqCst));
300
301        assert!(completions.complete(future.request_id(), Ok("value".to_string())));
302        assert_eq!(1, waker.wakes.load(Ordering::SeqCst));
303
304        assert_eq!(Poll::Ready(Ok("value".to_string())), poll(&mut future, &waker));
305        assert_eq!(0, completions.pending_count());
306    }
307
308    #[test]
309    fn a_rejection_is_the_outcome_of_the_future() {
310        let completions = Completions::new();
311        let tracker = RecordingTracker::default();
312        let waker = Arc::new(CountingWaker::default());
313
314        let mut future = completions.await_promise(&tracker, &"promise".to_string());
315        assert_eq!(Poll::Pending, poll(&mut future, &waker));
316        let error = PromiseError::new("NotAllowedError", "Read permission denied.");
317        assert!(completions.complete(future.request_id(), Err(error.clone())));
318
319        assert_eq!(Poll::Ready(Err(error)), poll(&mut future, &waker));
320    }
321
322    #[test]
323    fn an_answer_that_comes_before_the_first_poll_is_kept() {
324        let completions = Completions::new();
325        let tracker = RecordingTracker::default();
326        let waker = Arc::new(CountingWaker::default());
327
328        let mut future = completions.await_promise(&tracker, &"promise".to_string());
329        assert!(completions.complete(future.request_id(), Ok("early".to_string())));
330
331        assert_eq!(Poll::Ready(Ok("early".to_string())), poll(&mut future, &waker));
332        assert_eq!(0, waker.wakes.load(Ordering::SeqCst));
333    }
334
335    #[test]
336    fn answers_complete_their_own_request_in_any_order() {
337        let completions = Completions::new();
338        let tracker = RecordingTracker::default();
339        let waker = Arc::new(CountingWaker::default());
340
341        let mut first = completions.await_promise(&tracker, &"first".to_string());
342        let mut second = completions.await_promise(&tracker, &"second".to_string());
343        assert_eq!(Poll::Pending, poll(&mut first, &waker));
344        assert_eq!(Poll::Pending, poll(&mut second, &waker));
345
346        assert!(completions.complete(second.request_id(), Ok("2".to_string())));
347        assert_eq!(Poll::Pending, poll(&mut first, &waker));
348        assert_eq!(Poll::Ready(Ok("2".to_string())), poll(&mut second, &waker));
349
350        assert!(completions.complete(first.request_id(), Ok("1".to_string())));
351        assert_eq!(Poll::Ready(Ok("1".to_string())), poll(&mut first, &waker));
352    }
353
354    #[test]
355    fn a_dropped_future_forgets_its_request_and_a_later_answer_is_ignored() {
356        let completions = Completions::new();
357        let tracker = RecordingTracker::default();
358
359        let future = completions.await_promise(&tracker, &"promise".to_string());
360        let request_id = future.request_id();
361        drop(future);
362
363        assert_eq!(0, completions.pending_count());
364        assert!(!completions.complete(request_id, Ok("late".to_string())));
365    }
366
367    #[test]
368    fn an_answer_to_an_unknown_or_answered_request_is_ignored() {
369        let completions = Completions::new();
370        let tracker = RecordingTracker::default();
371        let waker = Arc::new(CountingWaker::default());
372
373        assert!(!completions.complete(7, Ok("stray".to_string())));
374
375        let mut future = completions.await_promise(&tracker, &"promise".to_string());
376        assert!(completions.complete(future.request_id(), Ok("first".to_string())));
377        assert!(!completions.complete(future.request_id(), Ok("second".to_string())));
378        assert_eq!(Poll::Ready(Ok("first".to_string())), poll(&mut future, &waker));
379    }
380
381    #[test]
382    fn a_request_id_still_waiting_is_not_given_again() {
383        let completions = Completions::new();
384        let tracker = RecordingTracker::default();
385
386        let waiting = completions.await_promise(&tracker, &"waiting".to_string());
387        // The next id would wrap around to the one still waiting.
388        completions.last_request_id.set(waiting.request_id().wrapping_sub(1));
389
390        let next = completions.await_promise(&tracker, &"next".to_string());
391        assert_ne!(waiting.request_id(), next.request_id());
392    }
393
394    #[test]
395    #[should_panic(expected = "polled after it completed")]
396    fn polling_again_after_the_outcome_panics() {
397        let completions = Completions::new();
398        let tracker = RecordingTracker::default();
399        let waker = Arc::new(CountingWaker::default());
400
401        let mut future = completions.await_promise(&tracker, &"promise".to_string());
402        completions.complete(future.request_id(), Ok("value".to_string()));
403        assert_eq!(Poll::Ready(Ok("value".to_string())), poll(&mut future, &waker));
404        let _ = poll(&mut future, &waker);
405    }
406
407    #[test]
408    fn a_rejection_is_an_io_error_that_keeps_the_name_and_the_message() {
409        for (name, kind) in [
410            ("NotFoundError", std::io::ErrorKind::NotFound),
411            ("NotAllowedError", std::io::ErrorKind::PermissionDenied),
412            ("SecurityError", std::io::ErrorKind::PermissionDenied),
413            ("AbortError", std::io::ErrorKind::Interrupted),
414            ("TypeError", std::io::ErrorKind::Other),
415            ("", std::io::ErrorKind::Other),
416        ] {
417            let error: std::io::Error = PromiseError::new(name, "The user aborted a request.").into();
418            assert_eq!(kind, error.kind(), "{name}");
419            let rejection = error.get_ref().and_then(|inner| inner.downcast_ref::<PromiseError>()).expect("the rejection");
420            assert_eq!(name, rejection.name());
421            assert_eq!("The user aborted a request.", rejection.message());
422        }
423    }
424
425    #[test]
426    fn the_error_is_shown_with_its_name() {
427        assert_eq!("NotAllowedError: denied", PromiseError::new("NotAllowedError", "denied").to_string());
428        assert_eq!("reason", PromiseError::new("", "reason").to_string());
429    }
430}