Skip to main content

nodejs/stdlib/
async_hooks.rs

1//! Node `async_hooks` module — honest minimal implementation.
2//!
3//! node-js has no per-async-resource id tracking and no async-context
4//! propagation, so most of this module is a deliberate, documented no-op whose
5//! only job is to let code that defensively imports `async_hooks` load and run
6//! without crashing:
7//!
8//!   - `executionAsyncId()` returns a fixed `1`, `triggerAsyncId()` returns `0`.
9//!     These are NOT real resource ids — there is no async-resource graph.
10//!   - `createHook({ init, before, after, destroy })` returns a hook object with
11//!     chainable `enable()`/`disable()`. The registered callbacks are stored
12//!     nowhere and NEVER FIRE — node-js does not instrument async resource
13//!     lifetimes. This is intentional; do not treat it as a gap to "fill" by
14//!     faking hook invocations.
15//!
16//! What IS real is `AsyncLocalStorage` for the SYNCHRONOUS case: `run(store, cb)`
17//! makes `getStore()` return `store` for the duration of `cb` (and restores the
18//! previous store afterwards), and `enterWith(store)` sets the current store for
19//! subsequent synchronous `getStore()` calls. Because there is no async-context
20//! propagation, a store set with `enterWith` (or visible inside `run`) does NOT
21//! automatically follow into `setTimeout`/Promise callbacks — cross-async
22//! propagation is not modeled. Within straight-line synchronous code the store is
23//! correct.
24//!
25//! Instances are `@@native`-tagged objects (`AsyncLocalStorage` / `AsyncHook`)
26//! dispatched through `instance_call`; the parent wires `construct`,
27//! `native_tag`, `instance_has_method`, and `instance_call` (see the report).
28
29use crate::host::{invoke, with_host};
30use fusevm::Value;
31use indexmap::IndexMap;
32use std::cell::RefCell;
33use std::collections::HashMap;
34
35thread_local! {
36    /// Per-`AsyncLocalStorage`-instance store stack, keyed by the instance's heap
37    /// index. Push on `run`/`enterWith`, pop on `run` exit. The top is what
38    /// `getStore()` returns. A stack (not a single slot) so nested `run` calls
39    /// restore the enclosing store correctly.
40    static STORES: RefCell<HashMap<u32, Vec<Value>>> = RefCell::new(HashMap::new());
41}
42
43/// Module-level callable members.
44pub const METHODS: &[&str] = &["executionAsyncId", "triggerAsyncId", "createHook"];
45
46/// Instance method names by native tag — for the parent's `instance_has_method`
47/// so a method *read* (`als.run.bind(...)`) resolves before it is invoked.
48pub const ALS_METHODS: &[&str] = &["getStore", "run", "enterWith", "exit", "disable"];
49pub const HOOK_METHODS: &[&str] = &["enable", "disable"];
50
51pub fn call(method: &str, _args: &[Value]) -> Option<Result<Value, String>> {
52    Some(match method {
53        // Fixed placeholders — there is no async-resource id graph.
54        "executionAsyncId" => Ok(Value::Float(1.0)),
55        "triggerAsyncId" => Ok(Value::Float(0.0)),
56        // The hook object; its callbacks never fire (see module docs).
57        "createHook" => Ok(new_hook()),
58        _ => return None,
59    })
60}
61
62/// Construct a stdlib class instance (`new AsyncLocalStorage()`). `None` for any
63/// other name so the parent's `construct` can fall through.
64pub fn construct(name: &str, _args: &[Value]) -> Option<Result<Value, String>> {
65    match name {
66        "AsyncLocalStorage" => Some(Ok(new_native("AsyncLocalStorage"))),
67        _ => None,
68    }
69}
70
71/// A fresh `@@native`-tagged object carrying `tag`.
72fn new_native(tag: &'static str) -> Value {
73    with_host(|h| {
74        let mut m = IndexMap::new();
75        m.insert("@@native".into(), h.new_str(tag));
76        h.new_object(m)
77    })
78}
79
80/// The object returned by `createHook`. Its `enable`/`disable` are no-ops that
81/// return the hook itself (Node's chainable API); no callbacks are ever invoked.
82fn new_hook() -> Value {
83    new_native("AsyncHook")
84}
85
86/// Dispatch a method on a native `async_hooks` instance.
87pub fn instance_call(
88    tag: &str,
89    recv: &Value,
90    method: &str,
91    args: Vec<Value>,
92) -> Result<Value, String> {
93    match tag {
94        // A createHook() result: enable/disable are no-ops returning `this` so
95        // `createHook(...).enable()` chains work. No hook callbacks fire.
96        "AsyncHook" => match method {
97            "enable" | "disable" => Ok(recv.clone()),
98            _ => Err(crate::host::type_error(&format!(
99                "{method} is not a function"
100            ))),
101        },
102        "AsyncLocalStorage" => als_call(recv, method, args),
103        _ => Err(crate::host::type_error(&format!(
104            "{method} is not a function"
105        ))),
106    }
107}
108
109/// The instance's heap index (its store-stack key), or `0` for a non-heap value.
110fn key(recv: &Value) -> u32 {
111    match recv {
112        Value::Obj(i) => *i,
113        _ => 0,
114    }
115}
116
117fn als_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
118    let id = key(recv);
119    match method {
120        // The current store (top of this instance's stack), or undefined.
121        "getStore" => Ok(STORES.with(|s| {
122            s.borrow()
123                .get(&id)
124                .and_then(|v| v.last().cloned())
125                .unwrap_or(Value::Undef)
126        })),
127        // run(store, callback, ...args): set the store, call the callback with the
128        // remaining args, restore the previous store, return the callback result.
129        "run" => {
130            let store = args.first().cloned().unwrap_or(Value::Undef);
131            let cb = args.get(1).cloned().unwrap_or(Value::Undef);
132            let rest = args.get(2..).map(|s| s.to_vec()).unwrap_or_default();
133            with_store(id, store, cb, rest)
134        }
135        // exit(callback, ...args): run the callback with the store unset (undefined
136        // pushed) for its duration.
137        "exit" => {
138            let cb = args.first().cloned().unwrap_or(Value::Undef);
139            let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
140            with_store(id, Value::Undef, cb, rest)
141        }
142        // enterWith(store): set the current store for subsequent synchronous
143        // getStore() calls (not popped automatically; not propagated across async).
144        "enterWith" => {
145            let store = args.first().cloned().unwrap_or(Value::Undef);
146            STORES.with(|s| s.borrow_mut().entry(id).or_default().push(store));
147            Ok(Value::Undef)
148        }
149        // disable(): drop all stores for this instance.
150        "disable" => {
151            STORES.with(|s| {
152                s.borrow_mut().remove(&id);
153            });
154            Ok(Value::Undef)
155        }
156        _ => Err(crate::host::type_error(&format!(
157            "{method} is not a function"
158        ))),
159    }
160}
161
162/// Push `store`, invoke `cb` with `rest` (releasing every host borrow first, so
163/// the callback may re-enter the host), then always pop — even on error.
164fn with_store(id: u32, store: Value, cb: Value, rest: Vec<Value>) -> Result<Value, String> {
165    STORES.with(|s| s.borrow_mut().entry(id).or_default().push(store));
166    let r = invoke(&cb, rest, None);
167    STORES.with(|s| {
168        if let Some(v) = s.borrow_mut().get_mut(&id) {
169            v.pop();
170        }
171    });
172    r
173}