From f945efecee3541e8303ab2e9541ddbd2ff20a0c4 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 27 Oct 2026 15:55:31 -0500 Subject: [PATCH] Run JavaScript WritableStream, TransformStream and pipes natively In JavaScript sources, WritableStream with its default writer and controller, TransformStream with its controller, pipeTo and pipeThrough, and TextEncoderStream and TextDecoderStream join the native web stream classes, following Node 24's webstreams step by step: sinks and transformers, backpressure and the writers' ready and closed promises, abort with the controller's signal, the pipe's shared write requests and its abort signal, and UTF-8 encoding and decoding across chunks settle in Node's order. The streams take own properties, which a pipe does not use for its own reads and writes, and the global object answers the classes by computed name in programs that name them. A JavaScript class that extends one of these classes keeps the SC1090 refusal, and TextDecoderStream refuses encodings other than UTF-8. The node24-web-stream lifecycle, errors, queues, transforms, pipes, pipe-errors, pipe-abort, tee and encoding cases now match Node. --- .../builtin-apis/streams-and-readline.mdx | 13 +- .../src/generated/node-v24-compatibility.json | 6 +- .../generated/node-v24-internal.json | 12 +- internal/compatibility/static-support.json | 3 +- .../lowering/builtins/web-streams-dyn.ts | 36 + .../src/frontend/lowering/global-values.ts | 12 +- .../compiler/src/frontend/lowering/lowerer.ts | 3 + packages/compiler/src/ir/runtime-features.ts | 5 +- ...eb-stream-transform-pipes-untyped.mjs.json | 6 + .../web-stream-writable-untyped.mjs.json | 6 + packages/runtime/src/scr_dyn.h | 9 +- packages/runtime/src/scr_webstreams.c | 2752 ++++++++++++++++- .../web-stream-transform-pipes-untyped.mjs | 151 + tests/corpus/web-stream-writable-untyped.mjs | 144 + ...eb-stream-transform-pipes-untyped.mjs.json | 1 + .../web-stream-writable-untyped.mjs.json | 1 + tests/harness/npm.test.ts | 11 +- 17 files changed, 3114 insertions(+), 57 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-transform-pipes-untyped.mjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-writable-untyped.mjs.json create mode 100644 tests/corpus/web-stream-transform-pipes-untyped.mjs create mode 100644 tests/corpus/web-stream-writable-untyped.mjs create mode 100644 tests/harness/corpus-costs/web-stream-transform-pipes-untyped.mjs.json create mode 100644 tests/harness/corpus-costs/web-stream-writable-untyped.mjs.json diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index 57a43e67a..f3883fe8d 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -34,8 +34,8 @@ A `Duplex.from` body function may take the `{ signal }` argument, which is aborted with the error the Duplex is destroyed with. `stream.compose` accepts streams and async generator functions in any position, an async function as the last stage, and an array, a string, or a generator as the first stage; one stage is `Duplex.from` of it. A web `ReadableStream` is read as an async iterable by `Readable.from` and as the first stage of `compose` or `pipeline`, and through its reader by `Readable.fromWeb` (with `objectMode`, `highWaterMark`, `encoding`, and `signal` in an options object literal) and `Duplex.from`. - `new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk). - A web stream in a later compose stage, `Readable.toWeb`, a `TransformStream` with a transformer or queuing strategies, the `WritableStream` constructor, `pipeTo`, and `pipeThrough` are unsupported. + In TypeScript, `new TransformStream()` without arguments builds the identity transform: its readable side is a web `ReadableStream`, its writable side supports `getWriter`, `abort`, `close`, and `locked`, and the writer supports `write`, `close`, `abort`, `releaseLock`, `ready`, `closed`, and `desiredSize`, with Node's backpressure (a write completes when the readable side reads its chunk). + A web stream in a later compose stage and `Readable.toWeb` are unsupported, and in TypeScript, so are a `TransformStream` with a transformer or queuing strategies, the `WritableStream` constructor, `pipeTo`, and `pipeThrough`. The `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream. `readable.compose(stage, { signal })` is `stream.compose(readable, stage)` with the signal added; the options are an object literal whose `signal` is a variable or a property read. `pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, with or without function stages, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags. @@ -60,8 +60,9 @@ The `readline/promises` `Readline` class writes to the same outputs. `readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences. Chained listener registration is unsupported. - `node:stream/web` exports the same `ReadableStream` class as the global; in TypeScript, its other classes are unsupported. - In JavaScript, `ReadableStream`, `ReadableStreamDefaultReader`, `ReadableStreamDefaultController`, `CountQueuingStrategy`, and `ByteLengthQueuingStrategy`, as globals and as `node:stream/web` exports, are constructor values with Node's prototypes, brand checks, and errors. - Their underlying sources (`start`, `pull`, `cancel`), queuing strategies with a `size` function, readers, `tee`, `ReadableStream.from`, and async iteration settle promises in Node's order. + `node:stream/web` exports the same classes as the globals; in TypeScript, only `ReadableStream` and the identity `TransformStream` have lowerings. + In JavaScript, `ReadableStream`, `WritableStream`, and `TransformStream` with their default readers, writers, and controllers, `CountQueuingStrategy`, `ByteLengthQueuingStrategy`, `TextEncoderStream`, and `TextDecoderStream`, as globals and as `node:stream/web` exports, are constructor values with Node's prototypes, brand checks, and errors. + Their underlying sources, sinks, and transformers, queuing strategies with a `size` function, readers and writers, `tee`, `pipeTo` and `pipeThrough` with their options and an abort signal, `ReadableStream.from`, and async iteration settle promises in Node's order, and the streams take own properties. + `TextDecoderStream` decodes UTF-8; its other encodings are unsupported. A JavaScript `ReadableStream` given as a `Response` or `Request` body, or as `fetch`'s body, is read through a stream of its own: `response.body` is that stream rather than the one passed in, and the stream passed in is locked from the start. - In JavaScript, a byte stream (`type: 'bytes'`), `pipeTo`, `pipeThrough`, `Object.getPrototypeOf` on these streams, readers, controllers, and strategies, their use as `WeakMap` and `WeakSet` keys, `ReadableStream.from` of a function that is not iterable, and the other `node:stream/web` classes are unsupported. + In JavaScript, a byte stream (`type: 'bytes'`), a BYOB reader, a class that extends one of these classes, `Object.getPrototypeOf` on these values, their use as `WeakMap` and `WeakSet` keys, and `ReadableStream.from` of a function that is not iterable are unsupported. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 56e26c232..387f5b21c 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -81289,9 +81289,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented static async-iteration subset.", + "verification": "test-backed" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 43aa5e38b..00d6f8db3 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -111929,6 +111929,8 @@ "tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/web-stream-readable-untyped.mjs", + "tests/corpus/web-stream-transform-pipes-untyped.mjs", + "tests/corpus/web-stream-writable-untyped.mjs", "tests/harness/fetch-conformance.test.ts" ] }, @@ -113011,8 +113013,11 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webstreams" + "status": "partial", + "evidence": "compiler-feature:webstreams.WritableStream", + "tests": [ + "tests/corpus/web-stream-writable-untyped.mjs" + ] }, "anchorSource": "exact" }, @@ -113405,7 +113410,8 @@ "status": "partial", "evidence": "compiler-feature:webstreams.TransformStream", "tests": [ - "tests/corpus/7743-transform-stream-identity.ts" + "tests/corpus/7743-transform-stream-identity.ts", + "tests/corpus/web-stream-transform-pipes-untyped.mjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 25a693c7c..1242ad9a7 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -434,7 +434,8 @@ { "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] }, { "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] }, { "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/web-stream-readable-untyped.mjs", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] }, - { "chapter": "webstreams", "symbols": ["TransformStream"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] }, + { "chapter": "webstreams", "symbols": ["TransformStream"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts", "tests/corpus/web-stream-transform-pipes-untyped.mjs"] }, + { "chapter": "webstreams", "symbols": ["WritableStream"], "status": "partial", "evidence": ["tests/corpus/web-stream-writable-untyped.mjs"] }, { "chapter": "globals", "signatures": ["Class: `TransformStream`"], "status": "partial", "evidence": ["tests/corpus/7743-transform-stream-identity.ts"] } ], "dedicated": [ diff --git a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts index c465c2920..24f603198 100644 --- a/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts +++ b/packages/compiler/src/frontend/lowering/builtins/web-streams-dyn.ts @@ -21,6 +21,13 @@ export const WEB_STREAM_CLASSES = [ "ReadableStreamDefaultController", "CountQueuingStrategy", "ByteLengthQueuingStrategy", + "WritableStream", + "WritableStreamDefaultWriter", + "WritableStreamDefaultController", + "TransformStream", + "TransformStreamDefaultController", + "TextEncoderStream", + "TextDecoderStream", ] as const; const CLASS_NAMES: readonly string[] = WEB_STREAM_CLASSES; @@ -48,6 +55,33 @@ export function webStreamClassKindOf(lowerer: Lowerer, node: ts.Expression): num return index < 0 ? null : index; } +/** Whether a JavaScript source of the program names one of the classes. + * The global object answers them by a computed name only then + * (global-values.ts), so a program without web streams does not link + * them for its other computed global reads. */ +export function programNamesWebStreamClass(lowerer: Lowerer): boolean { + if (lowerer.namesWebStreamClass !== undefined) return lowerer.namesWebStreamClass; + const names: ReadonlySet = new Set(WEB_STREAM_CLASSES); + let found = false; + for (const file of lowerer.program.getSourceFiles()) { + if (file.isDeclarationFile || !isJsSourceFile(file)) continue; + ts.walkPreorder(file, (node) => { + if ( + ts.isIdentifier(node) && + names.has(node.text) && + webStreamClassKindOf(lowerer, node) !== null + ) { + found = true; + return "stop"; + } + return undefined; + }); + if (found) break; + } + lowerer.namesWebStreamClass = found; + return found; +} + export function webStreamClassValue(kind: number, loc: SrcLoc): IrExpr { return { kind: "libCall", fn: "webstream.class", args: [numLit(kind, loc)], type: DYN, loc }; } @@ -130,6 +164,8 @@ const WEB_STREAM_INTERFACES: ReadonlySet = new Set([ "ReadableStreamReadValueResult", "ReadableStreamReadDoneResult", "UnderlyingSource", + "UnderlyingSink", + "Transformer", "QueuingStrategy", "QueuingStrategySize", ]); diff --git a/packages/compiler/src/frontend/lowering/global-values.ts b/packages/compiler/src/frontend/lowering/global-values.ts index 264c99337..21593203d 100644 --- a/packages/compiler/src/frontend/lowering/global-values.ts +++ b/packages/compiler/src/frontend/lowering/global-values.ts @@ -32,6 +32,11 @@ import { builtinNamespaceValue } from "./builtin-namespaces.js"; import { uriFunctionValue } from "./expressions/identifiers.js"; import { navigatorValue } from "./builtins/navigator.js"; import { primitiveCtorClosure } from "./lower-exprs.js"; +import { + WEB_STREAM_CLASSES, + programNamesWebStreamClass, + webStreamClassValue, +} from "./builtins/web-streams-dyn.js"; const LOOKUP_FN = "%global.builtinValue"; @@ -177,9 +182,14 @@ export function globalValuesInstall( const name: IrExpr = { kind: "varRef", localId: "name.0", type: STRING, loc }; const body: IrStmt[] = []; for (const global of [...new Set(names)].sort()) { + const webStream = (WEB_STREAM_CLASSES as readonly string[]).indexOf(global); const value: IrExpr | null = REFUSED.has(global) ? { kind: "unitLit", unit: "null", type: { kind: "nullT" }, loc } - : jsGlobalValueByName(lowerer, global, loc); + : webStream >= 0 + ? programNamesWebStreamClass(lowerer) + ? webStreamClassValue(webStream, loc) + : null + : jsGlobalValueByName(lowerer, global, loc); if (value === null) continue; body.push({ kind: "if", diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 1320c7774..aab91ce8b 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3645,6 +3645,9 @@ export class Lowerer { /** global-values.ts's answer per JavaScript source: the receiver of its * first read of the global object by a computed name, or null. */ readonly computedGlobalReads = new Map(); + /** web-streams-dyn.ts's answer: whether a JavaScript source names a web + * stream class; undefined until asked. */ + namesWebStreamClass: boolean | undefined = undefined; /** The first lifted function of each name (liftedFnNamed), for the * liftedFns indexed so far. liftedFns only grows. */ private readonly liftedFnsByName = new Map(); diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 9a98cef01..a19351c22 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -1044,8 +1044,9 @@ const RUNTIME_FEATURE_RULES = { mime: rule({ prefixes: ["mime."] }), /** Web streams in checked-dynamic code (scr_webstreams.c). */ webStreams: rule({ prefixes: ["webstream."] }), - /** AbortController and AbortSignal (scr_abort.c, or the fetch unit). */ - abortSignal: rule({ prefixes: ["abort."] }), + /** AbortController and AbortSignal (scr_abort.c, or the fetch unit). + * A WritableStream controller's signal is one (scr_webstreams.c). */ + abortSignal: rule({ prefixes: ["abort.", "webstream."] }), /** util.debounce and util.throttle (scr_util_timing.c). */ utilTiming: rule({ fns: ["util.debounce", "util.throttle"] }), /** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */ diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-transform-pipes-untyped.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-transform-pipes-untyped.mjs.json new file mode 100644 index 000000000..d1041347b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-transform-pipes-untyped.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/web-stream-transform-pipes-untyped.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-writable-untyped.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-writable-untyped.mjs.json new file mode 100644 index 000000000..42be7ae57 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/web-stream-writable-untyped.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/web-stream-writable-untyped.mjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 0fafb706e..ab94282f1 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -244,7 +244,14 @@ typedef enum { Y(WS_COUNT_STRATEGY) /* CountQueuingStrategy (scr_webstreams.c) */ \ Y(WS_RCONTROLLER) /* ReadableStreamDefaultController (scr_webstreams.c) */ \ Y(WS_READABLE) /* ReadableStream for checked-dynamic code (scr_webstreams.c) */ \ - Y(WS_READER) /* ReadableStreamDefaultReader (scr_webstreams.c) */ + Y(WS_READER) /* ReadableStreamDefaultReader (scr_webstreams.c) */ \ + Y(WS_TCONTROLLER) /* TransformStreamDefaultController (scr_webstreams.c) */ \ + Y(WS_TEXT_DECODER) /* TextDecoderStream (scr_webstreams.c) */ \ + Y(WS_TEXT_ENCODER) /* TextEncoderStream (scr_webstreams.c) */ \ + Y(WS_TRANSFORM) /* TransformStream for checked-dynamic code (scr_webstreams.c) */ \ + Y(WS_WCONTROLLER) /* WritableStreamDefaultController (scr_webstreams.c) */ \ + Y(WS_WRITABLE) /* WritableStream for checked-dynamic code (scr_webstreams.c) */ \ + Y(WS_WRITER) /* WritableStreamDefaultWriter (scr_webstreams.c) */ /* Values below the largest pinned one that no tag holds, each with the * reason. Never reuse one without removing it here. */ diff --git a/packages/runtime/src/scr_webstreams.c b/packages/runtime/src/scr_webstreams.c index e4b192b87..8ae438902 100644 --- a/packages/runtime/src/scr_webstreams.c +++ b/packages/runtime/src/scr_webstreams.c @@ -1,6 +1,7 @@ -/* WHATWG streams for checked-dynamic code: ReadableStream with its default - * reader and controller, and the queuing strategies, as untyped JavaScript - * reads them (globals and node:stream/web). The algorithms follow Node 24's +/* WHATWG streams for checked-dynamic code: ReadableStream, WritableStream + * and TransformStream with their default readers, writers and controllers, + * and the queuing strategies, as untyped JavaScript reads them (globals and + * node:stream/web). The algorithms follow Node 24's * lib/internal/webstreams step by step, including where Node departs from * the specification's promise usage (its read fast path, the lazily * materialized closed promises), so promise jobs run in Node's order. @@ -47,15 +48,24 @@ static void *ws_alloc(size_t size) { typedef struct WsObjType { void (*trace)(void *obj, ScrTraceVisit visit, void *ctx); void (*drop)(void *obj, bool gc); + /* For the states a class's handles show: the class name and its + * getters. */ + const char *cls; + const ScrDynHandleAccessor *accessors; } WsObjType; +/* `own` holds the properties a program set on the state's object: handles + * compare by their state, so every handle shows them. */ typedef struct WsObj { size_t rc; const WsObjType *type; + ScrDyn *own; /* +1, or NULL */ } WsObj; static void ws_obj_trace(void *obj, ScrTraceVisit visit, void *ctx) { - ((WsObj *)obj)->type->trace(obj, visit, ctx); + WsObj *o = obj; + if (o->own) visit(o->own, ctx); + o->type->trace(obj, visit, ctx); } static void ws_obj_gcfree(void *obj) { @@ -65,7 +75,9 @@ static void ws_obj_gcfree(void *obj) { } static void ws_obj_destroy(void *obj) { - ((WsObj *)obj)->type->drop(obj, false); + WsObj *o = obj; + if (o->own) scr_dyn_release(o->own); + o->type->drop(obj, false); scr_obj_free_note(); scr_cyc_free(obj); } @@ -96,6 +108,22 @@ static void ws_release(void *obj) { } } +/* The classes' getters (scr_dyn_handle_class.c), which own properties do + * not shadow. */ +static const ScrDynHandleAccessor ws_readable_accessors[] = {{"locked", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_reader_accessors[] = {{"closed", true}, {NULL, false}}; +static const ScrDynHandleAccessor ws_rcontroller_accessors[] = {{"desiredSize", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_writable_accessors[] = {{"locked", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_writer_accessors[] = { + {"closed", true}, {"desiredSize", false}, {"ready", true}, {NULL, false}}; +static const ScrDynHandleAccessor ws_wcontroller_accessors[] = {{"signal", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_transform_accessors[] = {{"readable", false}, {"writable", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_tcontroller_accessors[] = {{"desiredSize", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_text_encoder_accessors[] = { + {"encoding", false}, {"readable", false}, {"writable", false}, {NULL, false}}; +static const ScrDynHandleAccessor ws_text_decoder_accessors[] = { + {"encoding", false}, {"fatal", false}, {"ignoreBOM", false}, {"readable", false}, {"writable", false}, {NULL, false}}; + static void ws_visit(const void *child, ScrTraceVisit visit, void *ctx) { if (child) visit((void *)child, ctx); } @@ -610,13 +638,15 @@ struct WsReader { static SCR_TL ScrDyn *ws_released_error; /* "Reader released" */ static SCR_TL ScrDyn *ws_releasing_error; /* "Releasing reader" */ +static SCR_TL ScrDyn *ws_writer_released_error_value; /* "Writer has been released" */ static SCR_TL bool ws_teardown_registered; static void ws_teardown(void) { if (ws_released_error) scr_dyn_release(ws_released_error); if (ws_releasing_error) scr_dyn_release(ws_releasing_error); + if (ws_writer_released_error_value) scr_dyn_release(ws_writer_released_error_value); if (ws_resolved_promise) scr_promise_release(ws_resolved_promise); - ws_released_error = ws_releasing_error = NULL; + ws_released_error = ws_releasing_error = ws_writer_released_error_value = NULL; ws_resolved_promise = NULL; } @@ -664,7 +694,7 @@ static void ws_readable_drop(void *obj, bool gc) { if (s->closed_materialized) ws_drop_promise(s->closed.promise, gc); } -static const WsObjType ws_readable_type = {ws_readable_trace, ws_readable_drop}; +static const WsObjType ws_readable_type = {ws_readable_trace, ws_readable_drop, "ReadableStream", ws_readable_accessors}; static void ws_rcontroller_trace(void *obj, ScrTraceVisit visit, void *ctx) { WsRController *c = obj; @@ -688,7 +718,7 @@ static void ws_rcontroller_drop(void *obj, bool gc) { ws_drop_obj(c->stream, gc); } -static const WsObjType ws_rcontroller_type = {ws_rcontroller_trace, ws_rcontroller_drop}; +static const WsObjType ws_rcontroller_type = {ws_rcontroller_trace, ws_rcontroller_drop, "ReadableStreamDefaultController", ws_rcontroller_accessors}; static void ws_reader_trace(void *obj, ScrTraceVisit visit, void *ctx) { WsReader *r = obj; @@ -715,7 +745,7 @@ static void ws_reader_drop(void *obj, bool gc) { ws_drop_obj(r->stream, gc); } -static const WsObjType ws_reader_type = {ws_reader_trace, ws_reader_drop}; +static const WsObjType ws_reader_type = {ws_reader_trace, ws_reader_drop, "ReadableStreamDefaultReader", ws_reader_accessors}; /* ── handles ───────────────────────────────────────────────────────── */ @@ -2019,6 +2049,2054 @@ static bool ws_readable_unusable(const ScrDyn *stream) { static const ScrWebStreamBridge ws_fetch_bridge = {ws_reader_read_into, ws_readable_unusable}; +/* ── WritableStream (lib/internal/webstreams/writablestream.js) ──── */ + +typedef enum { WS_W_WRITABLE, WS_W_ERRORING, WS_W_ERRORED, WS_W_CLOSED } WsWState; + +typedef struct WsWritable WsWritable; +typedef struct WsWriter WsWriter; +typedef struct WsWController WsWController; + +/* Native sink algorithms for streams the runtime makes (a TransformStream's + * writable side): write and close answer the algorithm's result (+1 dyn, + * or NULL with the exception pending), abort a promise (+1). The context + * is a WsObj the controller holds until it clears its algorithms. */ +typedef struct WsSinkOps { + ScrDyn *(*write)(void *ctx, WsWController *c, ScrDyn *chunk); + ScrPromise *(*close)(void *ctx, WsWController *c); + ScrPromise *(*abort)(void *ctx, WsWController *c, ScrDyn *reason); +} WsSinkOps; + +/* A promise record (PromiseWithResolvers, resolvedRecord or + * rejectedHandledRecord): the promise (+1), and whether its resolvers are + * live. NULL until materialized. A pipe parks on a writer's backpressure + * by installing a record whose resolve is its hook (a forever-pending + * promise with a no-op reject, as Node's pipeTo does). */ +typedef struct WsRecord { + ScrPromise *promise; + bool live; + void (*hook)(void *ctx); + void *hook_ctx; /* weak: the pipe owns the writer */ +} WsRecord; + +typedef struct WsPipe WsPipe; + +/* A write request: its promise (+1), or the shared tracker of the pipe + * that wrote the chunk (+1; pipeTo). */ +typedef struct WsWriteRequest { + ScrPromise *promise; + WsPipe *tracker; +} WsWriteRequest; + +struct WsWritable { + WsObj obj; + WsWState state; + ScrDyn *stored_error; /* +1, or NULL for undefined */ + WsWriter *writer; /* +1 while locked */ + WsWController *controller; + ScrPromise *close_request; /* +1 while queued */ + WsWriteRequest inflight_write; /* both NULL when none is in flight */ + ScrPromise *inflight_close; /* +1 */ + bool close_queued_or_inflight; + /* pendingAbortRequest */ + ScrPromise *abort_request; /* +1 */ + ScrDyn *abort_reason; /* +1, or NULL for undefined */ + bool abort_was_already_erroring; + bool backpressure; + /* writeRequests, a ring. */ + WsWriteRequest *requests; + size_t requests_head, requests_len, requests_cap; +}; + +struct WsWriter { + WsObj obj; + WsWritable *stream; /* +1 while attached */ + WsRecord closed; + WsRecord ready; +}; + +struct WsWController { + WsObj obj; + WsWritable *stream; /* +1 */ + WsQueue queue; + double queue_total_size; + double high_water_mark; + bool started, algorithms_cleared; + ScrDyn *sink; + ScrDyn *write_fn, *close_fn, *abort_fn; + ScrDyn *size_fn; /* NULL = the default size algorithm */ + const WsSinkOps *native; + void *native_ctx; /* +1 */ + /* The AbortController, made when the signal is first read; until then + * an abort records its reason (no listener can observe it). */ + ScrDyn *abort_controller; /* +1 */ + bool aborted; + ScrDyn *aborted_reason; /* +1 */ +}; + +/* kCloseSentinel. */ +static ScrDyn ws_close_sentinel = {.rc = SIZE_MAX, .kind = SCR_DYN_UNDEF}; + +static ScrDyn *ws_writer_released_error(void) { + if (!ws_writer_released_error_value) { + ws_register_teardown(); + ws_writer_released_error_value = ws_state_error_value("Writer has been released"); + } + return ws_writer_released_error_value; +} + +static void ws_writable_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsWritable *s = obj; + ws_visit(s->stored_error, visit, ctx); + ws_visit(s->writer, visit, ctx); + ws_visit(s->controller, visit, ctx); + ws_visit(s->close_request, visit, ctx); + ws_visit(s->inflight_write.promise, visit, ctx); + ws_visit(s->inflight_write.tracker, visit, ctx); + ws_visit(s->inflight_close, visit, ctx); + ws_visit(s->abort_request, visit, ctx); + ws_visit(s->abort_reason, visit, ctx); + for (size_t i = 0; i < s->requests_len; i++) { + WsWriteRequest *q = &s->requests[(s->requests_head + i) % s->requests_cap]; + ws_visit(q->promise, visit, ctx); + ws_visit(q->tracker, visit, ctx); + } +} + +static void ws_writable_drop(void *obj, bool gc) { + WsWritable *s = obj; + ws_drop_dyn(s->stored_error, gc); + ws_drop_obj(s->writer, gc); + ws_drop_obj(s->controller, gc); + ws_drop_promise(s->close_request, gc); + ws_drop_promise(s->inflight_write.promise, gc); + ws_drop_obj(s->inflight_write.tracker, gc); + ws_drop_promise(s->inflight_close, gc); + ws_drop_promise(s->abort_request, gc); + ws_drop_dyn(s->abort_reason, gc); + for (size_t i = 0; i < s->requests_len; i++) { + WsWriteRequest *q = &s->requests[(s->requests_head + i) % s->requests_cap]; + ws_drop_promise(q->promise, gc); + ws_drop_obj(q->tracker, gc); + } + free(s->requests); +} + +static const WsObjType ws_writable_type = {ws_writable_trace, ws_writable_drop, "WritableStream", ws_writable_accessors}; + +static void ws_writer_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsWriter *w = obj; + ws_visit(w->stream, visit, ctx); + ws_visit(w->closed.promise, visit, ctx); + ws_visit(w->ready.promise, visit, ctx); +} + +static void ws_writer_drop(void *obj, bool gc) { + WsWriter *w = obj; + ws_drop_obj(w->stream, gc); + ws_drop_promise(w->closed.promise, gc); + ws_drop_promise(w->ready.promise, gc); +} + +static const WsObjType ws_writer_type = {ws_writer_trace, ws_writer_drop, "WritableStreamDefaultWriter", ws_writer_accessors}; + +static void ws_wcontroller_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsWController *c = obj; + ws_visit(c->stream, visit, ctx); + ws_queue_trace(&c->queue, visit, ctx); + ws_visit(c->sink, visit, ctx); + ws_visit(c->write_fn, visit, ctx); + ws_visit(c->close_fn, visit, ctx); + ws_visit(c->abort_fn, visit, ctx); + ws_visit(c->size_fn, visit, ctx); + ws_visit(c->native_ctx, visit, ctx); + ws_visit(c->abort_controller, visit, ctx); + ws_visit(c->aborted_reason, visit, ctx); +} + +static void ws_wcontroller_drop(void *obj, bool gc) { + WsWController *c = obj; + ws_queue_drop(&c->queue, gc); + ws_drop_dyn(c->sink, gc); + ws_drop_dyn(c->write_fn, gc); + ws_drop_dyn(c->close_fn, gc); + ws_drop_dyn(c->abort_fn, gc); + ws_drop_dyn(c->size_fn, gc); + ws_drop_obj(c->native_ctx, gc); + ws_drop_dyn(c->abort_controller, gc); + ws_drop_dyn(c->aborted_reason, gc); + ws_drop_obj(c->stream, gc); +} + +static const WsObjType ws_wcontroller_type = {ws_wcontroller_trace, ws_wcontroller_drop, "WritableStreamDefaultController", ws_wcontroller_accessors}; + +static ScrDyn *ws_writable_box(WsWritable *s) { return ws_box(s, SCR_DYNH_WS_WRITABLE); } +static ScrDyn *ws_writer_box(WsWriter *w) { return ws_box(w, SCR_DYNH_WS_WRITER); } +static ScrDyn *ws_wcontroller_box(WsWController *c) { return ws_box(c, SCR_DYNH_WS_WCONTROLLER); } + +static WsWritable *ws_writable_of(const ScrDyn *d) { + return d && d->kind == SCR_DYN_HANDLE && d->v.handle.tag == SCR_DYNH_WS_WRITABLE ? d->v.handle.ptr : NULL; +} + +/* undefined for a NULL stored value (+1). */ +static ScrDyn *ws_or_undef(ScrDyn *d) { return d ? scr_dyn_retain(d) : ws_undef(); } + +/* ── records ── */ + +static void ws_record_set(WsRecord *r, ScrPromise *p, bool live) { + if (r->promise) scr_promise_release(r->promise); + r->promise = p; + r->live = live; + r->hook = NULL; + r->hook_ctx = NULL; +} + +static void ws_record_clear(WsRecord *r) { ws_record_set(r, NULL, false); } + +static bool ws_record_pending(const WsRecord *r) { + return r->promise && scr_promise_is_pending(r->promise); +} + +/* record.resolve?.() */ +static void ws_record_resolve(WsRecord *r) { + if (r->hook) { + r->hook(r->hook_ctx); + return; + } + if (!r->promise || !r->live || !scr_promise_is_pending(r->promise)) return; + ScrDyn *u = ws_undef(); + ws_fulfill(r->promise, u); + scr_dyn_release(u); +} + +static void ws_record_reject(WsRecord *r, ScrDyn *reason) { + if (r->hook || !r->promise || !r->live || !scr_promise_is_pending(r->promise)) return; + ScrDyn *e = ws_or_undef(reason); + ws_reject(r->promise, e); + scr_dyn_release(e); +} + +static void ws_record_resolved(WsRecord *r) { + ScrDyn *u = ws_undef(); + ws_record_set(r, ws_promise_resolved(u), false); + scr_dyn_release(u); +} + +static void ws_record_rejected_handled(WsRecord *r, ScrDyn *reason) { + ScrDyn *e = ws_or_undef(reason); + ScrPromise *p = ws_promise_rejected(e); + scr_dyn_release(e); + ws_set_handled(p); + ws_record_set(r, p, false); +} + +static void ws_promise_fulfill_undefined(ScrPromise *p) { + ScrDyn *u = ws_undef(); + ws_fulfill(p, u); + scr_dyn_release(u); +} + +static void ws_promise_reject_with(ScrPromise *p, ScrDyn *reason) { + ScrDyn *e = ws_or_undef(reason); + ws_reject(p, e); + scr_dyn_release(e); +} + +/* PromiseReject(e) / PromiseResolve() as boxed values. */ +static ScrPromise *ws_promise_rejected_with(ScrDyn *reason) { + ScrDyn *e = ws_or_undef(reason); + ScrPromise *p = ws_promise_rejected(e); + scr_dyn_release(e); + return p; +} + +static ScrPromise *ws_promise_resolved_undefined(void) { + ScrDyn *u = ws_undef(); + ScrPromise *p = ws_promise_resolved(u); + scr_dyn_release(u); + return p; +} + +static ScrPromise *ws_promise_state_rejected(const char *message) { + ScrDyn *error = ws_state_error_value(message); + ScrPromise *p = ws_promise_rejected(error); + scr_dyn_release(error); + return p; +} + +/* ── the write request queue ── */ + +static void ws_requests_push(WsWritable *s, WsWriteRequest q) { + if (s->requests_len == s->requests_cap) { + size_t cap = s->requests_cap ? s->requests_cap * 2 : 4; + WsWriteRequest *requests = malloc(cap * sizeof *requests); + if (!requests) ws_oom(); + for (size_t i = 0; i < s->requests_len; i++) + requests[i] = s->requests[(s->requests_head + i) % s->requests_cap]; + free(s->requests); + s->requests = requests; + s->requests_head = 0; + s->requests_cap = cap; + } + s->requests[(s->requests_head + s->requests_len) % s->requests_cap] = q; + s->requests_len++; +} + +static WsWriteRequest ws_requests_shift(WsWritable *s) { + WsWriteRequest p = s->requests[s->requests_head]; + s->requests_head = (s->requests_head + 1) % s->requests_cap; + s->requests_len--; + if (!s->requests_len) s->requests_head = 0; + return p; +} + +static void ws_pipe_tracker_resolve(WsPipe *p); +static void ws_pipe_tracker_reject(WsPipe *p, ScrDyn *error); + +static bool ws_write_request_live(WsWriteRequest q) { return q.promise || q.tracker; } + +/* request.resolve() and request.reject(error); the request's reference + * goes. */ +static void ws_write_request_resolve(WsWriteRequest q) { + if (q.promise) { + ws_promise_fulfill_undefined(q.promise); + scr_promise_release(q.promise); + } else if (q.tracker) { + ws_pipe_tracker_resolve(q.tracker); + ws_release(q.tracker); + } +} + +static void ws_write_request_reject(WsWriteRequest q, ScrDyn *error) { + ScrDyn *e = ws_or_undef(error); + if (q.promise) { + ws_reject(q.promise, e); + scr_promise_release(q.promise); + } else if (q.tracker) { + ws_pipe_tracker_reject(q.tracker, e); + ws_release(q.tracker); + } + scr_dyn_release(e); +} + +/* ── the stream's abstract operations ── */ + +static bool ws_writable_locked(WsWritable *s) { return s->writer != NULL; } + +static void ws_wcontroller_advance_queue_if_needed(WsWController *c); +static void ws_wcontroller_clear_algorithms(WsWController *c); +static void ws_writable_finish_erroring(WsWritable *s); + +/* writerClosedPromise / writerReadyPromise: the records, materialized. */ +static ScrPromise *ws_writer_closed_promise(WsWriter *w) { + if (!w->closed.promise) { + WsWritable *s = w->stream; + if (!s) ws_record_rejected_handled(&w->closed, ws_writer_released_error()); + else if (s->state == WS_W_WRITABLE || s->state == WS_W_ERRORING) ws_record_set(&w->closed, scr_promise_new(), true); + else if (s->state == WS_W_CLOSED) ws_record_resolved(&w->closed); + else ws_record_rejected_handled(&w->closed, s->stored_error); + } + return w->closed.promise; +} + +static ScrPromise *ws_writer_ready_promise(WsWriter *w) { + if (!w->ready.promise) { + WsWritable *s = w->stream; + if (!s) ws_record_rejected_handled(&w->ready, ws_writer_released_error()); + else if (s->state == WS_W_WRITABLE) { + if (!s->close_queued_or_inflight && s->backpressure) ws_record_set(&w->ready, scr_promise_new(), true); + else ws_record_resolved(&w->ready); + } else if (s->state == WS_W_CLOSED) ws_record_resolved(&w->ready); + else ws_record_rejected_handled(&w->ready, s->stored_error); + } + return w->ready.promise; +} + +static void ws_writer_ensure_ready_rejected(WsWriter *w, ScrDyn *error) { + if (ws_record_pending(&w->ready)) { + ws_record_reject(&w->ready, error); + ws_set_handled(w->ready.promise); + } else { + ws_record_clear(&w->ready); + } +} + +static void ws_writer_ensure_closed_rejected(WsWriter *w, ScrDyn *error) { + if (ws_record_pending(&w->closed)) { + ws_record_reject(&w->closed, error); + ws_set_handled(w->closed.promise); + } else { + ws_record_clear(&w->closed); + } +} + +/* abortController.abort(reason) */ +static void ws_wcontroller_signal_abort(WsWController *c, ScrDyn *reason) { + if (c->aborted) return; + c->aborted = true; + c->aborted_reason = ws_or_undef(reason); + if (!c->abort_controller) return; + ScrDyn *arg = c->aborted_reason; + ScrDyn *r = scr_dyn_invoke(c->abort_controller, "abort", &arg, 1, "abortController.abort"); + if (r) scr_dyn_release(r); +} + +static void ws_writable_start_erroring(WsWritable *s, ScrDyn *reason) { + WsWController *c = s->controller; + s->state = WS_W_ERRORING; + s->stored_error = ws_or_undef(reason); + if (s->writer) ws_writer_ensure_ready_rejected(s->writer, s->stored_error); + if (!ws_write_request_live(s->inflight_write) && !s->inflight_close && c->started) ws_writable_finish_erroring(s); +} + +static void ws_writable_reject_close_and_closed_if_needed(WsWritable *s) { + if (s->close_request) { + ScrPromise *p = s->close_request; + s->close_request = NULL; + s->close_queued_or_inflight = false; + ws_promise_reject_with(p, s->stored_error); + scr_promise_release(p); + } + if (s->writer && ws_record_pending(&s->writer->closed)) { + ws_set_handled(s->writer->closed.promise); + ws_record_reject(&s->writer->closed, s->stored_error); + } +} + +/* The abort request's reaction to the sink's abort algorithm. */ +typedef struct WsAbortWait { + WsObj obj; + WsWritable *stream; + ScrPromise *abort_request; +} WsAbortWait; + +static void ws_abort_wait_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsAbortWait *a = obj; + ws_visit(a->stream, visit, ctx); + ws_visit(a->abort_request, visit, ctx); +} + +static void ws_abort_wait_drop(void *obj, bool gc) { + WsAbortWait *a = obj; + ws_drop_obj(a->stream, gc); + ws_drop_promise(a->abort_request, gc); +} + +static const WsObjType ws_abort_wait_type = {ws_abort_wait_trace, ws_abort_wait_drop}; + +static void ws_abort_sink_fulfilled(void *ctx, ScrDyn *value) { + (void)value; + WsAbortWait *a = ctx; + ws_promise_fulfill_undefined(a->abort_request); + ws_writable_reject_close_and_closed_if_needed(a->stream); +} + +static void ws_abort_sink_rejected(void *ctx, ScrDyn *error) { + WsAbortWait *a = ctx; + ws_reject(a->abort_request, error); + ws_writable_reject_close_and_closed_if_needed(a->stream); +} + +/* controller[kAbort](reason): the abort algorithm's promise (+1). */ +static ScrPromise *ws_wcontroller_abort_steps(WsWController *c, ScrDyn *reason) { + ScrPromise *result; + if (c->algorithms_cleared) { + result = ws_promise_resolved_undefined(); + } else if (c->native) { + result = c->native->abort(c->native_ctx, c, reason); + } else if (c->abort_fn) { + /* createPromiseCallback1Param: an async arrow around the call. */ + ScrDyn *abort = scr_dyn_retain(c->abort_fn); + ScrDyn *sink = scr_dyn_retain(c->sink); + ScrDyn *arg = ws_or_undef(reason); + ScrDyn *r = ws_call(abort, sink, &arg, 1, "sink.abort"); + scr_dyn_release(arg); + scr_dyn_release(abort); + scr_dyn_release(sink); + if (!r) { + result = ws_promise_rejected_pending(); + } else { + result = ws_promise_resolving(r); + scr_dyn_release(r); + } + } else { + result = ws_promise_resolved_undefined(); /* nonOpCancel */ + } + ws_wcontroller_clear_algorithms(c); + return result; +} + +static void ws_writable_finish_erroring(WsWritable *s) { + s->state = WS_W_ERRORED; + WsWController *c = s->controller; + /* controller[kError](): resetQueue */ + ws_queue_reset(&c->queue); + c->queue_total_size = 0; + ScrDyn *stored = ws_or_undef(s->stored_error); + while (s->requests_len) ws_write_request_reject(ws_requests_shift(s), stored); + if (!s->abort_request) { + ws_writable_reject_close_and_closed_if_needed(s); + scr_dyn_release(stored); + return; + } + ScrPromise *abort_request = s->abort_request; + ScrDyn *abort_reason = s->abort_reason; + bool was_already_erroring = s->abort_was_already_erroring; + s->abort_request = NULL; + s->abort_reason = NULL; + s->abort_was_already_erroring = false; + if (was_already_erroring) { + ws_reject(abort_request, stored); + ws_writable_reject_close_and_closed_if_needed(s); + scr_promise_release(abort_request); + if (abort_reason) scr_dyn_release(abort_reason); + scr_dyn_release(stored); + return; + } + /* The closed promises reject once the sink's abort settles: materialize + * them now, so an observation in between sees them pending. */ + if (s->writer && !s->writer->closed.promise) ws_record_set(&s->writer->closed, scr_promise_new(), true); + ScrPromise *sink = ws_wcontroller_abort_steps(c, abort_reason); + WsAbortWait *a = ws_obj_new(sizeof *a, &ws_abort_wait_type); + a->stream = ws_retain(s); + a->abort_request = abort_request; /* the reference moves */ + ws_then(sink, ws_abort_sink_fulfilled, ws_abort_sink_rejected, a, false); + scr_promise_release(sink); + if (abort_reason) scr_dyn_release(abort_reason); + scr_dyn_release(stored); +} + +static void ws_writable_deal_with_rejection(WsWritable *s, ScrDyn *error) { + if (s->state == WS_W_WRITABLE) { + ws_writable_start_erroring(s, error); + return; + } + ws_writable_finish_erroring(s); +} + +/* writableStreamAbort: +1 promise. */ +static ScrPromise *ws_writable_abort(WsWritable *s, ScrDyn *reason) { + if (s->state == WS_W_CLOSED || s->state == WS_W_ERRORED) return ws_promise_resolved_undefined(); + ws_wcontroller_signal_abort(s->controller, reason); + if (s->state == WS_W_CLOSED || s->state == WS_W_ERRORED) return ws_promise_resolved_undefined(); + if (s->abort_request) return scr_promise_retain(s->abort_request); + bool was_already_erroring = false; + if (s->state == WS_W_ERRORING) { + was_already_erroring = true; + reason = NULL; + } + s->abort_request = scr_promise_new(); + s->abort_reason = reason && reason->kind != SCR_DYN_UNDEF ? scr_dyn_retain(reason) : NULL; + s->abort_was_already_erroring = was_already_erroring; + ScrPromise *result = scr_promise_retain(s->abort_request); + if (!was_already_erroring) ws_writable_start_erroring(s, reason); + return result; +} + +static void ws_wcontroller_close(WsWController *c); + +/* writableStreamClose: +1 promise. */ +static ScrPromise *ws_writable_close(WsWritable *s) { + if (s->state == WS_W_CLOSED || s->state == WS_W_ERRORED) + return ws_promise_state_rejected("WritableStream is closed"); + s->close_request = scr_promise_new(); + s->close_queued_or_inflight = true; + ScrPromise *result = scr_promise_retain(s->close_request); + if (s->writer && s->backpressure && s->state == WS_W_WRITABLE) ws_record_resolve(&s->writer->ready); + ws_wcontroller_close(s->controller); + return result; +} + +static void ws_writable_update_backpressure(WsWController *c, WsWritable *s) { + bool backpressure = c->high_water_mark - c->queue_total_size <= 0; + bool changed = s->backpressure != backpressure; + s->backpressure = backpressure; + if (s->writer && changed) { + if (backpressure) ws_record_clear(&s->writer->ready); + else ws_record_resolve(&s->writer->ready); + } +} + +static void ws_writable_finish_inflight_write(WsWritable *s) { + WsWriteRequest q = s->inflight_write; + s->inflight_write = (WsWriteRequest){NULL, NULL}; + ws_write_request_resolve(q); +} + +static void ws_writable_finish_inflight_write_with_error(WsWritable *s, ScrDyn *error) { + WsWriteRequest q = s->inflight_write; + s->inflight_write = (WsWriteRequest){NULL, NULL}; + ws_write_request_reject(q, error); + ws_writable_deal_with_rejection(s, error); +} + +static void ws_writable_finish_inflight_close(WsWritable *s) { + ScrPromise *p = s->inflight_close; + s->inflight_close = NULL; + s->close_queued_or_inflight = false; + ws_promise_fulfill_undefined(p); + scr_promise_release(p); + if (s->state == WS_W_ERRORING) { + if (s->stored_error) scr_dyn_release(s->stored_error); + s->stored_error = NULL; + if (s->abort_request) { + ScrPromise *a = s->abort_request; + s->abort_request = NULL; + if (s->abort_reason) scr_dyn_release(s->abort_reason); + s->abort_reason = NULL; + ws_promise_fulfill_undefined(a); + scr_promise_release(a); + } + } + s->state = WS_W_CLOSED; + if (s->writer) ws_record_resolve(&s->writer->closed); +} + +static void ws_writable_finish_inflight_close_with_error(WsWritable *s, ScrDyn *error) { + ScrPromise *p = s->inflight_close; + s->inflight_close = NULL; + s->close_queued_or_inflight = false; + ws_reject(p, error); + scr_promise_release(p); + if (s->abort_request) { + ScrPromise *a = s->abort_request; + s->abort_request = NULL; + if (s->abort_reason) scr_dyn_release(s->abort_reason); + s->abort_reason = NULL; + ws_reject(a, error); + scr_promise_release(a); + } + ws_writable_deal_with_rejection(s, error); +} + +/* ── the writer ── */ + +static WsWriter *ws_writer_new(WsWritable *s) { + if (ws_writable_locked(s)) { + ws_state_error("WritableStream is locked"); + return NULL; + } + WsWriter *w = ws_obj_new(sizeof *w, &ws_writer_type); + w->stream = ws_retain(s); + s->writer = ws_retain(w); + return w; +} + +static void ws_writer_release_lock(WsWriter *w) { + WsWritable *s = w->stream; + ScrDyn *released = ws_writer_released_error(); + ws_writer_ensure_ready_rejected(w, released); + ws_writer_ensure_closed_rejected(w, released); + s->writer = NULL; + w->stream = NULL; + ws_release(w); /* the stream's reference */ + ws_release(s); +} + +static ScrDyn *ws_writer_desired_size(WsWriter *w) { + WsWritable *s = w->stream; + switch (s->state) { + case WS_W_ERRORED: + case WS_W_ERRORING: return scr_dyn_new_null(); + case WS_W_CLOSED: return scr_dyn_new_num(0); + default: return scr_dyn_new_num(s->controller->high_water_mark - s->controller->queue_total_size); + } +} + +/* writableStreamDefaultControllerGetChunkSize: the size algorithm's raw + * result (+1), or NULL for 1. */ +static ScrDyn *ws_wcontroller_chunk_size(WsWController *c, ScrDyn *chunk); +static void ws_wcontroller_write(WsWController *c, ScrDyn *chunk, ScrDyn *size); + +/* writableStreamDefaultWriterWrite: +1 promise. */ +static ScrPromise *ws_writer_write(WsWriter *w, ScrDyn *chunk) { + WsWritable *s = w->stream; + ws_retain(s); + WsWController *c = s->controller; + ScrDyn *size = ws_wcontroller_chunk_size(c, chunk); + ScrPromise *result; + if (s != w->stream) result = ws_promise_state_rejected("Mismatched WritableStreams"); + else if (s->state == WS_W_ERRORED) result = ws_promise_rejected_with(s->stored_error); + else if (s->close_queued_or_inflight || s->state == WS_W_CLOSED) + result = ws_promise_state_rejected("WritableStream is closed"); + else if (s->state == WS_W_ERRORING) result = ws_promise_rejected_with(s->stored_error); + else { + ScrPromise *request = scr_promise_new(); + ws_requests_push(s, (WsWriteRequest){request, NULL}); + result = scr_promise_retain(request); + ws_wcontroller_write(c, chunk, size); + } + if (size) scr_dyn_release(size); + ws_release(s); + return result; +} + +/* ── the default controller ── */ + +static void ws_wcontroller_clear_algorithms(WsWController *c) { + if (c->algorithms_cleared) return; + c->algorithms_cleared = true; + ScrDyn *sink = c->sink, *write = c->write_fn, *close = c->close_fn, *abort = c->abort_fn, *size = c->size_fn; + void *native_ctx = c->native_ctx; + c->sink = c->write_fn = c->close_fn = c->abort_fn = c->size_fn = NULL; + c->native_ctx = NULL; + if (sink) scr_dyn_release(sink); + if (write) scr_dyn_release(write); + if (close) scr_dyn_release(close); + if (abort) scr_dyn_release(abort); + if (size) scr_dyn_release(size); + ws_release(native_ctx); +} + +static void ws_wcontroller_error(WsWController *c, ScrDyn *error) { + ws_wcontroller_clear_algorithms(c); + ws_writable_start_erroring(c->stream, error); +} + +static void ws_wcontroller_error_if_needed(WsWController *c, ScrDyn *error) { + if (c->stream->state == WS_W_WRITABLE) ws_wcontroller_error(c, error); +} + +static ScrDyn *ws_wcontroller_chunk_size(WsWController *c, ScrDyn *chunk) { + if (c->algorithms_cleared || !c->size_fn) return NULL; + ScrDyn *size_fn = scr_dyn_retain(c->size_fn); + ScrDyn *size = ws_call(size_fn, scr_dyn_undefined(), &chunk, 1, "strategy.size"); + scr_dyn_release(size_fn); + if (!size) { + ScrDyn *error = ws_take_pending(); + ws_wcontroller_error_if_needed(c, error); + scr_dyn_release(error); + } + return size; +} + +/* writableStreamDefaultControllerWrite: enqueueValueWithSize coerces the + * size; a failure errors the stream. */ +static void ws_wcontroller_write(WsWController *c, ScrDyn *chunk, ScrDyn *size_value) { + double size = 1; + if (size_value) { + size = scr_dyn_number_coerce(size_value); + if (!scr_exc_pending() && (isnan(size) || size < 0 || isinf(size))) ws_range_arg_value("size", size_value); + if (scr_exc_pending()) { + ScrDyn *error = ws_take_pending(); + ws_wcontroller_error_if_needed(c, error); + scr_dyn_release(error); + return; + } + } + ws_queue_push(&c->queue, chunk, size); + c->queue_total_size += size; + WsWritable *s = c->stream; + if (!s->close_queued_or_inflight && s->state == WS_W_WRITABLE) ws_writable_update_backpressure(c, s); + ws_wcontroller_advance_queue_if_needed(c); +} + +static void ws_wcontroller_close(WsWController *c) { + ws_queue_push(&c->queue, &ws_close_sentinel, 0); + ws_wcontroller_advance_queue_if_needed(c); +} + +static void ws_write_fulfilled(void *ctx, ScrDyn *value) { + (void)value; + WsWController *c = ctx; + WsWritable *s = c->stream; + ws_writable_finish_inflight_write(s); + double size; + ScrDyn *chunk = ws_queue_shift(&c->queue, &size); + scr_dyn_release(chunk); + c->queue_total_size = fmax(0, c->queue_total_size - size); + if (!s->close_queued_or_inflight && s->state == WS_W_WRITABLE) ws_writable_update_backpressure(c, s); + ws_wcontroller_advance_queue_if_needed(c); +} + +static void ws_write_rejected(void *ctx, ScrDyn *error) { + WsWController *c = ctx; + if (c->stream->state == WS_W_WRITABLE) ws_wcontroller_clear_algorithms(c); + ws_writable_finish_inflight_write_with_error(c->stream, error); +} + +static void ws_wcontroller_process_write(WsWController *c, ScrDyn *chunk) { + WsWritable *s = c->stream; + s->inflight_write = ws_requests_shift(s); + ScrDyn *result; + if (c->native) { + result = c->native->write(c->native_ctx, c, chunk); + } else if (c->write_fn) { + ScrDyn *controller = ws_wcontroller_box(c); + ScrDyn *write = scr_dyn_retain(c->write_fn); + ScrDyn *sink = scr_dyn_retain(c->sink); + ScrDyn *args[2] = {chunk, controller}; + result = ws_call(write, sink, args, 2, "sink.write"); + scr_dyn_release(write); + scr_dyn_release(sink); + scr_dyn_release(controller); + } else { + result = ws_undef(); /* nonOpCallback */ + } + if (result == WS_PARKED) return; + if (!result) { + ScrPromise *rejected = ws_promise_rejected_pending(); + ws_then(rejected, ws_write_fulfilled, ws_write_rejected, ws_retain(c), false); + scr_promise_release(rejected); + return; + } + ws_then_algorithm_result(result, ws_write_fulfilled, ws_write_rejected, c); + scr_dyn_release(result); +} + +static void ws_close_fulfilled(void *ctx, ScrDyn *value) { + (void)value; + ws_writable_finish_inflight_close(((WsWController *)ctx)->stream); +} + +static void ws_close_rejected(void *ctx, ScrDyn *error) { + ws_writable_finish_inflight_close_with_error(((WsWController *)ctx)->stream, error); +} + +static void ws_wcontroller_process_close(WsWController *c) { + WsWritable *s = c->stream; + s->inflight_close = s->close_request; + s->close_request = NULL; + double size; + ScrDyn *sentinel = ws_queue_shift(&c->queue, &size); + (void)sentinel; + c->queue_total_size = fmax(0, c->queue_total_size - size); + ScrPromise *sink_close; + if (c->native) { + sink_close = c->native->close(c->native_ctx, c); + } else if (c->close_fn) { + /* createPromiseCallbackNoParams: an async arrow around the call. */ + ScrDyn *close = scr_dyn_retain(c->close_fn); + ScrDyn *sink = scr_dyn_retain(c->sink); + ScrDyn *r = ws_call(close, sink, NULL, 0, "sink.close"); + scr_dyn_release(close); + scr_dyn_release(sink); + if (!r) { + sink_close = ws_promise_rejected_pending(); + } else { + sink_close = ws_promise_resolving(r); + scr_dyn_release(r); + } + } else { + sink_close = ws_promise_resolved_undefined(); /* nonOpCancel */ + } + ws_wcontroller_clear_algorithms(c); + ws_then(sink_close, ws_close_fulfilled, ws_close_rejected, ws_retain(c), false); + scr_promise_release(sink_close); +} + +static void ws_wcontroller_advance_queue_if_needed(WsWController *c) { + WsWritable *s = c->stream; + if (!c->started || ws_write_request_live(s->inflight_write)) return; + if (s->state == WS_W_ERRORING) { + ws_writable_finish_erroring(s); + return; + } + if (!c->queue.length) return; + ScrDyn *value = c->queue.values[c->queue.head]; + if (value == &ws_close_sentinel) ws_wcontroller_process_close(c); + else ws_wcontroller_process_write(c, value); +} + +static void ws_wstart_fulfilled(void *ctx, ScrDyn *value) { + (void)value; + WsWController *c = ctx; + c->started = true; + ws_wcontroller_advance_queue_if_needed(c); +} + +static void ws_wstart_rejected(void *ctx, ScrDyn *error) { + WsWController *c = ctx; + c->started = true; + ws_writable_deal_with_rejection(c->stream, error); +} + +/* setupWritableStreamDefaultController after the algorithms are in place. */ +static void ws_wcontroller_started_with(WsWController *c, ScrDyn *start_result) { + if (!ws_is_object(start_result)) { + ws_queue_microtask(ws_wstart_fulfilled, ws_retain(c)); + return; + } + ScrPromise *p = ws_promise_resolving(start_result); + if (!p) p = ws_promise_rejected_pending(); + ws_then(p, ws_wstart_fulfilled, ws_wstart_rejected, ws_retain(c), false); + scr_promise_release(p); +} + +static WsWController *ws_writable_attach(WsWritable *s, double high_water_mark) { + WsWController *c = ws_obj_new(sizeof *c, &ws_wcontroller_type); + c->stream = ws_retain(s); + c->high_water_mark = high_water_mark; + s->controller = c; /* the stream's reference */ + ws_writable_update_backpressure(c, s); + return c; +} + +static WsWritable *ws_writable_alloc(void) { + WsWritable *s = ws_obj_new(sizeof *s, &ws_writable_type); + s->state = WS_W_WRITABLE; + return s; +} + +/* The signal getter: the AbortController's signal, made on first read. */ +static ScrDyn *ws_wcontroller_signal(WsWController *c) { + if (!c->abort_controller) { + c->abort_controller = scr_fetch_abort_controller_new(); + if (c->aborted) { + ScrDyn *arg = c->aborted_reason; + ScrDyn *r = scr_dyn_invoke(c->abort_controller, "abort", &arg, 1, "abortController.abort"); + if (!r) return NULL; + scr_dyn_release(r); + } + } + ScrStr *key = scr_str_new("signal", 6); + ScrDyn *signal = scr_dyn_handle_key_get(c->abort_controller, key); + scr_str_release(key); + return signal; +} + +/* Bind must be called on a function: Function.prototype.bind on a start + * member that is not callable. */ +static void ws_bind_error(void) { + static const char message[] = "Bind must be called on a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); +} + +/* new WritableStream(sink, strategy): +1 handle, or NULL with the + * exception pending. */ +static ScrDyn *ws_writable_construct(ScrDyn *const *args, size_t argc) { + ScrDyn *sink = argc > 0 && args[0]->kind != SCR_DYN_UNDEF ? args[0] : NULL; + ScrDyn *strategy = argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? args[1] : NULL; + ScrDyn *empty = NULL; + if (!sink || !strategy) empty = scr_dyn_new_obj(); + if (!sink) sink = empty; + if (!strategy) strategy = empty; + ScrDyn *result = NULL; + ScrDyn *type = NULL, *size = NULL, *hwm = NULL, *start = NULL, *write = NULL, *close = NULL, *abort = NULL; + if (!ws_validate_object(sink, "sink", false) || !ws_validate_object(strategy, "strategy", true)) goto done; + if (!(type = ws_get(sink, "type"))) goto done; + if (type->kind != SCR_DYN_UNDEF) { + ws_range_arg_value("type", type); + goto done; + } + if (!(size = ws_get(strategy, "size"))) goto done; + if (size->kind != SCR_DYN_UNDEF && !ws_validate_function(size, "strategy.size")) goto done; + if (!(hwm = ws_get(strategy, "highWaterMark"))) goto done; + double high_water_mark; + if (!ws_extract_high_water_mark(hwm, 1, &high_water_mark)) goto done; + if (!(start = ws_get(sink, "start"))) goto done; + if (!(write = ws_get(sink, "write"))) goto done; + if (!(close = ws_get(sink, "close"))) goto done; + if (!(abort = ws_get(sink, "abort"))) goto done; + bool has_start = scr_dyn_truthy(start), has_write = scr_dyn_truthy(write); + bool has_close = scr_dyn_truthy(close), has_abort = scr_dyn_truthy(abort); + if (has_start && !scr_dyn_is_callable(start)) { + ws_bind_error(); + goto done; + } + if (has_write && !ws_validate_function(write, "sink.write")) goto done; + if (has_close && !ws_validate_function(close, "sink.close")) goto done; + if (has_abort && !ws_validate_function(abort, "sink.abort")) goto done; + ws_install(); + WsWritable *s = ws_writable_alloc(); + WsWController *c = ws_writable_attach(s, high_water_mark); + c->sink = scr_dyn_retain(sink); + if (has_write) c->write_fn = scr_dyn_retain(write); + if (has_close) c->close_fn = scr_dyn_retain(close); + if (has_abort) c->abort_fn = scr_dyn_retain(abort); + if (size->kind != SCR_DYN_UNDEF) c->size_fn = scr_dyn_retain(size); + ScrDyn *start_result; + if (has_start) { + ScrDyn *controller = ws_wcontroller_box(c); + start_result = ws_call(start, sink, &controller, 1, "sink.start"); + scr_dyn_release(controller); + } else { + start_result = ws_undef(); + } + if (!start_result) { + ws_release(s); + goto done; + } + ws_wcontroller_started_with(c, start_result); + scr_dyn_release(start_result); + result = ws_writable_box(s); + ws_release(s); +done: + if (empty) scr_dyn_release(empty); + if (type) scr_dyn_release(type); + if (size) scr_dyn_release(size); + if (hwm) scr_dyn_release(hwm); + if (start) scr_dyn_release(start); + if (write) scr_dyn_release(write); + if (close) scr_dyn_release(close); + if (abort) scr_dyn_release(abort); + return result; +} + +/* ── TransformStream (lib/internal/webstreams/transformstream.js) ── */ + +typedef struct WsTransform WsTransform; +typedef struct WsTController WsTController; + +static ScrDyn *ws_promise_box(ScrPromise *p); + +struct WsTransform { + WsObj obj; + WsReadable *readable; /* +1 */ + WsWritable *writable; /* +1 */ + WsTController *controller; + bool backpressure; + /* The continuation slots that stand for [[backpressureChangePromise]]. */ + bool pull_pending; + bool pending_write_parked; + ScrDyn *pending_write_chunk; /* +1 */ +}; + +struct WsTController { + WsObj obj; + WsTransform *stream; /* +1 */ + bool algorithms_cleared; + ScrDyn *transformer; + ScrDyn *transform_fn; /* NULL = the default transform (enqueue) */ + ScrDyn *flush_fn; + ScrDyn *cancel_fn; + ScrPromise *finish_promise; /* +1 */ +}; + +static void ws_transform_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsTransform *t = obj; + ws_visit(t->readable, visit, ctx); + ws_visit(t->writable, visit, ctx); + ws_visit(t->controller, visit, ctx); + ws_visit(t->pending_write_chunk, visit, ctx); +} + +static void ws_transform_drop(void *obj, bool gc) { + WsTransform *t = obj; + ws_drop_obj(t->readable, gc); + ws_drop_obj(t->writable, gc); + ws_drop_obj(t->controller, gc); + ws_drop_dyn(t->pending_write_chunk, gc); +} + +static const WsObjType ws_transform_type = {ws_transform_trace, ws_transform_drop, "TransformStream", ws_transform_accessors}; + +static void ws_tcontroller_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsTController *c = obj; + ws_visit(c->stream, visit, ctx); + ws_visit(c->transformer, visit, ctx); + ws_visit(c->transform_fn, visit, ctx); + ws_visit(c->flush_fn, visit, ctx); + ws_visit(c->cancel_fn, visit, ctx); + ws_visit(c->finish_promise, visit, ctx); +} + +static void ws_tcontroller_drop(void *obj, bool gc) { + WsTController *c = obj; + ws_drop_obj(c->stream, gc); + ws_drop_dyn(c->transformer, gc); + ws_drop_dyn(c->transform_fn, gc); + ws_drop_dyn(c->flush_fn, gc); + ws_drop_dyn(c->cancel_fn, gc); + ws_drop_promise(c->finish_promise, gc); +} + +static const WsObjType ws_tcontroller_type = {ws_tcontroller_trace, ws_tcontroller_drop, "TransformStreamDefaultController", ws_tcontroller_accessors}; + +static ScrDyn *ws_transform_box(WsTransform *t) { return ws_box(t, SCR_DYNH_WS_TRANSFORM); } +static ScrDyn *ws_tcontroller_box(WsTController *c) { return ws_box(c, SCR_DYNH_WS_TCONTROLLER); } + +static void ws_tcontroller_clear_algorithms(WsTController *c) { + if (c->algorithms_cleared) return; + c->algorithms_cleared = true; + ScrDyn *transformer = c->transformer, *transform = c->transform_fn, *flush = c->flush_fn, *cancel = c->cancel_fn; + c->transformer = c->transform_fn = c->flush_fn = c->cancel_fn = NULL; + if (transformer) scr_dyn_release(transformer); + if (transform) scr_dyn_release(transform); + if (flush) scr_dyn_release(flush); + if (cancel) scr_dyn_release(cancel); +} + +/* The parked write's continuation when the writable side errored + * meanwhile: the write reaction rejects with its stored error. */ +typedef struct WsWriteReject { + WsObj obj; + WsWController *controller; + ScrDyn *error; +} WsWriteReject; + +static void ws_write_reject_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsWriteReject *r = obj; + ws_visit(r->controller, visit, ctx); + ws_visit(r->error, visit, ctx); +} + +static void ws_write_reject_drop(void *obj, bool gc) { + WsWriteReject *r = obj; + ws_drop_obj(r->controller, gc); + ws_drop_dyn(r->error, gc); +} + +static const WsObjType ws_write_reject_type = {ws_write_reject_trace, ws_write_reject_drop}; + +static void ws_write_reject_step(void *ctx, ScrDyn *unused) { + (void)unused; + WsWriteReject *r = ctx; + ws_write_rejected(r->controller, r->error); +} + +static ScrPromise *ws_tcontroller_perform_transform(WsTController *c, ScrDyn *chunk); + +/* writeContinuation: completes the parked write. */ +static void ws_transform_write_continuation(void *ctx, ScrDyn *unused) { + (void)unused; + WsTransform *t = ctx; + ScrDyn *chunk = t->pending_write_chunk; + t->pending_write_parked = false; + t->pending_write_chunk = NULL; + WsWritable *w = t->writable; + WsWController *wc = w->controller; + if (w->state == WS_W_ERRORING) { + WsWriteReject *r = ws_obj_new(sizeof *r, &ws_write_reject_type); + r->controller = ws_retain(wc); + r->error = ws_or_undef(w->stored_error); + ws_queue_microtask(ws_write_reject_step, r); + if (chunk) scr_dyn_release(chunk); + return; + } + ScrDyn *value = chunk ? chunk : ws_undef(); + ScrPromise *result = ws_tcontroller_perform_transform(t->controller, value); + scr_dyn_release(value); + ws_then(result, ws_write_fulfilled, ws_write_rejected, ws_retain(wc), false); + scr_promise_release(result); +} + +/* transformStreamSetBackpressure. */ +static void ws_transform_set_backpressure(WsTransform *t, bool backpressure) { + t->backpressure = backpressure; + if (backpressure) { + if (t->pull_pending) { + t->pull_pending = false; + ws_queue_microtask(ws_pull_fulfilled, ws_retain(t->readable->controller)); + } + } else if (t->pending_write_parked) { + ws_queue_microtask(ws_transform_write_continuation, ws_retain(t)); + } +} + +static void ws_transform_unblock_write(WsTransform *t) { + if (t->backpressure) ws_transform_set_backpressure(t, false); +} + +static void ws_transform_error_writable_and_unblock_write(WsTransform *t, ScrDyn *error) { + ws_tcontroller_clear_algorithms(t->controller); + ws_wcontroller_error_if_needed(t->writable->controller, error); + ws_transform_unblock_write(t); +} + +static void ws_transform_error(WsTransform *t, ScrDyn *error) { + ws_rcontroller_error(t->readable->controller, error); + ws_transform_error_writable_and_unblock_write(t, error); +} + +/* transformStreamDefaultControllerEnqueue: false with the exception + * pending. */ +static bool ws_tcontroller_enqueue(WsTController *c, ScrDyn *chunk) { + WsTransform *t = c->stream; + WsRController *rc = t->readable->controller; + if (!ws_rcontroller_can_close_or_enqueue(rc)) { + ws_state_error("Unable to enqueue"); + return false; + } + if (!ws_rcontroller_enqueue(rc, chunk)) { + ScrDyn *error = ws_take_pending(); + ws_transform_error_writable_and_unblock_write(t, error); + scr_dyn_release(error); + scr_dyn_throw(ws_or_undef(t->readable->stored_error)); + return false; + } + bool backpressure = !ws_rcontroller_should_call_pull(rc); + if (backpressure != t->backpressure) ws_transform_set_backpressure(t, true); + return true; +} + +static void ws_tcontroller_terminate(WsTController *c) { + WsTransform *t = c->stream; + ws_retain(t); + ws_rcontroller_close(t->readable->controller); + ScrDyn *error = ws_state_error_value("TransformStream has been terminated"); + ws_transform_error_writable_and_unblock_write(t, error); + scr_dyn_release(error); + ws_release(t); +} + +/* The perform-transform rejection step: errors the stream, then rethrows + * so the derived promise rejects with the error. */ +static void ws_perform_transform_rejected(void *ctx, ScrDyn *error) { + WsTController *c = ctx; + ws_transform_error(c->stream, error); + scr_dyn_throw(scr_dyn_retain(error)); +} + +/* transformStreamDefaultControllerPerformTransform: +1 promise. */ +static ScrPromise *ws_tcontroller_perform_transform(WsTController *c, ScrDyn *chunk) { + if (c->algorithms_cleared) return scr_promise_retain(ws_resolved_singleton()); + ScrDyn *result; + if (c->transform_fn) { + ScrDyn *controller = ws_tcontroller_box(c); + ScrDyn *transform = scr_dyn_retain(c->transform_fn); + ScrDyn *transformer = scr_dyn_retain(c->transformer); + ScrDyn *args[2] = {chunk, controller}; + result = ws_call(transform, transformer, args, 2, "transformer.transform"); + scr_dyn_release(transform); + scr_dyn_release(transformer); + scr_dyn_release(controller); + } else { + /* defaultTransformAlgorithm */ + result = ws_tcontroller_enqueue(c, chunk) ? ws_undef() : NULL; + } + ScrPromise *p; + if (!result) { + p = ws_promise_rejected_pending(); + } else if (!ws_is_object(result)) { + p = scr_promise_retain(ws_resolved_singleton()); + } else { + p = ws_promise_resolve(result); + if (!p) p = ws_promise_rejected_pending(); + } + if (result) scr_dyn_release(result); + ScrPromise *derived = ws_then(p, NULL, ws_perform_transform_rejected, ws_retain(c), true); + scr_promise_release(p); + return derived; +} + +/* The steps the sink close, sink abort and source cancel algorithms run on + * their callback's promise. */ +typedef enum { WS_FINISH_CLOSE, WS_FINISH_ABORT, WS_FINISH_CANCEL } WsFinishKind; + +typedef struct WsFinish { + WsObj obj; + WsTransform *stream; + ScrPromise *finish; /* +1 */ + ScrDyn *reason; /* +1, the abort or cancel reason */ + WsFinishKind kind; +} WsFinish; + +static void ws_finish_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsFinish *f = obj; + ws_visit(f->stream, visit, ctx); + ws_visit(f->finish, visit, ctx); + ws_visit(f->reason, visit, ctx); +} + +static void ws_finish_drop(void *obj, bool gc) { + WsFinish *f = obj; + ws_drop_obj(f->stream, gc); + ws_drop_promise(f->finish, gc); + ws_drop_dyn(f->reason, gc); +} + +static const WsObjType ws_finish_type = {ws_finish_trace, ws_finish_drop}; + +static void ws_finish_fulfilled(void *ctx, ScrDyn *unused) { + (void)unused; + WsFinish *f = ctx; + WsTransform *t = f->stream; + if (f->kind == WS_FINISH_CANCEL) { + WsWritable *w = t->writable; + if (w->state == WS_W_ERRORED) { + ws_promise_reject_with(f->finish, w->stored_error); + return; + } + ws_wcontroller_error_if_needed(w->controller, f->reason); + ws_transform_unblock_write(t); + ws_promise_fulfill_undefined(f->finish); + return; + } + WsReadable *r = t->readable; + if (r->state == WS_STATE_ERRORED) { + ws_promise_reject_with(f->finish, r->stored_error); + return; + } + if (f->kind == WS_FINISH_CLOSE) ws_rcontroller_close(r->controller); + else ws_rcontroller_error(r->controller, f->reason); + ws_promise_fulfill_undefined(f->finish); +} + +static void ws_finish_rejected(void *ctx, ScrDyn *error) { + WsFinish *f = ctx; + WsTransform *t = f->stream; + if (f->kind == WS_FINISH_CANCEL) { + ws_wcontroller_error_if_needed(t->writable->controller, error); + ws_transform_unblock_write(t); + } else { + ws_rcontroller_error(t->readable->controller, error); + } + ws_reject(f->finish, error); +} + +/* A transformer callback wrapped by createPromiseCallback1Param, or the + * nonOp default: +1 promise. */ +static ScrPromise *ws_tcontroller_callback(WsTController *c, ScrDyn *fn, ScrDyn *arg, const char *what) { + if (!fn) return ws_promise_resolved_undefined(); + ScrDyn *callee = scr_dyn_retain(fn); + ScrDyn *transformer = scr_dyn_retain(c->transformer); + ScrDyn *r = ws_call(callee, transformer, &arg, 1, what); + scr_dyn_release(callee); + scr_dyn_release(transformer); + if (!r) return ws_promise_rejected_pending(); + ScrPromise *p = ws_promise_resolving(r); + scr_dyn_release(r); + return p; +} + +/* The finish promise of the transform's close, abort or cancel: +1. */ +static ScrPromise *ws_transform_finish(WsTransform *t, WsFinishKind kind, ScrDyn *reason) { + WsTController *c = t->controller; + if (c->finish_promise) return scr_promise_retain(c->finish_promise); + c->finish_promise = scr_promise_new(); + ScrPromise *callback; + if (kind == WS_FINISH_CLOSE) { + ScrDyn *controller = ws_tcontroller_box(c); + callback = ws_tcontroller_callback(c, c->flush_fn, controller, "transformer.flush"); + scr_dyn_release(controller); + } else { + ScrDyn *arg = ws_or_undef(reason); + callback = ws_tcontroller_callback(c, c->cancel_fn, arg, "transformer.cancel"); + scr_dyn_release(arg); + } + ws_tcontroller_clear_algorithms(c); + WsFinish *f = ws_obj_new(sizeof *f, &ws_finish_type); + f->stream = ws_retain(t); + f->finish = scr_promise_retain(c->finish_promise); + f->reason = ws_or_undef(reason); + f->kind = kind; + ws_then(callback, ws_finish_fulfilled, ws_finish_rejected, f, false); + scr_promise_release(callback); + return scr_promise_retain(c->finish_promise); +} + +/* The writable side's algorithms. */ +static ScrDyn *ws_transform_sink_write(void *ctx, WsWController *wc, ScrDyn *chunk) { + (void)wc; + WsTransform *t = ctx; + if (t->backpressure) { + t->pending_write_parked = true; + t->pending_write_chunk = scr_dyn_retain(chunk); + return WS_PARKED; + } + return ws_promise_box(ws_tcontroller_perform_transform(t->controller, chunk)); +} + +static ScrPromise *ws_transform_sink_close(void *ctx, WsWController *wc) { + (void)wc; + return ws_transform_finish(ctx, WS_FINISH_CLOSE, NULL); +} + +/* `async function transformStreamDefaultSinkAbortAlgorithm`: the result + * adopts the finish promise. */ +static ScrPromise *ws_transform_sink_abort(void *ctx, WsWController *wc, ScrDyn *reason) { + (void)wc; + ScrPromise *finish = ws_transform_finish(ctx, WS_FINISH_ABORT, reason); + ScrDyn *box = ws_promise_box(finish); + ScrPromise *result = ws_promise_resolving(box); + scr_dyn_release(box); + return result; +} + +static const WsSinkOps ws_transform_sink_ops = {ws_transform_sink_write, ws_transform_sink_close, + ws_transform_sink_abort}; + +/* The readable side's algorithms. */ +static ScrDyn *ws_transform_source_pull(void *ctx, WsRController *rc) { + (void)rc; + WsTransform *t = ctx; + ws_transform_set_backpressure(t, false); + t->pull_pending = true; + return WS_PARKED; +} + +static ScrPromise *ws_transform_source_cancel(void *ctx, WsRController *rc, ScrDyn *reason) { + (void)rc; + return ws_transform_finish(ctx, WS_FINISH_CANCEL, reason); +} + +static const WsSourceOps ws_transform_source_ops = {ws_transform_source_pull, ws_transform_source_cancel}; + +/* `start` called through FunctionPrototypeCall: a value that is not + * callable throws its TypeError. */ +static void ws_function_call_error(void) { + static const char message[] = "FunctionPrototypeCall is not a function"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); +} + +/* new TransformStream(transformer, writableStrategy, readableStrategy): + * +1 handle, or NULL with the exception pending. */ +static ScrDyn *ws_transform_construct(ScrDyn *const *args, size_t argc) { + ScrDyn *transformer = argc > 0 && args[0]->kind != SCR_DYN_UNDEF ? args[0] : NULL; + ScrDyn *wstrategy = argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? args[1] : NULL; + ScrDyn *rstrategy = argc > 2 && args[2]->kind != SCR_DYN_UNDEF ? args[2] : NULL; + ScrDyn *empty = NULL; + if (!transformer || !wstrategy || !rstrategy) empty = scr_dyn_new_obj(); + if (!transformer) transformer = empty; + if (!wstrategy) wstrategy = empty; + if (!rstrategy) rstrategy = empty; + ScrDyn *result = NULL; + ScrDyn *rtype = NULL, *wtype = NULL, *start = NULL, *rhwm = NULL, *rsize = NULL, *whwm = NULL, *wsize = NULL; + ScrDyn *transform = NULL, *flush = NULL, *cancel = NULL; + if (!ws_validate_object(transformer, "transformer", false) || + !ws_validate_object(wstrategy, "writableStrategy", true) || + !ws_validate_object(rstrategy, "readableStrategy", true)) + goto done; + if (!(rtype = ws_get(transformer, "readableType"))) goto done; + if (!(wtype = ws_get(transformer, "writableType"))) goto done; + if (!(start = ws_get(transformer, "start"))) goto done; + if (rtype->kind != SCR_DYN_UNDEF) { + ws_range_arg_value("transformer.readableType", rtype); + goto done; + } + if (wtype->kind != SCR_DYN_UNDEF) { + ws_range_arg_value("transformer.writableType", wtype); + goto done; + } + if (!(rhwm = ws_get(rstrategy, "highWaterMark"))) goto done; + if (!(rsize = ws_get(rstrategy, "size"))) goto done; + if (!(whwm = ws_get(wstrategy, "highWaterMark"))) goto done; + if (!(wsize = ws_get(wstrategy, "size"))) goto done; + double readable_hwm, writable_hwm; + if (!ws_extract_high_water_mark(rhwm, 0, &readable_hwm)) goto done; + if (rsize->kind != SCR_DYN_UNDEF && !ws_validate_function(rsize, "strategy.size")) goto done; + if (!ws_extract_high_water_mark(whwm, 1, &writable_hwm)) goto done; + if (wsize->kind != SCR_DYN_UNDEF && !ws_validate_function(wsize, "strategy.size")) goto done; + ws_install(); + WsTransform *t = ws_obj_new(sizeof *t, &ws_transform_type); + ScrPromise *start_promise = scr_promise_new(); + ScrDyn *start_box = scr_dyn_new_promise(start_promise); + /* initializeTransformStream: the writable side first. */ + t->writable = ws_writable_alloc(); + WsWController *wc = ws_writable_attach(t->writable, writable_hwm); + wc->native = &ws_transform_sink_ops; + wc->native_ctx = ws_retain(t); + if (wsize->kind != SCR_DYN_UNDEF) wc->size_fn = scr_dyn_retain(wsize); + ws_wcontroller_started_with(wc, start_box); + t->readable = ws_readable_alloc(); + WsRController *rc = ws_rcontroller_attach(t->readable, readable_hwm); + rc->native = &ws_transform_source_ops; + rc->native_ctx = ws_retain(t); + if (rsize->kind != SCR_DYN_UNDEF) rc->size_fn = scr_dyn_retain(rsize); + ws_rcontroller_started_with(rc, start_box); + scr_dyn_release(start_box); + ws_transform_set_backpressure(t, true); + /* setupTransformStreamDefaultControllerFromTransformer */ + WsTController *c = ws_obj_new(sizeof *c, &ws_tcontroller_type); + c->stream = ws_retain(t); + t->controller = c; /* the stream's reference */ + bool ok = (transform = ws_get(transformer, "transform")) && (flush = ws_get(transformer, "flush")) && + (cancel = ws_get(transformer, "cancel")); + if (ok && scr_dyn_truthy(transform)) ok = ws_validate_function(transform, "transformer.transform"); + if (ok && scr_dyn_truthy(flush)) ok = ws_validate_function(flush, "transformer.flush"); + if (ok && scr_dyn_truthy(cancel)) ok = ws_validate_function(cancel, "transformer.cancel"); + if (!ok) { + scr_promise_release(start_promise); + ws_release(t); + goto done; + } + c->transformer = scr_dyn_retain(transformer); + if (scr_dyn_truthy(transform)) c->transform_fn = scr_dyn_retain(transform); + if (scr_dyn_truthy(flush)) c->flush_fn = scr_dyn_retain(flush); + if (scr_dyn_truthy(cancel)) c->cancel_fn = scr_dyn_retain(cancel); + if (start->kind != SCR_DYN_UNDEF) { + if (!scr_dyn_is_callable(start)) { + ws_function_call_error(); + scr_promise_release(start_promise); + ws_release(t); + goto done; + } + ScrDyn *controller = ws_tcontroller_box(c); + ScrDyn *value = ws_call(start, transformer, &controller, 1, "transformer.start"); + scr_dyn_release(controller); + if (!value) { + scr_promise_release(start_promise); + ws_release(t); + goto done; + } + scr_promise_resolve_dyn(start_promise, value); + } else { + ws_promise_fulfill_undefined(start_promise); + } + scr_promise_release(start_promise); + result = ws_transform_box(t); + ws_release(t); +done: + if (empty) scr_dyn_release(empty); + ScrDyn *gets[] = {rtype, wtype, start, rhwm, rsize, whwm, wsize, transform, flush, cancel}; + for (size_t i = 0; i < sizeof gets / sizeof gets[0]; i++) + if (gets[i]) scr_dyn_release(gets[i]); + return result; +} + +/* ── pipeTo (readableStreamPipeTo) ────────────────────────────────── */ + +/* writableStreamDefaultWriterWriteWithRequest: the pipe's write of one + * chunk, tracked by its shared request; a write that cannot proceed + * latches the failure on it. */ +static void ws_writer_write_with_tracker(WsWriter *w, ScrDyn *chunk, WsPipe *tracker); + +/* writableStreamDefaultWriterCloseWithErrorPropagation: +1 promise. */ +static ScrPromise *ws_writer_close_with_error_propagation(WsWriter *w) { + WsWritable *s = w->stream; + if (s->close_queued_or_inflight || s->state == WS_W_CLOSED) return ws_promise_resolved_undefined(); + if (s->state == WS_W_ERRORED) return ws_promise_rejected_with(s->stored_error); + return ws_writable_close(s); +} + +typedef enum { + WS_PIPE_ABORT_DEST, + WS_PIPE_CANCEL_SOURCE, + WS_PIPE_CLOSE_DEST, + WS_PIPE_SIGNAL, +} WsPipeAction; + +struct WsPipe { + WsObj obj; + WsReadable *source; + WsWritable *dest; + WsReader *reader; + WsWriter *writer; + bool prevent_close, prevent_abort, prevent_cancel; + ScrDyn *signal; /* +1, or NULL */ + ScrDyn *disposable; /* +1: the abort listener's dispose */ + bool shutting_down; + ScrPromise *promise; /* the pipeTo result, +1 */ + /* The shared write request. */ + size_t pending; + bool failed; + ScrDyn *failure; /* +1 */ + ScrPromise *stall; /* +1 while armed */ + ScrDyn *pending_chunk; + ScrPromise *ready_promise; /* the ready hook's forever-pending promise */ + /* The shutdown in progress. */ + WsPipeAction action; + bool rejected; + ScrDyn *original_error; /* +1, or NULL for undefined */ + ScrDyn *action_error; /* the action's argument (+1) */ +}; + +static void ws_pipe_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsPipe *p = obj; + ws_visit(p->source, visit, ctx); + ws_visit(p->dest, visit, ctx); + ws_visit(p->reader, visit, ctx); + ws_visit(p->writer, visit, ctx); + ws_visit(p->signal, visit, ctx); + ws_visit(p->disposable, visit, ctx); + ws_visit(p->promise, visit, ctx); + ws_visit(p->failure, visit, ctx); + ws_visit(p->stall, visit, ctx); + ws_visit(p->pending_chunk, visit, ctx); + ws_visit(p->ready_promise, visit, ctx); + ws_visit(p->original_error, visit, ctx); + ws_visit(p->action_error, visit, ctx); +} + +static void ws_pipe_drop(void *obj, bool gc) { + WsPipe *p = obj; + ws_drop_obj(p->source, gc); + ws_drop_obj(p->dest, gc); + ws_drop_obj(p->reader, gc); + ws_drop_obj(p->writer, gc); + ws_drop_dyn(p->signal, gc); + ws_drop_dyn(p->disposable, gc); + ws_drop_promise(p->promise, gc); + ws_drop_dyn(p->failure, gc); + ws_drop_promise(p->stall, gc); + ws_drop_dyn(p->pending_chunk, gc); + ws_drop_promise(p->ready_promise, gc); + ws_drop_dyn(p->original_error, gc); + ws_drop_dyn(p->action_error, gc); +} + +static const WsObjType ws_pipe_type = {ws_pipe_trace, ws_pipe_drop}; + +static void ws_pipe_tracker_resolve(WsPipe *p) { + if (--p->pending == 0 && p->stall) { + ScrPromise *stall = p->stall; + p->stall = NULL; + if (p->failed) ws_reject(stall, p->failure); + else ws_promise_fulfill_undefined(stall); + scr_promise_release(stall); + } +} + +static void ws_pipe_tracker_reject(WsPipe *p, ScrDyn *error) { + p->pending--; + p->failed = true; + if (p->failure) scr_dyn_release(p->failure); + p->failure = scr_dyn_retain(error); + if (p->stall) { + ScrPromise *stall = p->stall; + p->stall = NULL; + ws_reject(stall, error); + scr_promise_release(stall); + } +} + +static void ws_writer_write_with_tracker(WsWriter *w, ScrDyn *chunk, WsPipe *tracker) { + WsWritable *s = w->stream; + ws_retain(s); + WsWController *c = s->controller; + ScrDyn *size = ws_wcontroller_chunk_size(c, chunk); + ScrDyn *failure = NULL; + if (s != w->stream) failure = ws_state_error_value("Mismatched WritableStreams"); + else if (s->state == WS_W_ERRORED) failure = ws_or_undef(s->stored_error); + else if (s->close_queued_or_inflight || s->state == WS_W_CLOSED) + failure = ws_state_error_value("WritableStream is closed"); + else if (s->state == WS_W_ERRORING) failure = ws_or_undef(s->stored_error); + if (failure) { + tracker->failed = true; + if (tracker->failure) scr_dyn_release(tracker->failure); + tracker->failure = failure; + } else { + ws_requests_push(s, (WsWriteRequest){NULL, ws_retain(tracker)}); + tracker->pending++; + ws_wcontroller_write(c, chunk, size); + } + if (size) scr_dyn_release(size); + ws_release(s); +} + +static void ws_pipe_finalize(WsPipe *p, bool rejected, ScrDyn *error) { + ws_retain(p); + if (p->writer->stream) ws_writer_release_lock(p->writer); + if (p->reader->stream) ws_reader_generic_release(p->reader); + if (p->disposable) { + ScrDyn *dispose = p->disposable; + p->disposable = NULL; + ScrDyn *r = scr_dyn_call(dispose, NULL, 0, "[Symbol.dispose]"); + if (r) scr_dyn_release(r); + else if (scr_exc_pending()) scr_exc_clear(); + scr_dyn_release(dispose); + } + if (rejected) ws_promise_reject_with(p->promise, error); + else ws_promise_fulfill_undefined(p->promise); + ws_release(p); +} + +static void ws_pipe_finalize_original(void *ctx, ScrDyn *unused) { + (void)unused; + WsPipe *p = ctx; + ws_pipe_finalize(p, p->rejected, p->original_error); +} + +static void ws_pipe_finalize_error(void *ctx, ScrDyn *error) { ws_pipe_finalize(ctx, true, error); } + +/* waitForPendingWrites: +1 promise. */ +static ScrPromise *ws_pipe_wait_for_pending_writes(WsPipe *p) { + if (p->pending == 0) return p->failed ? ws_promise_rejected_with(p->failure) : ws_promise_resolved_undefined(); + if (p->stall) scr_promise_release(p->stall); + p->stall = scr_promise_new(); + return scr_promise_retain(p->stall); +} + +/* Promise.all over the abort algorithm's actions, through the primordial + * SafePromiseAll: each entry resolves a promise of its own, and the + * combined result settles a promise the caller's then observes. */ +typedef struct WsPipeAll { + WsObj obj; + ScrPromise *all; /* +1 */ + size_t remaining; + bool settled; +} WsPipeAll; + +static void ws_pipe_all_trace(void *obj, ScrTraceVisit visit, void *ctx) { ws_visit(((WsPipeAll *)obj)->all, visit, ctx); } + +static void ws_pipe_all_drop(void *obj, bool gc) { ws_drop_promise(((WsPipeAll *)obj)->all, gc); } + +static const WsObjType ws_pipe_all_type = {ws_pipe_all_trace, ws_pipe_all_drop}; + +static void ws_pipe_all_fulfilled(void *ctx, ScrDyn *unused) { + (void)unused; + WsPipeAll *a = ctx; + if (a->settled || --a->remaining != 0) return; + a->settled = true; + ws_promise_fulfill_undefined(a->all); +} + +static void ws_pipe_all_rejected(void *ctx, ScrDyn *error) { + WsPipeAll *a = ctx; + if (a->settled) return; + a->settled = true; + ws_reject(a->all, error); +} + +/* The shutdown action's promise: +1. */ +static ScrPromise *ws_pipe_run_action(WsPipe *p) { + ScrDyn *error = p->action_error; + switch (p->action) { + case WS_PIPE_ABORT_DEST: return ws_writable_abort(p->dest, error); + case WS_PIPE_CANCEL_SOURCE: return ws_readable_cancel(p->source, error); + case WS_PIPE_CLOSE_DEST: return ws_writer_close_with_error_propagation(p->writer); + case WS_PIPE_SIGNAL: break; + } + ScrPromise *entries[2]; + size_t n = 0; + if (!p->prevent_abort) + entries[n++] = p->dest->state == WS_W_WRITABLE ? ws_writable_abort(p->dest, error) : ws_promise_resolved_undefined(); + if (!p->prevent_cancel) + entries[n++] = p->source->state == WS_STATE_READABLE ? ws_readable_cancel(p->source, error) + : ws_promise_resolved_undefined(); + WsPipeAll *a = ws_obj_new(sizeof *a, &ws_pipe_all_type); + a->all = scr_promise_new(); + a->remaining = n; + if (n == 0) { + a->settled = true; + ws_promise_fulfill_undefined(a->all); + } + for (size_t i = 0; i < n; i++) { + ScrDyn *box = ws_promise_box(entries[i]); + ScrPromise *own = ws_promise_resolving(box); + scr_dyn_release(box); + ws_then(own, ws_pipe_all_fulfilled, ws_pipe_all_rejected, ws_retain(a), false); + scr_promise_release(own); + } + ScrPromise *result = ws_then_void(a->all); + ws_release(a); + return result; +} + +static void ws_pipe_complete(void *ctx, ScrDyn *unused) { + (void)unused; + WsPipe *p = ctx; + ScrPromise *action = ws_pipe_run_action(p); + ws_then(action, ws_pipe_finalize_original, ws_pipe_finalize_error, ws_retain(p), false); + scr_promise_release(action); +} + +static void ws_pipe_shutdown_with_action(WsPipe *p, WsPipeAction action, bool rejected, ScrDyn *original_error, + ScrDyn *action_error) { + if (p->shutting_down) return; + p->shutting_down = true; + p->action = action; + p->rejected = rejected; + p->original_error = original_error ? scr_dyn_retain(original_error) : NULL; + p->action_error = ws_or_undef(action_error); + if (p->dest->state == WS_W_WRITABLE && !p->dest->close_queued_or_inflight) { + ScrPromise *wait = ws_pipe_wait_for_pending_writes(p); + ws_then(wait, ws_pipe_complete, ws_pipe_finalize_error, ws_retain(p), false); + scr_promise_release(wait); + return; + } + ws_pipe_complete(p, NULL); +} + +static void ws_pipe_shutdown(WsPipe *p, bool rejected, ScrDyn *error) { + if (p->shutting_down) return; + p->shutting_down = true; + p->rejected = rejected; + p->original_error = error ? scr_dyn_retain(error) : NULL; + if (p->dest->state == WS_W_WRITABLE && !p->dest->close_queued_or_inflight) { + ScrPromise *wait = ws_pipe_wait_for_pending_writes(p); + ws_then(wait, ws_pipe_finalize_original, ws_pipe_finalize_error, ws_retain(p), false); + scr_promise_release(wait); + return; + } + ws_pipe_finalize(p, rejected, error); +} + +static void ws_pipe_abort_algorithm(WsPipe *p) { + ScrDyn *error = ws_get(p->signal, "reason"); + if (!error) { + error = ws_take_pending(); + } + ws_pipe_shutdown_with_action(p, WS_PIPE_SIGNAL, true, error, error); + scr_dyn_release(error); +} + +static void ws_pipe_pump(WsPipe *p); + +static void ws_pipe_pump_hook(void *ctx) { ws_pipe_pump(ctx); } + +static void ws_pipe_pump_step(void *ctx, ScrDyn *unused) { + (void)unused; + ws_pipe_pump(ctx); +} + +static void ws_pipe_park_on_ready(WsPipe *p) { + if (!p->ready_promise) p->ready_promise = scr_promise_new(); + WsRecord *ready = &p->writer->ready; + ws_record_set(ready, scr_promise_retain(p->ready_promise), true); + ready->hook = ws_pipe_pump_hook; + ready->hook_ctx = p; +} + +static void ws_pipe_forward_chunk(void *ctx, ScrDyn *unused) { + (void)unused; + WsPipe *p = ctx; + ScrDyn *chunk = p->pending_chunk; + p->pending_chunk = NULL; + ws_writer_write_with_tracker(p->writer, chunk, p); + scr_dyn_release(chunk); + ws_pipe_pump(p); +} + +static void ws_pipe_read_chunk(WsReadRequest *q, ScrDyn *chunk) { + WsPipe *p = q->ctx; + if (p->pending_chunk) scr_dyn_release(p->pending_chunk); + p->pending_chunk = scr_dyn_retain(chunk); + ws_queue_microtask(ws_pipe_forward_chunk, p); /* the request's reference moves */ + free(q); +} + +static void ws_pipe_read_done(WsReadRequest *q) { + ws_release(q->ctx); + free(q); +} + +static void ws_pipe_read_error(WsReadRequest *q, ScrDyn *error) { + (void)error; + ws_pipe_read_done(q); +} + +static const WsReadRequestOps ws_pipe_read_ops = {ws_pipe_read_chunk, ws_pipe_read_done, ws_pipe_read_error}; + +static void ws_pipe_pump(WsPipe *p) { + if (p->shutting_down) return; + WsWritable *dest = p->dest; + if (dest->backpressure) { + ws_pipe_park_on_ready(p); + return; + } + WsReadable *source = p->source; + WsRController *c = source->controller; + if (source->state == WS_STATE_READABLE && c->queue.length > 0) { + ws_retain(p); + while (c->queue.length > 0) { + if (p->shutting_down) { + ws_release(p); + return; + } + double size; + ScrDyn *chunk = ws_queue_shift(&c->queue, &size); + c->queue_total_size = fmax(0, c->queue_total_size - size); + if (c->close_requested && !c->queue.length) { + ws_rcontroller_clear_algorithms(c); + ws_readable_close(source); + } + ws_writer_write_with_tracker(p->writer, chunk, p); + scr_dyn_release(chunk); + if (dest->backpressure) break; + if (dest->state != WS_W_WRITABLE || dest->close_queued_or_inflight) break; + } + ws_rcontroller_call_pull_if_needed(c); + if (source->state == WS_STATE_CLOSED) { + ws_release(p); + return; + } + if (dest->backpressure) { + ws_pipe_park_on_ready(p); + ws_release(p); + return; + } + ws_queue_microtask(ws_pipe_pump_step, p); /* the reference moves */ + return; + } + WsReadRequest *q = ws_alloc(sizeof *q); + q->ops = &ws_pipe_read_ops; + q->ctx = ws_retain(p); + ws_reader_read_request(p->reader, q); +} + +/* The watchers on the reader's and writer's closed promises. */ +static void ws_pipe_source_errored(void *ctx, ScrDyn *error) { + WsPipe *p = ctx; + if (!p->prevent_abort) ws_pipe_shutdown_with_action(p, WS_PIPE_ABORT_DEST, true, error, error); + else ws_pipe_shutdown(p, true, error); +} + +static void ws_pipe_dest_errored(void *ctx, ScrDyn *error) { + WsPipe *p = ctx; + if (!p->prevent_cancel) ws_pipe_shutdown_with_action(p, WS_PIPE_CANCEL_SOURCE, true, error, error); + else ws_pipe_shutdown(p, true, error); +} + +static void ws_pipe_source_closed(void *ctx, ScrDyn *unused) { + (void)unused; + WsPipe *p = ctx; + if (!p->prevent_close) ws_pipe_shutdown_with_action(p, WS_PIPE_CLOSE_DEST, false, NULL, NULL); + else ws_pipe_shutdown(p, false, NULL); +} + +static void ws_pipe_ignore(void *ctx, ScrDyn *unused) { + (void)ctx; + (void)unused; +} + +static ScrDyn *ws_pipe_abort_listener(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + WsPipe *p = scr_box_get_ref(closure->caps[0]); + ws_pipe_abort_algorithm(p); + ws_release(p); + return ws_undef(); +} + +/* readableStreamPipeTo: +1 promise. The streams are unlocked. */ +static ScrPromise *ws_readable_pipe_to(WsReadable *source, WsWritable *dest, bool prevent_close, bool prevent_abort, + bool prevent_cancel, ScrDyn *signal) { + WsReader *reader = ws_reader_new(source); + if (!reader) return ws_promise_rejected_pending(); + WsWriter *writer = ws_writer_new(dest); + if (!writer) { + ws_release(reader); + return ws_promise_rejected_pending(); + } + source->disturbed = true; + WsPipe *p = ws_obj_new(sizeof *p, &ws_pipe_type); + p->source = ws_retain(source); + p->dest = ws_retain(dest); + p->reader = reader; + p->writer = writer; + p->prevent_close = prevent_close; + p->prevent_abort = prevent_abort; + p->prevent_cancel = prevent_cancel; + p->promise = scr_promise_new(); + ScrPromise *result = scr_promise_retain(p->promise); + if (signal) { + p->signal = scr_dyn_retain(signal); + ScrDyn *aborted = ws_get(signal, "aborted"); + bool is_aborted = aborted && scr_dyn_truthy(aborted); + if (aborted) scr_dyn_release(aborted); + else if (scr_exc_pending()) scr_exc_clear(); + if (is_aborted) { + ws_pipe_abort_algorithm(p); + ws_release(p); + return result; + } + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(ws_retain, ws_release, ws_obj_trace); + scr_box_set_ref(closure->caps[0], ws_retain(p)); + ScrDyn *listener = scr_dyn_new_func(closure, ws_pipe_abort_listener, 0, "", "abortAlgorithm"); + p->disposable = scr_events_add_abort_listener(signal, listener); + scr_dyn_release(listener); + if (!p->disposable && scr_exc_pending()) scr_exc_clear(); + } + ws_pipe_pump(p); + if (source->state == WS_STATE_ERRORED) ws_pipe_source_errored(p, source->stored_error); + else ws_then(ws_reader_closed_promise(reader), NULL, ws_pipe_source_errored, ws_retain(p), false); + if (dest->state == WS_W_ERRORED) ws_pipe_dest_errored(p, dest->stored_error); + else ws_then(ws_writer_closed_promise(writer), NULL, ws_pipe_dest_errored, ws_retain(p), false); + if (source->state == WS_STATE_CLOSED) ws_pipe_source_closed(p, NULL); + else ws_then(ws_reader_closed_promise(reader), ws_pipe_source_closed, ws_pipe_ignore, ws_retain(p), false); + if (dest->close_queued_or_inflight || dest->state == WS_W_CLOSED) { + ScrDyn *error = ws_state_error_value("Destination WritableStream is closed"); + if (!prevent_cancel) ws_pipe_shutdown_with_action(p, WS_PIPE_CANCEL_SOURCE, true, error, error); + else ws_pipe_shutdown(p, true, error); + scr_dyn_release(error); + } + ws_release(p); + return result; +} + +/* pipeTo(destination, options) and pipeThrough(transform, options): the + * options read in Node's order, and the checks before the pipe starts. + * false with the exception pending. */ +static bool ws_pipe_options(ScrDyn *options, bool *prevent_close, bool *prevent_abort, bool *prevent_cancel, + ScrDyn **signal_out) { + *signal_out = NULL; + if (!ws_validate_object(options, "options", true)) return false; + ScrDyn *abort = ws_get(options, "preventAbort"); + if (!abort) return false; + ScrDyn *cancel = ws_get(options, "preventCancel"); + if (!cancel) { + scr_dyn_release(abort); + return false; + } + ScrDyn *close = ws_get(options, "preventClose"); + if (!close) { + scr_dyn_release(abort); + scr_dyn_release(cancel); + return false; + } + ScrDyn *signal = ws_get(options, "signal"); + *prevent_abort = scr_dyn_truthy(abort); + *prevent_cancel = scr_dyn_truthy(cancel); + *prevent_close = scr_dyn_truthy(close); + scr_dyn_release(abort); + scr_dyn_release(cancel); + scr_dyn_release(close); + if (!signal) return false; + if (signal->kind == SCR_DYN_UNDEF) { + scr_dyn_release(signal); + return true; + } + /* validateAbortSignal: an object with an `aborted` member. */ + bool valid = false; + if (ws_is_object(signal)) { + ScrStr *key = scr_str_new("aborted", 7); + valid = scr_dyn_in(signal, key); + scr_str_release(key); + if (scr_exc_pending()) { + scr_dyn_release(signal); + return false; + } + } + if (!valid) { + scr_dyn_arg_type_fail("options.signal", "an instance of AbortSignal", signal); + scr_dyn_release(signal); + return false; + } + *signal_out = signal; + return true; +} + +/* ── own properties ── */ + +/* An own property a program set on the state's object (+1), or NULL. */ +static ScrDyn *ws_own_get(void *obj, const char *key, size_t len) { + /* scr_dyn_handle_class_method reads methods through a handle without a + * state. */ + if (!obj) return NULL; + ScrDyn *own = ((WsObj *)obj)->own; + ScrDyn *value = own ? scr_dyn_obj_get(own, key, len) : NULL; + return value ? scr_dyn_retain(value) : NULL; +} + +/* A method call that an own property of the name overrides: true with + * *result set (+1, or NULL with the exception pending). */ +static bool ws_own_invoke(void *obj, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what, ScrDyn **result) { + ScrDyn *fn = ws_own_get(obj, method, strlen(method)); + if (!fn) return false; + if (!scr_dyn_is_callable(fn)) { + scr_dyn_release(fn); + char buf[256]; + int n = snprintf(buf, sizeof buf, "%s is not a function", what && *what ? what : method); + scr_throw_error_msg(SCR_ERR_TYPE, buf, (size_t)n); + *result = NULL; + return true; + } + *result = ws_call(fn, self, args, argc, what); + scr_dyn_release(fn); + return true; +} + +/* A write of a property on the state's object: an own property, except a + * getter-only member of the class, which strict code cannot write. */ +static bool ws_own_set(void *h, const char *key, size_t len, const ScrDyn *value) { + WsObj *o = h; + for (const ScrDynHandleAccessor *a = o->type->accessors; a && a->name; a++) { + if (strlen(a->name) != len || memcmp(a->name, key, len) != 0) continue; + char buf[256]; + int n = snprintf(buf, sizeof buf, "Cannot set property %.*s of #<%s> which has only a getter", (int)len, key, + o->type->cls); + scr_throw_error_msg(SCR_ERR_TYPE, buf, (size_t)n); + return true; + } + if (!o->own) o->own = scr_dyn_new_obj(); + scr_dyn_obj_set(o->own, key, len, scr_dyn_retain((ScrDyn *)value)); + return true; +} + /* ── the handle surfaces ───────────────────────────────────────────── */ static bool ws_name(const char *key, size_t len, const char *want) { @@ -2092,6 +4170,8 @@ static ScrDyn *ws_readable_get_reader(WsReadable *s, ScrDyn *options) { static ScrDyn *ws_readable_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; (void)self; (void)what; WsReadable *s = ptr; @@ -2129,10 +4209,73 @@ static ScrDyn *ws_readable_invoke(void *ptr, ScrDyn *self, const char *method, S if (empty) scr_dyn_release(empty); return iterator; } - if (ws_name(method, len, "pipeTo") || ws_name(method, len, "pipeThrough")) { - static const char message[] = "ReadableStream.prototype.pipeTo and pipeThrough are not supported yet"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); - return NULL; + if (ws_name(method, len, "pipeTo")) { + ScrDyn *destination = ws_arg(args, argc, 0); + WsWritable *dest = ws_writable_of(destination); + if (!dest) { + scr_dyn_arg_type_fail("transform.writable", "an instance of WritableStream", destination); + return ws_rejected_pending_box(); + } + ScrDyn *options = ws_arg(args, argc, 1); + ScrDyn *empty = options->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + bool prevent_close = false, prevent_abort = false, prevent_cancel = false; + ScrDyn *signal; + bool ok = ws_pipe_options(empty ? empty : options, &prevent_close, &prevent_abort, &prevent_cancel, &signal); + if (empty) scr_dyn_release(empty); + if (!ok) return ws_rejected_pending_box(); + ScrPromise *result; + if (ws_readable_locked(s)) result = ws_promise_state_rejected("The ReadableStream is locked"); + else if (ws_writable_locked(dest)) result = ws_promise_state_rejected("The WritableStream is locked"); + else result = ws_readable_pipe_to(s, dest, prevent_close, prevent_abort, prevent_cancel, signal); + if (signal) scr_dyn_release(signal); + return ws_promise_box(result); + } + if (ws_name(method, len, "pipeThrough")) { + ScrDyn *transform = ws_arg(args, argc, 0); + ScrDyn *readable = ws_get(transform, "readable"); + if (!readable) return NULL; + if (!ws_stream_of(readable)) { + scr_dyn_arg_type_fail("transform.readable", "an instance of ReadableStream", readable); + scr_dyn_release(readable); + return NULL; + } + ScrDyn *writable = ws_get(transform, "writable"); + if (!writable) { + scr_dyn_release(readable); + return NULL; + } + WsWritable *dest = ws_writable_of(writable); + if (!dest) { + scr_dyn_arg_type_fail("transform.writable", "an instance of WritableStream", writable); + scr_dyn_release(readable); + scr_dyn_release(writable); + return NULL; + } + ScrDyn *options = ws_arg(args, argc, 1); + ScrDyn *empty = options->kind == SCR_DYN_UNDEF ? scr_dyn_new_obj() : NULL; + bool prevent_close = false, prevent_abort = false, prevent_cancel = false; + ScrDyn *signal = NULL; + bool ok = ws_pipe_options(empty ? empty : options, &prevent_close, &prevent_abort, &prevent_cancel, &signal); + if (empty) scr_dyn_release(empty); + if (ok && ws_readable_locked(s)) { + ws_state_error("The ReadableStream is locked"); + ok = false; + } else if (ok && ws_writable_locked(dest)) { + ws_state_error("The WritableStream is locked"); + ok = false; + } + if (ok) { + ScrPromise *piped = ws_readable_pipe_to(s, dest, prevent_close, prevent_abort, prevent_cancel, signal); + ws_set_handled(piped); + scr_promise_release(piped); + } + if (signal) scr_dyn_release(signal); + scr_dyn_release(writable); + if (!ok) { + scr_dyn_release(readable); + return NULL; + } + return readable; } return ws_invoke_member(self, method, args, argc, what); } @@ -2140,6 +4283,8 @@ static ScrDyn *ws_readable_invoke(void *ptr, ScrDyn *self, const char *method, S static ScrDyn *ws_readable_get(void *ptr, const char *key, size_t len) { WsReadable *s = ptr; if (ws_name(key, len, "locked")) return scr_dyn_new_bool(ws_readable_locked(s)); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; return scr_dyn_handle_class_member(SCR_DYNH_WS_READABLE, ptr, key, len); } @@ -2158,7 +4303,6 @@ static const ScrDynHandleMethod ws_readable_methods[] = { {NULL, 0, false, NULL}, }; -static const ScrDynHandleAccessor ws_readable_accessors[] = {{"locked", false}, {NULL, false}}; static const ScrDynHandleStatic ws_readable_statics[] = {{"from", 1, ws_readable_from_thunk}, {NULL, 0, NULL}}; @@ -2212,6 +4356,8 @@ static ScrDyn *ws_reader_read(WsReader *r) { static ScrDyn *ws_reader_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; (void)self; (void)what; WsReader *r = ptr; @@ -2231,6 +4377,8 @@ static ScrDyn *ws_reader_invoke(void *ptr, ScrDyn *self, const char *method, Scr static ScrDyn *ws_reader_get(void *ptr, const char *key, size_t len) { WsReader *r = ptr; if (ws_name(key, len, "closed")) return scr_dyn_new_promise(ws_reader_closed_promise(r)); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; return scr_dyn_handle_class_member(SCR_DYNH_WS_READER, ptr, key, len); } @@ -2238,7 +4386,6 @@ static const ScrDynHandleMethod ws_reader_methods[] = { {"read", 0, true, NULL}, {"releaseLock", 0, false, NULL}, {"cancel", 0, true, NULL}, {NULL, 0, false, NULL}, }; -static const ScrDynHandleAccessor ws_reader_accessors[] = {{"closed", true}, {NULL, false}}; static const char *const ws_reader_order[] = {"read", "releaseLock", "closed", "cancel", NULL}; @@ -2250,6 +4397,8 @@ static const ScrDynHandleClass ws_reader_class = { static ScrDyn *ws_rcontroller_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; (void)self; (void)what; WsRController *c = ptr; @@ -2279,6 +4428,8 @@ static ScrDyn *ws_rcontroller_invoke(void *ptr, ScrDyn *self, const char *method static ScrDyn *ws_rcontroller_get(void *ptr, const char *key, size_t len) { if (ws_name(key, len, "desiredSize")) return ws_rcontroller_desired_size(ptr); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; return scr_dyn_handle_class_member(SCR_DYNH_WS_RCONTROLLER, ptr, key, len); } @@ -2286,7 +4437,6 @@ static const ScrDynHandleMethod ws_rcontroller_methods[] = { {"close", 0, false, NULL}, {"enqueue", 0, false, NULL}, {"error", 0, false, NULL}, {NULL, 0, false, NULL}, }; -static const ScrDynHandleAccessor ws_rcontroller_accessors[] = {{"desiredSize", false}, {NULL, false}}; static const char *const ws_rcontroller_order[] = {"desiredSize", "close", "enqueue", "error", NULL}; @@ -2294,6 +4444,507 @@ static const ScrDynHandleClass ws_rcontroller_class = { "ReadableStreamDefaultController", 0, NULL, ws_rcontroller_accessors, NULL, ws_rcontroller_order, }; +/* WritableStream */ +static ScrDyn *ws_writable_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; + WsWritable *s = ptr; + size_t len = strlen(method); + if (ws_name(method, len, "abort")) { + if (ws_writable_locked(s)) return ws_promise_box(ws_promise_state_rejected("WritableStream is locked")); + return ws_promise_box(ws_writable_abort(s, ws_arg(args, argc, 0))); + } + if (ws_name(method, len, "close")) { + if (ws_writable_locked(s)) return ws_promise_box(ws_promise_state_rejected("WritableStream is locked")); + if (s->close_queued_or_inflight) + return ws_promise_box(ws_promise_state_rejected("Failure closing WritableStream")); + return ws_promise_box(ws_writable_close(s)); + } + if (ws_name(method, len, "getWriter")) { + WsWriter *w = ws_writer_new(s); + if (!w) return NULL; + ScrDyn *box = ws_writer_box(w); + ws_release(w); + return box; + } + return ws_invoke_member(self, method, args, argc, what); +} + +static ScrDyn *ws_writable_get(void *ptr, const char *key, size_t len) { + WsWritable *s = ptr; + if (ws_name(key, len, "locked")) return scr_dyn_new_bool(ws_writable_locked(s)); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; + return scr_dyn_handle_class_member(SCR_DYNH_WS_WRITABLE, ptr, key, len); +} + +static const ScrDynHandleMethod ws_writable_methods[] = { + {"abort", 0, true, NULL}, {"close", 0, true, NULL}, {"getWriter", 0, false, NULL}, {NULL, 0, false, NULL}, +}; + + +static const char *const ws_writable_order[] = {"locked", "abort", "close", "getWriter", NULL}; + +static const ScrDynHandleClass ws_writable_class = { + "WritableStream", 0, ws_writable_construct, ws_writable_accessors, NULL, ws_writable_order, +}; + +/* WritableStreamDefaultWriter */ +static ScrDyn *ws_writer_construct(ScrDyn *const *args, size_t argc) { + ScrDyn *stream = ws_arg(args, argc, 0); + WsWritable *s = ws_writable_of(stream); + if (!s) { + scr_dyn_arg_type_fail("stream", "an instance of WritableStream", stream); + return NULL; + } + WsWriter *w = ws_writer_new(s); + if (!w) return NULL; + ScrDyn *box = ws_writer_box(w); + ws_release(w); + return box; +} + +static const char ws_writer_unbound[] = "Writer is not bound to a WritableStream"; + +static ScrDyn *ws_writer_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; + WsWriter *w = ptr; + size_t len = strlen(method); + if (ws_name(method, len, "abort")) { + if (!w->stream) return ws_promise_box(ws_promise_state_rejected(ws_writer_unbound)); + return ws_promise_box(ws_writable_abort(w->stream, ws_arg(args, argc, 0))); + } + if (ws_name(method, len, "close")) { + if (!w->stream) return ws_promise_box(ws_promise_state_rejected(ws_writer_unbound)); + if (w->stream->close_queued_or_inflight) + return ws_promise_box(ws_promise_state_rejected("Failure to close WritableStream")); + return ws_promise_box(ws_writable_close(w->stream)); + } + if (ws_name(method, len, "releaseLock")) { + if (w->stream) ws_writer_release_lock(w); + return ws_undef(); + } + if (ws_name(method, len, "write")) { + if (!w->stream) return ws_promise_box(ws_promise_state_rejected(ws_writer_unbound)); + return ws_promise_box(ws_writer_write(w, ws_arg(args, argc, 0))); + } + return ws_invoke_member(self, method, args, argc, what); +} + +static ScrDyn *ws_writer_get(void *ptr, const char *key, size_t len) { + WsWriter *w = ptr; + if (ws_name(key, len, "closed")) return scr_dyn_new_promise(ws_writer_closed_promise(w)); + if (ws_name(key, len, "ready")) return scr_dyn_new_promise(ws_writer_ready_promise(w)); + if (ws_name(key, len, "desiredSize")) { + if (!w->stream) { + ws_state_error(ws_writer_unbound); + return NULL; + } + return ws_writer_desired_size(w); + } + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; + return scr_dyn_handle_class_member(SCR_DYNH_WS_WRITER, ptr, key, len); +} + +static const ScrDynHandleMethod ws_writer_methods[] = { + {"abort", 0, true, NULL}, {"close", 0, true, NULL}, {"releaseLock", 0, false, NULL}, + {"write", 0, true, NULL}, {NULL, 0, false, NULL}, +}; + + +static const char *const ws_writer_order[] = {"closed", "desiredSize", "ready", "abort", + "close", "releaseLock", "write", NULL}; + +static const ScrDynHandleClass ws_writer_class = { + "WritableStreamDefaultWriter", 1, ws_writer_construct, ws_writer_accessors, NULL, ws_writer_order, +}; + +/* WritableStreamDefaultController */ +static ScrDyn *ws_wcontroller_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; + WsWController *c = ptr; + size_t len = strlen(method); + if (ws_name(method, len, "error")) { + if (c->stream->state == WS_W_WRITABLE) ws_wcontroller_error(c, ws_arg(args, argc, 0)); + return ws_undef(); + } + return ws_invoke_member(self, method, args, argc, what); +} + +static ScrDyn *ws_wcontroller_get(void *ptr, const char *key, size_t len) { + if (ws_name(key, len, "signal")) return ws_wcontroller_signal(ptr); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; + return scr_dyn_handle_class_member(SCR_DYNH_WS_WCONTROLLER, ptr, key, len); +} + +static const ScrDynHandleMethod ws_wcontroller_methods[] = {{"error", 0, false, NULL}, {NULL, 0, false, NULL}}; + + +static const char *const ws_wcontroller_order[] = {"signal", "error", NULL}; + +static const ScrDynHandleClass ws_wcontroller_class = { + "WritableStreamDefaultController", 0, NULL, ws_wcontroller_accessors, NULL, ws_wcontroller_order, +}; + +/* TransformStream */ +static ScrDyn *ws_transform_get(void *ptr, const char *key, size_t len) { + WsTransform *t = ptr; + if (ws_name(key, len, "readable")) return ws_stream_box(t->readable); + if (ws_name(key, len, "writable")) return ws_writable_box(t->writable); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; + return scr_dyn_handle_class_member(SCR_DYNH_WS_TRANSFORM, ptr, key, len); +} + +static ScrDyn *ws_transform_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; + (void)ptr; + return ws_invoke_member(self, method, args, argc, what); +} + + +static const ScrDynHandleClass ws_transform_class = { + "TransformStream", 0, ws_transform_construct, ws_transform_accessors, NULL, NULL, +}; + +/* TransformStreamDefaultController */ +static ScrDyn *ws_tcontroller_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, + size_t argc, const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; + WsTController *c = ptr; + size_t len = strlen(method); + if (ws_name(method, len, "enqueue")) { + if (!ws_tcontroller_enqueue(c, ws_arg(args, argc, 0))) return NULL; + return ws_undef(); + } + if (ws_name(method, len, "error")) { + ws_transform_error(c->stream, ws_arg(args, argc, 0)); + return ws_undef(); + } + if (ws_name(method, len, "terminate")) { + ws_tcontroller_terminate(c); + return ws_undef(); + } + return ws_invoke_member(self, method, args, argc, what); +} + +static ScrDyn *ws_tcontroller_get(void *ptr, const char *key, size_t len) { + WsTController *c = ptr; + if (ws_name(key, len, "desiredSize")) return ws_rcontroller_desired_size(c->stream->readable->controller); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; + return scr_dyn_handle_class_member(SCR_DYNH_WS_TCONTROLLER, ptr, key, len); +} + +static const ScrDynHandleMethod ws_tcontroller_methods[] = { + {"enqueue", 0, false, NULL}, {"error", 0, false, NULL}, {"terminate", 0, false, NULL}, {NULL, 0, false, NULL}, +}; + + +static const char *const ws_tcontroller_order[] = {"desiredSize", "enqueue", "error", "terminate", NULL}; + +static const ScrDynHandleClass ws_tcontroller_class = { + "TransformStreamDefaultController", 0, NULL, ws_tcontroller_accessors, NULL, ws_tcontroller_order, +}; + +/* ── TextEncoderStream and TextDecoderStream (webstreams/encoding.js) */ + +/* A TransformStream whose transformer callbacks are native functions, as + * Node's arrow-function transformers are. */ +typedef struct WsTextStream { + WsObj obj; + WsTransform *transform; /* +1 */ + bool decoder; + /* TextEncoderStream: the trailing lone high surrogate of the last chunk + * (its 3-byte WTF-8 storage), or NULL. */ + ScrStr *pending_high; + /* TextDecoderStream: the decoder's encoding id (-1 = utf-8), flags and + * streaming state. */ + double encoding; + bool fatal, ignore_bom; + ScrDyn *state; /* +1 */ +} WsTextStream; + +static void ws_text_trace(void *obj, ScrTraceVisit visit, void *ctx) { + WsTextStream *t = obj; + ws_visit(t->transform, visit, ctx); + ws_visit(t->state, visit, ctx); +} + +static void ws_text_drop(void *obj, bool gc) { + WsTextStream *t = obj; + ws_drop_obj(t->transform, gc); + if (t->pending_high) scr_str_release(t->pending_high); + ws_drop_dyn(t->state, gc); +} + +static const WsObjType ws_text_encoder_type = {ws_text_trace, ws_text_drop, "TextEncoderStream", + ws_text_encoder_accessors}; +static const WsObjType ws_text_decoder_type = {ws_text_trace, ws_text_drop, "TextDecoderStream", + ws_text_decoder_accessors}; + +static WsTextStream *ws_text_of(ScrClosure *closure) { return scr_box_get_ref(closure->caps[0]); } + +/* The controller argument of a transformer callback. */ +static WsTController *ws_text_controller(ScrDyn *const *args, size_t argc) { + ScrDyn *c = ws_arg(args, argc, 1); + if (c->kind == SCR_DYN_HANDLE && c->v.handle.tag == SCR_DYNH_WS_TCONTROLLER) return c->v.handle.ptr; + return NULL; +} + +/* String(value): ToString, with a symbol's description. */ +static ScrStr *ws_string_of(ScrDyn *value) { + if (value->kind == SCR_DYN_SYMBOL) return scr_sym_to_string(value->v.symbol.value); + return scr_dyn_string_coerce_js(value); +} + +/* The bytes as a Uint8Array chunk enqueued on the transform: false with + * the exception pending. */ +static bool ws_text_enqueue_bytes(WsTController *c, ScrBytes *bytes) { + bytes->is_buffer = false; + ScrDyn *chunk = scr_dyn_new_bytes(bytes); + bool ok = ws_tcontroller_enqueue(c, chunk); + scr_dyn_release(chunk); + return ok; +} + +static ScrDyn *ws_text_encoder_transform(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + WsTextStream *t = ws_text_of(closure); + WsTController *c = ws_text_controller(args, argc); + ScrDyn *result = NULL; + ScrStr *chunk = ws_string_of(ws_arg(args, argc, 0)); + if (!chunk) goto done; + if (chunk->len == 0) { + result = ws_undef(); + goto done; + } + if (t->pending_high) { + ScrStr *joined = scr_str_concat(t->pending_high, chunk); + scr_str_release(chunk); + scr_str_release(t->pending_high); + t->pending_high = NULL; + chunk = joined; + } + /* A lone high surrogate D800..DBFF is ED A0..AF xx. */ + size_t len = chunk->len; + if (len >= 3 && (unsigned char)chunk->data[len - 3] == 0xED && + ((unsigned char)chunk->data[len - 2] & 0xF0) == 0xA0) { + t->pending_high = scr_str_new(chunk->data + len - 3, 3); + ScrStr *rest = scr_str_new(chunk->data, len - 3); + scr_str_release(chunk); + chunk = rest; + if (chunk->len == 0) { + result = ws_undef(); + goto done; + } + } + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *bytes = scr_bytes_from_str(chunk, utf8); + scr_str_release(utf8); + bool ok = c && ws_text_enqueue_bytes(c, bytes); + scr_bytes_release(bytes); + if (ok) result = ws_undef(); +done: + if (chunk) scr_str_release(chunk); + ws_release(t); + return result; +} + +static ScrDyn *ws_text_encoder_flush(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + WsTextStream *t = ws_text_of(closure); + /* flush receives the controller as its only argument. */ + ScrDyn *cv = ws_arg(args, argc, 0); + WsTController *c = cv->kind == SCR_DYN_HANDLE && cv->v.handle.tag == SCR_DYNH_WS_TCONTROLLER ? cv->v.handle.ptr : NULL; + ScrDyn *result = ws_undef(); + if (t->pending_high && c) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, 3); + bytes->data[0] = 0xEF; + bytes->data[1] = 0xBF; + bytes->data[2] = 0xBD; + if (!ws_text_enqueue_bytes(c, bytes)) { + scr_dyn_release(result); + result = NULL; + } + scr_bytes_release(bytes); + } + ws_release(t); + return result; +} + +static ScrDyn *ws_text_decoder_transform(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + WsTextStream *t = ws_text_of(closure); + WsTController *c = ws_text_controller(args, argc); + ScrDyn *result = NULL; + ScrDyn *chunk = ws_arg(args, argc, 0); + if (chunk->kind == SCR_DYN_UNDEF) { + scr_dyn_arg_type_fail("chunk", "of type string", chunk); + ws_release(t); + return NULL; + } + ScrBytes *bytes = scr_bytes_buffer_source(chunk); + if (!bytes) { + ws_release(t); + return NULL; + } + ScrStr *text = scr_text_decode_stream(t->state, bytes, t->encoding, t->fatal, t->ignore_bom, true); + scr_bytes_release(bytes); + if (text) { + bool ok = true; + if (text->len) { + ScrDyn *value = scr_dyn_new_str(text); + ok = c && ws_tcontroller_enqueue(c, value); + scr_dyn_release(value); + } + scr_str_release(text); + if (ok) result = ws_undef(); + } + ws_release(t); + return result; +} + +static ScrDyn *ws_text_decoder_flush(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + WsTextStream *t = ws_text_of(closure); + ScrDyn *cv = ws_arg(args, argc, 0); + WsTController *c = cv->kind == SCR_DYN_HANDLE && cv->v.handle.tag == SCR_DYNH_WS_TCONTROLLER ? cv->v.handle.ptr : NULL; + ScrDyn *result = NULL; + ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); + ScrStr *text = scr_text_decode_stream(t->state, empty, t->encoding, t->fatal, t->ignore_bom, false); + scr_bytes_release(empty); + if (text) { + bool ok = true; + if (text->len) { + ScrDyn *value = scr_dyn_new_str(text); + ok = c && ws_tcontroller_enqueue(c, value); + scr_dyn_release(value); + } + scr_str_release(text); + if (ok && c) ws_tcontroller_terminate(c); + if (ok) result = ws_undef(); + } + ws_release(t); + return result; +} + +static ScrDyn *ws_text_callback(WsTextStream *t, ScrDynThunk thunk, uint32_t arity, const char *name) { + ScrClosure *closure = scr_closure_new(NULL, 1); + closure->caps[0] = scr_box_new_obj(ws_retain, ws_release, ws_obj_trace); + scr_box_set_ref(closure->caps[0], ws_retain(t)); + return scr_dyn_new_func(closure, thunk, arity, "", name); +} + +/* The transform with the stream's native transformer: false with the + * exception pending. */ +static bool ws_text_attach(WsTextStream *t, ScrDynThunk transform, ScrDynThunk flush) { + ScrDyn *transformer = scr_dyn_new_obj(); + scr_dyn_obj_set(transformer, "transform", 9, ws_text_callback(t, transform, 2, "transform")); + scr_dyn_obj_set(transformer, "flush", 5, ws_text_callback(t, flush, 1, "flush")); + ScrDyn *box = ws_transform_construct(&transformer, 1); + scr_dyn_release(transformer); + if (!box) return false; + t->transform = ws_retain(box->v.handle.ptr); + scr_dyn_release(box); + return true; +} + +static ScrDyn *ws_text_encoder_construct(ScrDyn *const *args, size_t argc) { + (void)args; + (void)argc; + ws_install(); + WsTextStream *t = ws_obj_new(sizeof *t, &ws_text_encoder_type); + ScrDyn *result = NULL; + if (ws_text_attach(t, ws_text_encoder_transform, ws_text_encoder_flush)) + result = ws_box(t, SCR_DYNH_WS_TEXT_ENCODER); + ws_release(t); + return result; +} + +/* new TextDecoder(encoding, options) as TextDecoderStream makes it. */ +static ScrDyn *ws_text_decoder_construct(ScrDyn *const *args, size_t argc) { + ScrDyn *label = argc > 0 && args[0]->kind != SCR_DYN_UNDEF ? args[0] : NULL; + ScrDyn *options = argc > 1 && args[1]->kind != SCR_DYN_UNDEF ? args[1] : NULL; + double encoding = -1; + if (label) { + encoding = scr_text_decoder_encoding(label); + if (scr_exc_pending()) return NULL; + } + bool fatal = false, ignore_bom = false; + if (options) { + if (!ws_validate_object(options, "options", true)) return NULL; + ScrDyn *f = ws_get(options, "fatal"); + if (!f) return NULL; + fatal = scr_dyn_truthy(f); + scr_dyn_release(f); + ScrDyn *b = ws_get(options, "ignoreBOM"); + if (!b) return NULL; + ignore_bom = scr_dyn_truthy(b); + scr_dyn_release(b); + } + if (encoding >= 0) { + ScrStr *name = scr_text_decoder_name(encoding); + char buf[160]; + int n = snprintf(buf, sizeof buf, "TextDecoderStream for the '%.*s' encoding is not supported yet", (int)name->len, + name->data); + scr_str_release(name); + scr_throw_error_msg_code(SCR_ERR_ERROR, buf, (size_t)n, "SC2020"); + return NULL; + } + ws_install(); + WsTextStream *t = ws_obj_new(sizeof *t, &ws_text_decoder_type); + t->decoder = true; + t->encoding = encoding; + t->fatal = fatal; + t->ignore_bom = ignore_bom; + t->state = scr_dyn_new_obj(); + ScrDyn *result = NULL; + if (ws_text_attach(t, ws_text_decoder_transform, ws_text_decoder_flush)) + result = ws_box(t, SCR_DYNH_WS_TEXT_DECODER); + ws_release(t); + return result; +} + +static ScrDyn *ws_text_get(void *ptr, const char *key, size_t len) { + WsTextStream *t = ptr; + ScrDynHandleTag tag = t->decoder ? SCR_DYNH_WS_TEXT_DECODER : SCR_DYNH_WS_TEXT_ENCODER; + if (ws_name(key, len, "encoding")) { + ScrStr *name = scr_text_decoder_name(t->decoder ? t->encoding : -1); + ScrDyn *value = scr_dyn_new_str(name); + scr_str_release(name); + return value; + } + if (ws_name(key, len, "readable")) return ws_stream_box(t->transform->readable); + if (ws_name(key, len, "writable")) return ws_writable_box(t->transform->writable); + if (t->decoder && ws_name(key, len, "fatal")) return scr_dyn_new_bool(t->fatal); + if (t->decoder && ws_name(key, len, "ignoreBOM")) return scr_dyn_new_bool(t->ignore_bom); + ScrDyn *own = ws_own_get(ptr, key, len); + if (own) return own; + return scr_dyn_handle_class_member(tag, ptr, key, len); +} + +static ScrDyn *ws_text_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, + const char *what) { + ScrDyn *own_result; + if (ws_own_invoke(ptr, self, method, args, argc, what, &own_result)) return own_result; + return ws_invoke_member(self, method, args, argc, what); +} + +static const ScrDynHandleClass ws_text_encoder_class = { + "TextEncoderStream", 0, ws_text_encoder_construct, ws_text_encoder_accessors, NULL, NULL, +}; + +static const ScrDynHandleClass ws_text_decoder_class = { + "TextDecoderStream", 0, ws_text_decoder_construct, ws_text_decoder_accessors, NULL, NULL, +}; + /* ── queuing strategies ────────────────────────────────────────────── */ typedef struct WsStrategy { @@ -2412,14 +5063,14 @@ static bool ws_set_noop(void *h, const char *key, size_t len, const ScrDyn *valu return false; } -#define WS_OPS(NAME, CLS, RETAIN, RELEASE, INVOKE, GET, METHODS, INVALID) \ +#define WS_OPS(NAME, CLS, RETAIN, RELEASE, INVOKE, GET, SET, METHODS, INVALID) \ static const ScrDynHandleOps NAME = { \ .cls = CLS, \ .retain = RETAIN, \ .release = RELEASE, \ .invoke = INVOKE, \ .get = GET, \ - .set = ws_set_noop, \ + .set = SET, \ .json_empty_object = true, \ .to_string_tag = CLS, \ .methods = METHODS, \ @@ -2433,7 +5084,7 @@ static const ScrDynHandleOps ws_readable_ops = { .release = ws_release, .invoke = ws_readable_invoke, .get = ws_readable_get, - .set = ws_set_noop, + .set = ws_own_set, .json_empty_object = true, .to_string_tag = "ReadableStream", .symbol_get = ws_readable_symbol_get, @@ -2442,16 +5093,43 @@ static const ScrDynHandleOps ws_readable_ops = { .rejected = ws_rejected_value, }; -WS_OPS(ws_reader_ops, "ReadableStreamDefaultReader", ws_retain, ws_release, ws_reader_invoke, - ws_reader_get, ws_reader_methods, +WS_OPS(ws_reader_ops, "ReadableStreamDefaultReader", ws_retain, ws_release, ws_reader_invoke, ws_reader_get, ws_own_set, ws_reader_methods, "Value of \"this\" must be of type ReadableStreamDefaultReader"); -WS_OPS(ws_rcontroller_ops, "ReadableStreamDefaultController", ws_retain, ws_release, - ws_rcontroller_invoke, ws_rcontroller_get, ws_rcontroller_methods, +WS_OPS(ws_rcontroller_ops, "ReadableStreamDefaultController", ws_retain, ws_release, ws_rcontroller_invoke, ws_rcontroller_get, ws_own_set, ws_rcontroller_methods, "Value of \"this\" must be of type ReadableStreamDefaultController"); -WS_OPS(ws_count_strategy_ops, "CountQueuingStrategy", ws_strategy_retain, ws_strategy_release, ws_strategy_invoke, - ws_strategy_get, NULL, "Value of \"this\" must be of type CountQueuingStrategy"); -WS_OPS(ws_byte_strategy_ops, "ByteLengthQueuingStrategy", ws_strategy_retain, ws_strategy_release, - ws_strategy_invoke, ws_strategy_get, NULL, +WS_OPS(ws_writable_ops, "WritableStream", ws_retain, ws_release, ws_writable_invoke, ws_writable_get, ws_own_set, + ws_writable_methods, "Value of \"this\" must be of type WritableStream"); +WS_OPS(ws_writer_ops, "WritableStreamDefaultWriter", ws_retain, ws_release, ws_writer_invoke, ws_writer_get, ws_own_set, + ws_writer_methods, "Value of \"this\" must be of type WritableStreamDefaultWriter"); +WS_OPS(ws_wcontroller_ops, "WritableStreamDefaultController", ws_retain, ws_release, ws_wcontroller_invoke, ws_wcontroller_get, ws_own_set, ws_wcontroller_methods, "Value of \"this\" must be of type WritableStreamDefaultController"); +WS_OPS(ws_transform_ops, "TransformStream", ws_retain, ws_release, ws_transform_invoke, ws_transform_get, ws_own_set, NULL, + "Value of \"this\" must be of type TransformStream"); +WS_OPS(ws_tcontroller_ops, "TransformStreamDefaultController", ws_retain, ws_release, ws_tcontroller_invoke, ws_tcontroller_get, ws_own_set, ws_tcontroller_methods, "Value of \"this\" must be of type TransformStreamDefaultController"); +/* TextEncoderStream and TextDecoderStream have no @@toStringTag. */ +static const ScrDynHandleOps ws_text_encoder_ops = { + .cls = "TextEncoderStream", + .retain = ws_retain, + .release = ws_release, + .invoke = ws_text_invoke, + .get = ws_text_get, + .set = ws_own_set, + .json_empty_object = true, + .invalid_this = "Value of \"this\" must be of type TextEncoderStream", + .rejected = ws_rejected_value, +}; +static const ScrDynHandleOps ws_text_decoder_ops = { + .cls = "TextDecoderStream", + .retain = ws_retain, + .release = ws_release, + .invoke = ws_text_invoke, + .get = ws_text_get, + .set = ws_own_set, + .json_empty_object = true, + .invalid_this = "Value of \"this\" must be of type TextDecoderStream", + .rejected = ws_rejected_value, +}; +WS_OPS(ws_count_strategy_ops, "CountQueuingStrategy", ws_strategy_retain, ws_strategy_release, ws_strategy_invoke, ws_strategy_get, ws_set_noop, NULL, "Value of \"this\" must be of type CountQueuingStrategy"); +WS_OPS(ws_byte_strategy_ops, "ByteLengthQueuingStrategy", ws_strategy_retain, ws_strategy_release, ws_strategy_invoke, ws_strategy_get, ws_set_noop, NULL, "Value of \"this\" must be of type ByteLengthQueuingStrategy"); static SCR_TL bool ws_installed; @@ -2470,18 +5148,30 @@ static void ws_install(void) { scr_dyn_handle_class_install(SCR_DYNH_WS_RCONTROLLER, &ws_rcontroller_ops, &ws_rcontroller_class); scr_dyn_handle_class_install(SCR_DYNH_WS_COUNT_STRATEGY, &ws_count_strategy_ops, &ws_count_strategy_class); scr_dyn_handle_class_install(SCR_DYNH_WS_BYTE_STRATEGY, &ws_byte_strategy_ops, &ws_byte_strategy_class); - /* The fetch unit's streams, readers and controllers (a Response body) - * are instances of the same classes. */ + scr_dyn_handle_class_install(SCR_DYNH_WS_WRITABLE, &ws_writable_ops, &ws_writable_class); + scr_dyn_handle_class_install(SCR_DYNH_WS_WRITER, &ws_writer_ops, &ws_writer_class); + scr_dyn_handle_class_install(SCR_DYNH_WS_WCONTROLLER, &ws_wcontroller_ops, &ws_wcontroller_class); + scr_dyn_handle_class_install(SCR_DYNH_WS_TRANSFORM, &ws_transform_ops, &ws_transform_class); + scr_dyn_handle_class_install(SCR_DYNH_WS_TCONTROLLER, &ws_tcontroller_ops, &ws_tcontroller_class); + scr_dyn_handle_class_install(SCR_DYNH_WS_TEXT_ENCODER, &ws_text_encoder_ops, &ws_text_encoder_class); + scr_dyn_handle_class_install(SCR_DYNH_WS_TEXT_DECODER, &ws_text_decoder_ops, &ws_text_decoder_class); + /* The fetch unit's streams, readers, controllers, writers and identity + * transforms (a Response body) are instances of the same classes. */ scr_dyn_handle_class_alias(SCR_DYNH_WEB_STREAM, SCR_DYNH_WS_READABLE); scr_dyn_handle_class_alias(SCR_DYNH_WEB_READER, SCR_DYNH_WS_READER); scr_dyn_handle_class_alias(SCR_DYNH_WEB_CONTROLLER, SCR_DYNH_WS_RCONTROLLER); + scr_dyn_handle_class_alias(SCR_DYNH_WEB_WRITABLE, SCR_DYNH_WS_WRITABLE); + scr_dyn_handle_class_alias(SCR_DYNH_WEB_WRITER, SCR_DYNH_WS_WRITER); + scr_dyn_handle_class_alias(SCR_DYNH_WEB_TRANSFORM, SCR_DYNH_WS_TRANSFORM); } /* The classes in the frontend's WEB_STREAM_CLASSES order. */ static ScrDynHandleTag ws_class_tag(double kind) { static const ScrDynHandleTag tags[] = { - SCR_DYNH_WS_READABLE, SCR_DYNH_WS_READER, SCR_DYNH_WS_RCONTROLLER, SCR_DYNH_WS_COUNT_STRATEGY, - SCR_DYNH_WS_BYTE_STRATEGY, + SCR_DYNH_WS_READABLE, SCR_DYNH_WS_READER, SCR_DYNH_WS_RCONTROLLER, + SCR_DYNH_WS_COUNT_STRATEGY, SCR_DYNH_WS_BYTE_STRATEGY, SCR_DYNH_WS_WRITABLE, + SCR_DYNH_WS_WRITER, SCR_DYNH_WS_WCONTROLLER, SCR_DYNH_WS_TRANSFORM, + SCR_DYNH_WS_TCONTROLLER, SCR_DYNH_WS_TEXT_ENCODER, SCR_DYNH_WS_TEXT_DECODER, }; int index = (int)kind; if (index < 0 || index >= (int)(sizeof tags / sizeof tags[0])) scr_trap("scriptc: unknown web stream class\n"); diff --git a/tests/corpus/web-stream-transform-pipes-untyped.mjs b/tests/corpus/web-stream-transform-pipes-untyped.mjs new file mode 100644 index 000000000..3bf171fd6 --- /dev/null +++ b/tests/corpus/web-stream-transform-pipes-untyped.mjs @@ -0,0 +1,151 @@ +// Web TransformStream and pipes from untyped JavaScript: transformers with +// start, transform, flush and cancel, backpressure between the sides, +// controller errors and terminate, pipeTo and pipeThrough with their +// options, abort signals, own properties on streams, and encoder and +// decoder streams, in Node's promise order. +import { TransformStream as WebTransformStream, TransformStreamDefaultController } from "node:stream/web"; + +const log = []; +const out = (...args) => log.push(args.map(String).join(" ")); +const caught = (fn) => { + try { + fn(); + return "ok"; + } catch (e) { + return `${e.name}:${e.code ?? ""}:${e.message}`; + } +}; +const settled = (p) => p.then((v) => `ok:${v}`, (e) => `${e?.name}:${e?.code ?? ""}:${e?.message ?? e}`); +const collect = async (readable) => { + const got = []; + for await (const chunk of readable) got.push(chunk); + return got.join(","); +}; + +out("classes", TransformStream === WebTransformStream, TransformStream.name, TransformStream.length, caught(() => new TransformStreamDefaultController())); +out("prototype", Object.keys(TransformStream.prototype).join(","), Object.keys(TransformStreamDefaultController.prototype).join(",")); + +{ + const order = []; + const t = new TransformStream({ + start(c) { + order.push(`start ${c.desiredSize}`); + c.enqueue("S"); + }, + transform(chunk, c) { + order.push(`transform ${chunk}`); + c.enqueue(chunk.toUpperCase()); + }, + flush(c) { + order.push("flush"); + c.enqueue("F"); + }, + }); + const writer = t.writable.getWriter(); + writer.write("a"); + writer.write("b"); + writer.close(); + out("transform", await collect(t.readable), order.join(" | "), await settled(writer.closed)); +} + +{ + const t = new TransformStream({}, undefined, { highWaterMark: 1 }); + const writer = t.writable.getWriter(); + out("backpressure", writer.desiredSize, t.readable.locked); + const first = writer.write("x"); + const second = writer.write("y"); + out("queued", writer.desiredSize); + const reader = t.readable.getReader(); + out("read", (await reader.read()).value, (await reader.read()).value, await settled(first), await settled(second)); +} + +{ + const failure = new Error("transform-failure"); + const t = new TransformStream({ transform() { throw failure; } }); + const writer = t.writable.getWriter(); + const reader = t.readable.getReader(); + const read = settled(reader.read()); + const write = settled(writer.write("z")); + out("transform-error", await write, await read, await settled(writer.closed)); +} + +{ + /** @type {any} */ + let controller; + const t = new TransformStream({ start(c) { controller = c; } }); + controller.enqueue("before"); + controller.terminate(); + const reader = t.readable.getReader(); + out("terminate", (await reader.read()).value, (await reader.read()).done, await settled(t.writable.getWriter().write("after")), caught(() => controller.enqueue("late"))); +} + +{ + const cancels = []; + const t = new TransformStream({ cancel(reason) { cancels.push(`cancel ${reason}`); } }); + out("cancel", await settled(t.readable.cancel("stop")), cancels.join(","), await settled(t.writable.getWriter().closed)); +} + +{ + const source = ReadableStream.from(["p", "q", "r"]); + const got = []; + const sink = new WritableStream({ write(chunk) { got.push(chunk); } }); + const piped = source.pipeThrough(new TransformStream({ transform(chunk, c) { c.enqueue(chunk + chunk); } })).pipeTo(sink); + out("pipe", await settled(piped), got.join(","), source.locked); +} + +{ + const source = new ReadableStream({ start(c) { c.enqueue(1); c.close(); } }); + const sink = new WritableStream(); + out("prevent-close", await settled(source.pipeTo(sink, { preventClose: true })), sink.locked, await settled(sink.getWriter().write(2))); +} + +{ + const failure = new Error("source-failure"); + const source = new ReadableStream({ start(c) { c.error(failure); } }); + const aborts = []; + const sink = new WritableStream({ abort(reason) { aborts.push(reason === failure); } }); + out("source-error", await settled(source.pipeTo(sink)), aborts.join(",")); +} + +{ + const controller = new AbortController(); + const reason = new Error("signal"); + const source = new ReadableStream({ pull() {}, cancel(e) { out("signal-cancel", e === reason); } }, { highWaterMark: 0 }); + const sink = new WritableStream({ abort(e) { out("signal-abort", e === reason); } }); + const piped = source.pipeTo(sink, { signal: controller.signal }); + await Promise.resolve(); + controller.abort(reason); + out("signal", await settled(piped), source.locked, sink.locked); +} + +{ + const source = ReadableStream.from(["own"]); + const sink = new WritableStream(); + /** @type {any} */ (source).getReader = () => { throw new Error("public reader"); }; + /** @type {any} */ (sink).tag = "kept"; + await source.pipeTo(sink); + out("own-properties", /** @type {any} */ (sink).tag, typeof /** @type {any} */ (source).getReader, caught(() => { /** @type {any} */ (source).locked = true; })); +} + +out( + "invalid", + await settled(ReadableStream.from([]).pipeTo(/** @type {any} */ ({}))), + caught(() => ReadableStream.from([]).pipeThrough(/** @type {any} */ ({}))), + caught(() => new TransformStream({ readableType: "bytes" })), + caught(() => new TransformStream({ transform: 1 })), +); + +{ + const bytes = []; + await ReadableStream.from(["hé", "\ud83d", "\ude00", "\ud800"]) + .pipeThrough(new TextEncoderStream()) + .pipeTo(new WritableStream({ write(chunk) { bytes.push(Array.from(chunk).join(".")); } })); + out("encoder", bytes.join(" | "), new TextEncoderStream().encoding, Object.prototype.toString.call(new TextEncoderStream())); + const text = []; + await ReadableStream.from([new Uint8Array([0xe2, 0x82]), new Uint8Array([0xac, 0x21]), new Uint8Array([0xf0])]) + .pipeThrough(new TextDecoderStream()) + .pipeTo(new WritableStream({ write(chunk) { text.push(chunk); } })); + const decoder = new TextDecoderStream("utf-8", { fatal: true }); + out("decoder", text.join("|"), decoder.encoding, decoder.fatal, decoder.ignoreBOM, caught(() => new TextDecoderStream("nope"))); +} +console.log(log.join("\n")); diff --git a/tests/corpus/web-stream-writable-untyped.mjs b/tests/corpus/web-stream-writable-untyped.mjs new file mode 100644 index 000000000..314d6b37e --- /dev/null +++ b/tests/corpus/web-stream-writable-untyped.mjs @@ -0,0 +1,144 @@ +// Web WritableStream from untyped JavaScript: the classes as constructor +// values, prototypes and brand checks, writers with backpressure and their +// ready and closed promises, queuing strategies, sink errors, close and +// abort ordering, the controller's signal, and argument errors, in Node's +// promise order. +import { WritableStream as WebWritableStream, WritableStreamDefaultWriter, WritableStreamDefaultController } from "node:stream/web"; + +const log = []; +const out = (...args) => log.push(args.map(String).join(" ")); +const caught = (fn) => { + try { + fn(); + return "ok"; + } catch (e) { + return `${e.name}:${e.code ?? ""}:${e.message}`; + } +}; +const settled = (p) => p.then((v) => `ok:${v}`, (e) => `${e?.name}:${e?.code ?? ""}:${e?.message ?? e}`); + +out("classes", typeof WritableStream, WritableStream === WebWritableStream, WritableStream.name, WritableStream.length, WritableStreamDefaultWriter.length); +out("illegal", caught(() => new WritableStreamDefaultController()), caught(() => /** @type {any} */ (WritableStream)())); +out("prototype", Object.keys(WritableStream.prototype).join(","), Object.keys(WritableStreamDefaultWriter.prototype).join(","), Object.keys(WritableStreamDefaultController.prototype).join(",")); + +{ + const order = []; + const chunks = []; + const stream = new WritableStream( + { + start() { order.push("start"); }, + write(chunk, controller) { + order.push(`write ${chunk}`); + chunks.push(chunk); + return new Promise((resolve) => setTimeout(resolve, 1)); + }, + close() { order.push("close"); }, + }, + new CountQueuingStrategy({ highWaterMark: 2 }), + ); + const writer = stream.getWriter(); + out("writer", writer instanceof WritableStreamDefaultWriter, stream.locked, writer.desiredSize, caught(() => stream.getWriter())); + const writes = [writer.write("a"), writer.write("b"), writer.write("c")]; + out("queued", writer.desiredSize, Object.prototype.toString.call(stream), Object.prototype.toString.call(writer)); + let readySettled = false; + writer.ready.then(() => { readySettled = true; }); + await Promise.resolve(); + out("backpressure", readySettled, order.join(" | ")); + for (const w of writes) await w; + await writer.ready; + out("drained", writer.desiredSize, readySettled, chunks.join("")); + await writer.close(); + out("closed", await settled(writer.closed), writer.desiredSize, order.join(" | "), await settled(writer.write("late"))); + writer.releaseLock(); + out("released", stream.locked, await settled(writer.closed), caught(() => writer.desiredSize)); +} + +{ + const sizes = []; + const stream = new WritableStream( + { write() {} }, + { highWaterMark: 10, size(chunk) { sizes.push(chunk.length); return chunk.length; } }, + ); + const writer = stream.getWriter(); + const good = writer.write("abcd"); + out("sizes", writer.desiredSize, sizes.join(",")); + const bad = writer.write({ length: -1 }); + out("bad-size", await settled(good), await settled(bad), await settled(writer.closed), writer.desiredSize); +} + +{ + const failure = new Error("sink-write"); + const stream = new WritableStream({ write() { throw failure; } }); + const writer = stream.getWriter(); + const first = writer.write("x"); + const second = writer.write("y"); + out("write-error", await settled(first), await settled(second), await settled(writer.closed), await settled(writer.ready)); +} + +{ + const log2 = []; + /** @type {any} */ + let controller; + const reason = new Error("stop"); + const stream = new WritableStream({ + start(c) { + controller = c; + c.signal.addEventListener("abort", () => log2.push(`signal ${c.signal.reason === reason}`)); + }, + write(chunk) { log2.push(`write ${chunk}`); }, + abort(e) { log2.push(`abort ${e === reason}`); }, + }); + const writer = stream.getWriter(); + await writer.write("one"); + const abort = writer.abort(reason); + log2.push("aborting"); + out("abort", await settled(abort), log2.join(" | "), controller.signal.aborted, await settled(writer.closed), writer.desiredSize); +} + +{ + /** @type {any} */ + let controller; + const stream = new WritableStream({ start(c) { controller = c; } }); + const writer = stream.getWriter(); + controller.error(new Error("controller-error")); + out("controller-error", await settled(writer.write("z")), await settled(writer.closed), writer.desiredSize, await settled(stream.abort())); +} + +{ + const stream = new WritableStream(); + const close = stream.close(); + out("direct-close", await settled(stream.close()), await settled(close), await settled(stream.abort())); +} + +out( + "brands", + caught(() => WritableStream.prototype.getWriter.call({})), + await settled(WritableStream.prototype.abort.call({})), + await settled(/** @type {any} */ (Object.getOwnPropertyDescriptor(WritableStreamDefaultWriter.prototype, "ready")).get.call({})), + caught(() => /** @type {any} */ (Object.getOwnPropertyDescriptor(WritableStreamDefaultWriter.prototype, "desiredSize")).get.call({})), + caught(() => WritableStreamDefaultController.prototype.error.call({})), +); +out( + "invalid", + caught(() => new WritableStream(null)), + caught(() => new WritableStream({ type: "bytes" })), + caught(() => new WritableStream({ write: 1 })), + caught(() => new WritableStream({ start: 1 })), + caught(() => new WritableStream({}, { highWaterMark: -1 })), + caught(() => new WritableStreamDefaultWriter(/** @type {any} */ ({}))), +); +{ + const classes = { WritableStream, WritableStreamDefaultWriter, TransformStream }; + const isInstance = (value, C) => value instanceof C; + const stream = new WritableStream(); + const transform = new TransformStream(); + out( + "by-value", + isInstance(stream, classes.WritableStream), + isInstance(stream.getWriter(), classes.WritableStreamDefaultWriter), + isInstance(transform, classes.TransformStream), + isInstance(transform.writable, classes.WritableStream), + isInstance(stream, classes.TransformStream), + ); +} +console.log(log.join("\n")); diff --git a/tests/harness/corpus-costs/web-stream-transform-pipes-untyped.mjs.json b/tests/harness/corpus-costs/web-stream-transform-pipes-untyped.mjs.json new file mode 100644 index 000000000..7b5e4df47 --- /dev/null +++ b/tests/harness/corpus-costs/web-stream-transform-pipes-untyped.mjs.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 55000 } diff --git a/tests/harness/corpus-costs/web-stream-writable-untyped.mjs.json b/tests/harness/corpus-costs/web-stream-writable-untyped.mjs.json new file mode 100644 index 000000000..b5641a69c --- /dev/null +++ b/tests/harness/corpus-costs/web-stream-writable-untyped.mjs.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 6700 } diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index a17ca231a..11347c8da 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -57,6 +57,7 @@ const KNOWN: Record = { divergence: "Timeout handles are numbers (typeof) and lose their ref flag once fired or cleared", }, + // require() inside a function. "island-web-plumbing": { run: "SC1090" }, "lazy-traps": { build: "SC2013" }, "misc-shims": { run: "SC1090" }, @@ -87,15 +88,6 @@ const KNOWN: Record = { "node24-url-mutation": { run: "SC1090" }, "node24-util-adapters": { run: "Object.create with this value is not supported yet" }, "node24-util-types": { run: "SC1090" }, - "node24-web-stream-encoding": { run: "SC2020" }, - "node24-web-stream-errors": { run: "SC1090" }, - "node24-web-stream-lifecycle": { run: "SC2020" }, - "node24-web-stream-pipe-abort": { run: "SC2020" }, - "node24-web-stream-pipe-errors": { run: "SC2020" }, - "node24-web-stream-pipes": { run: "SC2020" }, - "node24-web-stream-queues": { run: "SC2020" }, - "node24-web-stream-tee": { run: "SC1090" }, - "node24-web-stream-transforms": { run: "SC1090" }, "package-imports": { build: "SC2013" }, "promise-bridge": { build: "SC1090" }, "self-name": { build: "SC2013" }, @@ -103,6 +95,7 @@ const KNOWN: Record = { "util-shims": { run: "SC2020" }, // The package redeclares the var it exported: Node copies the first value. "var-export-redeclared": { run: "SC1090" }, + // `class Upper extends TransformStream`: extending a builtin class from JavaScript. "web-streams": { run: "SC1090" }, "workspace-copied": { build: "SC2013" }, "workspace-unclean": { build: "SC2013" },