Skip to main content

nodejs/stdlib/
stream.rs

1//! Node `stream` module: native base classes + module helper functions.
2//!
3//! The base classes (`Readable`/`Writable`/`Duplex`/`Transform`/`PassThrough`/
4//! `Stream`) are EventEmitter-backed objects (same `@@native`/`@@on`/`@@once`
5//! shape as `net` sockets) exposing the surface higher layers touch today:
6//! `on`/`once`/`emit`, `write`/`end`, `push`/`read`, and a best-effort `pipe`.
7//! `http`'s `req`/`res` are their own native objects (see `http.rs`); this module
8//! exists so `require('stream')` yields the base constructors and the module
9//! helper functions (`finished`, `pipeline`, `isReadable`, …).
10//!
11//! Lifecycle state is tracked with hidden boolean props set as terminal events
12//! fire: `@@ended` (readable end), `@@finished` (writable finish), `@@destroyed`
13//! (close/destroy), `@@errored` (error value), `@@disturbed` (read/resume/pipe).
14//! `finished(stream, cb)` callbacks live in a `@@finished` array drained on the
15//! first terminal event so the callback fires exactly once.
16
17use crate::host::{with_host, JsObj};
18use fusevm::Value;
19use indexmap::IndexMap;
20use std::cell::Cell;
21
22// Module-state default high-water marks (byte mode / object mode). Node v26
23// defaults: 65536 bytes, 16 objects; `setDefaultHighWaterMark` mutates these.
24thread_local! {
25    static DEFAULT_HWM_BYTES: Cell<f64> = const { Cell::new(65536.0) };
26    static DEFAULT_HWM_OBJ: Cell<f64> = const { Cell::new(16.0) };
27}
28
29/// The base classes exported by `require('stream')`.
30pub const CLASSES: &[&str] = &[
31    "Readable",
32    "Writable",
33    "Duplex",
34    "Transform",
35    "PassThrough",
36    "Stream",
37];
38
39/// The module free-functions exported by `require('stream')`.
40pub const METHODS: &[&str] = &[
41    "finished",
42    "pipeline",
43    "addAbortSignal",
44    "destroy",
45    "isReadable",
46    "isWritable",
47    "isErrored",
48    "isDestroyed",
49    "isDisturbed",
50    "getDefaultHighWaterMark",
51    "setDefaultHighWaterMark",
52];
53
54/// True if `name` is one of the stream base-class constructors.
55pub fn is_class(name: &str) -> bool {
56    CLASSES.contains(&name)
57}
58
59/// `stream.<Class>` property (a constructor value), reachable via
60/// `namespace_property` → `stdlib::constant`.
61pub fn constant(name: &str) -> Option<Value> {
62    if is_class(name) {
63        Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))))
64    } else {
65        None
66    }
67}
68
69/// `new Readable()` / `Writable` / `Duplex` / `Transform` / `PassThrough` /
70/// `Stream`.
71pub fn construct(name: &str, args: &[Value]) -> Value {
72    // A `push`ed-data queue lives on the object as an array for `read`.
73    let mut extra = IndexMap::new();
74    let queue = with_host(|h| h.new_array(Vec::new()));
75    extra.insert("@@queue".into(), queue);
76    // `new Writable({ write(chunk, enc, cb) {…} })` supplies the implementation
77    // the stream is supposed to run — that option is the whole point of
78    // constructing one directly, and it was DISCARDED: `construct` took only the
79    // class name, so a custom sink silently swallowed every chunk. Keep the
80    // callbacks the write path uses.
81    if let Some(opts) = args.first() {
82        for (opt, key) in [("write", "@@writeImpl"), ("final", "@@finalImpl")] {
83            if let Some(f) = opt_callable(opts, opt) {
84                extra.insert(key.into(), f);
85            }
86        }
87    }
88    super::net::new_emitter_object(name, extra)
89}
90
91/// An own property of `v` that is callable, else `None`.
92fn opt_callable(v: &Value, key: &str) -> Option<Value> {
93    let f = with_host(|h| match h.get(v) {
94        Some(JsObj::Object(m)) => m.get(key).cloned(),
95        _ => None,
96    })?;
97    with_host(|h| crate::host::is_callable(h, &f)).then_some(f)
98}
99
100/// Run the `write(chunk, encoding, callback)` implementation the constructor was
101/// given, if any. The callback is required by the contract, so a no-op function
102/// is supplied when the implementation asks for one.
103fn run_write_impl(recv: &Value, chunk: &Value) -> Result<(), String> {
104    let Some(f) = hidden_prop(recv, "@@writeImpl") else {
105        return Ok(());
106    };
107    let enc = with_host(|h| h.new_str("utf8".to_string()));
108    // `_write` is handed a `callback` it is contractually required to call.
109    // Nothing here waits on backpressure, so it only has to BE callable —
110    // a `write(c, e, cb) { …; cb(); }` implementation throws without it.
111    let cb = with_host(|h| h.alloc(JsObj::Builtin("@@streamWriteCallback".into())));
112    crate::host::invoke(&f, vec![chunk.clone(), enc, cb], None)?;
113    Ok(())
114}
115
116fn hidden_prop(recv: &Value, key: &str) -> Option<Value> {
117    with_host(|h| match h.get(recv) {
118        Some(JsObj::Object(m)) => m.get(key).cloned(),
119        _ => None,
120    })
121}
122
123/// Module free-function dispatch (`stream.finished`, `stream.isReadable`, …).
124pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
125    let s0 = || args.first().cloned().unwrap_or(Value::Undef);
126    Some(match method {
127        "getDefaultHighWaterMark" => Ok(get_default_hwm(args)),
128        "setDefaultHighWaterMark" => Ok(set_default_hwm(args)),
129        "isReadable" => Ok(Value::Bool(is_readable(&s0()))),
130        "isWritable" => Ok(Value::Bool(is_writable(&s0()))),
131        "isErrored" => Ok(Value::Bool(flag(&s0(), "@@errored"))),
132        "isDestroyed" => Ok(Value::Bool(flag(&s0(), "@@destroyed"))),
133        "isDisturbed" => Ok(Value::Bool(flag(&s0(), "@@disturbed"))),
134        "destroy" => Ok(destroy_stream(args)),
135        "finished" => Ok(finished(args)),
136        "pipeline" => pipeline(args),
137        "addAbortSignal" => Ok(add_abort_signal(args)),
138        _ => return None,
139    })
140}
141
142fn get_default_hwm(args: &[Value]) -> Value {
143    let obj = args
144        .first()
145        .map(|v| with_host(|h| h.truthy(v)))
146        .unwrap_or(false);
147    let n = if obj {
148        DEFAULT_HWM_OBJ.with(|c| c.get())
149    } else {
150        DEFAULT_HWM_BYTES.with(|c| c.get())
151    };
152    Value::Float(n)
153}
154
155fn set_default_hwm(args: &[Value]) -> Value {
156    let obj = args
157        .first()
158        .map(|v| with_host(|h| h.truthy(v)))
159        .unwrap_or(false);
160    let val = super::arg_num(args, 1);
161    if obj {
162        DEFAULT_HWM_OBJ.with(|c| c.set(val));
163    } else {
164        DEFAULT_HWM_BYTES.with(|c| c.set(val));
165    }
166    Value::Undef
167}
168
169// ── lifecycle-flag helpers ──────────────────────────────────────────────────
170
171fn tag_of(recv: &Value) -> Option<String> {
172    with_host(|h| match h.get(recv) {
173        Some(JsObj::Object(p)) => p.get("@@native").map(|v| h.str_of(v)),
174        _ => None,
175    })
176}
177
178fn flag(recv: &Value, key: &str) -> bool {
179    with_host(|h| match h.get(recv) {
180        Some(JsObj::Object(p)) => p.get(key).map(|v| h.truthy(v)).unwrap_or(false),
181        _ => false,
182    })
183}
184
185fn set_flag(recv: &Value, key: &str, v: Value) {
186    with_host(|h| {
187        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
188            p.insert(key.to_string(), v);
189        }
190    });
191}
192
193fn is_readable(s: &Value) -> bool {
194    let Some(t) = tag_of(s) else { return false };
195    matches!(
196        t.as_str(),
197        "Readable" | "Duplex" | "Transform" | "PassThrough"
198    ) && !flag(s, "@@destroyed")
199        && !flag(s, "@@ended")
200}
201
202fn is_writable(s: &Value) -> bool {
203    let Some(t) = tag_of(s) else { return false };
204    matches!(
205        t.as_str(),
206        "Writable" | "Duplex" | "Transform" | "PassThrough"
207    ) && !flag(s, "@@destroyed")
208        && !flag(s, "@@finished")
209}
210
211// ── `finished` callback registry ────────────────────────────────────────────
212
213fn add_finished(recv: &Value, cb: Value) {
214    with_host(|h| {
215        let existing = match h.get(recv) {
216            Some(JsObj::Object(p)) => p.get("@@finished").cloned(),
217            _ => None,
218        };
219        let arr = match existing {
220            Some(a) if matches!(h.get(&a), Some(JsObj::Array(_))) => a,
221            _ => {
222                let a = h.new_array(Vec::new());
223                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
224                    p.insert("@@finished".into(), a.clone());
225                }
226                a
227            }
228        };
229        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
230            items.push(cb);
231        }
232    });
233}
234
235fn take_finished(recv: &Value) -> Vec<Value> {
236    with_host(|h| {
237        let arr = match h.get_mut(recv) {
238            Some(JsObj::Object(p)) => p.shift_remove("@@finished"),
239            _ => None,
240        };
241        match arr {
242            Some(av) => match h.get(&av) {
243                Some(JsObj::Array(items)) => items.clone(),
244                _ => Vec::new(),
245            },
246            None => Vec::new(),
247        }
248    })
249}
250
251/// Emit `name` (with `extra` args), set the matching lifecycle flag, and drain
252/// `finished` callbacks on the first terminal event so each fires once.
253fn emit_event(recv: &Value, name: &str, extra: Vec<Value>) -> Result<Value, String> {
254    let mut a = vec![with_host(|h| h.new_str(name))];
255    a.extend(extra.iter().cloned());
256    let r = super::events::instance_call(recv, "emit", a)?;
257    match name {
258        "end" => set_flag(recv, "@@ended", Value::Bool(true)),
259        "finish" => set_flag(recv, "@@finished", Value::Bool(true)),
260        "close" => set_flag(recv, "@@destroyed", Value::Bool(true)),
261        "error" => set_flag(
262            recv,
263            "@@errored",
264            extra.first().cloned().unwrap_or(Value::Bool(true)),
265        ),
266        _ => {}
267    }
268    if matches!(name, "end" | "finish" | "close" | "error") {
269        let cbs = take_finished(recv);
270        let arg = if name == "error" {
271            extra.first().cloned().unwrap_or(Value::Undef)
272        } else {
273            Value::Undef
274        };
275        for cb in cbs {
276            crate::host::invoke(&cb, vec![arg.clone()], None)?;
277        }
278    }
279    Ok(r)
280}
281
282// ── module free functions ───────────────────────────────────────────────────
283
284/// `stream.finished(stream[, options], callback)` — invoke `callback(err)` once
285/// when the stream ends/finishes/closes/errors. Fires immediately if the stream
286/// has already reached a terminal state. Returns `undefined` (Node returns a
287/// cleanup fn; not tracked — best-effort).
288fn finished(args: &[Value]) -> Value {
289    let stream = args.first().cloned().unwrap_or(Value::Undef);
290    let cb = args
291        .iter()
292        .rev()
293        .find(|v| with_host(|h| crate::host::is_callable(h, v)))
294        .cloned()
295        .unwrap_or(Value::Undef);
296    if flag(&stream, "@@ended") || flag(&stream, "@@finished") || flag(&stream, "@@destroyed") {
297        let _ = crate::host::invoke(&cb, vec![Value::Undef], None);
298    } else {
299        add_finished(&stream, cb);
300    }
301    Value::Undef
302}
303
304/// `stream.pipeline(source, ...transforms, dest[, callback])` — chain via
305/// `.pipe()` and register `callback` on the destination's completion. Returns
306/// the destination stream.
307fn pipeline(args: &[Value]) -> Result<Value, String> {
308    if args.is_empty() {
309        // Node validates the LAST argument (the callback slot) first, so an
310        // empty call reports that property, not a bespoke arity sentence.
311        return Err(crate::host::invalid_arg_type(
312            "streams[stream.length - 1]",
313            "property",
314            "function",
315            &Value::Undef,
316        ));
317    }
318    let cb_idx = args
319        .iter()
320        .rposition(|v| with_host(|h| crate::host::is_callable(h, v)));
321    let (streams, cb) = match cb_idx {
322        Some(i) if i == args.len() - 1 => (&args[..i], Some(args[i].clone())),
323        _ => (args, None),
324    };
325    for w in streams.windows(2) {
326        crate::host::call_method(&w[0], "pipe", vec![w[1].clone()])?;
327    }
328    let last = streams.last().cloned().unwrap_or(Value::Undef);
329    if let Some(cb) = cb {
330        add_finished(&last, cb);
331    }
332    Ok(last)
333}
334
335/// `stream.destroy(stream[, err])` — emit `error` (if `err` given) then `close`
336/// and mark the stream destroyed.
337fn destroy_stream(args: &[Value]) -> Value {
338    let stream = args.first().cloned().unwrap_or(Value::Undef);
339    if flag(&stream, "@@destroyed") {
340        return stream;
341    }
342    if let Some(e) = args.get(1).cloned() {
343        if !with_host(|h| h.is_nullish(&e)) {
344            let _ = emit_event(&stream, "error", vec![e]);
345        }
346    }
347    let _ = emit_event(&stream, "close", vec![]);
348    set_flag(&stream, "@@destroyed", Value::Bool(true));
349    stream
350}
351
352/// `stream.addAbortSignal(signal, stream)` — best-effort: `AbortSignal` is not
353/// modeled in this runtime, so this returns `stream` unchanged.
354fn add_abort_signal(args: &[Value]) -> Value {
355    args.get(1).cloned().unwrap_or(Value::Undef)
356}
357
358/// Instance dispatch for a stream base class. EventEmitter methods are delegated
359/// to `events`; `emit` routes through `emit_event` for lifecycle tracking.
360pub fn instance_call(
361    tag: &str,
362    recv: &Value,
363    method: &str,
364    args: Vec<Value>,
365) -> Result<Value, String> {
366    let _ = tag;
367    if method == "emit" {
368        let name = args
369            .first()
370            .map(|v| with_host(|h| h.str_of(v)))
371            .unwrap_or_default();
372        let extra = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
373        return emit_event(recv, &name, extra);
374    }
375    // `emit` is intercepted above (lifecycle tracking); every other name in
376    // `events::METHODS` delegates. Reading the set from `events` rather than
377    // re-listing it is what puts `listeners`/`setMaxListeners`/`getMaxListeners`
378    // on a stream — the local copy was missing all three.
379    if super::events::METHODS.contains(&method) {
380        return super::events::instance_call(recv, method, args);
381    }
382    match method {
383        "write" => {
384            let chunk = args.first().cloned().unwrap_or(Value::Undef);
385            run_write_impl(recv, &chunk)?;
386            emit_event(recv, "data", vec![chunk])?;
387            Ok(Value::Bool(true))
388        }
389        "end" => {
390            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
391                run_write_impl(recv, chunk)?;
392                emit_event(recv, "data", vec![chunk.clone()])?;
393            }
394            emit_event(recv, "finish", vec![])?;
395            emit_event(recv, "end", vec![])?;
396            Ok(recv.clone())
397        }
398        "push" => {
399            let chunk = args.first().cloned().unwrap_or(Value::Undef);
400            if with_host(|h| h.is_nullish(&chunk)) {
401                emit_event(recv, "end", vec![])?;
402                return Ok(Value::Bool(false));
403            }
404            if let Some(q) = queue_of(recv) {
405                with_host(|h| {
406                    if let Some(JsObj::Array(items)) = h.get_mut(&q) {
407                        items.push(chunk.clone());
408                    }
409                });
410            }
411            emit_event(recv, "data", vec![chunk])?;
412            Ok(Value::Bool(true))
413        }
414        "read" => {
415            set_flag(recv, "@@disturbed", Value::Bool(true));
416            if let Some(q) = queue_of(recv) {
417                let next = with_host(|h| match h.get_mut(&q) {
418                    Some(JsObj::Array(items)) if !items.is_empty() => Some(items.remove(0)),
419                    _ => None,
420                });
421                if let Some(v) = next {
422                    return Ok(v);
423                }
424            }
425            Ok(with_host(|h| h.null()))
426        }
427        "pipe" => {
428            set_flag(recv, "@@disturbed", Value::Bool(true));
429            let dest = args.first().cloned().unwrap_or(Value::Undef);
430            if let Some(q) = queue_of(recv) {
431                let items = with_host(|h| match h.get(&q) {
432                    Some(JsObj::Array(items)) => items.clone(),
433                    _ => Vec::new(),
434                });
435                for chunk in items {
436                    crate::host::call_method(&dest, "write", vec![chunk])?;
437                }
438            }
439            Ok(dest)
440        }
441        "destroy" => {
442            if !flag(recv, "@@destroyed") {
443                if let Some(e) = args.first().filter(|v| !matches!(v, Value::Undef)) {
444                    let _ = emit_event(recv, "error", vec![e.clone()]);
445                }
446                let _ = emit_event(recv, "close", vec![]);
447                set_flag(recv, "@@destroyed", Value::Bool(true));
448            }
449            Ok(recv.clone())
450        }
451        "resume" => {
452            set_flag(recv, "@@disturbed", Value::Bool(true));
453            Ok(recv.clone())
454        }
455        "setEncoding" | "pause" | "cork" | "uncork" => Ok(recv.clone()),
456        _ => Err(crate::host::type_error(&format!(
457            "stream.{method} is not a function"
458        ))),
459    }
460}
461
462fn queue_of(recv: &Value) -> Option<Value> {
463    with_host(|h| match h.get(recv) {
464        Some(JsObj::Object(p)) => p.get("@@queue").cloned(),
465        _ => None,
466    })
467}