Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)
, '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

Commit 9ec9383

Browse files
mcollinaaduh95
authored andcommitted
stream: decouple transform backpressure changes
The spec's [[backpressureChangePromise]] is only ever observed by the transform source pull algorithm (settles when backpressure next becomes true) and by a sink write arriving under backpressure (settles when it next becomes false), both internal. Replace the promise record with direct continuation delivery: a parked pull is completed by enqueueing the readable controller's pull-fulfilled step on the shared resolved promise, and a parked write by a cached per-stream continuation that resolves the sink promise with the perform-transform promise, so adoption reproduces the previous derived-chain settle depth exactly. Each delivery lands at the same microtask position as the old record's reaction. transformStreamDefaultControllerPerformTransform now mirrors the reference implementation's promiseCall().then(undefined, rejection steps) directly instead of running an async wrapper pair per chunk: the transformer.transform callback is wrapped raw, a non-thenable result reuses the shared resolved promise, and a synchronous throw is delivered through a rejected promise, keeping the error timing inside a reaction. A pipe-through benchmark is added since the suite had no transform-throughput row. pipeThrough passthrough improves by ~8% and a writer-driven transform loop by ~14%; the pipe-to and read families are unchanged. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #65143 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent d286423 commit 9ec9383

3 files changed

Lines changed: 156 additions & 44 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
constcommon=require('../common.js');
3+
const{
4+
ReadableStream,
5+
TransformStream,
6+
}=require('node:stream/web');
7+
8+
constbench=common.createBenchmark(main,{
9+
n: [5e5],
10+
kind: ['default','transform'],
11+
});
12+
13+
asyncfunctionmain({ n, kind }){
14+
constb=Buffer.alloc(64);
15+
leti=0;
16+
constrs=newReadableStream({
17+
pull(controller){
18+
if(i++<n){
19+
controller.enqueue(b);
20+
}else{
21+
controller.close();
22+
}
23+
},
24+
});
25+
constts=kind==='default' ?
26+
newTransformStream() :
27+
newTransformStream({
28+
transform(chunk,controller){controller.enqueue(chunk);},
29+
});
30+
31+
constreader=rs.pipeThrough(ts).getReader();
32+
bench.start();
33+
for(;;){
34+
const{ done }=awaitreader.read();
35+
if(done)break;
36+
}
37+
bench.end(n);
38+
}

‎lib/internal/webstreams/transformstream.js‎

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const {
55
ObjectDefineProperties,
66
ObjectSetPrototypeOf,
77
PromisePrototypeThen,
8+
PromiseReject,
9+
PromiseResolve,
810
PromiseWithResolvers,
911
Symbol,
1012
SymbolToStringTag,
@@ -44,12 +46,14 @@ const {
4446

4547
const{
4648
createPromiseCallback1Param,
47-
createPromiseCallback2Params,
49+
createRawCallback2Params,
4850
customInspect,
4951
extractHighWaterMark,
5052
extractSizeAlgorithm,
5153
getNonWritablePropertyDescriptor,
5254
isBrandCheck,
55+
kParkedAlgorithmResult,
56+
kResolvedPromise,
5357
kState,
5458
kType,
5559
nonOpCancel,
@@ -258,7 +262,10 @@ function InternalTransferredTransformStream() {
258262
readable: undefined,
259263
writable: undefined,
260264
backpressure: undefined,
261-
backpressureChange: undefined,
265+
pullPending: false,
266+
pendingWrite: undefined,
267+
pendingWriteChunk: undefined,
268+
writeContinuation: undefined,
262269
controller: undefined,
263270
};
264271
}
@@ -348,7 +355,9 @@ const isTransformStream =
348355
constisTransformStreamDefaultController=
349356
isBrandCheck('TransformStreamDefaultController');
350357

351-
asyncfunctiondefaultTransformAlgorithm(chunk,controller){
358+
// Raw callback (see createRawCallback*): invoked inside the try/catch of
359+
// transformStreamDefaultControllerPerformTransform.
360+
functiondefaultTransformAlgorithm(chunk,controller){
352361
transformStreamDefaultControllerEnqueue(controller,chunk);
353362
}
354363

@@ -385,7 +394,12 @@ function initializeTransformStream(
385394
writable,
386395
controller: undefined,
387396
backpressure: undefined,
388-
backpressureChange: undefined,
397+
// Continuation slots replacing the spec's
398+
// [[backpressureChangePromise]]; see transformStreamSetBackpressure.
399+
pullPending: false,
400+
pendingWrite: undefined,
401+
pendingWriteChunk: undefined,
402+
writeContinuation: undefined,
389403
};
390404

391405
transformStreamSetBackpressure(stream,true);
@@ -422,24 +436,30 @@ function transformStreamUnblockWrite(stream) {
422436
// The spec's [[backpressureChangePromise]] is only ever observed by the
423437
// source pull algorithm (settles when backpressure next becomes true) and
424438
// by a sink write arriving while backpressure is set (settles when
425-
// backpressure next becomes false). Instead of allocating a fresh promise
426-
// record on every flip, the record is materialized lazily on first
427-
// observation and dropped once settled; flips nobody is waiting on
428-
// allocate nothing.
429-
functiontransformStreamBackpressureChangePromise(stream){
430-
conststate=stream[kState];
431-
return(state.backpressureChange??=PromiseWithResolvers()).promise;
432-
}
433-
439+
// backpressure next becomes false). Both observers are internal, so the
440+
// promise record is replaced by continuation slots: a parked pull is
441+
// completed by delivering the readable controller's pull-fulfilled step,
442+
// and a parked write by the cached write continuation (see
443+
// transformStreamDefaultSinkWriteAlgorithm). Each is enqueued on the
444+
// shared resolved promise at the exact microtask position the old
445+
// record's reaction would have had.
434446
functiontransformStreamSetBackpressure(stream,backpressure){
435447
conststate=stream[kState];
436448
assert(state.backpressure!==backpressure);
437-
constbackpressureChange=state.backpressureChange;
438-
if(backpressureChange!==undefined){
439-
state.backpressureChange=undefined;
440-
backpressureChange.resolve();
441-
}
442449
state.backpressure=backpressure;
450+
if(backpressure){
451+
if(state.pullPending){
452+
state.pullPending=false;
453+
// The pull-fulfilled step exists: a pull parked it (see
454+
// transformStreamDefaultSourcePullAlgorithm), and the readable
455+
// controller creates it before invoking the pull algorithm.
456+
PromisePrototypeThen(
457+
kResolvedPromise,
458+
state.readable[kState].controller[kState].pullFulfilled);
459+
}
460+
}elseif(state.pendingWrite!==undefined){
461+
PromisePrototypeThen(kResolvedPromise,state.writeContinuation);
462+
}
443463
}
444464

445465
functionsetupTransformStreamDefaultController(
@@ -456,6 +476,7 @@ function setupTransformStreamDefaultController(
456476
transformAlgorithm,
457477
flushAlgorithm,
458478
cancelAlgorithm,
479+
performTransformRejected: undefined,
459480
};
460481
stream[kState].controller=controller;
461482
}
@@ -468,7 +489,7 @@ function setupTransformStreamDefaultControllerFromTransformer(
468489
constflush=transformer?.flush;
469490
constcancel=transformer?.cancel;
470491
consttransformAlgorithm=transform ?
471-
createPromiseCallback2Params('transformer.transform',transform,transformer) :
492+
createRawCallback2Params('transformer.transform',transform,transformer) :
472493
defaultTransformAlgorithm;
473494
constflushAlgorithm=flush ?
474495
createPromiseCallback1Param('transformer.flush',flush,transformer) :
@@ -521,18 +542,40 @@ function transformStreamDefaultControllerError(controller, error) {
521542
transformStreamError(controller[kState].stream,error);
522543
}
523544

524-
asyncfunctiontransformStreamDefaultControllerPerformTransform(controller,chunk){
545+
// Mirrors the reference implementation's
546+
// `promiseCall(transformAlgorithm, ...).then(undefined, rejectionSteps)`:
547+
// the returned promise settles one microtask after the (coerced) result
548+
// does, and a rejection errors the transform stream before propagating.
549+
// The raw transform callback plus the shared resolved promise for
550+
// non-thenable results replace the previous async wrapper's two implicit
551+
// promises per chunk.
552+
functiontransformStreamDefaultControllerPerformTransform(controller,chunk){
553+
constcontrollerState=controller[kState];
554+
consttransformAlgorithm=controllerState.transformAlgorithm;
555+
if(transformAlgorithm===undefined){
556+
// Algorithms were cleared by a concurrent cancel/abort/close.
557+
returnkResolvedPromise;
558+
}
559+
letresult;
525560
try{
526-
consttransformAlgorithm=controller[kState].transformAlgorithm;
527-
if(transformAlgorithm===undefined){
528-
// Algorithms were cleared by a concurrent cancel/abort/close.
529-
return;
530-
}
531-
returnawaittransformAlgorithm(chunk,controller);
561+
result=transformAlgorithm(chunk,controller);
532562
}catch(error){
563+
result=PromiseReject(error);
564+
}
565+
if(result===null||
566+
(typeofresult!=='object'&&typeofresult!=='function')){
567+
result=kResolvedPromise;
568+
}else{
569+
result=PromiseResolve(result);
570+
}
571+
controllerState.performTransformRejected??=(error)=>{
533572
transformStreamError(controller[kState].stream,error);
534573
throwerror;
535-
}
574+
};
575+
returnPromisePrototypeThen(
576+
result,
577+
undefined,
578+
controllerState.performTransformRejected);
536579
}
537580

538581
functiontransformStreamDefaultControllerTerminate(controller){
@@ -553,26 +596,42 @@ function transformStreamDefaultControllerTerminate(controller) {
553596
}
554597

555598
functiontransformStreamDefaultSinkWriteAlgorithm(stream,chunk){
599+
conststate=stream[kState];
556600
const{
557601
writable,
558602
controller,
559-
}=stream[kState];
603+
}=state;
560604
assert(writable[kState].state==='writable');
561-
if(stream[kState].backpressure){
562-
constbackpressureChange=transformStreamBackpressureChangePromise(stream);
563-
returnPromisePrototypeThen(
564-
backpressureChange,
565-
()=>{
566-
const{
567-
writable,
568-
}=stream[kState];
569-
if(writable[kState].state==='erroring')
570-
throwwritable[kState].storedError;
571-
assert(writable[kState].state==='writable');
572-
returntransformStreamDefaultControllerPerformTransform(
605+
if(state.backpressure){
606+
// Park the chunk and one promise record; the backpressure -> false
607+
// flip delivers the cached continuation (see
608+
// transformStreamSetBackpressure) at the same microtask position as
609+
// the old [[backpressureChangePromise]] reaction. The continuation
610+
// resolves the sink promise with the perform-transform promise, so
611+
// adoption reproduces the old derived-chain settle depth exactly.
612+
// The writable dispatches a single write at a time, so one pending
613+
// slot suffices.
614+
assert(state.pendingWrite===undefined);
615+
constpendingWrite=PromiseWithResolvers();
616+
state.pendingWrite=pendingWrite;
617+
state.pendingWriteChunk=chunk;
618+
state.writeContinuation??=()=>{
619+
constpending=state.pendingWrite;
620+
constpendingChunk=state.pendingWriteChunk;
621+
state.pendingWrite=undefined;
622+
state.pendingWriteChunk=undefined;
623+
constwritableState=state.writable[kState];
624+
if(writableState.state==='erroring'){
625+
pending.reject(writableState.storedError);
626+
return;
627+
}
628+
assert(writableState.state==='writable');
629+
pending.resolve(
630+
transformStreamDefaultControllerPerformTransform(
573631
controller,
574-
chunk);
575-
});
632+
pendingChunk));
633+
};
634+
returnpendingWrite.promise;
576635
}
577636
returntransformStreamDefaultControllerPerformTransform(controller,chunk);
578637
}
@@ -642,9 +701,15 @@ function transformStreamDefaultSinkCloseAlgorithm(stream) {
642701
}
643702

644703
functiontransformStreamDefaultSourcePullAlgorithm(stream){
645-
assert(stream[kState].backpressure);
704+
conststate=stream[kState];
705+
assert(state.backpressure);
646706
transformStreamSetBackpressure(stream,false);
647-
returntransformStreamBackpressureChangePromise(stream);
707+
// Park the pull: the next backpressure -> true flip delivers the
708+
// pull-fulfilled step (see transformStreamSetBackpressure). The old
709+
// [[backpressureChangePromise]] this replaces was only ever resolved,
710+
// so the parked pull needs no rejection delivery.
711+
state.pullPending=true;
712+
returnkParkedAlgorithmResult;
648713
}
649714

650715
functiontransformStreamDefaultSourceCancelAlgorithm(stream,reason){

‎lib/internal/webstreams/util.js‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,12 @@ function createRawCallback2Params(name, fn, thisArg) {
355355
// the next microtask checkpoint without allocating a fresh promise.
356356
constkResolvedPromise=PromiseResolve();
357357

358+
// Returned by an internal algorithm to signal that it parked the
359+
// operation and takes responsibility for delivering the fulfilled (or
360+
// rejected) continuation itself later, instead of settling a promise
361+
// (see the transform stream source pull algorithm).
362+
constkParkedAlgorithmResult={__proto__: null};
363+
358364
// Wires the (possibly non-thenable) result of an underlying algorithm
359365
// callback to its fulfilled/rejected continuations. A non-thenable result
360366
// means fulfillment is guaranteed and no then() lookup is observable, so
@@ -364,6 +370,8 @@ const kResolvedPromise = PromiseResolve();
364370
// matches the spec's "a promise resolved with" conversion (identity for
365371
// native promises).
366372
functionthenAlgorithmResult(result,onFulfilled,onRejected){
373+
if(result===kParkedAlgorithmResult)
374+
return;
367375
if(result===null||
368376
(typeofresult!=='object'&&typeofresult!=='function')){
369377
PromisePrototypeThen(kResolvedPromise,onFulfilled);
@@ -457,6 +465,7 @@ module.exports = {
457465
isBrandCheck,
458466
isPromisePending,
459467
kEmptyQueue,
468+
kParkedAlgorithmResult,
460469
kResolvedPromise,
461470
kState,
462471
kType,

0 commit comments

Comments
 (0)