Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions benchmark/webstreams/pipe-to.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,29 +7,27 @@ const {

const bench = common.createBenchmark(main, {
n: [5e5],
highWaterMarkR: [512, 1024, 2048, 4096],
highWaterMarkW: [512, 1024, 2048, 4096],
highWaterMarkR: [1, 1024, 4096],
highWaterMarkW: [1, 1024, 4096],
});


async function main({ n, highWaterMarkR, highWaterMarkW }) {
const b = Buffer.alloc(1024);
let i = 0;
const rs = new ReadableStream({
highWaterMark: highWaterMarkR,
pull: function(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
}, { highWaterMark: highWaterMarkR });
const ws = new WritableStream({
highWaterMark: highWaterMarkW,
write(chunk, controller) {},
close() { bench.end(n); },
});
}, { highWaterMark: highWaterMarkW });

bench.start();
rs.pipeTo(ws);
Expand Down
100 changes: 72 additions & 28 deletions lib/internal/webstreams/readablestream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -101,6 +101,7 @@ const {
cloneAsUint8Array,
copyArrayBuffer,
createPromiseCallback1Param,
createRawCallback1Param,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -110,17 +111,18 @@ const {
getNonWritablePropertyDescriptor,
isBrandCheck,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpPull,
nonOpStart,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
} = require('internal/webstreams/util');

const {
Expand All@@ -137,7 +139,6 @@ const {
writableStreamDefaultWriterRelease,
writableStreamDefaultWriterWriteWithRequest,
writerClosedPromise,
writerReadyPromise,
} = require('internal/webstreams/writablestream');

const { Buffer } = require('buffer');
Expand DownExpand Up@@ -1445,7 +1446,7 @@ function readableStreamFromIterable(iterable) {
if (iterator === null || (typeof iterator !== 'object' && typeof iterator !== 'function')) {
throw new ERR_INVALID_STATE.TypeError('The iterator method must return an object');
}
const startAlgorithm = nonOpStart;
const startAlgorithm = nonOpCallback;

async function pullAlgorithm() {
const iterResult = await iterator.next();
Expand DownExpand Up@@ -1664,11 +1665,31 @@ function readableStreamPipeTo(
// the chunk travels through `pendingChunk`.
let pendingChunk;
let readRequest;
let readyHook;

// Ready promise rejection is handled by the destination-errored
// watcher.
function ignoreReadyRejection() {}

// Parks the pump on the destination's backpressure by installing a
// record that duck-types the writer's lazily-materialized
// [[readyPromise]] record: writableStreamUpdateBackpressure resolves it
// when backpressure clears (after publishing the new backpressure
// state), which re-enters the pump directly instead of rotating a
// fresh promise record plus reaction per flip. The pipe holds the only
// reference to the writer, so the record is never observable as a real
// ready promise; the erroring/release paths probe `promise` via
// isPromisePending() and call `reject`, so it carries a real
// forever-pending promise and a no-op reject.
function parkOnReady() {
readyHook ??= {
promise: new Promise(nonOpCallback),
resolve: pump,
reject: ignoreReadyRejection,
};
writer[kState].ready = readyHook;
}

function forwardChunk() {
const chunk = pendingChunk;
pendingChunk = undefined;
Expand All@@ -1680,10 +1701,7 @@ function readableStreamPipeTo(
if (shuttingDown) return;

if (dest[kState].backpressure) {
PromisePrototypeThen(
writerReadyPromise(writer).promise,
pump,
ignoreReadyRejection);
parkOnReady();
return;
}

Expand DownExpand Up@@ -1728,9 +1746,18 @@ function readableStreamPipeTo(
return;
}

// Yield to microtask queue between batches to allow events/signals
// to fire
queueMicrotask(pump);
// Park on backpressure directly: the ready hook resumes the pump
// when a completed write clears it.
if (dest[kState].backpressure) {
parkOnReady();
return;
}

// Yield to the microtask queue between batches so completed-write
// reactions and events/signals fire; a shared resolved promise
// enqueues the continuation at the same position as queueMicrotask
// without the per-batch scheduling overhead.
PromisePrototypeThen(kResolvedPromise, pump);
return;
}

Expand All@@ -1742,7 +1769,7 @@ function readableStreamPipeTo(
// synchronous write during enqueue(). See WHATWG Streams spec
// "ReadableStreamPipeTo" step 15's "chunk steps".
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {},
[kError]() {},
Expand DownExpand Up@@ -1857,7 +1884,7 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
// The microtask is required by the spec (ReadableStreamTee's
// "chunk steps" queue one).
pendingChunk = value;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
// The `process.nextTick()` is not part of the spec.
Expand DownExpand Up@@ -1901,9 +1928,9 @@ function readableStreamDefaultTee(stream, cloneForBranch2) {
}

branch1 =
createReadableStream(nonOpStart, pullAlgorithm, cancel1Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel1Algorithm);
branch2 =
createReadableStream(nonOpStart, pullAlgorithm, cancel2Algorithm);
createReadableStream(nonOpCallback, pullAlgorithm, cancel2Algorithm);

PromisePrototypeThen(
readerClosedPromise(reader).promise,
Expand DownExpand Up@@ -2010,7 +2037,7 @@ function readableByteStreamTee(stream) {
defaultReadRequest ??= {
[kChunk](chunk) {
pendingChunk = chunk;
queueMicrotask(forwardChunk);
PromisePrototypeThen(kResolvedPromise, forwardChunk);
},
[kClose]() {
reading = false;
Expand DownExpand Up@@ -2189,9 +2216,9 @@ function readableByteStreamTee(stream) {
}

branch1 =
createReadableByteStream(nonOpStart, pull1Algorithm, cancel1Algorithm);
createReadableByteStream(nonOpCallback, pull1Algorithm, cancel1Algorithm);
branch2 =
createReadableByteStream(nonOpStart, pull2Algorithm, cancel2Algorithm);
createReadableByteStream(nonOpCallback, pull2Algorithm, cancel2Algorithm);

forwardReaderError(reader);

Expand DownExpand Up@@ -2699,8 +2726,18 @@ function readableStreamDefaultControllerPull(controller) {
controller[kState].pullRejected =
(error) => readableStreamDefaultControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// The pull algorithm may be a raw callback (a wrapped user source.pull
// returns its result uncoerced; a synchronous throw surfaces here) or an
// internal algorithm that always returns a promise; thenAlgorithmResult
// handles both.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -2824,10 +2861,10 @@ function setupReadableStreamDefaultControllerFromSource(
const cancel = source?.cancel;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand DownExpand Up@@ -3519,8 +3556,15 @@ function readableByteStreamControllerCallPullIfNeeded(controller) {
controller[kState].pullRejected =
(error) => readableByteStreamControllerError(controller, error);
}
PromisePrototypeThen(
controller[kState].pullAlgorithm(controller),
// See readableStreamDefaultControllerPull for the raw-callback contract.
let result;
try {
result = controller[kState].pullAlgorithm(controller);
} catch (error) {
result = PromiseReject(error);
}
thenAlgorithmResult(
result,
controller[kState].pullFulfilled,
controller[kState].pullRejected);
}
Expand DownExpand Up@@ -3698,10 +3742,10 @@ function setupReadableByteStreamControllerFromSource(
const autoAllocateChunkSize = source?.autoAllocateChunkSize;
const startAlgorithm = start ?
FunctionPrototypeBind(start, source, controller) :
nonOpStart;
nonOpCallback;
const pullAlgorithm = pull ?
createPromiseCallback1Param('source.pull', pull, source) :
nonOpPull;
createRawCallback1Param('source.pull', pull, source) :
nonOpCallback;
const cancelAlgorithm = cancel ?
createPromiseCallback1Param('source.cancel', cancel, source) :
nonOpCancel;
Expand Down
53 changes: 45 additions & 8 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -338,6 +338,40 @@ function createPromiseCallbackNoParams(name, fn, thisArg) {
return async () => FunctionPrototypeCall(fn, thisArg);
}

// Raw variants that skip the async wrapper's implicit result promise.
// Consumers of a raw callback invoke it inside try/catch and route the
// result through thenAlgorithmResult() below.
function createRawCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return (arg) => FunctionPrototypeCall(fn, thisArg, arg);
}

function createRawCallback2Params(name, fn, thisArg) {
validateFunction(fn, name);
return (arg1, arg2) => FunctionPrototypeCall(fn, thisArg, arg1, arg2);
}

// A single shared, forever-resolved promise used to enqueue a reaction at
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Wires the (possibly non-thenable) result of an underlying algorithm
// callback to its fulfilled/rejected continuations. A non-thenable result
// means fulfillment is guaranteed and no then() lookup is observable, so
// the fulfillment step is enqueued directly at the exact microtask
// position the coerced promise's reaction would have had, skipping the
// per-chunk promise allocation. For thenable results PromiseResolve()
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
Comment thread
jasnell marked this conversation as resolved.
PromisePrototypeThen(kResolvedPromise, onFulfilled);
} else {
PromisePrototypeThen(PromiseResolve(result), onFulfilled, onRejected);
}
}

function createPromiseCallback1Param(name, fn, thisArg) {
validateFunction(fn, name);
return async (arg) => FunctionPrototypeCall(fn, thisArg, arg);
Expand DownExpand Up@@ -384,14 +418,14 @@ function setPromiseHandled(promise) {

async function nonOpFlush() {}

function nonOpStart() {}

async function nonOpPull() {}
// Shared non-op for the start/pull/write algorithm callbacks, which all
// follow the raw-callback contract (see createRawCallback*): the
// non-thenable return takes the allocation-free fast path in
// thenAlgorithmResult().
function nonOpCallback() {}

async function nonOpCancel() {}

async function nonOpWrite() {}

let transfer;
function lazyTransfer() {
if (transfer === undefined)
Expand All@@ -411,6 +445,8 @@ module.exports = {
createPromiseCallbackNoParams,
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback1Param,
createRawCallback2Params,
customInspect,
defaultSizeAlgorithm,
dequeueValue,
Expand All@@ -421,18 +457,19 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kResolvedPromise,
kState,
kType,
lazyTransfer,
materializeQueue,
nonOpCallback,
nonOpCancel,
nonOpFlush,
nonOpPull,
nonOpStart,
nonOpWrite,

peekQueueValue,
rejectedHandledRecord,
resetQueue,
resolvedRecord,
setPromiseHandled,
thenAlgorithmResult,
};
Loading
Loading