nodejs/stdlib/timers.rs
1//! Node `timers` and `timers/promises` modules.
2//!
3//! `require('timers')` re-exports the SAME timer primitives that already exist as
4//! globals (`setTimeout`/`setInterval`/`setImmediate` + their `clear*`), so this
5//! module owns NO queue of its own: every method delegates straight to
6//! `builtins::call_builtin_function`, which schedules onto the single shared
7//! `JsHost.macrotasks` queue. `timers.foo(...)` is therefore observably identical
8//! to the global `foo(...)`.
9//!
10//! `require('timers/promises')` returns the promise-based variants: `setTimeout`
11//! and `setImmediate` resolve a Promise after the delay instead of invoking a
12//! callback. They are built on the SAME two substrates — the global timer
13//! scheduler and the `@@presolve:<id>` native-continuation convention that
14//! `builtins.rs` uses for Promise resolve reactions — so no new mechanism is
15//! introduced: a timer is scheduled whose callback is the promise's resolver.
16
17use super::arg_num;
18use crate::host::{with_host, JsObj};
19use fusevm::Value;
20use indexmap::IndexMap;
21
22// ── timer handle objects (`Timeout` / `Immediate`) ───────────────────────────
23
24/// Methods on a `Timeout` (returned by `setTimeout`/`setInterval`). Node's
25/// prototype carries exactly `refresh`, `unref`, `ref`, `hasRef`, `close`;
26/// `valueOf`/`toString` back the primitive coercion described on
27/// [`new_handle`].
28pub const TIMEOUT_METHODS: &[&str] = &[
29 "ref",
30 "unref",
31 "hasRef",
32 "refresh",
33 "close",
34 "toString",
35 // A `Timeout` coerces to its own id, which is how the handle can be passed
36 // straight back to `clearTimeout` in code that stored `Number(t)`. The
37 // symbol form was missing, so `t[Symbol.toPrimitive]` was not a function.
38 "@@toPrimitive",
39];
40
41/// Methods on an `Immediate` (returned by `setImmediate`). Node's `Immediate`
42/// prototype has no `refresh` — there is no countdown to restart.
43pub const IMMEDIATE_METHODS: &[&str] = &["ref", "unref", "hasRef", "close", "valueOf", "toString"];
44
45/// Build the handle object a `set*` call returns: an `@@native`-tagged object
46/// (`"Timeout"` or `"Immediate"`) carrying the scheduler's timer id in a hidden
47/// `@@timerId` slot.
48///
49/// Node's handles coerce to their integer id (`String(t)` is `"2"`), which is
50/// what keeps `clearTimeout` working for code that stashed the return value in a
51/// number. `valueOf`/`toString` reproduce that for both ToPrimitive hints —
52/// Node reaches it via an own `Symbol.toPrimitive`, which this does not model.
53pub fn new_handle(id: u64, tag: &'static str) -> Value {
54 with_host(|h| {
55 let mut m = IndexMap::new();
56 m.insert("@@native".into(), h.new_str(tag));
57 m.insert("@@timerId".into(), Value::Float(id as f64));
58 h.new_object(m)
59 })
60}
61
62/// The timer id behind a handle object, or `None` for anything else.
63///
64/// Read straight off the heap slot rather than through `ToPrimitive`: the
65/// `clear*` builtins run inside a `with_host` borrow, and coercing via a
66/// `valueOf` call would re-enter `with_host` and panic on the `RefCell`.
67pub fn handle_id(v: &Value) -> Option<u64> {
68 with_host(|h| match h.get(v) {
69 Some(JsObj::Object(p)) => p.get("@@timerId").map(|n| h.to_number(n) as u64),
70 _ => None,
71 })
72}
73
74/// Dispatch a method on a `Timeout`/`Immediate` handle.
75///
76/// `ref`/`unref`/`refresh` return the handle itself (Node chains them); they are
77/// inert once the timer has fired or been cleared, since the scheduler entry is
78/// gone. `close` is Node's alias for clearing the timer.
79pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
80 let id = handle_id(recv).unwrap_or(0);
81 match method {
82 "ref" => {
83 with_host(|h| h.set_timer_refed(id, true));
84 Ok(recv.clone())
85 }
86 "unref" => {
87 with_host(|h| h.set_timer_refed(id, false));
88 Ok(recv.clone())
89 }
90 "hasRef" => Ok(Value::Bool(with_host(|h| h.timer_has_ref(id)))),
91 "refresh" => {
92 with_host(|h| h.refresh_timer(id));
93 Ok(recv.clone())
94 }
95 "close" => {
96 with_host(|h| h.cancel_timer(id));
97 Ok(recv.clone())
98 }
99 // Only the SYMBOL form yields the id. `valueOf` is the inherited
100 // `Object.prototype.valueOf`, which returns the receiver — node's
101 // `Timeout` does not override it, and reporting the id there made
102 // `t.valueOf() === t` false.
103 "@@toPrimitive" => Ok(Value::Float(id as f64)),
104 "toString" => Ok(with_host(|h| h.new_str(id.to_string()))),
105 _ => Err(crate::host::type_error(&format!(
106 "timeout.{method} is not a function"
107 ))),
108 }
109}
110
111// ── timers (callback API) ────────────────────────────────────────────────────
112
113/// Methods of the `timers` module. Each name is also a global; `call` forwards to
114/// the identical global implementation, so there is one timer queue, not two.
115pub const METHODS: &[&str] = &[
116 "setTimeout",
117 "setInterval",
118 "setImmediate",
119 "clearTimeout",
120 "clearInterval",
121 "clearImmediate",
122];
123
124/// Dispatch a `timers.<method>` call by delegating to the matching global timer
125/// builtin. `clearImmediate` has no distinct global handler (the loop cancels by
126/// id regardless of kind), so it maps to `clearTimeout`.
127pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
128 let global = match method {
129 "setTimeout" | "setInterval" | "setImmediate" | "clearTimeout" | "clearInterval" => method,
130 // No separate global `clearImmediate` handler exists; cancellation is by
131 // timer id in both cases, so route it through `clearTimeout`.
132 "clearImmediate" => "clearTimeout",
133 _ => return None,
134 };
135 Some(crate::builtins::call_builtin_function(
136 global,
137 args.to_vec(),
138 ))
139}
140
141// ── timers/promises (Promise API) ────────────────────────────────────────────
142
143/// Methods of the `timers/promises` module (its namespace name carries no `.`, so
144/// `stdlib::is_method` treats the whole `"timers/promises"` as the namespace).
145///
146/// `setInterval(delay[, value])` (an async iterator) is NOT implemented: the
147/// `for await` machinery finds a native object's async iterator only via
148/// `host::user_async_iterator_fn`, which needs a *callable* `@@asyncIterator`
149/// stored property discoverable by `lookup_chain` — native-tagged objects
150/// dispatch methods through the parent `instance_call` table, not stored
151/// properties, so there is no way to expose it without editing `builtins.rs`/
152/// `host.rs` (out of scope here).
153pub const PROMISES_METHODS: &[&str] = &["setTimeout", "setImmediate", "setInterval"];
154
155/// The async-iterator surface `timers/promises.setInterval` returns.
156pub const INTERVAL_METHODS: &[&str] = &["next", "return", "@@asyncIterator"];
157
158/// Dispatch a `timers/promises.<method>` call.
159///
160/// `setTimeout(delay[, value])` → a Promise that fulfills with `value` (undefined
161/// if absent) after `delay` ms. `setImmediate([value])` → a Promise that fulfills
162/// with `value` on the next loop turn. Any trailing `options` argument (Node's
163/// `{ signal, ref }`) is accepted and ignored — abort/unref are not modeled.
164pub fn promises_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
165 match method {
166 "setTimeout" => {
167 let delay = arg_num(args, 0);
168 let value = args.get(1).cloned().unwrap_or(Value::Undef);
169 Some(Ok(schedule_promise("setTimeout", Some(delay), value)))
170 }
171 "setImmediate" => {
172 let value = args.first().cloned().unwrap_or(Value::Undef);
173 Some(Ok(schedule_promise("setImmediate", None, value)))
174 }
175 // `setInterval(delay, value)` is an ASYNC ITERABLE, not a promise: it
176 // yields `value` every `delay` for as long as it is iterated. It was
177 // missing, so `for await (const v of setInterval(...))` had nothing to
178 // call.
179 "setInterval" => {
180 let delay = arg_num(args, 0);
181 let value = args.get(1).cloned().unwrap_or(Value::Undef);
182 Some(Ok(interval_iterator(delay, value)))
183 }
184 _ => None,
185 }
186}
187
188/// The object `timers/promises.setInterval` hands back: an async iterator that
189/// resolves one `{ value, done: false }` per `delay`, and reports `done` once
190/// `return()` has been called (which is what `break` inside `for await` does).
191fn interval_iterator(delay: f64, value: Value) -> Value {
192 with_host(|h| {
193 let mut m = IndexMap::new();
194 m.insert("@@native".into(), h.new_str("IntervalIterator"));
195 m.insert("@@delay".into(), Value::Float(delay));
196 m.insert("@@value".into(), value);
197 m.insert("@@stopped".into(), Value::Bool(false));
198 h.new_object(m)
199 })
200}
201
202pub fn interval_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
203 let slot = |k: &str| {
204 with_host(|h| match h.get(recv) {
205 Some(JsObj::Object(p)) => p.get(k).cloned(),
206 _ => None,
207 })
208 };
209 match method {
210 // An async iterable is its own iterator here, as node's is.
211 "@@asyncIterator" => Ok(recv.clone()),
212 "next" => {
213 let stopped = slot("@@stopped").is_some_and(|v| with_host(|h| h.truthy(&v)));
214 let value = slot("@@value").unwrap_or(Value::Undef);
215 if stopped {
216 let done = iter_result(Value::Undef, true);
217 return Ok(resolved_promise(done));
218 }
219 let delay = slot("@@delay")
220 .map(|v| with_host(|h| h.to_number(&v)))
221 .unwrap_or(0.0);
222 let result = iter_result(value, false);
223 Ok(schedule_promise("setTimeout", Some(delay), result))
224 }
225 "return" => {
226 with_host(|h| {
227 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
228 p.insert("@@stopped".into(), Value::Bool(true));
229 }
230 });
231 let done = iter_result(Value::Undef, true);
232 Ok(resolved_promise(done))
233 }
234 _ => Err(crate::host::type_error(&format!(
235 "intervalIterator.{method} is not a function"
236 ))),
237 }
238}
239
240/// `{ value, done }` — the iterator-result shape both arms return.
241fn iter_result(value: Value, done: bool) -> Value {
242 with_host(|h| {
243 let mut m = IndexMap::new();
244 m.insert("value".into(), value);
245 m.insert("done".into(), Value::Bool(done));
246 h.new_object(m)
247 })
248}
249
250/// A promise already fulfilled with `v`.
251fn resolved_promise(v: Value) -> Value {
252 let (promise, id) = with_host(|h| {
253 let p = h.new_promise();
254 let id = h.promise_id(&p).unwrap_or(0);
255 (p, id)
256 });
257 crate::host::resolve_promise_val(id, v);
258 promise
259}
260
261/// Allocate a pending Promise and schedule its resolution with `value` via the
262/// existing global timer scheduler. The scheduled callback is a
263/// `Builtin("@@presolve:<id>")` value — the same native continuation
264/// `builtins.rs` invokes to fulfill a Promise — so when the timer fires it
265/// resolves the Promise with the timer's extra argument (`value`).
266fn schedule_promise(kind: &str, delay: Option<f64>, value: Value) -> Value {
267 // Create the promise and grab its id for the resolver continuation.
268 let (promise, id) = with_host(|h| {
269 let p = h.new_promise();
270 let id = h.promise_id(&p).unwrap_or(0);
271 (p, id)
272 });
273 // The resolver: invoked with `[value]` when the timer fires.
274 let resolver = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
275 // Route through the exact global scheduler. `setTimeout(cb, delay, value)`
276 // and `setImmediate(cb, value)` pass `value` on to the callback as its first
277 // argument, which `@@presolve:<id>` resolves the promise with.
278 let timer_args = match delay {
279 Some(d) => vec![resolver, Value::Float(d), value],
280 None => vec![resolver, value],
281 };
282 // Ignore the returned timer id; the promise is the module's return value.
283 let _ = crate::builtins::call_builtin_function(kind, timer_args);
284 promise
285}