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", "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}