Skip to content

Commit 5d8acf9

Browse files
committed
fix(relay-eager): wake a parked read by cancelling the reader, not by racing
The eager producer raced every read against one never-settled abort promise. Each completed read leaves a reaction attached to that pending promise, so a long stream retained one callback per chunk until abort — the exact retention class relay.ts documents avoiding at its own drain ("Deliberately NOT a shared Promise.race companion"), reintroduced in the relay this campaign added. Abort now cancels the reader instead, which settles the parked read on a silent upstream the same way relay.ts's stopDrain does, and the loop checks the signal once per iteration. The 31 eager-relay tests — cancel-drain expiry, shutdown while paused, synthetic tails, teardown, and rewrite framing — stay green, which is what proves the wake-up path is unchanged.
1 parent 503786a commit 5d8acf9

1 file changed

Lines changed: 11 additions & 9 deletions

File tree

src/server/relay-eager.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,17 +194,19 @@ export function relaySseEagerBounded(
194194
let syntheticKind: "incomplete" | "failed" | null = null;
195195
// reader.read() is not intrinsically tied to the upstream AbortController
196196
// (a fetch body usually rejects on abort, but that coupling is the fetch
197-
// implementation's, not the stream's). Race every read against the abort
198-
// signal so cancel-drain expiry and shutdown teardown ALWAYS break the
199-
// loop even on a silent upstream.
200-
const aborted: Promise<"aborted"> = new Promise(resolve => {
201-
if (upstream.signal.aborted) resolve("aborted");
202-
else upstream.signal.addEventListener("abort", () => resolve("aborted"), { once: true });
203-
});
197+
// implementation's, not the stream's), so abort must break a parked read on
198+
// a silent upstream. Cancelling the reader does that: the pending read
199+
// settles and the loop observes the abort. This is deliberately NOT a
200+
// shared `Promise.race([reader.read(), aborted])` companion — racing every
201+
// read against one never-settled promise retains a reaction per chunk, and
202+
// that is the exact retention class relay.ts avoids at its own drain.
203+
const wakeParkedRead = () => { reader.cancel(upstream.signal.reason).catch(() => {}); };
204+
if (upstream.signal.aborted) wakeParkedRead();
205+
else upstream.signal.addEventListener("abort", wakeParkedRead, { once: true });
204206
try {
205207
for (;;) {
206-
const result = await Promise.race([reader.read(), aborted]);
207-
if (result === "aborted") break;
208+
const result = await reader.read();
209+
if (upstream.signal.aborted) break;
208210
const { done: upstreamDone, value } = result;
209211
if (upstreamDone) {
210212
hooks.finishInspection();

0 commit comments

Comments
 (0)