Skip to main content

nodejs/stdlib/
domain.rs

1//! Node `domain` module (deprecated in Node, implemented here with its real
2//! error-trapping semantics). A `Domain` is an EventEmitter (same `@@native` +
3//! `@@on`/`@@once` shape as `events`/`net`) whose defining behaviour is
4//! `domain.run(fn)`: it runs `fn` and, if `fn` throws, emits the domain's
5//! `'error'` event with the thrown value instead of propagating the throw.
6//!
7//! Scope of the port: `.run`/`.bind`/`.intercept` genuinely trap synchronous
8//! throws from the function they wrap and route them to `'error'`. What is NOT
9//! wired is Node's *implicit* interception of errors emitted by emitters passed
10//! to `.add()` — node-js has no per-emitter active-domain hook, so `.add()`/
11//! `.remove()` only track membership (best-effort) and do not auto-forward those
12//! emitters' `'error'` events. Only code run through `.run`/`.bind`/`.intercept`
13//! is actually protected. `.enter()`/`.exit()` maintain a thread-local domain
14//! stack whose top is exposed as `domain.active`.
15
16use crate::host::{is_callable, take_exc_or_error, type_error, with_host, JsObj};
17use fusevm::Value;
18use indexmap::IndexMap;
19use std::cell::RefCell;
20
21/// Module-level methods (`require('domain').create()`).
22pub const METHODS: &[&str] = &["create", "createDomain"];
23
24/// Instance methods carried by a `Domain` beyond the shared EventEmitter surface
25/// (which `instance_has_method` adds via the emitter set).
26pub const DOMAIN_METHODS: &[&str] = &[
27    "run",
28    "add",
29    "remove",
30    "bind",
31    "intercept",
32    "enter",
33    "exit",
34    "dispose",
35];
36
37thread_local! {
38    /// The stack of entered domains; the top is `domain.active`. `.run`/`.enter`
39    /// push, `.exit`/`.run`-completion pop.
40    static STACK: RefCell<Vec<Value>> = const { RefCell::new(Vec::new()) };
41}
42
43// ── construction ─────────────────────────────────────────────────────────────
44
45/// A fresh `Domain` (emitter object tagged `"Domain"`), carrying a hidden
46/// `@@members` array for `.add`/`.remove` bookkeeping.
47pub fn new_domain() -> Value {
48    let members = with_host(|h| h.new_array(Vec::new()));
49    let mut extra = IndexMap::new();
50    extra.insert("@@members".to_string(), members);
51    super::net::new_emitter_object("Domain", extra)
52}
53
54/// `require('domain')` module dispatch.
55pub fn call(method: &str, _args: &[Value]) -> Option<Result<Value, String>> {
56    match method {
57        "create" | "createDomain" => Some(Ok(new_domain())),
58        _ => None,
59    }
60}
61
62/// `new domain.Domain()` (Node prefers `domain.create()`, but the constructor
63/// exists).
64pub fn construct(_args: &[Value]) -> Result<Value, String> {
65    Ok(new_domain())
66}
67
68/// `domain.active` — the current (top-of-stack) domain, or `null`.
69pub fn constant(name: &str) -> Option<Value> {
70    match name {
71        "active" => Some(active().unwrap_or_else(|| with_host(|h| h.null()))),
72        _ => None,
73    }
74}
75
76// ── instance dispatch ────────────────────────────────────────────────────────
77
78pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
79    // EventEmitter methods (`on`/`once`/`emit`/…) delegate to `events` verbatim.
80    if let Some(r) = emitter_dispatch(recv, method, &args) {
81        return r;
82    }
83    match method {
84        "run" => {
85            let f = args.first().cloned().unwrap_or(Value::Undef);
86            let call_args = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
87            domain_run(recv, &f, call_args)
88        }
89        "add" => {
90            if let Some(e) = args.into_iter().next() {
91                track(recv, e, true);
92            }
93            Ok(Value::Undef)
94        }
95        "remove" => {
96            if let Some(e) = args.into_iter().next() {
97                track(recv, e, false);
98            }
99            Ok(Value::Undef)
100        }
101        "bind" => Ok(make_wrapper(
102            recv,
103            args.into_iter().next().unwrap_or(Value::Undef),
104            "@@bound",
105        )),
106        "intercept" => Ok(make_wrapper(
107            recv,
108            args.into_iter().next().unwrap_or(Value::Undef),
109            "@@intercept",
110        )),
111        "enter" => {
112            enter(recv);
113            Ok(Value::Undef)
114        }
115        "exit" => {
116            exit(recv);
117            Ok(Value::Undef)
118        }
119        // Deprecated no-op (Node's `domain.dispose()` was removed as unsafe).
120        "dispose" => Ok(Value::Undef),
121        // Internal continuation for the wrappers produced by `.bind`/`.intercept`.
122        "@@bound" => {
123            let domain = get_prop(recv, "@@boundDomain").unwrap_or_else(|| recv.clone());
124            let f = get_prop(recv, "@@boundFn").unwrap_or(Value::Undef);
125            domain_run(&domain, &f, args)
126        }
127        "@@intercept" => {
128            let domain = get_prop(recv, "@@boundDomain").unwrap_or_else(|| recv.clone());
129            let f = get_prop(recv, "@@boundFn").unwrap_or(Value::Undef);
130            let err = args.first().cloned().unwrap_or(Value::Undef);
131            let is_err = with_host(|h| !matches!(err, Value::Undef) && !h.is_null(&err));
132            if is_err {
133                emit_error(&domain, err);
134                Ok(Value::Undef)
135            } else {
136                let rest = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
137                domain_run(&domain, &f, rest)
138            }
139        }
140        _ => Err(type_error(&format!("domain.{method} is not a function"))),
141    }
142}
143
144// ── core: run a function inside the domain, trapping throws ───────────────────
145
146/// Enter the domain, invoke `f(..call_args)`, exit. A throw is caught, the real
147/// thrown value is emitted as the domain's `'error'` (never propagated), and
148/// `undefined` is returned — the defining `domain` behaviour.
149fn domain_run(domain: &Value, f: &Value, call_args: Vec<Value>) -> Result<Value, String> {
150    if !with_host(|h| is_callable(h, f)) {
151        return Err(type_error("domain.run requires a function"));
152    }
153    enter(domain);
154    let r = crate::host::invoke(f, call_args, None);
155    exit(domain);
156    match r {
157        Ok(v) => Ok(v),
158        Err(e) => {
159            // Recover the live thrown value (a real Error object when the code
160            // did `throw new Error(...)`), then clear the host error/signal state
161            // so execution continues past the trapped throw.
162            let err = take_exc_or_error(&e);
163            with_host(|h| h.signal = None);
164            emit_error(domain, err);
165            Ok(Value::Undef)
166        }
167    }
168}
169
170/// Emit the domain's `'error'` event carrying `err`.
171fn emit_error(domain: &Value, err: Value) {
172    let name = with_host(|h| h.new_str("error"));
173    let _ = super::events::instance_call(domain, "emit", vec![name, err]);
174}
175
176/// Build the reusable wrapper returned by `.bind`/`.intercept`: a `BoundMethod`
177/// over a `Domain`-tagged holder object that stores the target fn + owning
178/// domain. Invoking it routes back through `instance_call` at `kind`.
179fn make_wrapper(domain: &Value, f: Value, kind: &str) -> Value {
180    let mut extra = IndexMap::new();
181    extra.insert("@@boundFn".to_string(), f);
182    extra.insert("@@boundDomain".to_string(), domain.clone());
183    let holder = super::net::new_emitter_object("Domain", extra);
184    with_host(|h| {
185        h.alloc(JsObj::BoundMethod {
186            recv: holder,
187            name: kind.to_string(),
188        })
189    })
190}
191
192// ── domain stack (`enter`/`exit`/`active`) ───────────────────────────────────
193
194fn enter(domain: &Value) {
195    STACK.with(|s| s.borrow_mut().push(domain.clone()));
196}
197
198fn exit(domain: &Value) {
199    STACK.with(|s| {
200        let mut s = s.borrow_mut();
201        // Node's `exit` removes this domain (and any it stacked above); the
202        // best-effort port drops the topmost occurrence of it.
203        if let Some(pos) = s.iter().rposition(|x| x == domain) {
204            s.remove(pos);
205        }
206    });
207}
208
209fn active() -> Option<Value> {
210    STACK.with(|s| s.borrow().last().cloned())
211}
212
213// ── `.add`/`.remove` membership bookkeeping (best-effort) ────────────────────
214
215fn track(recv: &Value, emitter: Value, add: bool) {
216    with_host(|h| {
217        let arr = match h.get(recv) {
218            Some(JsObj::Object(p)) => p.get("@@members").cloned(),
219            _ => None,
220        };
221        if let Some(a) = arr {
222            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
223                if add {
224                    if !items.iter().any(|x| x == &emitter) {
225                        items.push(emitter);
226                    }
227                } else if let Some(pos) = items.iter().position(|x| x == &emitter) {
228                    items.remove(pos);
229                }
230            }
231        }
232    });
233}
234
235// ── helpers ──────────────────────────────────────────────────────────────────
236
237fn get_prop(recv: &Value, key: &str) -> Option<Value> {
238    with_host(|h| match h.get(recv) {
239        Some(JsObj::Object(p)) => p.get(key).cloned(),
240        _ => None,
241    })
242}
243
244/// EventEmitter method delegation (shared shape with `net`/`http` emitters).
245fn emitter_dispatch(recv: &Value, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
246    match method {
247        "on"
248        | "once"
249        | "emit"
250        | "addListener"
251        | "prependListener"
252        | "prependOnceListener"
253        | "removeListener"
254        | "off"
255        | "removeAllListeners"
256        | "listeners"
257        | "listenerCount"
258        | "eventNames"
259        | "setMaxListeners"
260        | "getMaxListeners" => Some(super::events::instance_call(recv, method, args.to_vec())),
261        _ => None,
262    }
263}