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
//! Non-block tick executor for WebAssembly Rust.
//!
//! Ticker callbacks queue as [Tasks] to JavaScript event loop.
//! Instead of Microtasks or just stacking in,
//! [Tasks] won't block current host context and UI rendering thread.
//! See also <https://developer.mozilla.org/docs/Web/API/HTML_DOM_API/Microtask_guide/in_depth>
//!
//! [Tasks]: https://developer.mozilla.org/docs/Web/API/HTML_DOM_API/Microtask_guide
//!
//! |         Ticker         |           API           | Platform |      Interval<br/>Browser / Node      |
//! |:----------------------:|:-----------------------:|:--------:|:-------------------------------------:|
//! | [MessageChannelTicker] |   [Channel Messaging]   |    *     |             \>4µs / \<1µs             |
//! |    [ImmediateTicker]   |     [setImmediate]      |   Node   |                 \~1µs                 |
//! |    [TimeoutTicker]     |      [setTimeout]       |    *     | [\~4ms][setTimeout interval] / \~14ms |
//! | [AnimationFrameTicker] | [requestAnimationFrame] | Browser  |          According to device          |
//! |      [AutoTicker]      |      One of above       |    *     |                  N/A                  |
//!
//! [MessageChannelTicker]: ticker::MessageChannelTicker
//! [ImmediateTicker]: ticker::ImmediateTicker
//! [TimeoutTicker]: ticker::TimeoutTicker
//! [AnimationFrameTicker]: ticker::AnimationFrameTicker
//! [AutoTicker]: ticker::AutoTicker
//!
//! [Channel Messaging]: https://developer.mozilla.org/docs/Web/API/Channel_Messaging_API
//! [setTimeout]: https://developer.mozilla.org/docs/Web/API/setTimeout
//! [requestAnimationFrame]: https://developer.mozilla.org/docs/Web/API/Window/requestAnimationFrame
//! [setImmediate]: https://nodejs.org/en-us/learn/asynchronous-work/understanding-setimmediate
//!
//! [setTimeout interval]: https://developer.mozilla.org/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified

mod bindings;

/// Factory types implement [TickerFactory]
pub mod factory;
/// Types implement [Ticker]
pub mod ticker;

use wasm_bindgen::JsValue;

/// Running state of [Ticker]
#[derive(Clone, PartialEq, Debug)]
pub enum State {
    Started,
    Stopped,
    Error(JsValue),
}

pub trait TickerFactory {
    type Output: Ticker + Clone + PartialEq + Eq;
    fn new(task: impl FnMut() + 'static) -> Result<Self::Output, JsValue>;
}

/// A [Ticker] queues callback as a [Task] to JavaScript event loop.
///
/// Instead of Microtasks or just stacking in,
/// [Task] won't block current host context and UI rendering thread.
/// See also <https://developer.mozilla.org/docs/Web/API/HTML_DOM_API/Microtask_guide/in_depth>
///
/// [Task]: https://developer.mozilla.org/docs/Web/API/HTML_DOM_API/Microtask_guide
pub trait Ticker {
    /// Current state
    fn state(&self) -> State;

    /// Start queuing on next tick, this doesn't block current context.
    fn start(&self) -> Result<(), JsValue>;

    /// Call callback once and start queuing.
    fn start_immediate(&self) -> Result<(), JsValue>;

    /// Stop executing, implementations may not force cancel queued task,
    /// or just set state to [State::Stopped].
    fn stop(&self);

    /// Simply queue task once.
    fn spawn(task: impl FnOnce() + 'static) -> Result<(), JsValue>
    where
        Self: Sized;

    /// Queue task once and wrap return value by [Promise](js_sys::Promise).
    ///
    /// Requires [`Promise.withResolvers`][withResolvers] method.
    ///
    /// [withResolvers]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers
    fn spawn_promise(
        task: impl FnOnce() -> Result<JsValue, JsValue> + 'static,
    ) -> Result<js_sys::Promise, JsValue>
    where
        Self: Sized,
    {
        let resolvers = bindings::__wasm_ticker_binding_promise_resolvers()?;
        let promise = resolvers.__wasm_ticker_binding_promise();
        let resolve = resolvers.__wasm_ticker_binding_resolve();
        let reject = resolvers.__wasm_ticker_binding_reject();
        Self::spawn(move || {
            match task() {
                Ok(r) => resolve.call1(&JsValue::null(), &r),
                Err(e) => reject.call1(&JsValue::null(), &e),
            }
            .unwrap();
        })?;
        Ok(promise)
    }
}

/// [Ticker] with specialized JavaScript API.
pub trait NamedTicker: Ticker {
    /// Check if [Self] is available on current JavaScript Runtime.
    fn check() -> bool;
}

/// Tickers using JavaScript timer APIs.
pub trait TimerTicker: NamedTicker {
    /// Returned token type of JavaScript timer API like `setTimeout`.
    ///
    /// In browsers, this usually is [Number](js_sys::Number),
    /// but in Node it could be a [Object](js_sys::Object).
    type Token: AsRef<JsValue> + Clone;

    /// Get clone of latest [Self::Token] if exists.
    fn token(&self) -> Option<Self::Token>;
}