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) -> 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    super::net::new_emitter_object(name, extra)
77}
78
79/// Module free-function dispatch (`stream.finished`, `stream.isReadable`, …).
80pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
81    let s0 = || args.first().cloned().unwrap_or(Value::Undef);
82    Some(match method {
83        "getDefaultHighWaterMark" => Ok(get_default_hwm(args)),
84        "setDefaultHighWaterMark" => Ok(set_default_hwm(args)),
85        "isReadable" => Ok(Value::Bool(is_readable(&s0()))),
86        "isWritable" => Ok(Value::Bool(is_writable(&s0()))),
87        "isErrored" => Ok(Value::Bool(flag(&s0(), "@@errored"))),
88        "isDestroyed" => Ok(Value::Bool(flag(&s0(), "@@destroyed"))),
89        "isDisturbed" => Ok(Value::Bool(flag(&s0(), "@@disturbed"))),
90        "destroy" => Ok(destroy_stream(args)),
91        "finished" => Ok(finished(args)),
92        "pipeline" => pipeline(args),
93        "addAbortSignal" => Ok(add_abort_signal(args)),
94        _ => return None,
95    })
96}
97
98fn get_default_hwm(args: &[Value]) -> Value {
99    let obj = args
100        .first()
101        .map(|v| with_host(|h| h.truthy(v)))
102        .unwrap_or(false);
103    let n = if obj {
104        DEFAULT_HWM_OBJ.with(|c| c.get())
105    } else {
106        DEFAULT_HWM_BYTES.with(|c| c.get())
107    };
108    Value::Float(n)
109}
110
111fn set_default_hwm(args: &[Value]) -> Value {
112    let obj = args
113        .first()
114        .map(|v| with_host(|h| h.truthy(v)))
115        .unwrap_or(false);
116    let val = super::arg_num(args, 1);
117    if obj {
118        DEFAULT_HWM_OBJ.with(|c| c.set(val));
119    } else {
120        DEFAULT_HWM_BYTES.with(|c| c.set(val));
121    }
122    Value::Undef
123}
124
125// ── lifecycle-flag helpers ──────────────────────────────────────────────────
126
127fn tag_of(recv: &Value) -> Option<String> {
128    with_host(|h| match h.get(recv) {
129        Some(JsObj::Object(p)) => p.get("@@native").map(|v| h.str_of(v)),
130        _ => None,
131    })
132}
133
134fn flag(recv: &Value, key: &str) -> bool {
135    with_host(|h| match h.get(recv) {
136        Some(JsObj::Object(p)) => p.get(key).map(|v| h.truthy(v)).unwrap_or(false),
137        _ => false,
138    })
139}
140
141fn set_flag(recv: &Value, key: &str, v: Value) {
142    with_host(|h| {
143        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
144            p.insert(key.to_string(), v);
145        }
146    });
147}
148
149fn is_readable(s: &Value) -> bool {
150    let Some(t) = tag_of(s) else { return false };
151    matches!(
152        t.as_str(),
153        "Readable" | "Duplex" | "Transform" | "PassThrough"
154    ) && !flag(s, "@@destroyed")
155        && !flag(s, "@@ended")
156}
157
158fn is_writable(s: &Value) -> bool {
159    let Some(t) = tag_of(s) else { return false };
160    matches!(
161        t.as_str(),
162        "Writable" | "Duplex" | "Transform" | "PassThrough"
163    ) && !flag(s, "@@destroyed")
164        && !flag(s, "@@finished")
165}
166
167// ── `finished` callback registry ────────────────────────────────────────────
168
169fn add_finished(recv: &Value, cb: Value) {
170    with_host(|h| {
171        let existing = match h.get(recv) {
172            Some(JsObj::Object(p)) => p.get("@@finished").cloned(),
173            _ => None,
174        };
175        let arr = match existing {
176            Some(a) if matches!(h.get(&a), Some(JsObj::Array(_))) => a,
177            _ => {
178                let a = h.new_array(Vec::new());
179                if let Some(JsObj::Object(p)) = h.get_mut(recv) {
180                    p.insert("@@finished".into(), a.clone());
181                }
182                a
183            }
184        };
185        if let Some(JsObj::Array(items)) = h.get_mut(&arr) {
186            items.push(cb);
187        }
188    });
189}
190
191fn take_finished(recv: &Value) -> Vec<Value> {
192    with_host(|h| {
193        let arr = match h.get_mut(recv) {
194            Some(JsObj::Object(p)) => p.shift_remove("@@finished"),
195            _ => None,
196        };
197        match arr {
198            Some(av) => match h.get(&av) {
199                Some(JsObj::Array(items)) => items.clone(),
200                _ => Vec::new(),
201            },
202            None => Vec::new(),
203        }
204    })
205}
206
207/// Emit `name` (with `extra` args), set the matching lifecycle flag, and drain
208/// `finished` callbacks on the first terminal event so each fires once.
209fn emit_event(recv: &Value, name: &str, extra: Vec<Value>) -> Result<Value, String> {
210    let mut a = vec![with_host(|h| h.new_str(name))];
211    a.extend(extra.iter().cloned());
212    let r = super::events::instance_call(recv, "emit", a)?;
213    match name {
214        "end" => set_flag(recv, "@@ended", Value::Bool(true)),
215        "finish" => set_flag(recv, "@@finished", Value::Bool(true)),
216        "close" => set_flag(recv, "@@destroyed", Value::Bool(true)),
217        "error" => set_flag(
218            recv,
219            "@@errored",
220            extra.first().cloned().unwrap_or(Value::Bool(true)),
221        ),
222        _ => {}
223    }
224    if matches!(name, "end" | "finish" | "close" | "error") {
225        let cbs = take_finished(recv);
226        let arg = if name == "error" {
227            extra.first().cloned().unwrap_or(Value::Undef)
228        } else {
229            Value::Undef
230        };
231        for cb in cbs {
232            crate::host::invoke(&cb, vec![arg.clone()], None)?;
233        }
234    }
235    Ok(r)
236}
237
238// ── module free functions ───────────────────────────────────────────────────
239
240/// `stream.finished(stream[, options], callback)` — invoke `callback(err)` once
241/// when the stream ends/finishes/closes/errors. Fires immediately if the stream
242/// has already reached a terminal state. Returns `undefined` (Node returns a
243/// cleanup fn; not tracked — best-effort).
244fn finished(args: &[Value]) -> Value {
245    let stream = args.first().cloned().unwrap_or(Value::Undef);
246    let cb = args
247        .iter()
248        .rev()
249        .find(|v| with_host(|h| crate::host::is_callable(h, v)))
250        .cloned()
251        .unwrap_or(Value::Undef);
252    if flag(&stream, "@@ended") || flag(&stream, "@@finished") || flag(&stream, "@@destroyed") {
253        let _ = crate::host::invoke(&cb, vec![Value::Undef], None);
254    } else {
255        add_finished(&stream, cb);
256    }
257    Value::Undef
258}
259
260/// `stream.pipeline(source, ...transforms, dest[, callback])` — chain via
261/// `.pipe()` and register `callback` on the destination's completion. Returns
262/// the destination stream.
263fn pipeline(args: &[Value]) -> Result<Value, String> {
264    if args.is_empty() {
265        // Node validates the LAST argument (the callback slot) first, so an
266        // empty call reports that property, not a bespoke arity sentence.
267        return Err(crate::host::invalid_arg_type(
268            "streams[stream.length - 1]",
269            "property",
270            "function",
271            &Value::Undef,
272        ));
273    }
274    let cb_idx = args
275        .iter()
276        .rposition(|v| with_host(|h| crate::host::is_callable(h, v)));
277    let (streams, cb) = match cb_idx {
278        Some(i) if i == args.len() - 1 => (&args[..i], Some(args[i].clone())),
279        _ => (args, None),
280    };
281    for w in streams.windows(2) {
282        crate::host::call_method(&w[0], "pipe", vec![w[1].clone()])?;
283    }
284    let last = streams.last().cloned().unwrap_or(Value::Undef);
285    if let Some(cb) = cb {
286        add_finished(&last, cb);
287    }
288    Ok(last)
289}
290
291/// `stream.destroy(stream[, err])` — emit `error` (if `err` given) then `close`
292/// and mark the stream destroyed.
293fn destroy_stream(args: &[Value]) -> Value {
294    let stream = args.first().cloned().unwrap_or(Value::Undef);
295    if flag(&stream, "@@destroyed") {
296        return stream;
297    }
298    if let Some(e) = args.get(1).cloned() {
299        if !with_host(|h| h.is_nullish(&e)) {
300            let _ = emit_event(&stream, "error", vec![e]);
301        }
302    }
303    let _ = emit_event(&stream, "close", vec![]);
304    set_flag(&stream, "@@destroyed", Value::Bool(true));
305    stream
306}
307
308/// `stream.addAbortSignal(signal, stream)` — best-effort: `AbortSignal` is not
309/// modeled in this runtime, so this returns `stream` unchanged.
310fn add_abort_signal(args: &[Value]) -> Value {
311    args.get(1).cloned().unwrap_or(Value::Undef)
312}
313
314/// Instance dispatch for a stream base class. EventEmitter methods are delegated
315/// to `events`; `emit` routes through `emit_event` for lifecycle tracking.
316pub fn instance_call(
317    tag: &str,
318    recv: &Value,
319    method: &str,
320    args: Vec<Value>,
321) -> Result<Value, String> {
322    let _ = tag;
323    if method == "emit" {
324        let name = args
325            .first()
326            .map(|v| with_host(|h| h.str_of(v)))
327            .unwrap_or_default();
328        let extra = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
329        return emit_event(recv, &name, extra);
330    }
331    // `emit` is intercepted above (lifecycle tracking); every other name in
332    // `events::METHODS` delegates. Reading the set from `events` rather than
333    // re-listing it is what puts `listeners`/`setMaxListeners`/`getMaxListeners`
334    // on a stream — the local copy was missing all three.
335    if super::events::METHODS.contains(&method) {
336        return super::events::instance_call(recv, method, args);
337    }
338    match method {
339        "write" => {
340            let chunk = args.first().cloned().unwrap_or(Value::Undef);
341            emit_event(recv, "data", vec![chunk])?;
342            Ok(Value::Bool(true))
343        }
344        "end" => {
345            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
346                emit_event(recv, "data", vec![chunk.clone()])?;
347            }
348            emit_event(recv, "finish", vec![])?;
349            emit_event(recv, "end", vec![])?;
350            Ok(recv.clone())
351        }
352        "push" => {
353            let chunk = args.first().cloned().unwrap_or(Value::Undef);
354            if with_host(|h| h.is_nullish(&chunk)) {
355                emit_event(recv, "end", vec![])?;
356                return Ok(Value::Bool(false));
357            }
358            if let Some(q) = queue_of(recv) {
359                with_host(|h| {
360                    if let Some(JsObj::Array(items)) = h.get_mut(&q) {
361                        items.push(chunk.clone());
362                    }
363                });
364            }
365            emit_event(recv, "data", vec![chunk])?;
366            Ok(Value::Bool(true))
367        }
368        "read" => {
369            set_flag(recv, "@@disturbed", Value::Bool(true));
370            if let Some(q) = queue_of(recv) {
371                let next = with_host(|h| match h.get_mut(&q) {
372                    Some(JsObj::Array(items)) if !items.is_empty() => Some(items.remove(0)),
373                    _ => None,
374                });
375                if let Some(v) = next {
376                    return Ok(v);
377                }
378            }
379            Ok(with_host(|h| h.null()))
380        }
381        "pipe" => {
382            set_flag(recv, "@@disturbed", Value::Bool(true));
383            let dest = args.first().cloned().unwrap_or(Value::Undef);
384            if let Some(q) = queue_of(recv) {
385                let items = with_host(|h| match h.get(&q) {
386                    Some(JsObj::Array(items)) => items.clone(),
387                    _ => Vec::new(),
388                });
389                for chunk in items {
390                    crate::host::call_method(&dest, "write", vec![chunk])?;
391                }
392            }
393            Ok(dest)
394        }
395        "destroy" => {
396            if !flag(recv, "@@destroyed") {
397                if let Some(e) = args.first().filter(|v| !matches!(v, Value::Undef)) {
398                    let _ = emit_event(recv, "error", vec![e.clone()]);
399                }
400                let _ = emit_event(recv, "close", vec![]);
401                set_flag(recv, "@@destroyed", Value::Bool(true));
402            }
403            Ok(recv.clone())
404        }
405        "resume" => {
406            set_flag(recv, "@@disturbed", Value::Bool(true));
407            Ok(recv.clone())
408        }
409        "setEncoding" | "pause" | "cork" | "uncork" => Ok(recv.clone()),
410        _ => Err(crate::host::type_error(&format!(
411            "stream.{method} is not a function"
412        ))),
413    }
414}
415
416fn queue_of(recv: &Value) -> Option<Value> {
417    with_host(|h| match h.get(recv) {
418        Some(JsObj::Object(p)) => p.get("@@queue").cloned(),
419        _ => None,
420    })
421}