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