use js_sys::Promise; use std::cell::{Cell, RefCell}; use std::collections::VecDeque; use std::rc::Rc; use wasm_bindgen::prelude::*; struct QueueState { // The queue of Tasks which will be run in order. In practice this is all the // synchronous work of futures, and each `Task` represents calling `poll` on // a future "at the right time" tasks: RefCell>>, // This flag indicates whether we're currently executing inside of // `run_all` or have scheduled `run_all` to run in the future. This is // used to ensure that it's only scheduled once. is_spinning: Cell, } impl QueueState { fn run_all(&self) { debug_assert!(self.is_spinning.get()); // Runs all Tasks until empty. This blocks the event loop if a Future is // stuck in an infinite loop, so we may want to yield back to the main // event loop occasionally. For now though greedy execution should get // the job done. loop { let task = match self.tasks.borrow_mut().pop_front() { Some(task) => task, None => break, }; task.run(); } // All of the Tasks have been run, so it's now possible to schedule the // next tick again self.is_spinning.set(false); } } pub(crate) struct Queue { state: Rc, promise: Promise, closure: Closure, } impl Queue { pub(crate) fn push_task(&self, task: Rc) { self.state.tasks.borrow_mut().push_back(task); // If we're already inside the `run_all` loop then that'll pick up the // task we just enqueued. If we're not in `run_all`, though, then we need // to schedule a microtask. // // Note that we currently use a promise and a closure to do this, but // eventually we should probably use something like `queueMicrotask`: // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/queueMicrotask if !self.state.is_spinning.replace(true) { self.promise.then(&self.closure); } } } impl Queue { fn new() -> Self { let state = Rc::new(QueueState { is_spinning: Cell::new(false), tasks: RefCell::new(VecDeque::new()), }); Self { promise: Promise::resolve(&JsValue::undefined()), closure: { let state = Rc::clone(&state); // This closure will only be called on the next microtask event // tick Closure::wrap(Box::new(move |_| state.run_all())) }, state, } } } thread_local! { pub(crate) static QUEUE: Queue = Queue::new(); }