Accept Buffer-compatible chunk annotations and keep super(undefined) in byte mode

This commit is contained in:
Chris Tate
2026-10-22 08:35:56 -05:00
parent 1f357b3091
commit 2f3e25b431
4 changed files with 87 additions and 8 deletions
@@ -312,11 +312,13 @@ function lowerStreamCallback(
// A write or transform chunk of another type converts from the
// chunk's value when the callback runs (an object-mode side's value),
// or throws a TypeError there — on a side known to be byte mode the
// chunk is a Buffer, so the declared type is the fence's; an encoding
// of another type (`string | null | undefined`) converts from the
// encoding argument's value.
// chunk is a Buffer, so the declared type must be Node's or a
// supertype of it (`Buffer | string`); an encoding of another type
// (`string | null | undefined`) converts from the encoding argument's
// value.
if (
((i === 0 && chunkValues) || i === 1) &&
((i === 0 && (chunkValues || widensInto(lowerer, fullTuple[0]!, funcType.params[0]!, loc))) ||
i === 1) &&
(which === "write" || which === "transform") &&
funcType.params[i]!.kind !== "func" &&
canConvertToDyn(
@@ -1369,11 +1371,13 @@ export function lowerStreamSuperCall(
// set objectMode, and a literal says so with objectMode (or a Duplex's
// writableObjectMode) true; otherwise _write and _transform receive
// Buffers.
// super(undefined) and super(null) pass no options (followOptionsArg
// answers null), so their sides are byte mode too.
const optsNode = argNodes[0] !== undefined ? followOptionsArg(lowerer, argNodes[0]) : null;
const chunkValues =
argNodes[0] !== undefined &&
(optsNode === null ||
!ts.isObjectLiteralExpression(optsNode) ||
optsNode !== null &&
optsNode !== undefined &&
(!ts.isObjectLiteralExpression(optsNode) ||
literalWritesValues(optsNode, cls !== "%Readable" && cls !== "%Writable"));
const fallback = (name: string): IrExpr | null => {
const methodName = UNDERSCORE_METHODS.get(name);
+32 -1
View File
@@ -2,7 +2,8 @@
// and Writable in pipeline(), typed 'data' listeners, a Transform with
// readableObjectMode parsing bytes into records, an object-mode Duplex,
// a PassThrough's object-counted high-water mark, and Readable.from over
// a Map, a Set of records and an array mixing strings and Buffers.
// a Map, a Set of records and an array mixing strings and Buffers, and
// byte-mode chunk parameters typed Buffer | string.
import { Duplex, PassThrough, Readable, Transform, Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
@@ -119,6 +120,34 @@ async function collections(): Promise<void> {
for await (const t of Readable.from(tags)) console.log("set", JSON.stringify(t));
}
// Byte-mode sides pass Buffers: a chunk parameter typed as a supertype
// of Buffer takes them, inline and as an underscore method, and
// super(undefined) leaves the stream in byte mode.
class Collector extends Writable {
constructor() {
super(undefined);
}
_write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
console.log("collector", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString());
cb();
}
}
async function byteChunks(): Promise<void> {
const inline = new Writable({
write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
console.log("inline", Buffer.isBuffer(chunk), typeof chunk === "string" ? chunk : chunk.toString());
cb();
},
});
inline.write("one");
inline.end(Buffer.from("two"));
await new Promise<void>((resolve) => inline.on("finish", () => resolve()));
const collector = new Collector();
collector.end("three");
await new Promise<void>((resolve) => collector.on("finish", () => resolve()));
}
async function main(): Promise<void> {
await records();
await collections();
@@ -126,5 +155,7 @@ async function main(): Promise<void> {
sided();
duplex();
passthrough();
await new Promise<void>((resolve) => setTimeout(resolve, 5));
await byteChunks();
}
main();
@@ -0,0 +1,36 @@
// A byte-mode stream's write chunks are Buffers: an underscore method
// whose chunk parameter has another type is fenced, and super(undefined)
// passes no options, so the stream is in byte mode. A parameter that
// admits the Buffer (Buffer | string) is not fenced, inline or as a
// method.
import { Writable } from "node:stream";
class Numbers extends Writable {
constructor() {
super(undefined);
}
_write(chunk: number, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
console.log(chunk);
cb();
}
}
new Numbers().end("x");
const accepted = new Writable({
write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
console.log(typeof chunk === "string" ? chunk : chunk.toString());
cb();
},
});
accepted.end("z");
class Wide extends Writable {
constructor() {
super(undefined);
}
_write(chunk: Buffer | string, _encoding: BufferEncoding, cb: (err?: Error | null) => void): void {
console.log(typeof chunk === "string" ? chunk : chunk.toString());
cb();
}
}
new Wide().end("w");
@@ -0,0 +1,8 @@
stream-byte-mode-chunk-fences.ts:10:5 - error SC2020: 'a '_write' method whose parameter 0 is 'number' where Node passes 'Uint8Array'' is part of the standard library types but has no scriptc lowering yet
9 | constructor() {
10 | super(undefined);
| ^~~~~~~~~~~~~~~~
11 | }
hint: annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here)