Skip to content

Commit 6286432

Browse files
jasnelladuh95
authored andcommitted
stream: ensure that stateful transforms preserve this
Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode PR-URL: #65658 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 4010e69 commit 6286432

4 files changed

Lines changed: 57 additions & 9 deletions

File tree

lib/internal/streams/iter/pull.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,9 @@ function* withFlushSync(source) {
505505
yield null;
506506
}
507507

508-
function* applyStatefulSyncTransform(source, transform) {
509-
const output = transform(withFlushSync(source));
508+
function* applyStatefulSyncTransform(source, transform, receiver) {
509+
const output = FunctionPrototypeCall(
510+
transform, receiver, withFlushSync(source));
510511
for (const item of output) {
511512
if (item === null) continue;
512513
const batch = [];
@@ -537,7 +538,8 @@ function* createSyncPipeline(source, transforms) {
537538
current = applyFusedStatelessSyncTransforms(current, statelessRun);
538539
statelessRun = [];
539540
}
540-
current = applyStatefulSyncTransform(current, transform.transform);
541+
current = applyStatefulSyncTransform(
542+
current, transform.transform, transform);
541543
} else {
542544
ArrayPrototypePush(statelessRun, transform);
543545
}
@@ -648,8 +650,10 @@ async function* withFlushAsync(source) {
648650
yield null;
649651
}
650652

651-
async function* applyStatefulAsyncTransform(source, transform, options) {
652-
const output = transform(withFlushAsync(source), options);
653+
async function* applyStatefulAsyncTransform(
654+
source, transform, receiver, options) {
655+
const output = FunctionPrototypeCall(
656+
transform, receiver, withFlushAsync(source), options);
653657
for await (const item of output) {
654658
if (item === null) continue;
655659
// Fast path: item is already a Uint8Array[] batch (e.g. compression transforms)
@@ -681,8 +685,10 @@ async function* applyStatefulAsyncTransform(source, transform, options) {
681685
* skips isUint8ArrayBatch validation (transform guarantees valid output).
682686
* @yields {Uint8Array[]}
683687
*/
684-
async function* applyValidatedStatefulAsyncTransform(source, transform, options) {
685-
const output = transform(source, options);
688+
async function* applyValidatedStatefulAsyncTransform(
689+
source, transform, receiver, options) {
690+
const output = FunctionPrototypeCall(
691+
transform, receiver, source, options);
686692
for await (const batch of output) {
687693
if (batch.length > 0) {
688694
yield batch;
@@ -750,10 +756,10 @@ async function* createAsyncPipeline(source, transforms, signal) {
750756
const opts = { __proto__: null, signal: transformSignal };
751757
if (transform[kValidatedTransform]) {
752758
current = applyValidatedStatefulAsyncTransform(
753-
current, transform.transform, opts);
759+
current, transform.transform, transform, opts);
754760
} else {
755761
current = applyStatefulAsyncTransform(
756-
current, transform.transform, opts);
762+
current, transform.transform, transform, opts);
757763
}
758764
} else {
759765
ArrayPrototypePush(statelessRun, transform);

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ async function testPullStatefulTransform() {
4949
assert.strictEqual(data, 'data-ASYNC-END');
5050
}
5151

52+
async function testPullStatefulTransformReceiver() {
53+
const descriptor = {};
54+
descriptor.transform = common.mustCall(
55+
async function*(source) {
56+
assert.strictEqual(this, descriptor);
57+
for await (const chunks of source) {
58+
yield chunks;
59+
}
60+
});
61+
62+
assert.strictEqual(await text(pull(from('receiver'), descriptor)), 'receiver');
63+
}
64+
5265
async function testPullWithAbortSignal() {
5366
async function* gen() {
5467
yield [new Uint8Array([1])];
@@ -511,6 +524,7 @@ async function testTransformOptionsNotShared() {
511524
testPullIdentity(),
512525
testPullStatelessTransform(),
513526
testPullStatefulTransform(),
527+
testPullStatefulTransformReceiver(),
514528
testPullWithAbortSignal(),
515529
testPullNormalizesSourceAtCallTime(),
516530
testPullPreAbortOrdering(),

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ function testPullSyncStatefulTransform() {
7474
assert.strictEqual(data, 'data-END');
7575
}
7676

77+
function testPullSyncStatefulTransformReceiver() {
78+
const descriptor = {};
79+
descriptor.transform = common.mustCall(
80+
function*(source) {
81+
assert.strictEqual(this, descriptor);
82+
yield* source;
83+
});
84+
85+
assert.strictEqual(
86+
new TextDecoder().decode(bytesSync(pullSync(fromSync('receiver'), descriptor))),
87+
'receiver',
88+
);
89+
}
90+
7791
function testPullSyncChainedTransforms() {
7892
const addExcl = (chunks) => {
7993
if (chunks === null) return null;
@@ -210,6 +224,7 @@ Promise.all([
210224
testPullSyncNormalizesSourceAtCallTime(),
211225
testPullSyncStatelessTransform(),
212226
testPullSyncStatefulTransform(),
227+
testPullSyncStatefulTransformReceiver(),
213228
testPullSyncChainedTransforms(),
214229
testPullSyncSourceError(),
215230
testPullSyncEmptySource(),

test/parallel/test-stream-iter-transform-roundtrip.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ async function testGzipRoundTrip() {
4242
assert.strictEqual(result, input);
4343
}
4444

45+
async function testValidatedTransformReceiver() {
46+
const descriptor = compressGzip();
47+
const transform = descriptor.transform;
48+
descriptor.transform = common.mustCall(function(source, options) {
49+
assert.strictEqual(this, descriptor);
50+
return Reflect.apply(transform, this, [source, options]);
51+
});
52+
53+
const result = await bytes(pull(from('receiver'), descriptor));
54+
assert.ok(result.byteLength > 0);
55+
}
56+
4557
async function testGzipLargeData() {
4658
// 100KB of repeated text - exercises multi-chunk path
4759
const input = 'gzip large data test. '.repeat(5000);
@@ -250,6 +262,7 @@ async function testGzipWithLevel() {
250262
(async () => {
251263
// Gzip
252264
await testGzipRoundTrip();
265+
await testValidatedTransformReceiver();
253266
await testGzipLargeData();
254267
await testGzipActuallyCompresses();
255268

0 commit comments

Comments
 (0)