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", "unref", "hasRef", "refresh", "close", "valueOf", "toString",
30];
31
32/// Methods on an `Immediate` (returned by `setImmediate`). Node's `Immediate`
33/// prototype has no `refresh` — there is no countdown to restart.
34pub const IMMEDIATE_METHODS: &[&str] = &["ref", "unref", "hasRef", "close", "valueOf", "toString"];
35
36/// Build the handle object a `set*` call returns: an `@@native`-tagged object
37/// (`"Timeout"` or `"Immediate"`) carrying the scheduler's timer id in a hidden
38/// `@@timerId` slot.
39///
40/// Node's handles coerce to their integer id (`String(t)` is `"2"`), which is
41/// what keeps `clearTimeout` working for code that stashed the return value in a
42/// number. `valueOf`/`toString` reproduce that for both ToPrimitive hints —
43/// Node reaches it via an own `Symbol.toPrimitive`, which this does not model.
44pub fn new_handle(id: u64, tag: &'static str) -> Value {
45 with_host(|h| {
46 let mut m = IndexMap::new();
47 m.insert("@@native".into(), h.new_str(tag));
48 m.insert("@@timerId".into(), Value::Float(id as f64));
49 h.new_object(m)
50 })
51}
52
53/// The timer id behind a handle object, or `None` for anything else.
54///
55/// Read straight off the heap slot rather than through `ToPrimitive`: the
56/// `clear*` builtins run inside a `with_host` borrow, and coercing via a
57/// `valueOf` call would re-enter `with_host` and panic on the `RefCell`.
58pub fn handle_id(v: &Value) -> Option<u64> {
59 with_host(|h| match h.get(v) {
60 Some(JsObj::Object(p)) => p.get("@@timerId").map(|n| h.to_number(n) as u64),
61 _ => None,
62 })
63}
64
65/// Dispatch a method on a `Timeout`/`Immediate` handle.
66///
67/// `ref`/`unref`/`refresh` return the handle itself (Node chains them); they are
68/// inert once the timer has fired or been cleared, since the scheduler entry is
69/// gone. `close` is Node's alias for clearing the timer.
70pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
71 let id = handle_id(recv).unwrap_or(0);
72 match method {
73 "ref" => {
74 with_host(|h| h.set_timer_refed(id, true));
75 Ok(recv.clone())
76 }
77 "unref" => {
78 with_host(|h| h.set_timer_refed(id, false));
79 Ok(recv.clone())
80 }
81 "hasRef" => Ok(Value::Bool(with_host(|h| h.timer_has_ref(id)))),
82 "refresh" => {
83 with_host(|h| h.refresh_timer(id));
84 Ok(recv.clone())
85 }
86 "close" => {
87 with_host(|h| h.cancel_timer(id));
88 Ok(recv.clone())
89 }
90 "valueOf" => Ok(Value::Float(id as f64)),
91 "toString" => Ok(with_host(|h| h.new_str(id.to_string()))),
92 _ => Err(crate::host::type_error(&format!(
93 "timeout.{method} is not a function"
94 ))),
95 }
96}
97
98// ── timers (callback API) ────────────────────────────────────────────────────
99
100/// Methods of the `timers` module. Each name is also a global; `call` forwards to
101/// the identical global implementation, so there is one timer queue, not two.
102pub const METHODS: &[&str] = &[
103 "setTimeout",
104 "setInterval",
105 "setImmediate",
106 "clearTimeout",
107 "clearInterval",
108 "clearImmediate",
109];
110
111/// Dispatch a `timers.<method>` call by delegating to the matching global timer
112/// builtin. `clearImmediate` has no distinct global handler (the loop cancels by
113/// id regardless of kind), so it maps to `clearTimeout`.
114pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
115 let global = match method {
116 "setTimeout" | "setInterval" | "setImmediate" | "clearTimeout" | "clearInterval" => method,
117 // No separate global `clearImmediate` handler exists; cancellation is by
118 // timer id in both cases, so route it through `clearTimeout`.
119 "clearImmediate" => "clearTimeout",
120 _ => return None,
121 };
122 Some(crate::builtins::call_builtin_function(
123 global,
124 args.to_vec(),
125 ))
126}
127
128// ── timers/promises (Promise API) ────────────────────────────────────────────
129
130/// Methods of the `timers/promises` module (its namespace name carries no `.`, so
131/// `stdlib::is_method` treats the whole `"timers/promises"` as the namespace).
132///
133/// `setInterval(delay[, value])` (an async iterator) is NOT implemented: the
134/// `for await` machinery finds a native object's async iterator only via
135/// `host::user_async_iterator_fn`, which needs a *callable* `@@asyncIterator`
136/// stored property discoverable by `lookup_chain` — native-tagged objects
137/// dispatch methods through the parent `instance_call` table, not stored
138/// properties, so there is no way to expose it without editing `builtins.rs`/
139/// `host.rs` (out of scope here).
140pub const PROMISES_METHODS: &[&str] = &["setTimeout", "setImmediate"];
141
142/// Dispatch a `timers/promises.<method>` call.
143///
144/// `setTimeout(delay[, value])` → a Promise that fulfills with `value` (undefined
145/// if absent) after `delay` ms. `setImmediate([value])` → a Promise that fulfills
146/// with `value` on the next loop turn. Any trailing `options` argument (Node's
147/// `{ signal, ref }`) is accepted and ignored — abort/unref are not modeled.
148pub fn promises_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
149 match method {
150 "setTimeout" => {
151 let delay = arg_num(args, 0);
152 let value = args.get(1).cloned().unwrap_or(Value::Undef);
153 Some(Ok(schedule_promise("setTimeout", Some(delay), value)))
154 }
155 "setImmediate" => {
156 let value = args.first().cloned().unwrap_or(Value::Undef);
157 Some(Ok(schedule_promise("setImmediate", None, value)))
158 }
159 _ => None,
160 }
161}
162
163/// Allocate a pending Promise and schedule its resolution with `value` via the
164/// existing global timer scheduler. The scheduled callback is a
165/// `Builtin("@@presolve:<id>")` value — the same native continuation
166/// `builtins.rs` invokes to fulfill a Promise — so when the timer fires it
167/// resolves the Promise with the timer's extra argument (`value`).
168fn schedule_promise(kind: &str, delay: Option<f64>, value: Value) -> Value {
169 // Create the promise and grab its id for the resolver continuation.
170 let (promise, id) = with_host(|h| {
171 let p = h.new_promise();
172 let id = h.promise_id(&p).unwrap_or(0);
173 (p, id)
174 });
175 // The resolver: invoked with `[value]` when the timer fires.
176 let resolver = with_host(|h| h.alloc(JsObj::Builtin(format!("@@presolve:{id}"))));
177 // Route through the exact global scheduler. `setTimeout(cb, delay, value)`
178 // and `setImmediate(cb, value)` pass `value` on to the callback as its first
179 // argument, which `@@presolve:<id>` resolves the promise with.
180 let timer_args = match delay {
181 Some(d) => vec![resolver, Value::Float(d), value],
182 None => vec![resolver, value],
183 };
184 // Ignore the returned timer id; the promise is the module's return value.
185 let _ = crate::builtins::call_builtin_function(kind, timer_args);
186 promise
187}