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" },