Skip to main content

nodejs/stdlib/
diagnostics_channel.rs

1//! Node `diagnostics_channel` — in-process publish/subscribe named channels.
2//!
3//! A channel is a plain object tagged `@@native = "Channel"` carrying its name
4//! (`@@name`, also exposed as the enumerable `name`), a hidden `@@subs` array of
5//! subscriber callbacks, and a `hasSubscribers` data property kept in sync as
6//! subscribers come and go. Channels are interned by name in a thread-local
7//! registry so `channel('x') === channel('x')` and the module-level
8//! `subscribe/unsubscribe/hasSubscribers(name, …)` operate on the very same
9//! object a caller obtains from `channel(name)` — matching Node's guarantee that
10//! a channel is a single shared instance per name.
11//!
12//! Scope of fidelity: this is an **in-process** implementation. `publish(msg)`
13//! synchronously invokes each subscriber with `(message, channelName)` (Node's
14//! contract). It does not span worker threads or processes, and it does not
15//! implement the `tracingChannel` / async-context store surface. The registry is
16//! process-lifetime; handles are only meaningful within a single program run
17//! (they index the live heap).
18
19use crate::host::{with_host, JsObj};
20use fusevm::Value;
21use indexmap::IndexMap;
22use std::cell::RefCell;
23use std::collections::HashMap;
24
25thread_local! {
26    /// name → the single interned Channel object for that name.
27    static CHANNELS: RefCell<HashMap<String, Value>> = RefCell::new(HashMap::new());
28}
29
30pub const METHODS: &[&str] = &[
31    "channel",
32    "subscribe",
33    "unsubscribe",
34    "hasSubscribers",
35    "tracingChannel",
36    "boundedChannel",
37];
38
39/// Methods dispatched on an `@@native = "TracingChannel"` object (reported to the
40/// parent for `instance_has_method` / `instance_call` wiring).
41pub const TRACING_CHANNEL_METHODS: &[&str] = &["subscribe", "unsubscribe", "traceSync"];
42
43/// The five sub-channel names a `TracingChannel` groups.
44const TRACING_SUBS: &[&str] = &["start", "end", "asyncStart", "asyncEnd", "error"];
45
46pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
47    let name = super::arg_str(args, 0);
48    Some(match method {
49        "channel" => Ok(get_or_create(&name)),
50        // Module-level subscribe/unsubscribe address a channel by name, creating
51        // it on demand so a later `channel(name)` sees the same subscribers.
52        "subscribe" => {
53            let ch = get_or_create(&name);
54            add_sub(&ch, args.get(1).cloned().unwrap_or(Value::Undef));
55            Ok(Value::Undef)
56        }
57        "unsubscribe" => {
58            let ch = get_or_create(&name);
59            Ok(Value::Bool(remove_sub(
60                &ch,
61                &args.get(1).cloned().unwrap_or(Value::Undef),
62            )))
63        }
64        "hasSubscribers" => Ok(Value::Bool(sub_count(&get_or_create(&name)) > 0)),
65        // `tracingChannel(name)` groups five sub-channels (start/end/asyncStart/
66        // asyncEnd/error) that publish() fires around a traced operation.
67        "tracingChannel" => Ok(tracing_channel(&name)),
68        // `boundedChannel(name)` is a `channel()` whose async subscriber queue is
69        // capacity-bounded in Node. This in-process implementation is fully
70        // synchronous (publish invokes subscribers inline; see the module docs),
71        // so there is no queue to bound — it returns the same shared Channel as
72        // `channel(name)`. The bound is not enforced (documented, never faked).
73        "boundedChannel" => Ok(get_or_create(&name)),
74        _ => return None,
75    })
76}
77
78/// Non-function members of the `diagnostics_channel` namespace, exposed as
79/// constructor values for `instanceof`/typeof checks. Reachable via
80/// `namespace_property` IF the parent routes `"diagnostics_channel"` into
81/// `stdlib::constant`. The channel objects themselves come from `channel()` /
82/// `tracingChannel()`; these bare constructors are not directly instantiable
83/// (Node's `Channel` constructor also throws) — they exist so the names resolve.
84pub fn constant(name: &str) -> Option<Value> {
85    match name {
86        "Channel" | "TracingChannel" | "BoundedChannel" => {
87            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
88        }
89        _ => None,
90    }
91}
92
93/// Build a `TracingChannel` object grouping the five sub-channels (each a real,
94/// interned `Channel` named `tracing:<name>:<sub>`).
95fn tracing_channel(name: &str) -> Value {
96    let subs: Vec<(String, Value)> = TRACING_SUBS
97        .iter()
98        .map(|s| {
99            (
100                (*s).to_string(),
101                get_or_create(&format!("tracing:{name}:{s}")),
102            )
103        })
104        .collect();
105    with_host(|h| {
106        let mut m = IndexMap::new();
107        m.insert("@@native".into(), h.new_str("TracingChannel"));
108        m.insert("@@name".into(), h.new_str(name));
109        for (k, v) in subs {
110            m.insert(k, v);
111        }
112        h.new_object(m)
113    })
114}
115
116/// Instance dispatch for a `TracingChannel` object (`@@native = "TracingChannel"`).
117pub fn tracing_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
118    match method {
119        // `subscribe(handlers)` / `unsubscribe(handlers)`: `handlers` is an object
120        // keyed by sub-channel name (start/end/…); wire each present callback to
121        // the matching sub-channel.
122        "subscribe" | "unsubscribe" => {
123            let handlers = args.first().cloned().unwrap_or(Value::Undef);
124            for sub in TRACING_SUBS {
125                let cb = with_host(|h| match h.get(&handlers) {
126                    Some(JsObj::Object(p)) => p.get(*sub).cloned(),
127                    _ => None,
128                });
129                let (Some(cb), Some(ch)) = (cb, sub_channel(recv, sub)) else {
130                    continue;
131                };
132                if method == "subscribe" {
133                    add_sub(&ch, cb);
134                } else {
135                    remove_sub(&ch, &cb);
136                }
137            }
138            Ok(Value::Undef)
139        }
140        // `traceSync(fn[, ctx[, thisArg[, ...args]]])`: publish `ctx` to `start`,
141        // run `fn`, publish to `end` on success or `error` (then `end`) on throw,
142        // returning `fn`'s result. Synchronous — matches the sync trace contract.
143        "traceSync" => {
144            let fn_v = args.first().cloned().unwrap_or(Value::Undef);
145            let ctx = args.get(1).cloned().unwrap_or(Value::Undef);
146            let this = args.get(2).cloned();
147            let call_args: Vec<Value> = args.iter().skip(3).cloned().collect();
148            if let Some(start) = sub_channel(recv, "start") {
149                publish(&start, ctx.clone())?;
150            }
151            match crate::host::invoke(&fn_v, call_args, this) {
152                Ok(v) => {
153                    if let Some(end) = sub_channel(recv, "end") {
154                        publish(&end, ctx)?;
155                    }
156                    Ok(v)
157                }
158                Err(e) => {
159                    if let Some(err_ch) = sub_channel(recv, "error") {
160                        let _ = publish(&err_ch, ctx.clone());
161                    }
162                    if let Some(end) = sub_channel(recv, "end") {
163                        let _ = publish(&end, ctx);
164                    }
165                    Err(e)
166                }
167            }
168        }
169        _ => Err(crate::host::type_error(&format!(
170            "{method} is not a function"
171        ))),
172    }
173}
174
175/// The `TracingChannel`'s `sub` sub-channel object, if present.
176fn sub_channel(recv: &Value, sub: &str) -> Option<Value> {
177    with_host(|h| match h.get(recv) {
178        Some(JsObj::Object(p)) => p.get(sub).cloned(),
179        _ => None,
180    })
181}
182
183/// Instance dispatch for a Channel object (`@@native = "Channel"`).
184pub fn instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
185    match method {
186        "subscribe" => {
187            add_sub(recv, args.first().cloned().unwrap_or(Value::Undef));
188            Ok(Value::Undef)
189        }
190        "unsubscribe" => Ok(Value::Bool(remove_sub(
191            recv,
192            &args.first().cloned().unwrap_or(Value::Undef),
193        ))),
194        "publish" => publish(recv, args.first().cloned().unwrap_or(Value::Undef)),
195        _ => Err(crate::host::type_error(&format!(
196            "{method} is not a function"
197        ))),
198    }
199}
200
201/// The interned channel for `name`, creating it (empty) on first use.
202fn get_or_create(name: &str) -> Value {
203    if let Some(ch) = CHANNELS.with(|c| c.borrow().get(name).cloned()) {
204        return ch;
205    }
206    let ch = with_host(|h| {
207        let subs = h.new_array(Vec::new());
208        let mut m = IndexMap::new();
209        m.insert("@@native".into(), h.new_str("Channel"));
210        m.insert("@@name".into(), h.new_str(name));
211        m.insert("@@subs".into(), subs);
212        m.insert("name".into(), h.new_str(name));
213        m.insert("hasSubscribers".into(), Value::Bool(false));
214        h.new_object(m)
215    });
216    CHANNELS.with(|c| c.borrow_mut().insert(name.to_string(), ch.clone()));
217    ch
218}
219
220/// The channel's `@@subs` array handle, if any.
221fn subs_array(ch: &Value) -> Option<Value> {
222    with_host(|h| match h.get(ch) {
223        Some(JsObj::Object(p)) => p.get("@@subs").cloned(),
224        _ => None,
225    })
226}
227
228/// Current subscriber count.
229fn sub_count(ch: &Value) -> usize {
230    match subs_array(ch) {
231        Some(arr) => with_host(|h| match h.get(&arr) {
232            Some(JsObj::Array(items)) => items.len(),
233            _ => 0,
234        }),
235        None => 0,
236    }
237}
238
239/// Append a subscriber callback and refresh `hasSubscribers`.
240fn add_sub(ch: &Value, cb: Value) {
241    if let Some(arr) = subs_array(ch) {
242        with_host(|h| {
243            if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
244                items.push(cb);
245            }
246        });
247        refresh_has(ch);
248    }
249}
250
251/// Remove the first subscriber with the same heap identity as `cb`; returns
252/// whether one was removed.
253fn remove_sub(ch: &Value, cb: &Value) -> bool {
254    let removed = match subs_array(ch) {
255        Some(arr) => with_host(|h| {
256            if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
257                if let Some(i) = items.iter().position(|x| same_ref(x, cb)) {
258                    items.remove(i);
259                    return true;
260                }
261            }
262            false
263        }),
264        None => false,
265    };
266    if removed {
267        refresh_has(ch);
268    }
269    removed
270}
271
272/// Synchronously invoke every subscriber with `(message, channelName)`.
273fn publish(ch: &Value, msg: Value) -> Result<Value, String> {
274    // Snapshot the subscribers + the name in one borrow, then invoke outside it —
275    // a subscriber may re-enter the host (subscribe/publish/allocate).
276    let subs: Vec<Value> = with_host(|h| match h.get(ch) {
277        Some(JsObj::Object(p)) => match p.get("@@subs").and_then(|a| h.get(a)) {
278            Some(JsObj::Array(items)) => items.clone(),
279            _ => Vec::new(),
280        },
281        _ => Vec::new(),
282    });
283    let name_val = with_host(|h| match h.get(ch) {
284        Some(JsObj::Object(p)) => p.get("@@name").cloned().unwrap_or(Value::Undef),
285        _ => Value::Undef,
286    });
287    for cb in subs {
288        crate::host::invoke(&cb, vec![msg.clone(), name_val.clone()], None)?;
289    }
290    Ok(Value::Undef)
291}
292
293/// Update the `hasSubscribers` data property to reflect the current count.
294fn refresh_has(ch: &Value) {
295    let has = sub_count(ch) > 0;
296    with_host(|h| {
297        if let Some(JsObj::Object(p)) = h.get_mut(ch) {
298            p.insert("hasSubscribers".into(), Value::Bool(has));
299        }
300    });
301}
302
303/// Heap-identity comparison for two reference values (subscriber callbacks are
304/// always heap objects — functions or bound methods).
305fn same_ref(a: &Value, b: &Value) -> bool {
306    matches!((a, b), (Value::Obj(x), Value::Obj(y)) if x == y)
307}