From c6679d43fbc63840755e9e2e5c0e3a3063537284 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 25 Oct 2026 08:18:46 -0500 Subject: [PATCH] Add no empty chunk to a byte-mode Readable's buffer push() of an empty Buffer outside object mode put a zero-length entry into the read buffer, and the chunks pushed after it stalled behind it: a 'data' listener saw nothing more and a readline interface never closed. Node's readableAddChunk adds no empty chunk outside object mode; it clears the reading flag and may read more. The runtime now does the same, as it already did for an encoded push that decodes to nothing. --- .../corpus/7516-readable-empty-push.ts.json | 6 ++ packages/runtime/src/scr_stream.c | 8 ++- tests/corpus/7516-readable-empty-push.ts | 69 +++++++++++++++++++ .../7516-readable-empty-push.ts.json | 1 + 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json create mode 100644 tests/corpus/7516-readable-empty-push.ts create mode 100644 tests/harness/corpus-costs/7516-readable-empty-push.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json new file mode 100644 index 000000000..9388c4f9a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7516-readable-empty-push.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7516-readable-empty-push.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index 406d91599..201d6e34b 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -954,8 +954,14 @@ static bool scr_stream_add_chunk(ScrStream *s, ScrBytes *chunk, bool front) { } entry = dec; /* owned */ } else { - entry = scr_bytes_retain(chunk); entry_len = chunk->len; + if (entry_len == 0) { + /* Node's readableAddChunk adds no empty chunk outside object mode: + * it clears reading and may read more */ + if (!front) scr_stream_maybe_read_more(s); + return !st->r.ended && (st->r.length < st->r.hwm || st->r.length == 0); + } + entry = scr_bytes_retain(chunk); } if (entry_len > 0 && st->r.flowing == 1 && st->r.length == 0 && !st->r.in_read_sync && !front && diff --git a/tests/corpus/7516-readable-empty-push.ts b/tests/corpus/7516-readable-empty-push.ts new file mode 100644 index 000000000..27d88e3ed --- /dev/null +++ b/tests/corpus/7516-readable-empty-push.ts @@ -0,0 +1,69 @@ +// readable.push() of an empty Buffer outside object mode adds nothing, as +// in Node: the chunks pushed after it still reach 'data' listeners, +// readline and read(), and push(null) still ends the stream. In object +// mode an empty Buffer is a value like any other. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +function lines(chunks: string[]): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + const rl = createInterface({ input: src, crlfDelay: Infinity }); + rl.on("line", (line: string) => console.log("line", JSON.stringify(line))); + rl.on("close", () => { + console.log("readline closed"); + resolve(); + }); + for (const chunk of chunks) src.push(Buffer.from(chunk, "utf8")); + src.push(null); + }); +} + +function data(): Promise { + return new Promise((resolve) => { + const src = new Readable({ read() {} }); + src.on("data", (chunk: Buffer) => console.log("data", JSON.stringify(String(chunk)))); + src.on("end", () => { + console.log("data end"); + resolve(); + }); + console.log("push a", src.push(Buffer.from("a"))); + console.log("push empty", src.push(Buffer.alloc(0))); + console.log("push b", src.push(Buffer.from("b"))); + src.push(null); + }); +} + +function paused(): void { + const src = new Readable({ read() {} }); + src.push(Buffer.from("x")); + src.push(Buffer.alloc(0)); + src.push(Buffer.from("y")); + console.log("readableLength", src.readableLength); + console.log("read", JSON.stringify(String(src.read()))); +} + +function objectMode(): Promise { + return new Promise((resolve) => { + const src = new Readable({ objectMode: true, read() {} }); + src.on("data", (chunk: Buffer) => console.log("object chunk length", chunk.length)); + src.on("end", () => { + console.log("object end"); + resolve(); + }); + src.push(Buffer.alloc(0)); + src.push(Buffer.from("z")); + src.push(null); + }); +} + +async function main(): Promise { + await lines(["a\nb", "", "c\n"]); + await lines(["", "", "only\n"]); + await data(); + paused(); + await objectMode(); + console.log("done"); +} + +main(); diff --git a/tests/harness/corpus-costs/7516-readable-empty-push.ts.json b/tests/harness/corpus-costs/7516-readable-empty-push.ts.json new file mode 100644 index 000000000..1cfe56769 --- /dev/null +++ b/tests/harness/corpus-costs/7516-readable-empty-push.ts.json @@ -0,0 +1 @@ +{ "plain": 2200, "san": 3200 }