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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
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
38 changes: 38 additions & 0 deletions benchmark/webstreams/pipe-through.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
'use strict';
const common = require('../common.js');
const {
ReadableStream,
TransformStream,
} = require('node:stream/web');

const bench = common.createBenchmark(main, {
n: [5e5],
kind: ['default', 'transform'],
});

async function main({ n, kind }) {
const b = Buffer.alloc(64);
let i = 0;
const rs = new ReadableStream({
pull(controller) {
if (i++ < n) {
controller.enqueue(b);
} else {
controller.close();
}
},
});
const ts = kind === 'default' ?
new TransformStream() :
new TransformStream({
transform(chunk, controller) { controller.enqueue(chunk); },
});

const reader = rs.pipeThrough(ts).getReader();
bench.start();
for (;;) {
const { done } = await reader.read();
if (done) break;
}
bench.end(n);
}
153 changes: 109 additions & 44 deletions lib/internal/webstreams/transformstream.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@ const {
ObjectDefineProperties,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
PromiseWithResolvers,
Symbol,
SymbolToStringTag,
Expand DownExpand Up@@ -44,12 +46,14 @@ const {

const {
createPromiseCallback1Param,
createPromiseCallback2Params,
createRawCallback2Params,
customInspect,
extractHighWaterMark,
extractSizeAlgorithm,
getNonWritablePropertyDescriptor,
isBrandCheck,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
nonOpCancel,
Expand DownExpand Up@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
readable: undefined,
writable: undefined,
backpressure: undefined,
backpressureChange: undefined,
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
controller: undefined,
};
}
Expand DownExpand Up@@ -348,7 +355,9 @@ const isTransformStream =
const isTransformStreamDefaultController =
isBrandCheck('TransformStreamDefaultController');

async function defaultTransformAlgorithm(chunk, controller) {
// Raw callback (see createRawCallback*): invoked inside the try/catch of
// transformStreamDefaultControllerPerformTransform.
function defaultTransformAlgorithm(chunk, controller) {
transformStreamDefaultControllerEnqueue(controller, chunk);
}

Expand DownExpand Up@@ -385,7 +394,12 @@ function initializeTransformStream(
writable,
controller: undefined,
backpressure: undefined,
backpressureChange: undefined,
// Continuation slots replacing the spec's
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
pullPending: false,
pendingWrite: undefined,
pendingWriteChunk: undefined,
writeContinuation: undefined,
};

transformStreamSetBackpressure(stream, true);
Expand DownExpand Up@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
// The spec's [[backpressureChangePromise]] is only ever observed by the
// source pull algorithm (settles when backpressure next becomes true) and
// by a sink write arriving while backpressure is set (settles when
// backpressure next becomes false). Instead of allocating a fresh promise
// record on every flip, the record is materialized lazily on first
// observation and dropped once settled; flips nobody is waiting on
// allocate nothing.
function transformStreamBackpressureChangePromise(stream) {
const state = stream[kState];
return (state.backpressureChange ??= PromiseWithResolvers()).promise;
}

// backpressure next becomes false). Both observers are internal, so the
// promise record is replaced by continuation slots: a parked pull is
// completed by delivering the readable controller's pull-fulfilled step,
// and a parked write by the cached write continuation (see
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
// shared resolved promise at the exact microtask position the old
// record's reaction would have had.
function transformStreamSetBackpressure(stream, backpressure) {
const state = stream[kState];
assert(state.backpressure !== backpressure);
const backpressureChange = state.backpressureChange;
if (backpressureChange !== undefined) {
state.backpressureChange = undefined;
backpressureChange.resolve();
}
state.backpressure = backpressure;
if (backpressure) {
if (state.pullPending) {
state.pullPending = false;
// The pull-fulfilled step exists: a pull parked it (see
// transformStreamDefaultSourcePullAlgorithm), and the readable
// controller creates it before invoking the pull algorithm.
PromisePrototypeThen(
kResolvedPromise,
state.readable[kState].controller[kState].pullFulfilled);
}
} else if (state.pendingWrite !== undefined) {
PromisePrototypeThen(kResolvedPromise, state.writeContinuation);
}
}

function setupTransformStreamDefaultController(
Expand All@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
transformAlgorithm,
flushAlgorithm,
cancelAlgorithm,
performTransformRejected: undefined,
};
stream[kState].controller = controller;
}
Expand All@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
const flush = transformer?.flush;
const cancel = transformer?.cancel;
const transformAlgorithm = transform ?
createPromiseCallback2Params('transformer.transform', transform, transformer) :
createRawCallback2Params('transformer.transform', transform, transformer) :
defaultTransformAlgorithm;
const flushAlgorithm = flush ?
createPromiseCallback1Param('transformer.flush', flush, transformer) :
Expand DownExpand Up@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
transformStreamError(controller[kState].stream, error);
}

async function transformStreamDefaultControllerPerformTransform(controller, chunk) {
// Mirrors the reference implementation's
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
// the returned promise settles one microtask after the (coerced) result
// does, and a rejection errors the transform stream before propagating.
// The raw transform callback plus the shared resolved promise for
// non-thenable results replace the previous async wrapper's two implicit
// promises per chunk.
function transformStreamDefaultControllerPerformTransform(controller, chunk) {
const controllerState = controller[kState];
const transformAlgorithm = controllerState.transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return kResolvedPromise;
}
let result;
try {
const transformAlgorithm = controller[kState].transformAlgorithm;
if (transformAlgorithm === undefined) {
// Algorithms were cleared by a concurrent cancel/abort/close.
return;
}
return await transformAlgorithm(chunk, controller);
result = transformAlgorithm(chunk, controller);
} catch (error) {
result = PromiseReject(error);
}
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
result = kResolvedPromise;
} else {
result = PromiseResolve(result);
}
controllerState.performTransformRejected ??= (error) => {
transformStreamError(controller[kState].stream, error);
throw error;
}
};
return PromisePrototypeThen(
result,
undefined,
controllerState.performTransformRejected);
}

function transformStreamDefaultControllerTerminate(controller) {
Expand All@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
}

function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) {
const state = stream[kState];
const {
writable,
controller,
} = stream[kState];
} = state;
assert(writable[kState].state === 'writable');
if (stream[kState].backpressure) {
const backpressureChange = transformStreamBackpressureChangePromise(stream);
return PromisePrototypeThen(
backpressureChange,
() => {
const {
writable,
} = stream[kState];
if (writable[kState].state === 'erroring')
throw writable[kState].storedError;
assert(writable[kState].state === 'writable');
return transformStreamDefaultControllerPerformTransform(
if (state.backpressure) {
// Park the chunk and one promise record; the backpressure -> false
// flip delivers the cached continuation (see
// transformStreamSetBackpressure) at the same microtask position as
// the old [[backpressureChangePromise]] reaction. The continuation
// resolves the sink promise with the perform-transform promise, so
// adoption reproduces the old derived-chain settle depth exactly.
// The writable dispatches a single write at a time, so one pending
// slot suffices.
assert(state.pendingWrite === undefined);
const pendingWrite = PromiseWithResolvers();
state.pendingWrite = pendingWrite;
state.pendingWriteChunk = chunk;
state.writeContinuation ??= () => {
const pending = state.pendingWrite;
const pendingChunk = state.pendingWriteChunk;
state.pendingWrite = undefined;
state.pendingWriteChunk = undefined;
const writableState = state.writable[kState];
if (writableState.state === 'erroring') {
pending.reject(writableState.storedError);
return;
}
assert(writableState.state === 'writable');
pending.resolve(
transformStreamDefaultControllerPerformTransform(
controller,
chunk);
});
pendingChunk));
};
return pendingWrite.promise;
}
return transformStreamDefaultControllerPerformTransform(controller, chunk);
}
Expand DownExpand Up@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
}

function transformStreamDefaultSourcePullAlgorithm(stream) {
assert(stream[kState].backpressure);
const state = stream[kState];
assert(state.backpressure);
transformStreamSetBackpressure(stream, false);
return transformStreamBackpressureChangePromise(stream);
// Park the pull: the next backpressure -> true flip delivers the
// pull-fulfilled step (see transformStreamSetBackpressure). The old
// [[backpressureChangePromise]] this replaces was only ever resolved,
// so the parked pull needs no rejection delivery.
state.pullPending = true;
return kParkedAlgorithmResult;
}

function transformStreamDefaultSourceCancelAlgorithm(stream, reason) {
Expand Down
9 changes: 9 additions & 0 deletions lib/internal/webstreams/util.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
// the next microtask checkpoint without allocating a fresh promise.
const kResolvedPromise = PromiseResolve();

// Returned by an internal algorithm to signal that it parked the
// operation and takes responsibility for delivering the fulfilled (or
// rejected) continuation itself later, instead of settling a promise
// (see the transform stream source pull algorithm).
const kParkedAlgorithmResult = { __proto__: null };

// 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
Expand All@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
// matches the spec's "a promise resolved with" conversion (identity for
// native promises).
function thenAlgorithmResult(result, onFulfilled, onRejected) {
if (result === kParkedAlgorithmResult)
return;
if (result === null ||
(typeof result !== 'object' && typeof result !== 'function')) {
PromisePrototypeThen(kResolvedPromise, onFulfilled);
Expand DownExpand Up@@ -457,6 +465,7 @@ module.exports = {
isBrandCheck,
isPromisePending,
kEmptyQueue,
kParkedAlgorithmResult,
kResolvedPromise,
kState,
kType,
Expand Down
Loading