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}