Skip to content

Commit 4f40a85

Browse files
trivikraduh95
authored andcommitted
stream: flush each fused stateless transform
Ensure consecutive stateless stream/iter transforms each receive a final null flush after upstream flush output has been processed. Fixes: #63467 Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: #63468Fixes: #63467 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 885d246 commit 4f40a85

3 files changed

Lines changed: 76 additions & 30 deletions

File tree

‎lib/internal/streams/iter/pull.js‎

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,17 @@ function* processTransformResultSync(result) {
273273
result);
274274
}
275275

276+
/**
277+
* Append normalized transform result batches to an array (sync).
278+
* @param {Array<Uint8Array[]>} target
279+
* @param {*} result
280+
*/
281+
functionappendTransformResultSync(target,result){
282+
for(constbatchofprocessTransformResultSync(result)){
283+
ArrayPrototypePush(target,batch);
284+
}
285+
}
286+
276287
/**
277288
* Process transform result (async).
278289
* @yields {Uint8Array[]}
@@ -356,6 +367,18 @@ async function* processTransformResultAsync(result) {
356367
result);
357368
}
358369

370+
/**
371+
* Append normalized transform result batches to an array (async).
372+
* @param {Array<Uint8Array[]>} target
373+
* @param {*} result
374+
* @returns {Promise<void>}
375+
*/
376+
asyncfunctionappendTransformResultAsync(target,result){
377+
forawait(constbatchofprocessTransformResultAsync(result)){
378+
ArrayPrototypePush(target,batch);
379+
}
380+
}
381+
359382
// =============================================================================
360383
// Sync Pipeline Implementation
361384
// =============================================================================
@@ -398,18 +421,19 @@ function* applyFusedStatelessSyncTransforms(source, run) {
398421
yield*processTransformResultSync(current);
399422
}
400423
}
401-
// Flush
402-
letcurrent=null;
424+
// Flush each transform after all upstream data, including data emitted by
425+
// earlier flushes, has been processed by that transform.
426+
letpending=[];
403427
for(leti=0;i<run.length;i++){
404-
constresult=run[i](current);
405-
if(result===null){
406-
current=null;
407-
continue;
428+
constnext=[];
429+
for(letj=0;j<pending.length;j++){
430+
appendTransformResultSync(next,run[i](pending[j]));
408431
}
409-
current=result;
432+
appendTransformResultSync(next,run[i](null));
433+
pending=next;
410434
}
411-
if(current!=null){
412-
yield*processTransformResultSync(current);
435+
for(leti=0;i<pending.length;i++){
436+
yieldpending[i];
413437
}
414438
}
415439

@@ -522,30 +546,23 @@ async function* applyFusedStatelessAsyncTransforms(source, run, signal) {
522546
yield*processTransformResultAsync(current);
523547
}
524548
}
525-
// Flush: send null through each transform in order
526-
letcurrent=null;
549+
// Flush each transform after all upstream data, including data emitted by
550+
// earlier flushes, has been processed by that transform.
551+
letpending=[];
527552
for(leti=0;i<run.length;i++){
528-
constresult=run[i](current,{__proto__: null, signal });
529-
if(result===null){
530-
current=null;
531-
continue;
532-
}
533-
if(isPromise(result)){
534-
current=awaitresult;
535-
}else{
536-
current=result;
553+
constnext=[];
554+
for(letj=0;j<pending.length;j++){
555+
awaitappendTransformResultAsync(
556+
next,
557+
run[i](pending[j],{__proto__: null, signal }));
537558
}
559+
awaitappendTransformResultAsync(
560+
next,
561+
run[i](null,{__proto__: null, signal }));
562+
pending=next;
538563
}
539-
if(current!==null){
540-
if(isUint8ArrayBatch(current)){
541-
if(current.length>0)yieldcurrent;
542-
}elseif(isUint8Array(current)){
543-
yield[current];
544-
}elseif(typeofcurrent==='string'){
545-
yield[toUint8Array(current)];
546-
}else{
547-
yield*processTransformResultAsync(current);
548-
}
564+
for(leti=0;i<pending.length;i++){
565+
yieldpending[i];
549566
}
550567
}
551568

‎test/parallel/test-stream-iter-pull-async.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,19 @@ async function testPullStatelessTransformFlush() {
233233
assert.strictEqual(data,'data-TRAILER');
234234
}
235235

236+
// Consecutive stateless transforms each receive a final flush signal after
237+
// upstream flush output has been processed.
238+
asyncfunctiontestPullConsecutiveStatelessTransformFlush(){
239+
constenc=newTextEncoder();
240+
constaddAOnFlush=(chunks)=>(chunks===null ?
241+
[enc.encode('-A')] : chunks);
242+
constaddBOnFlush=(chunks)=>(chunks===null ?
243+
[enc.encode('-B')] : chunks);
244+
245+
constdata=awaittext(pull(from('x'),addAOnFlush,addBOnFlush));
246+
assert.strictEqual(data,'x-A-B');
247+
}
248+
236249
// Stateless transform flush error propagates
237250
asyncfunctiontestPullStatelessTransformFlushError(){
238251
constbadFlush=(chunks)=>{
@@ -357,6 +370,7 @@ async function testTransformOptionsNotShared() {
357370
testPullStatelessTransformError(),
358371
testPullStatefulTransformError(),
359372
testPullStatelessTransformFlush(),
373+
testPullConsecutiveStatelessTransformFlush(),
360374
testPullStatelessTransformFlushError(),
361375
testPullWithSyncSource(),
362376
testPullStringSource(),

‎test/parallel/test-stream-iter-pull-sync.js‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ function testPullSyncStatelessTransformFlush() {
127127
assert.strictEqual(data,'data-TRAILER');
128128
}
129129

130+
// Consecutive stateless transforms each receive a final flush signal after
131+
// upstream flush output has been processed.
132+
functiontestPullSyncConsecutiveStatelessTransformFlush(){
133+
constenc=newTextEncoder();
134+
constaddAOnFlush=(chunks)=>(chunks===null ?
135+
[enc.encode('-A')] : chunks);
136+
constaddBOnFlush=(chunks)=>(chunks===null ?
137+
[enc.encode('-B')] : chunks);
138+
139+
constdata=newTextDecoder().decode(bytesSync(
140+
pullSync(fromSync('x'),addAOnFlush,addBOnFlush)));
141+
assert.strictEqual(data,'x-A-B');
142+
}
143+
130144
// Stateless transform flush error propagates
131145
functiontestPullSyncStatelessTransformFlushError(){
132146
constbadFlush=(chunks)=>{
@@ -173,6 +187,7 @@ Promise.all([
173187
testPullSyncStatelessTransformError(),
174188
testPullSyncStatefulTransformError(),
175189
testPullSyncStatelessTransformFlush(),
190+
testPullSyncConsecutiveStatelessTransformFlush(),
176191
testPullSyncStatelessTransformFlushError(),
177192
testPullSyncInvalidTransform(),
178193
]).then(common.mustCall());

0 commit comments

Comments
 (0)