Skip to main content

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}