From 3bb877ec3c6a774e1677303106782780eb772445 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 26 May 2026 08:22:40 +0200 Subject: [PATCH] stream: drop noop .then in pipe orchestration Apply the same markPromiseAsHandled-instead-of-setPromiseHandled substitution as the per-chunk path to the four pipe orchestration call sites that fire once per pipe setup rather than per chunk: - readablestream.js: pipeThrough's inner readableStreamPipeTo promise - readablestream.js: pipeTo's run() loop promise - transfer.js: cross-realm readable pipe promise - transfer.js: cross-realm writable pipe promise In each case the chain Promise + noop closure that setPromiseHandled allocates has no observer beyond V8's unhandled-rejection tracker: errors flow back via the relevant stream's closed/errored state path, which the surrounding code already monitors. markPromiseAsHandled flips the same V8 flag without the allocation. No measurable benchmark impact (these sites fire once per pipe, not per chunk), but the change is consistent with the per-chunk fix and removes four unnecessary Promise + closure allocations from each pipe setup. WPT streams parity preserved: 1403 subtests passing, 0 unexpected failures. --- lib/internal/webstreams/readablestream.js | 16 ++++++++++++++-- lib/internal/webstreams/transfer.js | 12 +++++++++--- lib/internal/webstreams/util.js | 2 ++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/internal/webstreams/readablestream.js b/lib/internal/webstreams/readablestream.js index 876e3a5bf6e2f0..486ac48baf7063 100644 --- a/lib/internal/webstreams/readablestream.js +++ b/lib/internal/webstreams/readablestream.js @@ -111,6 +111,7 @@ const { kState, kType, lazyTransfer, + markPromiseAsHandled, nonOpCancel, nonOpPull, nonOpStart, @@ -387,7 +388,11 @@ class ReadableStream { !!preventAbort, !!preventCancel, signal); - setPromiseHandled(promise); + // The pipeTo promise's rejection is also propagated through the + // destination stream's state, which the returned readable side + // observes via its own error path. markAsHandled silences the + // unhandled-rejection event without allocating a no-op .then chain. + markPromiseAsHandled(promise); return readable; } @@ -1617,7 +1622,14 @@ function readableStreamPipeTo( disposable = addAbortListener(signal, abortAlgorithm); } - setPromiseHandled(run()); + // run() rejects when step() throws — i.e. when a write or a + // writer.ready/closed await fails. Those same errors reach pipeTo via + // watchErrored on writer.closed / reader.closed (registered just below), + // which calls shutdownWithAnAction and ultimately resolves pipeTo's outer + // promise. The run() promise itself is otherwise unobserved, so a + // markAsHandled is sufficient to silence the unhandled-rejection event + // without allocating a no-op .then chain. + markPromiseAsHandled(run()); watchErrored(source, reader[kState].close.promise, (error) => { if (!preventAbort) { diff --git a/lib/internal/webstreams/transfer.js b/lib/internal/webstreams/transfer.js index 9ce3f249ffd2bd..a0aecb18d9b937 100644 --- a/lib/internal/webstreams/transfer.js +++ b/lib/internal/webstreams/transfer.js @@ -9,7 +9,7 @@ const { const { kState, - setPromiseHandled, + markPromiseAsHandled, } = require('internal/webstreams/util'); const { @@ -279,7 +279,10 @@ function newCrossRealmReadableStream(writable, port) { const promise = readableStreamPipeTo(readable, writable, false, false, false); - setPromiseHandled(promise); + // pipeTo's rejection here flows back via the MessagePort and the wrapped + // source/sink; nothing else observes this internal promise. markAsHandled + // silences the unhandled-rejection event without allocating .then chain. + markPromiseAsHandled(promise); return { readable, @@ -295,7 +298,10 @@ function newCrossRealmWritableSink(readable, port) { const promise = readableStreamPipeTo(readable, writable, false, false, false); - setPromiseHandled(promise); + // pipeTo's rejection here flows back via the MessagePort and the wrapped + // source/sink; nothing else observes this internal promise. markAsHandled + // silences the unhandled-rejection event without allocating .then chain. + markPromiseAsHandled(promise); return { writable, diff --git a/lib/internal/webstreams/util.js b/lib/internal/webstreams/util.js index 808b0b069e57f7..20299685a96ad6 100644 --- a/lib/internal/webstreams/util.js +++ b/lib/internal/webstreams/util.js @@ -35,6 +35,7 @@ const { kPending, }, getPromiseDetails, + markPromiseAsHandled, } = internalBinding('util'); const assert = require('internal/assert'); @@ -225,6 +226,7 @@ module.exports = { kState, kType, lazyTransfer, + markPromiseAsHandled, nonOpCancel, nonOpFlush, nonOpPull,