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        return Err(crate::host::type_error(
266            "pipeline requires at least one stream",
267        ));
268    }
269    let cb_idx = args
270        .iter()
271        .rposition(|v| with_host(|h| crate::host::is_callable(h, v)));
272    let (streams, cb) = match cb_idx {
273        Some(i) if i == args.len() - 1 => (&args[..i], Some(args[i].clone())),
274        _ => (args, None),
275    };
276    for w in streams.windows(2) {
277        crate::host::call_method(&w[0], "pipe", vec![w[1].clone()])?;
278    }
279    let last = streams.last().cloned().unwrap_or(Value::Undef);
280    if let Some(cb) = cb {
281        add_finished(&last, cb);
282    }
283    Ok(last)
284}
285
286/// `stream.destroy(stream[, err])` — emit `error` (if `err` given) then `close`
287/// and mark the stream destroyed.
288fn destroy_stream(args: &[Value]) -> Value {
289    let stream = args.first().cloned().unwrap_or(Value::Undef);
290    if flag(&stream, "@@destroyed") {
291        return stream;
292    }
293    if let Some(e) = args.get(1).cloned() {
294        if !with_host(|h| h.is_nullish(&e)) {
295            let _ = emit_event(&stream, "error", vec![e]);
296        }
297    }
298    let _ = emit_event(&stream, "close", vec![]);
299    set_flag(&stream, "@@destroyed", Value::Bool(true));
300    stream
301}
302
303/// `stream.addAbortSignal(signal, stream)` — best-effort: `AbortSignal` is not
304/// modeled in this runtime, so this returns `stream` unchanged.
305fn add_abort_signal(args: &[Value]) -> Value {
306    args.get(1).cloned().unwrap_or(Value::Undef)
307}
308
309/// Instance dispatch for a stream base class. EventEmitter methods are delegated
310/// to `events`; `emit` routes through `emit_event` for lifecycle tracking.
311pub fn instance_call(
312    tag: &str,
313    recv: &Value,
314    method: &str,
315    args: Vec<Value>,
316) -> Result<Value, String> {
317    let _ = tag;
318    if method == "emit" {
319        let name = args
320            .first()
321            .map(|v| with_host(|h| h.str_of(v)))
322            .unwrap_or_default();
323        let extra = args.get(1..).map(|s| s.to_vec()).unwrap_or_default();
324        return emit_event(recv, &name, extra);
325    }
326    if matches!(
327        method,
328        "on" | "addListener"
329            | "prependListener"
330            | "once"
331            | "prependOnceListener"
332            | "removeListener"
333            | "off"
334            | "removeAllListeners"
335            | "listenerCount"
336            | "eventNames"
337    ) {
338        return super::events::instance_call(recv, method, args);
339    }
340    match method {
341        "write" => {
342            let chunk = args.first().cloned().unwrap_or(Value::Undef);
343            emit_event(recv, "data", vec![chunk])?;
344            Ok(Value::Bool(true))
345        }
346        "end" => {
347            if let Some(chunk) = args.first().filter(|v| !matches!(v, Value::Undef)) {
348                emit_event(recv, "data", vec![chunk.clone()])?;
349            }
350            emit_event(recv, "finish", vec![])?;
351            emit_event(recv, "end", vec![])?;
352            Ok(recv.clone())
353        }
354        "push" => {
355            let chunk = args.first().cloned().unwrap_or(Value::Undef);
356            if with_host(|h| h.is_nullish(&chunk)) {
357                emit_event(recv, "end", vec![])?;
358                return Ok(Value::Bool(false));
359            }
360            if let Some(q) = queue_of(recv) {
361                with_host(|h| {
362                    if let Some(JsObj::Array(items)) = h.get_mut(&q) {
363                        items.push(chunk.clone());
364                    }
365                });
366            }
367            emit_event(recv, "data", vec![chunk])?;
368            Ok(Value::Bool(true))
369        }
370        "read" => {
371            set_flag(recv, "@@disturbed", Value::Bool(true));
372            if let Some(q) = queue_of(recv) {
373                let next = with_host(|h| match h.get_mut(&q) {
374                    Some(JsObj::Array(items)) if !items.is_empty() => Some(items.remove(0)),
375                    _ => None,
376                });
377                if let Some(v) = next {
378                    return Ok(v);
379                }
380            }
381            Ok(with_host(|h| h.null()))
382        }
383        "pipe" => {
384            set_flag(recv, "@@disturbed", Value::Bool(true));
385            let dest = args.first().cloned().unwrap_or(Value::Undef);
386            if let Some(q) = queue_of(recv) {
387                let items = with_host(|h| match h.get(&q) {
388                    Some(JsObj::Array(items)) => items.clone(),
389                    _ => Vec::new(),
390                });
391                for chunk in items {
392                    crate::host::call_method(&dest, "write", vec![chunk])?;
393                }
394            }
395            Ok(dest)
396        }
397        "destroy" => {
398            if !flag(recv, "@@destroyed") {
399                if let Some(e) = args.first().filter(|v| !matches!(v, Value::Undef)) {
400                    let _ = emit_event(recv, "error", vec![e.clone()]);
401                }
402                let _ = emit_event(recv, "close", vec![]);
403                set_flag(recv, "@@destroyed", Value::Bool(true));
404            }
405            Ok(recv.clone())
406        }
407        "resume" => {
408            set_flag(recv, "@@disturbed", Value::Bool(true));
409            Ok(recv.clone())
410        }
411        "setEncoding" | "pause" | "cork" | "uncork" => Ok(recv.clone()),
412        _ => Err(crate::host::type_error(&format!(
413            "stream.{method} is not a function"
414        ))),
415    }
416}
417
418fn queue_of(recv: &Value) -> Option<Value> {
419    with_host(|h| match h.get(recv) {
420        Some(JsObj::Object(p)) => p.get("@@queue").cloned(),
421        _ => None,
422    })
423}