Run JavaScript WritableStream, TransformStream and pipes natively

In JavaScript sources, WritableStream with its default writer and
controller, TransformStream with its controller, pipeTo and pipeThrough,
and TextEncoderStream and TextDecoderStream join the native web stream
classes, following Node 24's webstreams step by step: sinks and
transformers, backpressure and the writers' ready and closed promises,
abort with the controller's signal, the pipe's shared write requests and
its abort signal, and UTF-8 encoding and decoding across chunks settle in
Node's order. The streams take own properties, which a pipe does not use
for its own reads and writes, and the global object answers the classes
by computed name in programs that name them.

A JavaScript class that extends one of these classes keeps the SC1090
refusal, and TextDecoderStream refuses encodings other than UTF-8.

The node24-web-stream lifecycle, errors, queues, transforms, pipes,
pipe-errors, pipe-abort, tee and encoding cases now match Node.
This commit is contained in:
Chris Tate
2026-10-27 15:55:31 -05:00
parent d82d1c4e94
commit f945efecee
17 changed files with 3114 additions and 57 deletions
@@ -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.
@@ -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"
}
},
{
@@ -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"
+2 -1
View File
@@ -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": [
@@ -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<string> = 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<string> = new Set([
"ReadableStreamReadValueResult",
"ReadableStreamReadDoneResult",
"UnderlyingSource",
"UnderlyingSink",
"Transformer",
"QueuingStrategy",
"QueuingStrategySize",
]);
@@ -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",
@@ -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<ts.SourceFile, ts.Expression | null>();
/** 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<string, IrFunction>();
+3 -2
View File
@@ -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). */
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/web-stream-transform-pipes-untyped.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/web-stream-writable-untyped.mjs"
],
"diags": []
}
+8 -1
View File
@@ -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. */
File diff suppressed because it is too large Load Diff
@@ -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"));
@@ -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"));
@@ -0,0 +1 @@
{ "plain": 4300, "san": 55000 }
@@ -0,0 +1 @@
{ "plain": 3900, "san": 6700 }
+2 -9
View File
@@ -57,6 +57,7 @@ const KNOWN: Record<string, Known> = {
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<string, Known> = {
"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<string, Known> = {
"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" },