diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json new file mode 100644 index 0000000000..f00b31e834 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readline-line-listener-changes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/readline-line-listener-changes.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_readline.c b/packages/runtime/src/scr_readline.c index 8d01086495..94becf5d7f 100644 --- a/packages/runtime/src/scr_readline.c +++ b/packages/runtime/src/scr_readline.c @@ -105,6 +105,17 @@ void scr_rl_emit(ScrRl *rl, int ev, ScrStr *line, ScrError *err) { RlList *l = &rl->ev[ev]; size_t n = l->n; if (n == 0) return; + /* One persistent line listener (the usual 'line' handler): no snapshot + * and no once-listener pass. The retain keeps the closure alive if the + * listener removes itself. */ + if (n == 1 && ev == RL_EV_LINE && !l->v[0].once && !l->v[0].iter) { + RlEntry e = l->v[0]; + if (scr_exc_pending()) return; + scr_closure_retain(e.cb); + ((ScrRlLineFn)e.fn)(e.cb, scr_str_retain(line)); + scr_closure_release(e.cb); + return; + } /* Most events have a listener or two: their snapshot stays on the stack. */ RlEntry small[4]; RlEntry *snap = n <= 4 ? small : malloc(n * sizeof *snap); diff --git a/tests/corpus/readline-line-listener-changes.ts b/tests/corpus/readline-line-listener-changes.ts new file mode 100644 index 0000000000..260a647749 --- /dev/null +++ b/tests/corpus/readline-line-listener-changes.ts @@ -0,0 +1,32 @@ +// 'line' listeners that change while a line is delivered: a listener added +// by the only line listener starts with the next line, after which both +// run in order; a once listener next to a persistent one runs for one line; +// and no listener sees a line twice. +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; + +const text = "a\nb\nc\nd\ne\nf\n"; + +const first = createInterface({ input: Readable.from([text]) }); +let seen = 0; +const counting = (line: string): void => { + seen++; + console.log("counting", line); + if (seen === 2) first.on("line", (later: string) => console.log("later", later)); +}; +first.on("line", counting); +first.on("close", () => { + console.log("first closed", seen); + + const second = createInterface({ input: Readable.from([text]) }); + second.once("line", (line: string) => console.log("once", line)); + second.on("line", (line: string) => console.log("every", line)); + second.on("close", () => { + console.log("second closed"); + + const third = createInterface({ input: Readable.from(["x\ny\n", "z\n"]) }); + const seenLines: string[] = []; + third.on("line", (line: string) => seenLines.push(line)); + third.on("close", () => console.log("third", seenLines.join(","))); + }); +}); diff --git a/tests/harness/corpus-costs/readline-line-listener-changes.ts.json b/tests/harness/corpus-costs/readline-line-listener-changes.ts.json new file mode 100644 index 0000000000..c5cd06a1fa --- /dev/null +++ b/tests/harness/corpus-costs/readline-line-listener-changes.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 7400 }