char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport timers and structured clone extensions to typescriptc42f8c8

main
8.0 KiB261 linesraw
1use std::convert::Infallible;
2use std::io;
3use std::rc::Rc;
4use std::sync::{Arc, Condvar, Mutex};
5use std::thread::JoinHandle;
6use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
7
8use avril_core::{Extension, Op};
9use futures::channel::oneshot;
10
11#[derive(Default)]
12struct State {
13    pending: Option<(Instant, oneshot::Sender<()>)>,
14    stopped: bool,
15}
16
17struct Timers {
18    origin: Instant,
19    time_origin: f64,
20    shared: Arc<(Mutex<State>, Condvar)>,
21    worker: Option<JoinHandle<()>>,
22}
23
24impl Timers {
25    fn new() -> io::Result<Self> {
26        let origin = Instant::now();
27        let time_origin = SystemTime::now()
28            .duration_since(UNIX_EPOCH)
29            .unwrap_or_default()
30            .as_secs_f64()
31            * 1000.0;
32        let shared = Arc::new((Mutex::new(State::default()), Condvar::new()));
33        let worker = shared.clone();
34        let worker = std::thread::Builder::new()
35            .name("avril:timers".into())
36            .spawn(move || {
37                let (lock, wake) = &*worker;
38                let mut state = lock.lock().unwrap();
39                loop {
40                    if state.stopped {
41                        return;
42                    }
43                    if let Some((deadline, _)) = &state.pending {
44                        let now = Instant::now();
45                        if *deadline <= now {
46                            let (_, reply) = state.pending.take().unwrap();
47                            let _ = reply.send(());
48                        } else {
49                            let delay = *deadline - now;
50                            state = wake.wait_timeout(state, delay).unwrap().0;
51                        }
52                    } else {
53                        state = wake.wait(state).unwrap();
54                    }
55                }
56            })?;
57        Ok(Self {
58            origin,
59            time_origin,
60            shared,
61            worker: Some(worker),
62        })
63    }
64
65    fn wait(&self, deadline: f64) -> io::Result<oneshot::Receiver<()>> {
66        let offset = Duration::try_from_secs_f64(deadline / 1000.0).map_err(io::Error::other)?;
67        let deadline = self
68            .origin
69            .checked_add(offset)
70            .ok_or_else(|| io::Error::other("Timer deadline is out of range"))?;
71        let (reply, result) = oneshot::channel();
72        let mut state = self.shared.0.lock().unwrap();
73        state.pending.take();
74        if deadline <= Instant::now() {
75            let _ = reply.send(());
76        } else {
77            state.pending = Some((deadline, reply));
78        }
79        self.shared.1.notify_one();
80        Ok(result)
81    }
82
83    fn cancel(&self) {
84        self.shared.0.lock().unwrap().pending.take();
85        self.shared.1.notify_one();
86    }
87}
88
89impl Drop for Timers {
90    fn drop(&mut self) {
91        {
92            let mut state = self.shared.0.lock().unwrap();
93            state.stopped = true;
94            state.pending.take();
95            self.shared.1.notify_one();
96        }
97        if let Some(worker) = self.worker.take() {
98            let _ = worker.join();
99        }
100    }
101}
102
103pub fn init() -> io::Result<Extension> {
104    let timers = Rc::new(Timers::new()?);
105    let (now, time_origin, wait, wait_unreferenced, cancel) = (
106        timers.clone(),
107        timers.clone(),
108        timers.clone(),
109        timers.clone(),
110        timers,
111    );
112    Ok(Extension {
113        name: "ext:timers",
114        dependencies: &[],
115        ops: vec![
116            Op::new("op_timer_now", move |(): ()| {
117                Ok::<_, Infallible>(now.origin.elapsed().as_secs_f64() * 1000.0)
118            }),
119            Op::new("op_timer_time_origin", move |(): ()| {
120                Ok::<_, Infallible>(time_origin.time_origin)
121            }),
122            Op::asynchronous("op_timer_wait", move |deadline: f64| {
123                let result = wait.wait(deadline);
124                async move {
125                    let _ = result?.await;
126                    Ok::<_, io::Error>(())
127                }
128            }),
129            Op::asynchronous_unreferenced("op_timer_wait_unreferenced", move |deadline: f64| {
130                let result = wait_unreferenced.wait(deadline);
131                async move {
132                    let _ = result?.await;
133                    Ok::<_, io::Error>(())
134                }
135            }),
136            Op::new("op_timer_cancel", move |(): ()| {
137                cancel.cancel();
138                Ok::<_, Infallible>(())
139            }),
140        ],
141        modules: &[
142            (
143                "ext:timers/main.js",
144                include_str!(concat!(env!("OUT_DIR"), "/main.js")),
145            ),
146            (
147                "ext:timers/timers.js",
148                include_str!(concat!(env!("OUT_DIR"), "/timers.js")),
149            ),
150        ],
151        entry_point: Some("ext:timers/main.js"),
152    })
153}
154
155#[cfg(test)]
156mod tests {
157    use avril_core::{Runtime, RuntimeOptions};
158
159    fn runtime() -> Runtime {
160        Runtime::new(RuntimeOptions {
161            extensions: vec![
162                avril_ext_web::with_broker(std::sync::Arc::default()),
163                super::init().unwrap(),
164            ],
165            ..Default::default()
166        })
167        .unwrap()
168    }
169
170    #[test]
171    fn callback_failure_does_not_disarm_remaining_timers() {
172        let mut runtime = runtime();
173        runtime
174            .execute_script::<()>(
175                "timers",
176                r#"
177            globalThis.calls = 0;
178            setTimeout(() => {throw Error('timer failed');});
179            setTimeout(() => calls++);
180        "#,
181            )
182            .unwrap();
183        assert!(
184            runtime
185                .run_event_loop()
186                .unwrap_err()
187                .to_string()
188                .contains("timer failed")
189        );
190        runtime.run_event_loop().unwrap();
191        assert_eq!(runtime.execute_script::<u32>("calls", "calls").unwrap(), 1);
192    }
193
194    #[test]
195    fn microtask_failures_drain_the_queue_without_poisoning_later_turns() {
196        let mut runtime = runtime();
197        let error = runtime
198            .execute_script::<()>(
199                "microtasks",
200                r#"
201                globalThis.calls = [];
202                queueMicrotask(() => {
203                    calls.push('first');
204                    queueMicrotask(() => calls.push('nested'));
205                    throw Error('first failure');
206                });
207                queueMicrotask(() => {
208                    calls.push('second');
209                    throw Error('second failure');
210                });
211                Promise.resolve().then(() => calls.push('promise'));
212                "#,
213            )
214            .unwrap_err()
215            .to_string();
216        assert!(error.contains("first failure"), "{error}");
217        assert_eq!(
218            runtime
219                .execute_script::<Vec<String>>("calls", "calls")
220                .unwrap(),
221            ["first", "second", "promise", "nested"]
222        );
223        runtime.run_event_loop().unwrap();
224        runtime
225            .execute_script::<()>(
226                "caught",
227                "queueMicrotask(() => {try {throw Error('caught');} catch {}});",
228            )
229            .unwrap();
230    }
231
232    #[test]
233    fn microtasks_report_non_error_exceptions() {
234        let mut runtime = runtime();
235        for value in ["undefined", "null", "42", "'failure'"] {
236            let error = runtime
237                .execute_script::<()>(
238                    "throw",
239                    &format!("queueMicrotask(() => {{throw {value};}});"),
240                )
241                .unwrap_err()
242                .to_string();
243            assert!(error.contains(value.trim_matches('\'')), "{error}");
244            runtime.run_event_loop().unwrap();
245        }
246    }
247
248    #[test]
249    fn dropping_inside_an_async_embedder_cancels_pending_waits() {
250        let executor = tokio::runtime::Builder::new_current_thread()
251            .build()
252            .unwrap();
253        executor.block_on(async {
254            let mut runtime = runtime();
255            runtime
256                .execute_script::<()>("pending", "setTimeout(() => {}, 1_000_000);")
257                .unwrap();
258            drop(runtime);
259        });
260    }
261}