ferrijs_std/web/
timers.rs1use std::sync::Arc;
22use std::time::Duration;
23
24use rquickjs::function::{Func, Rest};
25use rquickjs::{Class, Ctx, Function, JsLifetime, Value, class::Trace};
26use tokio::sync::Notify;
27
28pub trait CallbackPolicy: Clone + 'static {
30 fn capture(ctx: &Ctx<'_>) -> Option<Self>
32 where
33 Self: Sized;
34
35 fn enter<R>(ctx: &Ctx<'_>, policy: Option<&Self>, f: impl FnOnce() -> R) -> R
37 where
38 Self: Sized;
39}
40
41#[derive(Clone, Copy)]
43pub struct NoPolicy;
44
45impl CallbackPolicy for NoPolicy {
46 fn capture(_ctx: &Ctx<'_>) -> Option<Self> {
47 None
48 }
49
50 fn enter<R>(_ctx: &Ctx<'_>, _policy: Option<&Self>, f: impl FnOnce() -> R) -> R {
51 f()
52 }
53}
54
55#[derive(Trace, JsLifetime)]
57#[rquickjs::class]
58pub struct Timeout {
59 #[qjs(skip_trace)]
60 abort: Arc<Notify>,
61}
62
63fn clear_timeout(value: Rest<Value<'_>>) {
68 if let Some(v) = value.0.first() {
69 if let Ok(timeout) = Class::<Timeout>::from_value(v) {
70 timeout.borrow().abort.notify_one();
71 }
72 }
73}
74
75fn set_timeout_interval<'js, P: CallbackPolicy>(
76 ctx: Ctx<'js>,
77 cb: Function<'js>,
78 msec: Option<f64>,
79 args: Vec<Value<'js>>,
80 is_interval: bool,
81) -> rquickjs::Result<Class<'js, Timeout>> {
82 let msecs = match msec {
86 Some(ms) if ms.is_finite() && ms >= 0.0 && ms < f64::from(i32::MAX) => ms as u64,
87 _ => 0,
88 };
89 let duration = Duration::from_millis(msecs.max(4));
90
91 let abort = Arc::new(Notify::new());
92 let abort_ref = abort.clone();
93 let policy = P::capture(&ctx);
94
95 ctx.spawn(async move {
96 loop {
97 let mut interval = tokio::time::interval(duration);
98 interval.tick().await; let aborted = tokio::select! {
100 () = abort_ref.notified() => true,
101 _ = interval.tick() => false,
102 };
103 if aborted {
104 break;
105 }
106 let mut call_args = rquickjs::function::Args::new(cb.ctx().clone(), args.len());
109 let ok = call_args.push_args(args.iter().cloned()).is_ok();
110 if !ok || {
111 let res: rquickjs::Result<()> = P::enter(cb.ctx(), policy.as_ref(), || cb.call_arg(call_args));
112 res
113 .inspect_err(|err| tracing::warn!(target: "ferrijs::timers", "timer callback threw: {err}"))
114 .is_err()
115 } {
116 break;
117 }
118 if !is_interval {
119 break;
120 }
121 }
122 });
123
124 Class::instance(ctx, Timeout { abort })
125}
126
127fn set_timeout<'js, P: CallbackPolicy>(
128 ctx: Ctx<'js>,
129 cb: Function<'js>,
130 rest: Rest<Value<'js>>,
131) -> rquickjs::Result<Class<'js, Timeout>> {
132 let (msec, args) = split_delay_args(rest.0);
133 set_timeout_interval::<P>(ctx, cb, msec, args, false)
134}
135
136fn set_interval<'js, P: CallbackPolicy>(
137 ctx: Ctx<'js>,
138 cb: Function<'js>,
139 rest: Rest<Value<'js>>,
140) -> rquickjs::Result<Class<'js, Timeout>> {
141 let (msec, args) = split_delay_args(rest.0);
142 set_timeout_interval::<P>(ctx, cb, msec, args, true)
143}
144
145fn split_delay_args(mut rest: Vec<Value<'_>>) -> (Option<f64>, Vec<Value<'_>>) {
148 if rest.is_empty() {
149 return (None, rest);
150 }
151 let delay = rest.remove(0);
152 (delay.as_number(), rest)
153}
154
155fn set_immediate<'js, P: CallbackPolicy>(
160 ctx: Ctx<'js>,
161 cb: Function<'js>,
162 rest: Rest<Value<'js>>,
163) -> rquickjs::Result<()> {
164 match P::capture(&ctx) {
165 None => {
166 let mut args = rquickjs::function::Args::new(ctx, rest.0.len());
167 args.push_args(rest.0)?;
168 cb.defer_arg(args)
169 },
170 Some(policy) => {
171 let policy = Some(policy);
177 let wrapper = Function::new(ctx.clone(), move |args: Rest<Value<'_>>| {
178 deferred_call::<P>(policy.as_ref(), &args.0)
179 })?;
180 let mut args = rquickjs::function::Args::new(ctx, rest.0.len() + 1);
181 args.push_arg(cb)?;
182 args.push_args(rest.0)?;
183 wrapper.defer_arg(args)
184 },
185 }
186}
187
188fn deferred_call<P: CallbackPolicy>(policy: Option<&P>, args: &[Value<'_>]) -> rquickjs::Result<()> {
191 let inner = args.first().and_then(|v| v.as_function().cloned()).ok_or_else(|| {
192 rquickjs::Error::new_from_js_message("setImmediate", "Error", "deferred callback missing".to_string())
193 })?;
194 let ctx = inner.ctx().clone();
195 let mut call_args = rquickjs::function::Args::new(ctx.clone(), args.len().saturating_sub(1));
196 call_args.push_args(args.iter().skip(1).cloned())?;
197 P::enter(&ctx, policy, || inner.call_arg(call_args))
198}
199
200fn queue_microtask<'js, P: CallbackPolicy>(ctx: Ctx<'js>, cb: Function<'js>) -> rquickjs::Result<()> {
204 match P::capture(&ctx) {
205 None => cb.defer::<()>(()),
206 Some(policy) => {
207 let policy = Some(policy);
208 let wrapper = Function::new(ctx.clone(), move |args: Rest<Value<'_>>| {
209 deferred_call::<P>(policy.as_ref(), &args.0)
210 })?;
211 wrapper.defer((cb,))
212 },
213 }
214}
215
216pub fn install<P: CallbackPolicy>(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
222 let globals = ctx.globals();
223 globals.set("setTimeout", Func::from(set_timeout::<P>))?;
224 globals.set("clearTimeout", Func::from(clear_timeout))?;
225 globals.set("setInterval", Func::from(set_interval::<P>))?;
226 globals.set("clearInterval", Func::from(clear_timeout))?;
227 globals.set("setImmediate", Func::from(set_immediate::<P>))?;
228 globals.set("queueMicrotask", Func::from(queue_microtask::<P>))?;
232 Ok(())
233}