Skip to main content

nodejs/stdlib/
stream_web.rs

1//! Node `stream/web` module: the WHATWG Streams API over the host object heap.
2//!
3//! Every stream, reader, writer and controller is an `@@native`-tagged
4//! `JsObj::Object`; all internal state (queues, lock flags, callbacks, promises)
5//! lives in hidden `@@`-prefixed props. There is no real backpressure scheduler
6//! in this runtime, so the data-flow is modelled **synchronously**:
7//!
8//! * A `ReadableStream`'s `pull` callback is driven synchronously from
9//!   `reader.read()` when the internal queue is empty. If `pull` enqueues (the
10//!   common case) the `read()` promise resolves immediately; if the queue stays
11//!   empty (a controller-fed stream such as a `TransformStream`'s readable) a
12//!   genuine *pending* promise is returned and settled when a later `enqueue`,
13//!   `close` or `error` occurs. This makes the queue/close/error data semantics
14//!   correct even though timing is synchronous.
15//! * `pipeTo` / `pipeThrough` / `tee` drain their source synchronously. An
16//!   asynchronous-only `pull` (one that resolves a promise and enqueues *later*,
17//!   with nothing on the queue) is treated as end-of-stream by those operations
18//!   — the one place the synchronous model diverges from spec timing (documented,
19//!   not faked: no chunk is invented).
20//!
21//! `Symbol.asyncIterator` on a `ReadableStream` is DEFERRED: the host does not
22//! support async-iterator discovery on native objects. `getReader().read()` is
23//! the fully-working consumption path.
24
25use crate::host::{invoke, with_host, JsObj};
26use fusevm::Value;
27use indexmap::IndexMap;
28use std::io::{Read, Write};
29
30// ── class / tag registry ─────────────────────────────────────────────────────
31
32/// The classes exported by `require('stream/web')`. Each resolves to a
33/// `Builtin(name)` via `constant` and constructs via `construct`.
34pub const CLASSES: &[&str] = &[
35    "ReadableStream",
36    "ReadableStreamDefaultReader",
37    "ReadableStreamBYOBReader",
38    "ReadableStreamDefaultController",
39    "ReadableByteStreamController",
40    "ReadableStreamBYOBRequest",
41    "WritableStream",
42    "WritableStreamDefaultWriter",
43    "WritableStreamDefaultController",
44    "TransformStream",
45    "TransformStreamDefaultController",
46    "ByteLengthQueuingStrategy",
47    "CountQueuingStrategy",
48    "TextEncoderStream",
49    "TextDecoderStream",
50    "CompressionStream",
51    "DecompressionStream",
52];
53
54/// `stream/web` exports no free functions — everything is a class/constructor.
55pub const METHODS: &[&str] = &[];
56
57// `@@native` tags (== class names, so `instanceof`/`native_tag` line up).
58pub const READABLE_STREAM_TAG: &str = "ReadableStream";
59pub const RS_DEFAULT_READER_TAG: &str = "ReadableStreamDefaultReader";
60pub const RS_BYOB_READER_TAG: &str = "ReadableStreamBYOBReader";
61pub const RS_DEFAULT_CONTROLLER_TAG: &str = "ReadableStreamDefaultController";
62pub const RS_BYTE_CONTROLLER_TAG: &str = "ReadableByteStreamController";
63pub const RS_BYOB_REQUEST_TAG: &str = "ReadableStreamBYOBRequest";
64pub const WRITABLE_STREAM_TAG: &str = "WritableStream";
65pub const WS_DEFAULT_WRITER_TAG: &str = "WritableStreamDefaultWriter";
66pub const WS_DEFAULT_CONTROLLER_TAG: &str = "WritableStreamDefaultController";
67pub const TRANSFORM_STREAM_TAG: &str = "TransformStream";
68pub const TS_DEFAULT_CONTROLLER_TAG: &str = "TransformStreamDefaultController";
69pub const BYTE_LENGTH_STRATEGY_TAG: &str = "ByteLengthQueuingStrategy";
70pub const COUNT_STRATEGY_TAG: &str = "CountQueuingStrategy";
71pub const TEXT_ENCODER_STREAM_TAG: &str = "TextEncoderStream";
72pub const TEXT_DECODER_STREAM_TAG: &str = "TextDecoderStream";
73pub const COMPRESSION_STREAM_TAG: &str = "CompressionStream";
74pub const DECOMPRESSION_STREAM_TAG: &str = "DecompressionStream";
75
76// Instance-method lists (for `instance_has_method` wiring in mod.rs).
77pub const READABLE_STREAM_METHODS: &[&str] =
78    &["getReader", "cancel", "tee", "pipeTo", "pipeThrough"];
79pub const RS_DEFAULT_READER_METHODS: &[&str] = &["read", "releaseLock", "cancel"];
80pub const RS_BYOB_READER_METHODS: &[&str] = &["read", "releaseLock", "cancel"];
81pub const RS_DEFAULT_CONTROLLER_METHODS: &[&str] = &["enqueue", "close", "error"];
82pub const RS_BYTE_CONTROLLER_METHODS: &[&str] = &["enqueue", "close", "error"];
83pub const RS_BYOB_REQUEST_METHODS: &[&str] = &["respond", "respondWithNewView"];
84pub const WRITABLE_STREAM_METHODS: &[&str] = &["getWriter", "abort", "close"];
85pub const WS_DEFAULT_WRITER_METHODS: &[&str] = &["write", "close", "abort", "releaseLock"];
86pub const WS_DEFAULT_CONTROLLER_METHODS: &[&str] = &["error"];
87pub const TS_DEFAULT_CONTROLLER_METHODS: &[&str] = &["enqueue", "terminate", "error"];
88pub const STRATEGY_METHODS: &[&str] = &["size"];
89
90/// True if `name` is one of the `stream/web` class constructors.
91pub fn is_class(name: &str) -> bool {
92    CLASSES.contains(&name)
93}
94
95/// `require('stream/web').<Class>` → the constructor value.
96pub fn constant(name: &str) -> Option<Value> {
97    if is_class(name) {
98        Some(with_host(|h| h.alloc(JsObj::Builtin(name.to_string()))))
99    } else {
100        None
101    }
102}
103
104/// The method list for a `stream/web` tag (for `instance_has_method`).
105pub fn methods_for(tag: &str) -> &'static [&'static str] {
106    match tag {
107        READABLE_STREAM_TAG => READABLE_STREAM_METHODS,
108        RS_DEFAULT_READER_TAG => RS_DEFAULT_READER_METHODS,
109        RS_BYOB_READER_TAG => RS_BYOB_READER_METHODS,
110        RS_DEFAULT_CONTROLLER_TAG => RS_DEFAULT_CONTROLLER_METHODS,
111        RS_BYTE_CONTROLLER_TAG => RS_BYTE_CONTROLLER_METHODS,
112        RS_BYOB_REQUEST_TAG => RS_BYOB_REQUEST_METHODS,
113        WRITABLE_STREAM_TAG => WRITABLE_STREAM_METHODS,
114        WS_DEFAULT_WRITER_TAG => WS_DEFAULT_WRITER_METHODS,
115        WS_DEFAULT_CONTROLLER_TAG => WS_DEFAULT_CONTROLLER_METHODS,
116        TS_DEFAULT_CONTROLLER_TAG => TS_DEFAULT_CONTROLLER_METHODS,
117        BYTE_LENGTH_STRATEGY_TAG | COUNT_STRATEGY_TAG => STRATEGY_METHODS,
118        _ => &[],
119    }
120}
121
122// ── small object / prop helpers (each a single, non-nested `with_host`) ───────
123
124fn get_prop(recv: &Value, key: &str) -> Option<Value> {
125    with_host(|h| match h.get(recv) {
126        Some(JsObj::Object(p)) => p.get(key).cloned(),
127        _ => None,
128    })
129}
130
131fn set_prop(recv: &Value, key: &str, val: Value) {
132    with_host(|h| {
133        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
134            p.insert(key.to_string(), val);
135        }
136    });
137}
138
139fn remove_prop(recv: &Value, key: &str) {
140    with_host(|h| {
141        if let Some(JsObj::Object(p)) = h.get_mut(recv) {
142            p.shift_remove(key);
143        }
144    });
145}
146
147fn get_str(recv: &Value, key: &str) -> Option<String> {
148    get_prop(recv, key).map(|v| with_host(|h| h.str_of(&v)))
149}
150
151fn state_of(stream: &Value) -> String {
152    get_str(stream, "@@state").unwrap_or_else(|| "readable".into())
153}
154
155fn is_locked(stream: &Value) -> bool {
156    get_prop(stream, "locked")
157        .map(|v| with_host(|h| h.truthy(&v)))
158        .unwrap_or(false)
159}
160
161fn is_callable_val(v: &Value) -> bool {
162    with_host(|h| crate::host::is_callable(h, v))
163}
164
165/// A source/sink option that is a function, else `None`.
166fn opt_cb(obj: &Value, key: &str) -> Option<Value> {
167    get_prop(obj, key).filter(is_callable_val)
168}
169
170/// Build a `{ value, done }` iterator result.
171fn iter_result(value: Value, done: bool) -> Value {
172    with_host(|h| {
173        let mut m = IndexMap::new();
174        m.insert("value".into(), value);
175        m.insert("done".into(), Value::Bool(done));
176        h.new_object(m)
177    })
178}
179
180fn synth(msg: &str) -> Value {
181    with_host(|h| crate::builtins::synth_error(h, msg))
182}
183
184fn new_pending() -> Value {
185    with_host(|h| h.new_promise())
186}
187
188fn settle(p: &Value, val: Value) {
189    if let Some(id) = with_host(|h| h.promise_id(p)) {
190        crate::host::resolve_promise_val(id, val);
191    }
192}
193
194fn settle_reject(p: &Value, err: Value) {
195    if let Some(id) = with_host(|h| h.promise_id(p)) {
196        crate::host::reject_promise_val(id, err);
197    }
198}
199
200fn resolved(v: Value) -> Value {
201    crate::host::promise_of(&v)
202}
203
204fn rejected(err: Value) -> Value {
205    let p = new_pending();
206    settle_reject(&p, err);
207    p
208}
209
210/// Raw bytes of a chunk: a Buffer's `@@bytes`, a TypedArray's view, else its
211/// UTF-8 string form.
212fn chunk_bytes(v: &Value) -> Vec<u8> {
213    let via_field = with_host(|h| match h.get(v) {
214        Some(JsObj::Object(p)) => {
215            let field = if p.contains_key("@@bytes") {
216                Some("@@bytes")
217            } else if p.contains_key("@@buffer") {
218                // A typed array's bytes live in its ArrayBuffer, decoded
219                // through the view rather than read out of a private vector.
220                return Some(
221                    crate::stdlib::typedarray::elems_with_host(h, v)
222                        .iter()
223                        .map(|x| h.to_number(x) as u8)
224                        .collect::<Vec<u8>>(),
225                );
226            } else {
227                None
228            };
229            field.and_then(|f| match p.get(f).and_then(|a| h.get(a)) {
230                Some(JsObj::Array(items)) => Some(
231                    items
232                        .iter()
233                        .map(|x| h.to_number(x) as u8)
234                        .collect::<Vec<u8>>(),
235                ),
236                _ => None,
237            })
238        }
239        _ => None,
240    });
241    via_field.unwrap_or_else(|| with_host(|h| h.str_of(v)).into_bytes())
242}
243
244/// A chunk's `byteLength` (its own `byteLength` prop, else its byte count).
245fn chunk_byte_length(v: &Value) -> f64 {
246    if let Some(bl) = get_prop(v, "byteLength") {
247        return with_host(|h| h.to_number(&bl));
248    }
249    chunk_bytes(v).len() as f64
250}
251
252// ── array-prop helpers (queues / waiters) ─────────────────────────────────────
253
254fn arr_push(recv: &Value, key: &str, val: Value) {
255    with_host(|h| {
256        let arr = match h.get(recv) {
257            Some(JsObj::Object(p)) => p.get(key).cloned(),
258            _ => None,
259        };
260        if let Some(a) = arr {
261            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
262                items.push(val);
263            }
264        }
265    });
266}
267
268fn arr_shift(recv: &Value, key: &str) -> Option<Value> {
269    with_host(|h| {
270        let arr = match h.get(recv) {
271            Some(JsObj::Object(p)) => p.get(key).cloned(),
272            _ => None,
273        }?;
274        match h.get_mut(&arr) {
275            Some(JsObj::Array(items)) if !items.is_empty() => Some(items.remove(0)),
276            _ => None,
277        }
278    })
279}
280
281fn arr_len(recv: &Value, key: &str) -> usize {
282    with_host(|h| match h.get(recv) {
283        Some(JsObj::Object(p)) => match p.get(key).and_then(|a| h.get(a)) {
284            Some(JsObj::Array(items)) => items.len(),
285            _ => 0,
286        },
287        _ => 0,
288    })
289}
290
291fn arr_get(recv: &Value, key: &str, i: usize) -> Option<Value> {
292    with_host(|h| match h.get(recv) {
293        Some(JsObj::Object(p)) => match p.get(key).and_then(|a| h.get(a)) {
294            Some(JsObj::Array(items)) => items.get(i).cloned(),
295            _ => None,
296        },
297        _ => None,
298    })
299}
300
301fn arr_drain(recv: &Value, key: &str) -> Vec<Value> {
302    with_host(|h| {
303        let arr = match h.get(recv) {
304            Some(JsObj::Object(p)) => p.get(key).cloned(),
305            _ => None,
306        };
307        match arr.as_ref().and_then(|a| h.get_mut(a)) {
308            Some(JsObj::Array(items)) => std::mem::take(items),
309            _ => Vec::new(),
310        }
311    })
312}
313
314// ── ReadableStream internals ──────────────────────────────────────────────────
315
316/// Allocate a bare ReadableStream shell (state `readable`, empty queue/waiters).
317fn new_readable_shell() -> Value {
318    with_host(|h| {
319        let queue = h.new_array(Vec::new());
320        let waiters = h.new_array(Vec::new());
321        let mut m = IndexMap::new();
322        m.insert("@@native".into(), h.new_str(READABLE_STREAM_TAG));
323        m.insert("@@state".into(), h.new_str("readable"));
324        m.insert("@@queue".into(), queue);
325        m.insert("@@waiters".into(), waiters);
326        m.insert("locked".into(), Value::Bool(false));
327        h.new_object(m)
328    })
329}
330
331/// A default (or byte) controller wired back to `stream`.
332fn new_readable_controller(stream: &Value, byte: bool) -> Value {
333    let tag = if byte {
334        RS_BYTE_CONTROLLER_TAG
335    } else {
336        RS_DEFAULT_CONTROLLER_TAG
337    };
338    let ctrl = with_host(|h| {
339        let mut m = IndexMap::new();
340        m.insert("@@native".into(), h.new_str(tag));
341        m.insert("@@stream".into(), stream.clone());
342        m.insert("desiredSize".into(), Value::Float(1.0));
343        if byte {
344            m.insert("byobRequest".into(), h.null());
345        }
346        h.new_object(m)
347    });
348    set_prop(stream, "@@controller", ctrl.clone());
349    ctrl
350}
351
352/// Enqueue `chunk`: hand it to the oldest pending reader if one is waiting, else
353/// append to the internal queue.
354fn stream_enqueue(stream: &Value, chunk: Value) {
355    if state_of(stream) != "readable" {
356        return;
357    }
358    if let Some(waiter) = arr_shift(stream, "@@waiters") {
359        let r = iter_result(chunk, false);
360        settle(&waiter, r);
361    } else {
362        arr_push(stream, "@@queue", chunk);
363    }
364}
365
366/// Close the stream: settle every pending reader with `{done:true}`.
367fn stream_close(stream: &Value) {
368    if state_of(stream) != "readable" {
369        return;
370    }
371    set_prop(stream, "@@state", with_host(|h| h.new_str("closed")));
372    for waiter in arr_drain(stream, "@@waiters") {
373        let r = iter_result(Value::Undef, true);
374        settle(&waiter, r);
375    }
376    // Resolve a reader's `closed` promise if one is attached.
377    if let Some(reader) = get_prop(stream, "@@reader") {
378        if let Some(cp) = get_prop(&reader, "closed") {
379            settle(&cp, Value::Undef);
380        }
381    }
382}
383
384/// Error the stream: reject every pending reader with `err`.
385fn stream_error(stream: &Value, err: Value) {
386    if state_of(stream) != "readable" {
387        return;
388    }
389    set_prop(stream, "@@state", with_host(|h| h.new_str("errored")));
390    set_prop(stream, "@@stored_error", err.clone());
391    for waiter in arr_drain(stream, "@@waiters") {
392        settle_reject(&waiter, err.clone());
393    }
394    if let Some(reader) = get_prop(stream, "@@reader") {
395        if let Some(cp) = get_prop(&reader, "closed") {
396            settle_reject(&cp, err.clone());
397        }
398    }
399}
400
401/// A settled `read()` promise if data / close / error is immediately available.
402fn try_immediate_read(stream: &Value) -> Option<Value> {
403    match state_of(stream).as_str() {
404        "errored" => {
405            let e = get_prop(stream, "@@stored_error")
406                .unwrap_or_else(|| synth("TypeError: stream errored"));
407            Some(rejected(e))
408        }
409        _ if arr_len(stream, "@@queue") > 0 => {
410            let chunk = arr_shift(stream, "@@queue").unwrap_or(Value::Undef);
411            Some(resolved(iter_result(chunk, false)))
412        }
413        "closed" => Some(resolved(iter_result(Value::Undef, true))),
414        _ => None,
415    }
416}
417
418/// Drive one synchronous production step (a JS `pull` or a native tee-pull) when
419/// the queue is empty.
420fn drive_pull(stream: &Value) {
421    if let Some(kind) = get_str(stream, "@@native_pull") {
422        if kind == "tee" {
423            tee_pull(stream);
424        }
425        return;
426    }
427    if let Some(pull) = opt_cb(stream, "@@pull") {
428        let ctrl = get_prop(stream, "@@controller").unwrap_or(Value::Undef);
429        // A `pull` that returns a promise (async pull) is accepted but its later
430        // resolution is not awaited — synchronous enqueues are what drives data.
431        if let Err(msg) = invoke(&pull, vec![ctrl], None) {
432            stream_error(stream, synth(&msg));
433        }
434    }
435}
436
437/// `reader.read()` core: resolve immediately if possible, else drive `pull` once
438/// and re-check, else return a pending promise settled by a later enqueue/close.
439fn stream_read(stream: &Value) -> Value {
440    if let Some(p) = try_immediate_read(stream) {
441        return p;
442    }
443    drive_pull(stream);
444    if let Some(p) = try_immediate_read(stream) {
445        return p;
446    }
447    // Still readable and empty: a genuine pending read, settled on next enqueue.
448    let p = new_pending();
449    arr_push(stream, "@@waiters", p.clone());
450    p
451}
452
453/// `stream.cancel(reason)` / `reader.cancel(reason)`: close and run `cancel`.
454fn stream_cancel(stream: &Value, reason: Value) -> Value {
455    if state_of(stream) == "readable" {
456        // Discard buffered chunks, then close.
457        let _ = arr_drain(stream, "@@queue");
458        if let Some(cancel) = opt_cb(stream, "@@cancel") {
459            if let Err(msg) = invoke(&cancel, vec![reason], None) {
460                return rejected(synth(&msg));
461            }
462        }
463        stream_close(stream);
464    }
465    resolved(Value::Undef)
466}
467
468// ── ReadableStream construction ───────────────────────────────────────────────
469
470/// `new ReadableStream(underlyingSource, strategy)`.
471pub fn construct_readable(args: &[Value]) -> Result<Value, String> {
472    let source = args.first().cloned().unwrap_or(Value::Undef);
473    let byte = get_str(&source, "type").as_deref() == Some("bytes");
474
475    let stream = new_readable_shell();
476    if byte {
477        set_prop(&stream, "@@type", with_host(|h| h.new_str("bytes")));
478    }
479    if let Some(pull) = opt_cb(&source, "pull") {
480        set_prop(&stream, "@@pull", pull);
481    }
482    if let Some(cancel) = opt_cb(&source, "cancel") {
483        set_prop(&stream, "@@cancel", cancel);
484    }
485    let ctrl = new_readable_controller(&stream, byte);
486
487    if let Some(start) = opt_cb(&source, "start") {
488        if let Err(msg) = invoke(&start, vec![ctrl], None) {
489            stream_error(&stream, synth(&msg));
490        }
491    }
492    Ok(stream)
493}
494
495/// `stream.getReader([{ mode }])`.
496fn get_reader(stream: &Value, args: &[Value]) -> Result<Value, String> {
497    if is_locked(stream) {
498        return Err(crate::host::type_error("ReadableStream is locked"));
499    }
500    let byob = args.first().and_then(|o| get_str(o, "mode")).as_deref() == Some("byob");
501    let tag = if byob {
502        RS_BYOB_READER_TAG
503    } else {
504        RS_DEFAULT_READER_TAG
505    };
506
507    // The reader's `closed` promise reflects the stream's terminal state.
508    let closed = new_pending();
509    match state_of(stream).as_str() {
510        "closed" => settle(&closed, Value::Undef),
511        "errored" => settle_reject(
512            &closed,
513            get_prop(stream, "@@stored_error")
514                .unwrap_or_else(|| synth("TypeError: stream errored")),
515        ),
516        _ => {}
517    }
518
519    let reader = with_host(|h| {
520        let mut m = IndexMap::new();
521        m.insert("@@native".into(), h.new_str(tag));
522        m.insert("@@stream".into(), stream.clone());
523        m.insert("closed".into(), closed);
524        h.new_object(m)
525    });
526    set_prop(stream, "locked", Value::Bool(true));
527    set_prop(stream, "@@reader", reader.clone());
528    Ok(reader)
529}
530
531fn reader_release(reader: &Value) {
532    if let Some(stream) = get_prop(reader, "@@stream") {
533        set_prop(&stream, "locked", Value::Bool(false));
534        remove_prop(&stream, "@@reader");
535    }
536    remove_prop(reader, "@@stream");
537}
538
539// ── WritableStream internals ──────────────────────────────────────────────────
540
541/// Accept one chunk into a writable's sink (a JS `write`, or a built-in codec /
542/// transform dispatch keyed by `@@xform_kind`).
543fn ws_accept_chunk(ws: &Value, chunk: Value) -> Result<(), String> {
544    if let Some(kind) = get_str(ws, "@@xform_kind") {
545        let readable = get_prop(ws, "@@readable");
546        match kind.as_str() {
547            "textencode" => {
548                if let Some(r) = &readable {
549                    let bytes = with_host(|h| h.str_of(&chunk)).into_bytes();
550                    let buf = super::buffer::from_bytes(&bytes);
551                    stream_enqueue(r, buf);
552                }
553            }
554            "textdecode" => {
555                if let Some(r) = &readable {
556                    let bytes = chunk_bytes(&chunk);
557                    let s = with_host(|h| h.new_str(String::from_utf8_lossy(&bytes).into_owned()));
558                    stream_enqueue(r, s);
559                }
560            }
561            k if k.starts_with("compress:") || k.starts_with("decompress:") => {
562                let mut bytes = chunk_bytes(&chunk);
563                arr_push_bytes(ws, &mut bytes);
564            }
565            "js" => {
566                let ctrl = get_prop(ws, "@@tcontroller").unwrap_or(Value::Undef);
567                if let Some(transform) = opt_cb(ws, "@@transform") {
568                    invoke(&transform, vec![chunk, ctrl], None)?;
569                } else if let Some(r) = &readable {
570                    // Identity transform: pass the chunk straight through.
571                    stream_enqueue(r, chunk);
572                }
573            }
574            _ => {}
575        }
576        return Ok(());
577    }
578    if let Some(write) = opt_cb(ws, "@@write") {
579        let ctrl = get_prop(ws, "@@controller").unwrap_or(Value::Undef);
580        invoke(&write, vec![chunk, ctrl], None)?;
581    }
582    Ok(())
583}
584
585/// Append raw bytes onto a writable's `@@accum` byte buffer (for codec streams).
586fn arr_push_bytes(ws: &Value, bytes: &mut Vec<u8>) {
587    with_host(|h| {
588        let accum = match h.get(ws) {
589            Some(JsObj::Object(p)) => p.get("@@accum").cloned(),
590            _ => None,
591        };
592        if let Some(a) = accum {
593            if let Some(JsObj::Array(items)) = h.get_mut(&a) {
594                items.extend(bytes.drain(..).map(|b| Value::Float(b as f64)));
595            }
596        }
597    });
598}
599
600fn ws_accum_bytes(ws: &Value) -> Vec<u8> {
601    with_host(|h| match h.get(ws) {
602        Some(JsObj::Object(p)) => match p.get("@@accum").and_then(|a| h.get(a)) {
603            Some(JsObj::Array(items)) => items.iter().map(|x| h.to_number(x) as u8).collect(),
604            _ => Vec::new(),
605        },
606        _ => Vec::new(),
607    })
608}
609
610/// Finish a writable's sink: run flush / codec, close a paired readable, mark
611/// the writable closed and settle its `closed` promise.
612fn ws_finish(ws: &Value) -> Result<(), String> {
613    if let Some(kind) = get_str(ws, "@@xform_kind") {
614        let readable = get_prop(ws, "@@readable");
615        match kind.as_str() {
616            "js" => {
617                let ctrl = get_prop(ws, "@@tcontroller").unwrap_or(Value::Undef);
618                if let Some(flush) = opt_cb(ws, "@@flush") {
619                    invoke(&flush, vec![ctrl], None)?;
620                }
621                if let Some(r) = &readable {
622                    stream_close(r);
623                }
624            }
625            k if k.starts_with("compress:") || k.starts_with("decompress:") => {
626                let data = ws_accum_bytes(ws);
627                let out = run_codec(&kind, &data)?;
628                if let Some(r) = &readable {
629                    stream_enqueue(r, super::buffer::from_bytes(&out));
630                    stream_close(r);
631                }
632            }
633            _ => {
634                if let Some(r) = &readable {
635                    stream_close(r);
636                }
637            }
638        }
639    } else if let Some(close) = opt_cb(ws, "@@close") {
640        invoke(&close, vec![], None)?;
641    }
642    set_prop(ws, "@@state", with_host(|h| h.new_str("closed")));
643    if let Some(writer) = get_prop(ws, "@@writer") {
644        if let Some(cp) = get_prop(&writer, "closed") {
645            settle(&cp, Value::Undef);
646        }
647    }
648    Ok(())
649}
650
651/// Abort/error a writable: mark errored, error a paired readable, reject `closed`.
652fn ws_abort(ws: &Value, reason: Value) -> Result<(), String> {
653    if get_str(ws, "@@xform_kind").is_some() {
654        if let Some(r) = get_prop(ws, "@@readable") {
655            stream_error(&r, reason.clone());
656        }
657    } else if let Some(abort) = opt_cb(ws, "@@abort") {
658        invoke(&abort, vec![reason.clone()], None)?;
659    }
660    set_prop(ws, "@@state", with_host(|h| h.new_str("errored")));
661    set_prop(ws, "@@stored_error", reason.clone());
662    if let Some(writer) = get_prop(ws, "@@writer") {
663        if let Some(cp) = get_prop(&writer, "closed") {
664            settle_reject(&cp, reason);
665        }
666    }
667    Ok(())
668}
669
670/// `new WritableStream(underlyingSink, strategy)`.
671pub fn construct_writable(args: &[Value]) -> Result<Value, String> {
672    let sink = args.first().cloned().unwrap_or(Value::Undef);
673    let ws = with_host(|h| {
674        let mut m = IndexMap::new();
675        m.insert("@@native".into(), h.new_str(WRITABLE_STREAM_TAG));
676        m.insert("@@state".into(), h.new_str("writable"));
677        m.insert("locked".into(), Value::Bool(false));
678        h.new_object(m)
679    });
680    for key in ["write", "close", "abort"] {
681        if let Some(cb) = opt_cb(&sink, key) {
682            set_prop(&ws, &format!("@@{key}"), cb);
683        }
684    }
685    let ctrl = with_host(|h| {
686        let mut m = IndexMap::new();
687        m.insert("@@native".into(), h.new_str(WS_DEFAULT_CONTROLLER_TAG));
688        m.insert("@@stream".into(), ws.clone());
689        h.new_object(m)
690    });
691    set_prop(&ws, "@@controller", ctrl.clone());
692    if let Some(start) = opt_cb(&sink, "start") {
693        if let Err(msg) = invoke(&start, vec![ctrl], None) {
694            let _ = ws_abort(&ws, synth(&msg));
695        }
696    }
697    Ok(ws)
698}
699
700/// `writable.getWriter()`.
701fn get_writer(ws: &Value) -> Result<Value, String> {
702    if is_locked(ws) {
703        return Err(crate::host::type_error("WritableStream is locked"));
704    }
705    let ready = resolved(Value::Undef);
706    let closed = new_pending();
707    if state_of(ws) == "closed" {
708        settle(&closed, Value::Undef);
709    } else if state_of(ws) == "errored" {
710        settle_reject(
711            &closed,
712            get_prop(ws, "@@stored_error").unwrap_or(Value::Undef),
713        );
714    }
715    let writer = with_host(|h| {
716        let mut m = IndexMap::new();
717        m.insert("@@native".into(), h.new_str(WS_DEFAULT_WRITER_TAG));
718        m.insert("@@stream".into(), ws.clone());
719        m.insert("ready".into(), ready);
720        m.insert("closed".into(), closed);
721        m.insert("desiredSize".into(), Value::Float(1.0));
722        h.new_object(m)
723    });
724    set_prop(ws, "locked", Value::Bool(true));
725    set_prop(ws, "@@writer", writer.clone());
726    Ok(writer)
727}
728
729fn writer_release(writer: &Value) {
730    if let Some(ws) = get_prop(writer, "@@stream") {
731        set_prop(&ws, "locked", Value::Bool(false));
732        remove_prop(&ws, "@@writer");
733    }
734    remove_prop(writer, "@@stream");
735}
736
737// ── TransformStream ───────────────────────────────────────────────────────────
738
739/// Build the readable + writable pair shared by `TransformStream` and the
740/// built-in codec/text streams. `kind` selects the writable's sink behaviour.
741fn build_transform_pair(kind: &str) -> (Value, Value) {
742    let readable = new_readable_shell();
743    let ws = with_host(|h| {
744        let accum = h.new_array(Vec::new());
745        let mut m = IndexMap::new();
746        m.insert("@@native".into(), h.new_str(WRITABLE_STREAM_TAG));
747        m.insert("@@state".into(), h.new_str("writable"));
748        m.insert("locked".into(), Value::Bool(false));
749        m.insert("@@xform_kind".into(), h.new_str(kind));
750        m.insert("@@accum".into(), accum);
751        h.new_object(m)
752    });
753    set_prop(&ws, "@@readable", readable.clone());
754    (readable, ws)
755}
756
757/// `new TransformStream(transformer, writableStrategy, readableStrategy)`.
758pub fn construct_transform(args: &[Value]) -> Result<Value, String> {
759    let transformer = args.first().cloned().unwrap_or(Value::Undef);
760    let (readable, writable) = build_transform_pair("js");
761
762    // The transform controller enqueues into the readable side.
763    let tctrl = with_host(|h| {
764        let mut m = IndexMap::new();
765        m.insert("@@native".into(), h.new_str(TS_DEFAULT_CONTROLLER_TAG));
766        m.insert("@@readable".into(), readable.clone());
767        m.insert("@@writable".into(), writable.clone());
768        m.insert("desiredSize".into(), Value::Float(1.0));
769        h.new_object(m)
770    });
771    set_prop(&writable, "@@tcontroller", tctrl.clone());
772    if let Some(t) = opt_cb(&transformer, "transform") {
773        set_prop(&writable, "@@transform", t);
774    }
775    if let Some(f) = opt_cb(&transformer, "flush") {
776        set_prop(&writable, "@@flush", f);
777    }
778
779    let ts = with_host(|h| {
780        let mut m = IndexMap::new();
781        m.insert("@@native".into(), h.new_str(TRANSFORM_STREAM_TAG));
782        m.insert("readable".into(), readable);
783        m.insert("writable".into(), writable);
784        h.new_object(m)
785    });
786
787    if let Some(start) = opt_cb(&transformer, "start") {
788        invoke(&start, vec![tctrl], None)?;
789    }
790    Ok(ts)
791}
792
793// ── text / codec transform streams ────────────────────────────────────────────
794
795fn build_codec_stream(tag: &str, kind: &str, extra: Vec<(&str, Value)>) -> Value {
796    let (readable, writable) = build_transform_pair(kind);
797    with_host(|h| {
798        let mut m = IndexMap::new();
799        m.insert("@@native".into(), h.new_str(tag));
800        m.insert("readable".into(), readable);
801        m.insert("writable".into(), writable);
802        for (k, v) in extra {
803            m.insert(k.to_string(), v);
804        }
805        h.new_object(m)
806    })
807}
808
809pub fn construct_text_encoder_stream() -> Result<Value, String> {
810    let enc = with_host(|h| h.new_str("utf-8"));
811    Ok(build_codec_stream(
812        TEXT_ENCODER_STREAM_TAG,
813        "textencode",
814        vec![("encoding", enc)],
815    ))
816}
817
818pub fn construct_text_decoder_stream(args: &[Value]) -> Result<Value, String> {
819    let label = args
820        .first()
821        .map(|v| with_host(|h| h.str_of(v)))
822        .filter(|s| !s.is_empty() && s.as_str() != "undefined")
823        .unwrap_or_else(|| "utf-8".into());
824    let extra = with_host(|h| {
825        vec![
826            ("encoding", h.new_str(label.to_lowercase())),
827            ("fatal", Value::Bool(false)),
828            ("ignoreBOM", Value::Bool(false)),
829        ]
830    });
831    Ok(build_codec_stream(
832        TEXT_DECODER_STREAM_TAG,
833        "textdecode",
834        extra,
835    ))
836}
837
838/// `new CompressionStream(format)` — `format` ∈ {gzip, deflate, deflate-raw}.
839pub fn construct_compression(args: &[Value], decompress: bool) -> Result<Value, String> {
840    let format = super::arg_str(args, 0);
841    if !matches!(format.as_str(), "gzip" | "deflate" | "deflate-raw") {
842        return Err(crate::host::type_error(&format!(
843            "Unsupported compression format: '{format}'"
844        )));
845    }
846    let (tag, prefix) = if decompress {
847        (DECOMPRESSION_STREAM_TAG, "decompress")
848    } else {
849        (COMPRESSION_STREAM_TAG, "compress")
850    };
851    Ok(build_codec_stream(
852        tag,
853        &format!("{prefix}:{format}"),
854        Vec::new(),
855    ))
856}
857
858/// Run a buffered codec over `data` (called on stream close).
859fn run_codec(kind: &str, data: &[u8]) -> Result<Vec<u8>, String> {
860    use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder};
861    use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder};
862    use flate2::Compression;
863    let io = |e: std::io::Error| format!("Error: {e}");
864    match kind {
865        "compress:gzip" => {
866            let mut e = GzEncoder::new(Vec::new(), Compression::default());
867            e.write_all(data).map_err(io)?;
868            e.finish().map_err(io)
869        }
870        "compress:deflate" => {
871            let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
872            e.write_all(data).map_err(io)?;
873            e.finish().map_err(io)
874        }
875        "compress:deflate-raw" => {
876            let mut e = DeflateEncoder::new(Vec::new(), Compression::default());
877            e.write_all(data).map_err(io)?;
878            e.finish().map_err(io)
879        }
880        "decompress:gzip" => {
881            let mut out = Vec::new();
882            GzDecoder::new(data).read_to_end(&mut out).map_err(io)?;
883            Ok(out)
884        }
885        "decompress:deflate" => {
886            let mut out = Vec::new();
887            ZlibDecoder::new(data).read_to_end(&mut out).map_err(io)?;
888            Ok(out)
889        }
890        "decompress:deflate-raw" => {
891            let mut out = Vec::new();
892            DeflateDecoder::new(data)
893                .read_to_end(&mut out)
894                .map_err(io)?;
895            Ok(out)
896        }
897        _ => Err(crate::host::type_error("unknown codec")),
898    }
899}
900
901// ── queuing strategies ────────────────────────────────────────────────────────
902
903pub fn construct_strategy(tag: &str, args: &[Value]) -> Result<Value, String> {
904    let hwm = args
905        .first()
906        .and_then(|o| get_prop(o, "highWaterMark"))
907        .map(|v| with_host(|h| h.to_number(&v)))
908        .unwrap_or(f64::NAN);
909    Ok(with_host(|h| {
910        let mut m = IndexMap::new();
911        m.insert("@@native".into(), h.new_str(tag));
912        m.insert("highWaterMark".into(), Value::Float(hwm));
913        h.new_object(m)
914    }))
915}
916
917// ── byte-stream / BYOB (basic) ────────────────────────────────────────────────
918
919/// A pulled value from a source (used by `pipeTo`/`tee`/BYOB drains).
920enum Pulled {
921    Chunk(Value),
922    Done,
923    Errored(Value),
924}
925
926/// One synchronous production step for a drain: return a chunk, or `Done`/`Errored`.
927fn pull_one(stream: &Value) -> Pulled {
928    if state_of(stream) == "errored" {
929        return Pulled::Errored(get_prop(stream, "@@stored_error").unwrap_or(Value::Undef));
930    }
931    if let Some(c) = arr_shift(stream, "@@queue") {
932        return Pulled::Chunk(c);
933    }
934    if state_of(stream) == "closed" {
935        return Pulled::Done;
936    }
937    drive_pull(stream);
938    if state_of(stream) == "errored" {
939        return Pulled::Errored(get_prop(stream, "@@stored_error").unwrap_or(Value::Undef));
940    }
941    if let Some(c) = arr_shift(stream, "@@queue") {
942        return Pulled::Chunk(c);
943    }
944    // Empty after a pull step: end the drain (cannot suspend synchronously).
945    Pulled::Done
946}
947
948/// `byobReader.read(view)` — fills `view` from the next chunk (best-effort).
949fn byob_read(reader: &Value, args: &[Value]) -> Value {
950    let Some(stream) = get_prop(reader, "@@stream") else {
951        return rejected(synth("TypeError: reader has no associated stream"));
952    };
953    let view = args.first().cloned().unwrap_or(Value::Undef);
954    match pull_one(&stream) {
955        Pulled::Chunk(c) => {
956            let bytes = chunk_bytes(&c);
957            // Return a fresh view over the copied bytes (does not reuse the
958            // caller's ArrayBuffer — a documented BYOB simplification).
959            resolved(iter_result(super::buffer::from_bytes(&bytes), false))
960        }
961        Pulled::Done => resolved(iter_result(view, true)),
962        Pulled::Errored(e) => rejected(e),
963    }
964}
965
966// ── tee ───────────────────────────────────────────────────────────────────────
967
968/// Native pull for a tee branch: serve from the shared buffer at the branch's
969/// cursor, pulling one more chunk from the source when the cursor runs ahead.
970fn tee_pull(branch: &Value) {
971    let Some(shared) = get_prop(branch, "@@tee_shared") else {
972        return;
973    };
974    let idx = get_prop(branch, "@@tee_index")
975        .map(|v| with_host(|h| h.to_number(&v)) as usize)
976        .unwrap_or(0);
977
978    if let Some(chunk) = arr_get(&shared, "@@buf", idx) {
979        set_prop(branch, "@@tee_index", Value::Float((idx + 1) as f64));
980        stream_enqueue(branch, chunk);
981        return;
982    }
983    let done = get_prop(&shared, "@@done")
984        .map(|v| with_host(|h| h.truthy(&v)))
985        .unwrap_or(false);
986    if done {
987        stream_close(branch);
988        return;
989    }
990    let Some(source) = get_prop(&shared, "@@source") else {
991        stream_close(branch);
992        return;
993    };
994    match pull_one(&source) {
995        Pulled::Chunk(c) => {
996            arr_push(&shared, "@@buf", c.clone());
997            set_prop(branch, "@@tee_index", Value::Float((idx + 1) as f64));
998            stream_enqueue(branch, c);
999        }
1000        Pulled::Done => {
1001            set_prop(&shared, "@@done", Value::Bool(true));
1002            stream_close(branch);
1003        }
1004        Pulled::Errored(e) => stream_error(branch, e),
1005    }
1006}
1007
1008/// `stream.tee()` → `[branch1, branch2]`, lazily sharing the source.
1009fn tee(stream: &Value) -> Value {
1010    set_prop(stream, "locked", Value::Bool(true));
1011    let shared = with_host(|h| {
1012        let buf = h.new_array(Vec::new());
1013        let mut m = IndexMap::new();
1014        m.insert("@@source".into(), stream.clone());
1015        m.insert("@@buf".into(), buf);
1016        m.insert("@@done".into(), Value::Bool(false));
1017        h.new_object(m)
1018    });
1019    let make_branch = || {
1020        let b = new_readable_shell();
1021        new_readable_controller(&b, false);
1022        set_prop(&b, "@@native_pull", with_host(|h| h.new_str("tee")));
1023        set_prop(&b, "@@tee_shared", shared.clone());
1024        set_prop(&b, "@@tee_index", Value::Float(0.0));
1025        b
1026    };
1027    let b1 = make_branch();
1028    let b2 = make_branch();
1029    with_host(|h| h.new_array(vec![b1, b2]))
1030}
1031
1032// ── pipeTo / pipeThrough ──────────────────────────────────────────────────────
1033
1034/// `stream.pipeTo(destWritable)` — synchronously drain the source into the sink,
1035/// then close it. Returns a resolved (or rejected on error) promise.
1036fn pipe_to(stream: &Value, dest: &Value) -> Value {
1037    set_prop(stream, "locked", Value::Bool(true));
1038    set_prop(dest, "locked", Value::Bool(true));
1039    loop {
1040        match pull_one(stream) {
1041            Pulled::Chunk(c) => {
1042                if let Err(msg) = ws_accept_chunk(dest, c) {
1043                    return rejected(synth(&msg));
1044                }
1045            }
1046            Pulled::Done => break,
1047            Pulled::Errored(e) => {
1048                let _ = ws_abort(dest, e.clone());
1049                return rejected(e);
1050            }
1051        }
1052    }
1053    if let Err(msg) = ws_finish(dest) {
1054        return rejected(synth(&msg));
1055    }
1056    resolved(Value::Undef)
1057}
1058
1059/// `stream.pipeThrough({ writable, readable })` — pipe into `writable`, return
1060/// `readable`. The pipe runs synchronously so `readable` is already fed on return.
1061fn pipe_through(stream: &Value, args: &[Value]) -> Result<Value, String> {
1062    let transform = args.first().cloned().unwrap_or(Value::Undef);
1063    let writable = get_prop(&transform, "writable").ok_or_else(|| {
1064        crate::host::type_error("pipeThrough argument must have a writable and readable")
1065    })?;
1066    let readable = get_prop(&transform, "readable").ok_or_else(|| {
1067        crate::host::type_error("pipeThrough argument must have a writable and readable")
1068    })?;
1069    let _ = pipe_to(stream, &writable);
1070    Ok(readable)
1071}
1072
1073// ── construct dispatch ────────────────────────────────────────────────────────
1074
1075/// `new <Class>(...)` for every `stream/web` class. `None` if `name` is not ours.
1076pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
1077    Some(match name {
1078        "ReadableStream" => construct_readable(args),
1079        "WritableStream" => construct_writable(args),
1080        "TransformStream" => construct_transform(args),
1081        "TextEncoderStream" => construct_text_encoder_stream(),
1082        "TextDecoderStream" => construct_text_decoder_stream(args),
1083        "CompressionStream" => construct_compression(args, false),
1084        "DecompressionStream" => construct_compression(args, true),
1085        "ByteLengthQueuingStrategy" => construct_strategy(BYTE_LENGTH_STRATEGY_TAG, args),
1086        "CountQueuingStrategy" => construct_strategy(COUNT_STRATEGY_TAG, args),
1087        // The controller/reader classes are normally handed out by the streams
1088        // above; direct construction yields a bare tagged shell so `instanceof`
1089        // and manual wiring work.
1090        "ReadableStreamDefaultReader" => Ok(bare(RS_DEFAULT_READER_TAG)),
1091        "ReadableStreamBYOBReader" => Ok(bare(RS_BYOB_READER_TAG)),
1092        "ReadableStreamDefaultController" => Ok(bare(RS_DEFAULT_CONTROLLER_TAG)),
1093        "ReadableByteStreamController" => Ok(bare(RS_BYTE_CONTROLLER_TAG)),
1094        "ReadableStreamBYOBRequest" => Ok(bare(RS_BYOB_REQUEST_TAG)),
1095        "WritableStreamDefaultWriter" => Ok(bare(WS_DEFAULT_WRITER_TAG)),
1096        "WritableStreamDefaultController" => Ok(bare(WS_DEFAULT_CONTROLLER_TAG)),
1097        "TransformStreamDefaultController" => Ok(bare(TS_DEFAULT_CONTROLLER_TAG)),
1098        _ => return None,
1099    })
1100}
1101
1102fn bare(tag: &str) -> Value {
1103    with_host(|h| {
1104        let mut m = IndexMap::new();
1105        m.insert("@@native".into(), h.new_str(tag));
1106        h.new_object(m)
1107    })
1108}
1109
1110// ── instance dispatch ─────────────────────────────────────────────────────────
1111
1112/// Method dispatch for every `stream/web` native instance.
1113pub fn instance_call(
1114    tag: &str,
1115    recv: &Value,
1116    method: &str,
1117    args: Vec<Value>,
1118) -> Result<Value, String> {
1119    match tag {
1120        READABLE_STREAM_TAG => match method {
1121            "getReader" => get_reader(recv, &args),
1122            "cancel" => Ok(stream_cancel(
1123                recv,
1124                args.first().cloned().unwrap_or(Value::Undef),
1125            )),
1126            "tee" => Ok(tee(recv)),
1127            "pipeTo" => Ok(pipe_to(
1128                recv,
1129                &args.first().cloned().unwrap_or(Value::Undef),
1130            )),
1131            "pipeThrough" => pipe_through(recv, &args),
1132            _ => unknown(tag, method),
1133        },
1134        RS_DEFAULT_READER_TAG => match method {
1135            "read" => match get_prop(recv, "@@stream") {
1136                Some(stream) => Ok(stream_read(&stream)),
1137                None => Ok(rejected(synth(
1138                    "TypeError: reader has no associated stream",
1139                ))),
1140            },
1141            "releaseLock" => {
1142                reader_release(recv);
1143                Ok(Value::Undef)
1144            }
1145            "cancel" => match get_prop(recv, "@@stream") {
1146                Some(stream) => Ok(stream_cancel(
1147                    &stream,
1148                    args.first().cloned().unwrap_or(Value::Undef),
1149                )),
1150                None => Ok(resolved(Value::Undef)),
1151            },
1152            _ => unknown(tag, method),
1153        },
1154        RS_BYOB_READER_TAG => match method {
1155            "read" => Ok(byob_read(recv, &args)),
1156            "releaseLock" => {
1157                reader_release(recv);
1158                Ok(Value::Undef)
1159            }
1160            "cancel" => match get_prop(recv, "@@stream") {
1161                Some(stream) => Ok(stream_cancel(
1162                    &stream,
1163                    args.first().cloned().unwrap_or(Value::Undef),
1164                )),
1165                None => Ok(resolved(Value::Undef)),
1166            },
1167            _ => unknown(tag, method),
1168        },
1169        RS_DEFAULT_CONTROLLER_TAG | RS_BYTE_CONTROLLER_TAG => {
1170            let stream = get_prop(recv, "@@stream").unwrap_or(Value::Undef);
1171            match method {
1172                "enqueue" => {
1173                    stream_enqueue(&stream, args.first().cloned().unwrap_or(Value::Undef));
1174                    Ok(Value::Undef)
1175                }
1176                "close" => {
1177                    stream_close(&stream);
1178                    Ok(Value::Undef)
1179                }
1180                "error" => {
1181                    stream_error(&stream, args.first().cloned().unwrap_or(Value::Undef));
1182                    Ok(Value::Undef)
1183                }
1184                _ => unknown(tag, method),
1185            }
1186        }
1187        RS_BYOB_REQUEST_TAG => match method {
1188            // BYOB request is exposed for completeness; respond is a no-op in the
1189            // synchronous byte model (the reader copies bytes itself).
1190            "respond" | "respondWithNewView" => Ok(Value::Undef),
1191            _ => unknown(tag, method),
1192        },
1193        WRITABLE_STREAM_TAG => match method {
1194            "getWriter" => get_writer(recv),
1195            "close" => match ws_finish(recv) {
1196                Ok(()) => Ok(resolved(Value::Undef)),
1197                Err(msg) => Ok(rejected(synth(&msg))),
1198            },
1199            "abort" => match ws_abort(recv, args.first().cloned().unwrap_or(Value::Undef)) {
1200                Ok(()) => Ok(resolved(Value::Undef)),
1201                Err(msg) => Ok(rejected(synth(&msg))),
1202            },
1203            _ => unknown(tag, method),
1204        },
1205        WS_DEFAULT_WRITER_TAG => {
1206            let ws = get_prop(recv, "@@stream").unwrap_or(Value::Undef);
1207            match method {
1208                "write" => {
1209                    if state_of(&ws) == "errored" {
1210                        return Ok(rejected(
1211                            get_prop(&ws, "@@stored_error").unwrap_or(Value::Undef),
1212                        ));
1213                    }
1214                    match ws_accept_chunk(&ws, args.first().cloned().unwrap_or(Value::Undef)) {
1215                        Ok(()) => Ok(resolved(Value::Undef)),
1216                        Err(msg) => Ok(rejected(synth(&msg))),
1217                    }
1218                }
1219                "close" => match ws_finish(&ws) {
1220                    Ok(()) => Ok(resolved(Value::Undef)),
1221                    Err(msg) => Ok(rejected(synth(&msg))),
1222                },
1223                "abort" => match ws_abort(&ws, args.first().cloned().unwrap_or(Value::Undef)) {
1224                    Ok(()) => Ok(resolved(Value::Undef)),
1225                    Err(msg) => Ok(rejected(synth(&msg))),
1226                },
1227                "releaseLock" => {
1228                    writer_release(recv);
1229                    Ok(Value::Undef)
1230                }
1231                _ => unknown(tag, method),
1232            }
1233        }
1234        WS_DEFAULT_CONTROLLER_TAG => match method {
1235            "error" => {
1236                let ws = get_prop(recv, "@@stream").unwrap_or(Value::Undef);
1237                let _ = ws_abort(&ws, args.first().cloned().unwrap_or(Value::Undef));
1238                Ok(Value::Undef)
1239            }
1240            _ => unknown(tag, method),
1241        },
1242        TS_DEFAULT_CONTROLLER_TAG => {
1243            let readable = get_prop(recv, "@@readable").unwrap_or(Value::Undef);
1244            match method {
1245                "enqueue" => {
1246                    stream_enqueue(&readable, args.first().cloned().unwrap_or(Value::Undef));
1247                    Ok(Value::Undef)
1248                }
1249                "terminate" => {
1250                    stream_close(&readable);
1251                    Ok(Value::Undef)
1252                }
1253                "error" => {
1254                    let e = args.first().cloned().unwrap_or(Value::Undef);
1255                    stream_error(&readable, e.clone());
1256                    if let Some(ws) = get_prop(recv, "@@writable") {
1257                        let _ = ws_abort(&ws, e);
1258                    }
1259                    Ok(Value::Undef)
1260                }
1261                _ => unknown(tag, method),
1262            }
1263        }
1264        BYTE_LENGTH_STRATEGY_TAG => match method {
1265            "size" => Ok(Value::Float(chunk_byte_length(
1266                &args.first().cloned().unwrap_or(Value::Undef),
1267            ))),
1268            _ => unknown(tag, method),
1269        },
1270        COUNT_STRATEGY_TAG => match method {
1271            "size" => Ok(Value::Float(1.0)),
1272            _ => unknown(tag, method),
1273        },
1274        _ => unknown(tag, method),
1275    }
1276}
1277
1278fn unknown(tag: &str, method: &str) -> Result<Value, String> {
1279    Err(crate::host::type_error(&format!(
1280        "{tag}.{method} is not a function"
1281    )))
1282}