Skip to main content

nodejs/stdlib/
worker_threads.rs

1//! Node `worker_threads`: real OS-thread workers with fully isolated heaps.
2//!
3//! # Model (matches Node: workers do NOT share the JS heap)
4//!
5//! node-js's entire runtime — the `JsHost` heap, the module cache, the event
6//! loop channel — lives in `thread_local!`s (`host::HOST`, `module`'s statics).
7//! So spawning a fresh OS thread automatically gives that thread its OWN
8//! isolated interpreter and heap. A `Worker` here is therefore a real
9//! `std::thread` that calls `crate::eval_file`/`eval_str` on the worker file,
10//! running it against that thread's own clean `thread_local` host. Nothing on
11//! the JS heap is shared between the main thread and a worker, or between two
12//! workers — exactly Node's isolation guarantee.
13//!
14//! # Why messages cross as JSON strings, never `Value`s
15//!
16//! `fusevm::Value` is a per-thread heap handle (`Value::Obj(u32)` indexes the
17//! calling thread's `JsHost.heap`); it is neither `Send` nor meaningful on
18//! another thread. So a message can NEVER be a `Value`. Every message crosses
19//! the thread boundary as a plain `String` of JSON:
20//!
21//! ```text
22//!   sender thread:   value ─JSON.stringify([value])→ String  (on sender's heap)
23//!   channel:         String  (Send)
24//!   receiver thread: String ─JSON.parse(s)[0]→ value        (on receiver's heap)
25//! ```
26//!
27//! The value is wrapped in a one-element array before `JSON.stringify` so that
28//! top-level primitives AND `undefined` round-trip through a single always-valid
29//! JSON document (`JSON.stringify(undefined)` is itself `undefined`, not a
30//! string — the array wrapper avoids that). Deserialization unwraps `[0]` on the
31//! receiving thread's own heap.
32//!
33//! ## Serialization is a JSON subset of structured clone (documented limitation)
34//!
35//! Only JSON-serializable data transfers: objects, arrays, strings, numbers,
36//! booleans, null. `undefined` becomes `null` (JSON semantics), and functions,
37//! symbols, `Map`/`Set`, cycles, `BigInt`, and `ArrayBuffer` transfers are NOT
38//! supported (a `BigInt` makes `JSON.stringify` throw, surfaced as a thrown
39//! error from `postMessage`, matching Node's `DataCloneError` in spirit). This
40//! is an honest subset, never a silent fake.
41//!
42//! # Bidirectional message flow (both directions are real)
43//!
44//! * worker → main (`parentPort.postMessage`): the worker serializes on its own
45//!   heap and posts an `IoTask` onto the MAIN loop's `io_sender` (captured at
46//!   construction). The task runs on the main thread, deserializes into a fresh
47//!   `Value` on the main heap, and emits `'message'` on the `Worker` object.
48//! * main → worker (`worker.postMessage`): the main thread serializes and sends
49//!   the JSON string over an `mpsc` channel to the worker. A per-worker "bridge"
50//!   thread (started when the worker adds a `parentPort` `'message'` listener)
51//!   forwards each string as an `IoTask` onto the WORKER loop's `io_sender`; the
52//!   task runs on the worker thread, deserializes on the worker heap, and emits
53//!   `'message'` on `parentPort`. The bridge is required because the worker's
54//!   event loop (`host::run_event_loop`) blocks only on its own I/O channel and
55//!   this module cannot modify it — same pattern `net` uses for socket reads.
56//!
57//! # Liveness
58//!
59//! `new Worker` `incr_handle`s the MAIN loop so the process stays alive while the
60//! worker runs; the worker's `'exit'` `decr_handle`s it. On the worker side,
61//! registering a `parentPort` `'message'` listener `incr_handle`s the WORKER loop
62//! (keeping the worker alive to receive messages), and `terminate` releases it.
63//!
64//! # terminate is cooperative (documented limitation)
65//!
66//! Rust has no safe thread cancellation, so `terminate` signals the worker (via
67//! the bridge) to `decr_handle` and let its event loop unwind; a worker parked in
68//! its message loop exits promptly. A worker spinning in a tight *synchronous* JS
69//! loop is not force-killed — there is no safe preemption point. `terminate`
70//! returns `undefined` (awaiting it resolves to `undefined`).
71
72use crate::host::{with_host, IoTask, JsObj};
73use fusevm::Value;
74use indexmap::IndexMap;
75use std::cell::RefCell;
76use std::collections::{HashMap, HashSet, VecDeque};
77use std::sync::atomic::{AtomicU64, Ordering};
78use std::sync::mpsc::{Receiver, Sender};
79use std::sync::{Mutex, OnceLock};
80
81/// Module-level `worker_threads` functions. NOTE: these route only if the parent
82/// adds a `"worker_threads"` arm to `stdlib::is_method` and `stdlib::call` (the
83/// module previously had no callable methods) — see the report.
84pub const METHODS: &[&str] = &[
85    "getEnvironmentData",
86    "setEnvironmentData",
87    "receiveMessageOnPort",
88    "markAsUntransferable",
89    "isMarkedAsUntransferable",
90    "markAsUncloneable",
91    "moveMessagePortToContext",
92];
93
94/// Instance methods on a `BroadcastChannel` object.
95pub const BROADCAST_CHANNEL_METHODS: &[&str] = &[
96    "postMessage",
97    "close",
98    "ref",
99    "unref",
100    "addEventListener",
101    "removeEventListener",
102];
103
104/// Instance methods on a `Worker` (main-side handle), beyond the shared
105/// EventEmitter surface (`on`/`once`/`emit`/…).
106pub const WORKER_METHODS: &[&str] = &["postMessage", "terminate", "ref", "unref"];
107
108/// Instance methods on a `MessagePort` (the worker-side `parentPort`), beyond the
109/// shared EventEmitter surface.
110pub const PORT_METHODS: &[&str] = &["postMessage", "close", "start", "ref", "unref"];
111
112/// Global, cross-thread thread-id source. The main thread is id 0; each `Worker`
113/// (including nested workers spawned from a worker) gets a fresh positive id.
114static NEXT_THREAD_ID: AtomicU64 = AtomicU64::new(1);
115
116/// A message crossing the main→worker `mpsc` channel. Both variants are `Send`
117/// (a JSON `String` / unit) — never a `Value`.
118enum WorkerMsg {
119    Data(String),
120    Terminate,
121}
122
123/// An event to raise on a `Worker` object, produced by the worker thread and run
124/// as an `IoTask` on the MAIN thread. All fields are `Send` plain data.
125enum MainEvent {
126    Online,
127    Message(String),
128    Error(String),
129    Exit(i32),
130}
131
132/// Main-thread registry entry for a live worker (keyed by thread id).
133struct WorkerRec {
134    /// The `Worker` EventEmitter object (lives on the main heap).
135    emitter: Value,
136    /// Sends main→worker messages / the terminate signal.
137    to_worker: Sender<WorkerMsg>,
138}
139
140thread_local! {
141    /// Live workers owned by THIS thread (the main thread, or a worker that
142    /// itself spawned sub-workers). Only ever touched on the owning thread.
143    static WORKERS: RefCell<HashMap<u64, WorkerRec>> = RefCell::new(HashMap::new());
144}
145
146/// Per-worker-thread context, set once when the worker thread starts, read while
147/// its file runs. Absent (`None`) on the main thread — that is how `isMainThread`
148/// is computed.
149struct WorkerCtx {
150    thread_id: u64,
151    /// `workerData` serialized as JSON on the spawning (main) thread; deserialized
152    /// lazily on this thread when `workerData` is read.
153    worker_data_json: String,
154    /// The MAIN loop's `io_sender`, to post worker→main events.
155    main_tx: Sender<IoTask>,
156    /// This worker's id (registry key on the main side).
157    self_id: u64,
158    /// Receives main→worker messages; taken out when the bridge thread starts.
159    rx: Option<Receiver<WorkerMsg>>,
160    /// Whether the forwarding bridge thread has been started.
161    bridge_started: bool,
162}
163
164thread_local! {
165    static WORKER_CTX: RefCell<Option<WorkerCtx>> = const { RefCell::new(None) };
166    /// The worker-side `parentPort` object, created lazily on first access and
167    /// cached (so both the JS file and the delivery `IoTask` share one object).
168    static PARENT_PORT: RefCell<Option<Value>> = const { RefCell::new(None) };
169
170    // ── MessageChannel state (both ports live on ONE thread) ──────────────────
171    /// port id → the `MessagePort` object.
172    static CHANNEL_PORTS: RefCell<HashMap<u64, Value>> = RefCell::new(HashMap::new());
173    /// port id → its peer's port id (posting to one enqueues on the other).
174    static CH_PEER: RefCell<HashMap<u64, u64>> = RefCell::new(HashMap::new());
175    /// port id → JSON messages queued FOR that port (drained by
176    /// `receiveMessageOnPort` or async `'message'` delivery once started).
177    static CH_QUEUE: RefCell<HashMap<u64, VecDeque<String>>> = RefCell::new(HashMap::new());
178    /// port ids whose async `'message'` delivery is active (a listener was added
179    /// or `start()` was called).
180    static CH_STARTED: RefCell<HashSet<u64>> = RefCell::new(HashSet::new());
181
182    // ── BroadcastChannel state (in-process, SAME-thread only) ─────────────────
183    /// channel name → the live `BroadcastChannel` objects on this thread.
184    static BCAST: RefCell<HashMap<String, Vec<(u64, Value)>>> = RefCell::new(HashMap::new());
185
186    // ── markAsUntransferable / markAsUncloneable flags (heap ids) ─────────────
187    static UNTRANSFERABLE: RefCell<HashSet<u32>> = RefCell::new(HashSet::new());
188    static UNCLONEABLE: RefCell<HashSet<u32>> = RefCell::new(HashSet::new());
189}
190
191/// Process-global `environmentData` map (shared across threads, so a value set on
192/// the main thread is visible via `getEnvironmentData` on a worker). Values cross
193/// as JSON strings because `fusevm::Value` is not `Send`.
194fn env_data() -> &'static Mutex<HashMap<String, String>> {
195    static ENV_DATA: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
196    ENV_DATA.get_or_init(|| Mutex::new(HashMap::new()))
197}
198
199/// A fresh unique port / broadcast-channel id (shares the thread-id counter's
200/// space is unnecessary — its own counter keeps ids distinct within a thread).
201static NEXT_PORT_ID: AtomicU64 = AtomicU64::new(1);
202
203// ── serialization (JSON subset of structured clone) ──────────────────────────
204
205/// Serialize a value to a JSON string on the CURRENT thread's heap. Wrapped in a
206/// one-element array so primitives and `undefined` round-trip through one always-
207/// valid JSON document. Errors (e.g. a `BigInt`) propagate as a thrown error.
208fn serialize(v: &Value) -> Result<String, String> {
209    let arr = with_host(|h| h.new_array(vec![v.clone()]));
210    let json = crate::builtins::call_builtin_function("JSON.stringify", vec![arr])?;
211    Ok(with_host(|h| h.str_of(&json)))
212}
213
214/// Deserialize a JSON string produced by `serialize` into a fresh value on the
215/// CURRENT thread's heap (unwrapping the array wrapper).
216fn deserialize(json: &str) -> Result<Value, String> {
217    let sv = with_host(|h| h.new_str(json.to_string()));
218    let arr = crate::builtins::call_builtin_function("JSON.parse", vec![sv])?;
219    Ok(with_host(|h| match h.get(&arr) {
220        Some(JsObj::Array(items)) => items.first().cloned().unwrap_or(Value::Undef),
221        _ => Value::Undef,
222    }))
223}
224
225// ── small heap helpers ───────────────────────────────────────────────────────
226
227fn arg0(args: &[Value]) -> Value {
228    args.first().cloned().unwrap_or(Value::Undef)
229}
230
231fn get_prop(recv: &Value, key: &str) -> Option<Value> {
232    with_host(|h| match h.get(recv) {
233        Some(JsObj::Object(p)) => p.get(key).cloned(),
234        _ => None,
235    })
236}
237
238fn u64_prop(recv: &Value, key: &str) -> Option<u64> {
239    get_prop(recv, key).map(|v| with_host(|h| h.to_number(&v)) as u64)
240}
241
242/// Emit `name` (with `args`) on an emitter object, releasing the host borrow
243/// before dispatch (listeners re-enter the host).
244fn emit_event(emitter: &Value, name: &str, mut args: Vec<Value>) -> Result<(), String> {
245    let mut a = vec![with_host(|h| h.new_str(name))];
246    a.append(&mut args);
247    super::events::instance_call(emitter, "emit", a).map(|_| ())
248}
249
250// ── thread-context queries (drive the module constants) ──────────────────────
251
252fn is_worker_thread() -> bool {
253    WORKER_CTX.with(|c| c.borrow().is_some())
254}
255
256fn current_thread_id() -> u64 {
257    WORKER_CTX.with(|c| c.borrow().as_ref().map(|x| x.thread_id).unwrap_or(0))
258}
259
260/// `workerData` for this thread: the deserialized options payload on a worker,
261/// `undefined` on the main thread.
262fn current_worker_data() -> Value {
263    let json = WORKER_CTX.with(|c| c.borrow().as_ref().map(|x| x.worker_data_json.clone()));
264    match json {
265        Some(j) => deserialize(&j).unwrap_or(Value::Undef),
266        None => Value::Undef,
267    }
268}
269
270/// The worker-side `parentPort` (a `MessagePort` emitter), created once per worker
271/// thread and cached. Only meaningful on a worker thread.
272fn ensure_parent_port() -> Value {
273    if let Some(p) = PARENT_PORT.with(|p| p.borrow().clone()) {
274        return p;
275    }
276    let port = super::net::new_emitter_object("MessagePort", IndexMap::new());
277    PARENT_PORT.with(|p| *p.borrow_mut() = Some(port.clone()));
278    port
279}
280
281// ── module constants (reached via `stdlib::constant("worker_threads", name)`) ─
282
283/// Non-function members of the `worker_threads` namespace:
284/// `isMainThread`/`threadId`/`parentPort`/`workerData`, and the `Worker`
285/// constructor (as a `Builtin("Worker")` so `new Worker(...)` reaches
286/// `construct_worker`).
287pub fn constant(name: &str) -> Option<Value> {
288    match name {
289        "isMainThread" => Some(Value::Bool(!is_worker_thread())),
290        "threadId" => Some(Value::Float(current_thread_id() as f64)),
291        "parentPort" => Some(if is_worker_thread() {
292            ensure_parent_port()
293        } else {
294            // On the main thread `parentPort` is `null` (Node parity).
295            with_host(|h| h.null())
296        }),
297        "workerData" => Some(current_worker_data()),
298        "Worker" | "MessageChannel" | "BroadcastChannel" => {
299            Some(with_host(|h| h.alloc(JsObj::Builtin(name.into()))))
300        }
301        _ => None,
302    }
303}
304
305/// Module-level dispatch. Routes only if the parent adds a `"worker_threads"` arm
306/// to `stdlib::call` (see the report).
307pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
308    Some(match method {
309        "setEnvironmentData" => {
310            let key = super::arg_str(args, 0);
311            match args.get(1) {
312                // An `undefined` value deletes the key (Node behavior).
313                Some(v) if !matches!(v, Value::Undef) => match serialize(v) {
314                    Ok(json) => {
315                        if let Ok(mut m) = env_data().lock() {
316                            m.insert(key, json);
317                        }
318                        Ok(Value::Undef)
319                    }
320                    Err(e) => Err(e),
321                },
322                _ => {
323                    if let Ok(mut m) = env_data().lock() {
324                        m.remove(&key);
325                    }
326                    Ok(Value::Undef)
327                }
328            }
329        }
330        "getEnvironmentData" => {
331            let key = super::arg_str(args, 0);
332            let json = env_data().lock().ok().and_then(|m| m.get(&key).cloned());
333            match json {
334                Some(j) => deserialize(&j),
335                None => Ok(Value::Undef),
336            }
337        }
338        "receiveMessageOnPort" => Ok(receive_message_on_port(args.first())),
339        // Best-effort transfer/clone flags: node-js clones every message as a JSON
340        // subset (see module docs), so these flags do not alter serialization —
341        // they are recorded and reflected by the `is*` query for API fidelity.
342        "markAsUntransferable" => {
343            if let Some(Value::Obj(id)) = args.first() {
344                UNTRANSFERABLE.with(|s| s.borrow_mut().insert(*id));
345            }
346            Ok(Value::Undef)
347        }
348        "isMarkedAsUntransferable" => Ok(Value::Bool(matches!(
349            args.first(),
350            Some(Value::Obj(id)) if UNTRANSFERABLE.with(|s| s.borrow().contains(id))
351        ))),
352        "markAsUncloneable" => {
353            if let Some(Value::Obj(id)) = args.first() {
354                UNCLONEABLE.with(|s| s.borrow_mut().insert(*id));
355            }
356            Ok(Value::Undef)
357        }
358        // node-js has one context, so there is nothing to move the port into;
359        // return the port unchanged.
360        "moveMessagePortToContext" => Ok(arg0(args)),
361        _ => return None,
362    })
363}
364
365// ── construction: `new Worker(filename[, options])` ──────────────────────────
366
367/// Build a `Worker` and spawn its OS thread (runs on the MAIN thread). The worker
368/// thread runs `filename` (a file path, or the code itself when `options.eval` is
369/// truthy) against its own fresh `thread_local` host.
370pub fn construct_worker(args: &[Value]) -> Result<Value, String> {
371    let filename = with_host(|h| h.str_of(&arg0(args)));
372    let opts = args.get(1).cloned();
373    let is_eval = opts
374        .as_ref()
375        .and_then(|o| get_prop(o, "eval"))
376        .map(|v| with_host(|h| h.truthy(&v)))
377        .unwrap_or(false);
378    // Serialize workerData NOW, on the main heap, into a Send JSON string.
379    let worker_data_json = match opts.as_ref().and_then(|o| get_prop(o, "workerData")) {
380        Some(v) => serialize(&v)?,
381        None => serialize(&Value::Undef)?, // "[null]"
382    };
383
384    let id = NEXT_THREAD_ID.fetch_add(1, Ordering::SeqCst);
385    let (to_worker_tx, to_worker_rx) = std::sync::mpsc::channel::<WorkerMsg>();
386    let main_tx = with_host(|h| h.io_sender());
387
388    let mut extra = IndexMap::new();
389    extra.insert("@@wtid".into(), Value::Float(id as f64));
390    extra.insert("threadId".into(), Value::Float(id as f64));
391    let emitter = super::net::new_emitter_object("Worker", extra);
392
393    WORKERS.with(|w| {
394        w.borrow_mut().insert(
395            id,
396            WorkerRec {
397                emitter: emitter.clone(),
398                to_worker: to_worker_tx,
399            },
400        );
401    });
402    // Keep the MAIN loop alive while the worker runs.
403    with_host(|h| h.incr_handle());
404
405    let spawn_tx = main_tx.clone();
406    std::thread::spawn(move || {
407        worker_thread_main(
408            id,
409            filename,
410            is_eval,
411            worker_data_json,
412            spawn_tx,
413            to_worker_rx,
414        );
415    });
416
417    Ok(emitter)
418}
419
420/// The worker thread's entry point. Runs on a brand-new OS thread whose
421/// `thread_local` host/module state is clean and isolated.
422fn worker_thread_main(
423    id: u64,
424    filename: String,
425    is_eval: bool,
426    worker_data_json: String,
427    main_tx: Sender<IoTask>,
428    rx: Receiver<WorkerMsg>,
429) {
430    WORKER_CTX.with(|c| {
431        *c.borrow_mut() = Some(WorkerCtx {
432            thread_id: id,
433            worker_data_json,
434            main_tx: main_tx.clone(),
435            self_id: id,
436            rx: Some(rx),
437            bridge_started: false,
438        });
439    });
440
441    // `'online'` fires once the worker thread has begun executing.
442    post_to_main(&main_tx, id, MainEvent::Online);
443
444    // Run the worker's code on this thread's own isolated host. `eval_file` /
445    // `eval_str` call `reset_host()` (fresh heap) and drain the worker's event
446    // loop — which, if the worker added a `parentPort` 'message' listener, stays
447    // alive processing bridged messages until `terminate`.
448    let outcome = if is_eval {
449        crate::eval_str(&filename)
450    } else {
451        crate::eval_file(&filename)
452    };
453
454    match outcome {
455        Ok(_) => post_to_main(&main_tx, id, MainEvent::Exit(0)),
456        Err(e) => {
457            post_to_main(&main_tx, id, MainEvent::Error(e));
458            post_to_main(&main_tx, id, MainEvent::Exit(1));
459        }
460    }
461}
462
463/// Post a worker→main event as an `IoTask` onto the main loop. The closure is
464/// `Send` (captures only `id` + plain data) and runs `dispatch_main` on the main
465/// thread, where the `Worker` object and main heap live.
466fn post_to_main(main_tx: &Sender<IoTask>, id: u64, ev: MainEvent) {
467    let _ = main_tx.send(Box::new(move || dispatch_main(id, ev)));
468}
469
470/// Run a worker→main event on the MAIN thread.
471fn dispatch_main(id: u64, ev: MainEvent) -> Result<(), String> {
472    let emitter = WORKERS.with(|w| w.borrow().get(&id).map(|r| r.emitter.clone()));
473    let Some(emitter) = emitter else {
474        return Ok(());
475    };
476    match ev {
477        MainEvent::Online => emit_event(&emitter, "online", vec![]),
478        MainEvent::Message(json) => {
479            let v = deserialize(&json)?;
480            emit_event(&emitter, "message", vec![v])
481        }
482        MainEvent::Error(msg) => {
483            let err =
484                crate::builtins::construct_builtin("Error", vec![with_host(|h| h.new_str(msg))])?;
485            emit_event(&emitter, "error", vec![err])
486        }
487        MainEvent::Exit(code) => {
488            emit_event(&emitter, "exit", vec![Value::Float(code as f64)])?;
489            WORKERS.with(|w| {
490                w.borrow_mut().remove(&id);
491            });
492            with_host(|h| h.decr_handle());
493            // Wake the loop so a now-idle process can exit.
494            let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
495            Ok(())
496        }
497    }
498}
499
500// ── worker-side bridge: main→worker message delivery ─────────────────────────
501
502/// Start the forwarding thread that drains the main→worker `mpsc` channel and
503/// posts each message as an `IoTask` onto THIS worker's event loop. Idempotent;
504/// called when the worker adds a `parentPort` 'message' listener (or `start()`s
505/// the port). Also `incr_handle`s the worker loop so it stays alive to receive.
506fn start_parent_bridge() {
507    // Take the receiver + capture the worker loop's sender while we hold the ctx.
508    let worker_io: Option<Sender<IoTask>> = WORKER_CTX.with(|c| {
509        let mut cb = c.borrow_mut();
510        let ctx = cb.as_mut()?;
511        if ctx.bridge_started {
512            return None;
513        }
514        let rx = ctx.rx.take()?;
515        ctx.bridge_started = true;
516        let io = with_host(|h| h.io_sender());
517        with_host(|h| h.incr_handle());
518        // Spawn the forwarder; it owns `rx` and a clone of the worker io sender.
519        let io_for_thread = io.clone();
520        std::thread::spawn(move || {
521            while let Ok(msg) = rx.recv() {
522                match msg {
523                    WorkerMsg::Data(json) => {
524                        let _ = io_for_thread.send(Box::new(move || parent_deliver(json)));
525                    }
526                    WorkerMsg::Terminate => {
527                        // Release the worker loop so it can unwind and exit.
528                        let _ = io_for_thread.send(Box::new(|| {
529                            with_host(|h| h.decr_handle());
530                            Ok(())
531                        }));
532                        break;
533                    }
534                }
535            }
536        });
537        Some(io)
538    });
539    let _ = worker_io;
540}
541
542/// Deliver a main→worker message on the WORKER thread (runs inside the worker
543/// event loop): deserialize on the worker heap and emit `'message'` on
544/// `parentPort`.
545fn parent_deliver(json: String) -> Result<(), String> {
546    let port = ensure_parent_port();
547    let v = deserialize(&json)?;
548    emit_event(&port, "message", vec![v])
549}
550
551// ── instance dispatch (from `stdlib::instance_call`) ─────────────────────────
552
553/// The shared EventEmitter method names delegated to `events`.
554const EMITTER_METHODS: &[&str] = &[
555    "on",
556    "addListener",
557    "prependListener",
558    "once",
559    "prependOnceListener",
560    "emit",
561    "removeListener",
562    "off",
563    "removeAllListeners",
564    "listenerCount",
565    "listeners",
566    "eventNames",
567    "setMaxListeners",
568    "getMaxListeners",
569];
570
571pub fn instance_call(
572    tag: &str,
573    recv: &Value,
574    method: &str,
575    args: Vec<Value>,
576) -> Result<Value, String> {
577    match tag {
578        "Worker" => worker_call(recv, method, args),
579        "MessagePort" => port_call(recv, method, args),
580        "BroadcastChannel" => broadcast_call(recv, method, args),
581        _ => Err(crate::host::type_error(&format!(
582            "{method} is not a function"
583        ))),
584    }
585}
586
587/// Methods on the main-side `Worker` handle.
588fn worker_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
589    if EMITTER_METHODS.contains(&method) {
590        return super::events::instance_call(recv, method, args);
591    }
592    match method {
593        "postMessage" => {
594            let json = serialize(&arg0(&args))?;
595            if let Some(id) = u64_prop(recv, "@@wtid") {
596                WORKERS.with(|w| {
597                    if let Some(r) = w.borrow().get(&id) {
598                        let _ = r.to_worker.send(WorkerMsg::Data(json));
599                    }
600                });
601            }
602            Ok(Value::Undef)
603        }
604        "terminate" => {
605            // Signal the worker; its normal completion emits `'exit'`. Cooperative
606            // (see module docs) — returns `undefined`.
607            if let Some(id) = u64_prop(recv, "@@wtid") {
608                WORKERS.with(|w| {
609                    if let Some(r) = w.borrow().get(&id) {
610                        let _ = r.to_worker.send(WorkerMsg::Terminate);
611                    }
612                });
613            }
614            Ok(Value::Undef)
615        }
616        "ref" | "unref" => Ok(recv.clone()),
617        _ => Err(crate::host::type_error(&format!(
618            "worker.{method} is not a function"
619        ))),
620    }
621}
622
623/// Methods on the worker-side `parentPort` (`MessagePort`).
624fn port_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
625    // A `MessageChannel` port carries a `@@portid` and is handled separately from
626    // the worker `parentPort` (which posts to the spawning thread).
627    if let Some(pid) = u64_prop(recv, "@@portid") {
628        return channel_port_call(recv, pid, method, args);
629    }
630    if EMITTER_METHODS.contains(&method) {
631        let r = super::events::instance_call(recv, method, args.clone());
632        // Adding a 'message' listener implicitly starts message delivery.
633        if matches!(
634            method,
635            "on" | "addListener" | "prependListener" | "once" | "prependOnceListener"
636        ) {
637            let ev = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
638            if ev == "message" {
639                start_parent_bridge();
640            }
641        }
642        return r;
643    }
644    match method {
645        "postMessage" => {
646            let json = serialize(&arg0(&args))?;
647            WORKER_CTX.with(|c| {
648                if let Some(ctx) = c.borrow().as_ref() {
649                    post_to_main(&ctx.main_tx, ctx.self_id, MainEvent::Message(json));
650                }
651            });
652            Ok(Value::Undef)
653        }
654        "start" => {
655            start_parent_bridge();
656            Ok(Value::Undef)
657        }
658        "close" | "ref" | "unref" => Ok(recv.clone()),
659        _ => Err(crate::host::type_error(&format!(
660            "port.{method} is not a function"
661        ))),
662    }
663}
664
665// ── MessageChannel (a pair of linked in-process ports) ───────────────────────
666
667/// `new MessageChannel()` → `{ port1, port2 }`, two `MessagePort` objects linked
668/// so that `port1.postMessage(v)` is delivered to `port2` (and vice versa). Both
669/// ports live on the calling thread and share its heap; messages are cloned
670/// through the JSON subset (`serialize`/`deserialize`) so a posted object is a
671/// copy, not a shared reference — matching structured clone's copy semantics.
672/// Requires the parent to wire `MessageChannel` construction (see the report).
673pub fn construct_message_channel(_args: &[Value]) -> Result<Value, String> {
674    let id1 = NEXT_PORT_ID.fetch_add(1, Ordering::SeqCst);
675    let id2 = NEXT_PORT_ID.fetch_add(1, Ordering::SeqCst);
676    let mut e1 = IndexMap::new();
677    e1.insert("@@portid".into(), Value::Float(id1 as f64));
678    let port1 = super::net::new_emitter_object("MessagePort", e1);
679    let mut e2 = IndexMap::new();
680    e2.insert("@@portid".into(), Value::Float(id2 as f64));
681    let port2 = super::net::new_emitter_object("MessagePort", e2);
682
683    CHANNEL_PORTS.with(|m| {
684        let mut m = m.borrow_mut();
685        m.insert(id1, port1.clone());
686        m.insert(id2, port2.clone());
687    });
688    CH_PEER.with(|m| {
689        let mut m = m.borrow_mut();
690        m.insert(id1, id2);
691        m.insert(id2, id1);
692    });
693
694    Ok(with_host(|h| {
695        let mut m = IndexMap::new();
696        m.insert("port1".into(), port1);
697        m.insert("port2".into(), port2);
698        h.new_object(m)
699    }))
700}
701
702/// Method dispatch for a `MessageChannel` port (identified by its `@@portid`).
703fn channel_port_call(
704    recv: &Value,
705    pid: u64,
706    method: &str,
707    args: Vec<Value>,
708) -> Result<Value, String> {
709    if EMITTER_METHODS.contains(&method) {
710        let r = super::events::instance_call(recv, method, args.clone());
711        // Adding a 'message' listener starts async delivery for this port.
712        if matches!(
713            method,
714            "on" | "addListener" | "prependListener" | "once" | "prependOnceListener"
715        ) {
716            let ev = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
717            if ev == "message" {
718                start_channel_port(pid);
719            }
720        }
721        return r;
722    }
723    match method {
724        "postMessage" => {
725            let json = serialize(&arg0(&args))?;
726            channel_post(pid, json);
727            Ok(Value::Undef)
728        }
729        "start" => {
730            start_channel_port(pid);
731            Ok(Value::Undef)
732        }
733        "close" => {
734            CH_STARTED.with(|s| {
735                s.borrow_mut().remove(&pid);
736            });
737            Ok(Value::Undef)
738        }
739        "ref" | "unref" => Ok(recv.clone()),
740        _ => Err(crate::host::type_error(&format!(
741            "port.{method} is not a function"
742        ))),
743    }
744}
745
746/// Enqueue a JSON message on `from`'s peer, and schedule async delivery if the
747/// peer has started listening.
748fn channel_post(from: u64, json: String) {
749    let Some(peer) = CH_PEER.with(|m| m.borrow().get(&from).copied()) else {
750        return;
751    };
752    CH_QUEUE.with(|q| q.borrow_mut().entry(peer).or_default().push_back(json));
753    if CH_STARTED.with(|s| s.borrow().contains(&peer)) {
754        schedule_channel_delivery(peer);
755    }
756}
757
758/// Mark a port started and flush anything already queued to it.
759fn start_channel_port(pid: u64) {
760    let newly = CH_STARTED.with(|s| s.borrow_mut().insert(pid));
761    if !newly {
762        return;
763    }
764    let pending = CH_QUEUE.with(|q| q.borrow().get(&pid).map_or(0, |d| d.len()));
765    for _ in 0..pending {
766        schedule_channel_delivery(pid);
767    }
768}
769
770/// Post one delivery `IoTask` onto THIS thread's loop; keep the loop alive until
771/// it runs.
772fn schedule_channel_delivery(pid: u64) {
773    with_host(|h| h.incr_handle());
774    let io = with_host(|h| h.io_sender());
775    let _ = io.send(Box::new(move || channel_deliver(pid)));
776}
777
778/// Deliver (at most) one queued message to port `pid` by emitting `'message'`.
779/// Always `Ok(())`: a listener error is caught, never `?`-propagated (that would
780/// unwind the whole event loop — see the module docs).
781fn channel_deliver(pid: u64) -> Result<(), String> {
782    let json = CH_QUEUE.with(|q| q.borrow_mut().get_mut(&pid).and_then(|d| d.pop_front()));
783    if let Some(json) = json {
784        if let Some(port) = CHANNEL_PORTS.with(|m| m.borrow().get(&pid).cloned()) {
785            match deserialize(&json) {
786                Ok(v) => {
787                    if let Err(e) = emit_event(&port, "message", vec![v]) {
788                        eprintln!("{e}");
789                    }
790                }
791                Err(e) => eprintln!("{e}"),
792            }
793        }
794    }
795    with_host(|h| h.decr_handle());
796    Ok(())
797}
798
799/// `worker.receiveMessageOnPort(port)` → `{ message }` draining one queued message
800/// from `port`, or `undefined` if none is queued.
801fn receive_message_on_port(port: Option<&Value>) -> Value {
802    let Some(port) = port else {
803        return Value::Undef;
804    };
805    let Some(pid) = u64_prop(port, "@@portid") else {
806        return Value::Undef;
807    };
808    let json = CH_QUEUE.with(|q| q.borrow_mut().get_mut(&pid).and_then(|d| d.pop_front()));
809    match json {
810        Some(j) => match deserialize(&j) {
811            Ok(v) => with_host(|h| {
812                let mut m = IndexMap::new();
813                m.insert("message".into(), v);
814                h.new_object(m)
815            }),
816            Err(_) => Value::Undef,
817        },
818        None => Value::Undef,
819    }
820}
821
822// ── BroadcastChannel (in-process, SAME-thread pub/sub by name) ────────────────
823
824/// `new BroadcastChannel(name)` → an object that broadcasts `postMessage` data to
825/// every OTHER `BroadcastChannel` of the same name.
826///
827/// LIMITATION (documented, never faked): this is SAME-THREAD only. Node's
828/// BroadcastChannel spans worker threads; node-js delivers only to channels on
829/// the constructing thread (cross-thread delivery would need the worker bridge and
830/// is out of scope here). Like Node, a channel keeps the event loop alive (refs a
831/// handle) until `close()` / `unref()`.
832pub fn construct_broadcast_channel(args: &[Value]) -> Result<Value, String> {
833    let name = with_host(|h| h.str_of(&arg0(args)));
834    let id = NEXT_PORT_ID.fetch_add(1, Ordering::SeqCst);
835    let obj = with_host(|h| {
836        let listeners = h.new_array(Vec::new());
837        let mut m = IndexMap::new();
838        m.insert("@@native".into(), h.new_str("BroadcastChannel"));
839        m.insert("@@bcid".into(), Value::Float(id as f64));
840        m.insert("@@bcname".into(), h.new_str(name.clone()));
841        m.insert("@@listeners".into(), listeners);
842        m.insert("@@refed".into(), Value::Bool(true));
843        m.insert("name".into(), h.new_str(name.clone()));
844        m.insert("onmessage".into(), h.null());
845        m.insert("onmessageerror".into(), h.null());
846        h.new_object(m)
847    });
848    BCAST.with(|b| {
849        b.borrow_mut()
850            .entry(name)
851            .or_default()
852            .push((id, obj.clone()))
853    });
854    with_host(|h| h.incr_handle());
855    Ok(obj)
856}
857
858/// Method dispatch for a `BroadcastChannel` object.
859fn broadcast_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
860    match method {
861        "postMessage" => {
862            let json = serialize(&arg0(&args))?;
863            let name = str_prop(recv, "@@bcname");
864            let self_id = u64_prop(recv, "@@bcid");
865            let targets: Vec<Value> = BCAST.with(|b| match b.borrow().get(&name) {
866                Some(list) => list
867                    .iter()
868                    .filter(|(id, _)| Some(*id) != self_id)
869                    .map(|(_, v)| v.clone())
870                    .collect(),
871                None => Vec::new(),
872            });
873            for t in targets {
874                schedule_broadcast_delivery(t, json.clone());
875            }
876            Ok(Value::Undef)
877        }
878        "close" => {
879            let name = str_prop(recv, "@@bcname");
880            let self_id = u64_prop(recv, "@@bcid");
881            BCAST.with(|b| {
882                if let Some(list) = b.borrow_mut().get_mut(&name) {
883                    list.retain(|(id, _)| Some(*id) != self_id);
884                }
885            });
886            release_broadcast_ref(recv);
887            Ok(Value::Undef)
888        }
889        "addEventListener" => {
890            // Only 'message' is meaningful here; store the callback.
891            let ev = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
892            if ev == "message" {
893                if let Some(cb) = args.get(1) {
894                    if let Some(arr) = get_prop(recv, "@@listeners") {
895                        with_host(|h| {
896                            if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
897                                items.push(cb.clone());
898                            }
899                        });
900                    }
901                }
902            }
903            Ok(Value::Undef)
904        }
905        "removeEventListener" => Ok(Value::Undef),
906        "ref" => {
907            let refed = matches!(get_prop(recv, "@@refed"), Some(Value::Bool(true)));
908            if !refed {
909                with_host(|h| h.incr_handle());
910                set_bool(recv, "@@refed", true);
911            }
912            Ok(recv.clone())
913        }
914        "unref" => {
915            release_broadcast_ref(recv);
916            Ok(recv.clone())
917        }
918        _ => Err(crate::host::type_error(&format!(
919            "BroadcastChannel.{method} is not a function"
920        ))),
921    }
922}
923
924/// Drop the channel's event-loop handle if it currently holds one.
925fn release_broadcast_ref(recv: &Value) {
926    if matches!(get_prop(recv, "@@refed"), Some(Value::Bool(true))) {
927        with_host(|h| h.decr_handle());
928        set_bool(recv, "@@refed", false);
929    }
930}
931
932/// Schedule delivery of a broadcast message to one target channel.
933fn schedule_broadcast_delivery(target: Value, json: String) {
934    with_host(|h| h.incr_handle());
935    let io = with_host(|h| h.io_sender());
936    let _ = io.send(Box::new(move || broadcast_deliver(target, json)));
937}
938
939/// Deliver a broadcast message: build a `MessageEvent`-like `{ data, type }` and
940/// invoke the target's `onmessage` plus any `addEventListener('message')` handlers.
941/// Always `Ok(())` — handler errors are caught, never `?`-propagated.
942fn broadcast_deliver(target: Value, json: String) -> Result<(), String> {
943    let value = match deserialize(&json) {
944        Ok(v) => v,
945        Err(e) => {
946            eprintln!("{e}");
947            with_host(|h| h.decr_handle());
948            return Ok(());
949        }
950    };
951    let event = with_host(|h| {
952        let mut m = IndexMap::new();
953        m.insert("data".into(), value);
954        m.insert("type".into(), h.new_str("message"));
955        h.new_object(m)
956    });
957    let onmessage = get_prop(&target, "onmessage");
958    let mut handlers: Vec<Value> = Vec::new();
959    if let Some(cb) = onmessage {
960        if with_host(|h| crate::host::is_callable(h, &cb)) {
961            handlers.push(cb);
962        }
963    }
964    if let Some(arr) = get_prop(&target, "@@listeners") {
965        let listeners: Vec<Value> = with_host(|h| match h.get(&arr) {
966            Some(JsObj::Array(items)) => items.clone(),
967            _ => Vec::new(),
968        });
969        handlers.extend(listeners);
970    }
971    for cb in handlers {
972        if let Err(e) = crate::host::invoke(&cb, vec![event.clone()], None) {
973            eprintln!("{e}");
974        }
975    }
976    with_host(|h| h.decr_handle());
977    Ok(())
978}
979
980/// The string value of a hidden own property of `recv`.
981fn str_prop(recv: &Value, key: &str) -> String {
982    get_prop(recv, key)
983        .map(|v| with_host(|h| h.str_of(&v)))
984        .unwrap_or_default()
985}
986
987/// Set a boolean own property on `recv`.
988fn set_bool(recv: &Value, key: &str, val: bool) {
989    with_host(|h| {
990        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
991            p.insert(key.to_string(), Value::Bool(val));
992        }
993    });
994}