@gjsify/stream 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/cjs-compat.cjs +7 -0
- package/lib/esm/consumers/index.js +37 -4
- package/lib/esm/index.js +1096 -4
- package/lib/esm/promises/index.js +28 -4
- package/lib/esm/web/index.js +34 -3
- package/lib/types/consumers/index.d.ts +13 -3
- package/lib/types/index.d.ts +182 -5
- package/lib/types/promises/index.d.ts +8 -3
- package/lib/types/web/index.d.ts +3 -3
- package/package.json +23 -45
- package/src/consumers/index.spec.ts +107 -0
- package/src/consumers/index.ts +39 -3
- package/src/edge-cases.spec.ts +593 -0
- package/src/index.spec.ts +2239 -7
- package/src/index.ts +1348 -5
- package/src/promises/index.spec.ts +140 -0
- package/src/promises/index.ts +31 -3
- package/src/test.mts +5 -2
- package/src/web/index.ts +32 -3
- package/tsconfig.json +21 -9
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/consumers/index.js +0 -6
- package/lib/cjs/index.js +0 -6
- package/lib/cjs/promises/index.js +0 -6
- package/lib/cjs/web/index.js +0 -6
- package/test.gjs.js +0 -34839
- package/test.gjs.mjs +0 -34728
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -1234
- package/test.node.mjs +0 -315
- package/tsconfig.types.json +0 -8
- package/tsconfig.types.tsbuildinfo +0 -1
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
// Ported from refs/node-test/parallel/test-stream-pipeline.js,
|
|
2
|
+
// test-stream-finished.js, test-stream-transform-flush.js,
|
|
3
|
+
// test-stream-readable-from.js
|
|
4
|
+
// Original: MIT license, Node.js contributors
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
7
|
+
import { Readable, Writable, Transform, PassThrough, Duplex, pipeline, finished, addAbortSignal } from 'node:stream';
|
|
8
|
+
import { pipeline as pipelinePromise, finished as finishedPromise } from 'node:stream/promises';
|
|
9
|
+
import { Buffer } from 'node:buffer';
|
|
10
|
+
|
|
11
|
+
export default async () => {
|
|
12
|
+
|
|
13
|
+
// ===================== pipeline =====================
|
|
14
|
+
await describe('stream.pipeline', async () => {
|
|
15
|
+
await it('should pipe data through multiple streams', async () => {
|
|
16
|
+
const chunks: string[] = [];
|
|
17
|
+
await new Promise<void>((resolve, reject) => {
|
|
18
|
+
const source = new Readable({
|
|
19
|
+
read() {
|
|
20
|
+
this.push('hello');
|
|
21
|
+
this.push(null);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const transform = new Transform({
|
|
25
|
+
transform(chunk, _enc, cb) {
|
|
26
|
+
cb(null, chunk.toString().toUpperCase());
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const sink = new Writable({
|
|
30
|
+
write(chunk, _enc, cb) {
|
|
31
|
+
chunks.push(chunk.toString());
|
|
32
|
+
cb();
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
pipeline(source, transform, sink, (err) => {
|
|
36
|
+
if (err) reject(err);
|
|
37
|
+
else resolve();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
expect(chunks).toContain('HELLO');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
await it('should propagate errors from source', async () => {
|
|
44
|
+
const expectedError = new Error('source error');
|
|
45
|
+
const result = await new Promise<Error | null>((resolve) => {
|
|
46
|
+
const source = new Readable({
|
|
47
|
+
read() {
|
|
48
|
+
this.destroy(expectedError);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const sink = new Writable({
|
|
52
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
53
|
+
});
|
|
54
|
+
pipeline(source, sink, (err) => {
|
|
55
|
+
resolve(err);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
expect(result).toBe(expectedError);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await it('should propagate errors from transform', async () => {
|
|
62
|
+
const expectedError = new Error('transform error');
|
|
63
|
+
const result = await new Promise<Error | null>((resolve) => {
|
|
64
|
+
const source = new Readable({
|
|
65
|
+
read() {
|
|
66
|
+
this.push('data');
|
|
67
|
+
this.push(null);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const transform = new Transform({
|
|
71
|
+
transform(_chunk, _enc, cb) {
|
|
72
|
+
cb(expectedError);
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const sink = new Writable({
|
|
76
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
77
|
+
});
|
|
78
|
+
pipeline(source, transform, sink, (err) => {
|
|
79
|
+
resolve(err);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
expect(result).toBe(expectedError);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await it('should propagate errors from sink', async () => {
|
|
86
|
+
const expectedError = new Error('sink error');
|
|
87
|
+
const result = await new Promise<Error | null>((resolve) => {
|
|
88
|
+
const source = new Readable({
|
|
89
|
+
read() {
|
|
90
|
+
this.push('data');
|
|
91
|
+
this.push(null);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const sink = new Writable({
|
|
95
|
+
write(_chunk, _enc, cb) {
|
|
96
|
+
cb(expectedError);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
pipeline(source, sink, (err) => {
|
|
100
|
+
resolve(err);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
expect(result).toBe(expectedError);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await it('should call callback without error on success', async () => {
|
|
107
|
+
const result = await new Promise<boolean>((resolve) => {
|
|
108
|
+
const source = new Readable({
|
|
109
|
+
read() {
|
|
110
|
+
this.push('ok');
|
|
111
|
+
this.push(null);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const sink = new Writable({
|
|
115
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
116
|
+
});
|
|
117
|
+
pipeline(source, sink, (err) => {
|
|
118
|
+
resolve(!err);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
expect(result).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ===================== pipeline (promises) =====================
|
|
126
|
+
await describe('stream/promises pipeline', async () => {
|
|
127
|
+
await it('should resolve on success', async () => {
|
|
128
|
+
const chunks: string[] = [];
|
|
129
|
+
await pipelinePromise(
|
|
130
|
+
new Readable({
|
|
131
|
+
read() {
|
|
132
|
+
this.push('hello');
|
|
133
|
+
this.push(null);
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
new Writable({
|
|
137
|
+
write(chunk, _enc, cb) {
|
|
138
|
+
chunks.push(chunk.toString());
|
|
139
|
+
cb();
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
expect(chunks).toContain('hello');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await it('should reject on error', async () => {
|
|
147
|
+
let caught = false;
|
|
148
|
+
try {
|
|
149
|
+
await pipelinePromise(
|
|
150
|
+
new Readable({
|
|
151
|
+
read() {
|
|
152
|
+
this.destroy(new Error('fail'));
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
new Writable({
|
|
156
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
157
|
+
}),
|
|
158
|
+
);
|
|
159
|
+
} catch {
|
|
160
|
+
caught = true;
|
|
161
|
+
}
|
|
162
|
+
expect(caught).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ===================== finished =====================
|
|
167
|
+
await describe('stream.finished', async () => {
|
|
168
|
+
await it('should call callback when writable finishes', async () => {
|
|
169
|
+
const done = await new Promise<boolean>((resolve) => {
|
|
170
|
+
const writable = new Writable({
|
|
171
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
172
|
+
});
|
|
173
|
+
finished(writable, (err) => {
|
|
174
|
+
resolve(!err);
|
|
175
|
+
});
|
|
176
|
+
writable.end('data');
|
|
177
|
+
});
|
|
178
|
+
expect(done).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await it('should call callback when readable ends', async () => {
|
|
182
|
+
const done = await new Promise<boolean>((resolve) => {
|
|
183
|
+
const readable = new Readable({
|
|
184
|
+
read() {
|
|
185
|
+
this.push('data');
|
|
186
|
+
this.push(null);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
finished(readable, (err) => {
|
|
190
|
+
resolve(!err);
|
|
191
|
+
});
|
|
192
|
+
readable.resume();
|
|
193
|
+
});
|
|
194
|
+
expect(done).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await it('should call callback with error when stream errors', async () => {
|
|
198
|
+
const expectedError = new Error('stream error');
|
|
199
|
+
const result = await new Promise<Error | null | undefined>((resolve) => {
|
|
200
|
+
const readable = new Readable({
|
|
201
|
+
read() {
|
|
202
|
+
this.destroy(expectedError);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
finished(readable, (err) => {
|
|
206
|
+
resolve(err);
|
|
207
|
+
});
|
|
208
|
+
readable.resume();
|
|
209
|
+
});
|
|
210
|
+
expect(result).toBe(expectedError);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await it('should call callback with premature close error', async () => {
|
|
214
|
+
const result = await new Promise<boolean>((resolve) => {
|
|
215
|
+
const writable = new Writable({
|
|
216
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
217
|
+
});
|
|
218
|
+
finished(writable, (err) => {
|
|
219
|
+
// Premature close: closed without finishing
|
|
220
|
+
resolve(err != null);
|
|
221
|
+
});
|
|
222
|
+
writable.destroy();
|
|
223
|
+
});
|
|
224
|
+
expect(result).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await it('should return cleanup function', async () => {
|
|
228
|
+
const writable = new Writable({
|
|
229
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
230
|
+
});
|
|
231
|
+
const cleanup = finished(writable, () => {});
|
|
232
|
+
expect(typeof cleanup).toBe('function');
|
|
233
|
+
cleanup();
|
|
234
|
+
writable.end();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ===================== finished (promises) =====================
|
|
239
|
+
await describe('stream/promises finished', async () => {
|
|
240
|
+
await it('should resolve when stream finishes', async () => {
|
|
241
|
+
const writable = new Writable({
|
|
242
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
243
|
+
});
|
|
244
|
+
const promise = finishedPromise(writable);
|
|
245
|
+
writable.end('data');
|
|
246
|
+
await promise;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await it('should reject when stream errors', async () => {
|
|
250
|
+
let caught = false;
|
|
251
|
+
const writable = new Writable({
|
|
252
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
253
|
+
});
|
|
254
|
+
const promise = finishedPromise(writable);
|
|
255
|
+
writable.destroy(new Error('fail'));
|
|
256
|
+
try {
|
|
257
|
+
await promise;
|
|
258
|
+
} catch {
|
|
259
|
+
caught = true;
|
|
260
|
+
}
|
|
261
|
+
expect(caught).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ===================== Transform flush =====================
|
|
266
|
+
await describe('Transform _flush', async () => {
|
|
267
|
+
await it('should call _flush before finishing', async () => {
|
|
268
|
+
const chunks: string[] = [];
|
|
269
|
+
const transform = new Transform({
|
|
270
|
+
transform(chunk, _enc, cb) {
|
|
271
|
+
chunks.push(chunk.toString());
|
|
272
|
+
cb();
|
|
273
|
+
},
|
|
274
|
+
flush(cb) {
|
|
275
|
+
chunks.push('FLUSHED');
|
|
276
|
+
cb();
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
const result = await new Promise<string[]>((resolve) => {
|
|
280
|
+
const output: string[] = [];
|
|
281
|
+
transform.on('data', (chunk: Buffer) => output.push(chunk.toString()));
|
|
282
|
+
transform.on('end', () => resolve(output));
|
|
283
|
+
transform.write('a');
|
|
284
|
+
transform.write('b');
|
|
285
|
+
transform.end();
|
|
286
|
+
});
|
|
287
|
+
// Flush should have been called
|
|
288
|
+
expect(chunks).toContain('FLUSHED');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await it('should push data from flush', async () => {
|
|
292
|
+
const transform = new Transform({
|
|
293
|
+
transform(chunk, _enc, cb) {
|
|
294
|
+
cb(null, chunk);
|
|
295
|
+
},
|
|
296
|
+
flush(cb) {
|
|
297
|
+
this.push(Buffer.from('TAIL'));
|
|
298
|
+
cb();
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
const result = await new Promise<string>((resolve) => {
|
|
302
|
+
let data = '';
|
|
303
|
+
transform.on('data', (chunk: Buffer) => { data += chunk.toString(); });
|
|
304
|
+
transform.on('end', () => resolve(data));
|
|
305
|
+
transform.write('HEAD');
|
|
306
|
+
transform.end();
|
|
307
|
+
});
|
|
308
|
+
expect(result).toBe('HEADTAIL');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await it('should propagate flush errors', async () => {
|
|
312
|
+
const expectedError = new Error('flush error');
|
|
313
|
+
const result = await new Promise<Error | null>((resolve) => {
|
|
314
|
+
const transform = new Transform({
|
|
315
|
+
transform(_chunk, _enc, cb) { cb(); },
|
|
316
|
+
flush(cb) { cb(expectedError); },
|
|
317
|
+
});
|
|
318
|
+
transform.on('error', (err) => resolve(err));
|
|
319
|
+
transform.write('data');
|
|
320
|
+
transform.end();
|
|
321
|
+
});
|
|
322
|
+
expect(result).toBe(expectedError);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ===================== Readable.from =====================
|
|
327
|
+
await describe('Readable.from', async () => {
|
|
328
|
+
await it('should create Readable from array', async () => {
|
|
329
|
+
const readable = Readable.from(['a', 'b', 'c']);
|
|
330
|
+
const chunks: string[] = [];
|
|
331
|
+
for await (const chunk of readable) {
|
|
332
|
+
chunks.push(String(chunk));
|
|
333
|
+
}
|
|
334
|
+
expect(chunks.length).toBe(3);
|
|
335
|
+
expect(chunks[0]).toBe('a');
|
|
336
|
+
expect(chunks[1]).toBe('b');
|
|
337
|
+
expect(chunks[2]).toBe('c');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
await it('should create Readable from generator', async () => {
|
|
341
|
+
function* gen() {
|
|
342
|
+
yield 'x';
|
|
343
|
+
yield 'y';
|
|
344
|
+
}
|
|
345
|
+
const readable = Readable.from(gen());
|
|
346
|
+
const chunks: string[] = [];
|
|
347
|
+
for await (const chunk of readable) {
|
|
348
|
+
chunks.push(String(chunk));
|
|
349
|
+
}
|
|
350
|
+
expect(chunks.length).toBe(2);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await it('should create Readable from async generator', async () => {
|
|
354
|
+
async function* asyncGen() {
|
|
355
|
+
yield 'one';
|
|
356
|
+
yield 'two';
|
|
357
|
+
yield 'three';
|
|
358
|
+
}
|
|
359
|
+
const readable = Readable.from(asyncGen());
|
|
360
|
+
const chunks: string[] = [];
|
|
361
|
+
for await (const chunk of readable) {
|
|
362
|
+
chunks.push(String(chunk));
|
|
363
|
+
}
|
|
364
|
+
expect(chunks.length).toBe(3);
|
|
365
|
+
expect(chunks[2]).toBe('three');
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await it('should create Readable from string', async () => {
|
|
369
|
+
const readable = Readable.from('hello');
|
|
370
|
+
const chunks: string[] = [];
|
|
371
|
+
for await (const chunk of readable) {
|
|
372
|
+
chunks.push(String(chunk));
|
|
373
|
+
}
|
|
374
|
+
// String is treated as a single chunk
|
|
375
|
+
expect(chunks.length).toBe(1);
|
|
376
|
+
expect(chunks[0]).toBe('hello');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
await it('should create Readable from Buffer', async () => {
|
|
380
|
+
const readable = Readable.from(Buffer.from('test'));
|
|
381
|
+
const chunks: Buffer[] = [];
|
|
382
|
+
for await (const chunk of readable) {
|
|
383
|
+
chunks.push(Buffer.from(chunk as any));
|
|
384
|
+
}
|
|
385
|
+
expect(chunks.length).toBe(1);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// ===================== addAbortSignal =====================
|
|
390
|
+
await describe('stream.addAbortSignal', async () => {
|
|
391
|
+
await it('should destroy stream when signal is aborted', async () => {
|
|
392
|
+
const ac = new AbortController();
|
|
393
|
+
const readable = new Readable({ read() {} });
|
|
394
|
+
readable.on('error', () => { /* expected abort error */ });
|
|
395
|
+
addAbortSignal(ac.signal, readable);
|
|
396
|
+
ac.abort();
|
|
397
|
+
// Stream should be destroyed
|
|
398
|
+
await new Promise<void>((r) => setTimeout(r, 50));
|
|
399
|
+
expect(readable.destroyed).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
await it('should handle already-aborted signal', async () => {
|
|
403
|
+
const ac = new AbortController();
|
|
404
|
+
ac.abort();
|
|
405
|
+
const readable = new Readable({ read() {} });
|
|
406
|
+
readable.on('error', () => { /* expected abort error */ });
|
|
407
|
+
addAbortSignal(ac.signal, readable);
|
|
408
|
+
await new Promise<void>((r) => setTimeout(r, 50));
|
|
409
|
+
expect(readable.destroyed).toBe(true);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await it('should throw for non-AbortSignal first arg', async () => {
|
|
413
|
+
let threw = false;
|
|
414
|
+
try {
|
|
415
|
+
addAbortSignal('not a signal' as any, new Readable({ read() {} }));
|
|
416
|
+
} catch {
|
|
417
|
+
threw = true;
|
|
418
|
+
}
|
|
419
|
+
expect(threw).toBe(true);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// ===================== Backpressure =====================
|
|
424
|
+
await describe('stream backpressure', async () => {
|
|
425
|
+
await it('write() should return false when buffer is full', async () => {
|
|
426
|
+
const writable = new Writable({
|
|
427
|
+
highWaterMark: 1,
|
|
428
|
+
write(_chunk, _enc, cb) {
|
|
429
|
+
// Delay callback to simulate slow consumer
|
|
430
|
+
setTimeout(cb, 10);
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
// First write fills buffer
|
|
434
|
+
const first = writable.write('a');
|
|
435
|
+
// Second write should signal backpressure
|
|
436
|
+
const second = writable.write('b');
|
|
437
|
+
// At least one should be false (backpressure)
|
|
438
|
+
expect(first === false || second === false).toBe(true);
|
|
439
|
+
writable.end();
|
|
440
|
+
await new Promise<void>((r) => writable.on('finish', r));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await it('should emit drain after buffer empties', async () => {
|
|
444
|
+
const writable = new Writable({
|
|
445
|
+
highWaterMark: 1,
|
|
446
|
+
write(_chunk, _enc, cb) {
|
|
447
|
+
setTimeout(cb, 10);
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
const drainEmitted = new Promise<boolean>((resolve) => {
|
|
451
|
+
writable.on('drain', () => resolve(true));
|
|
452
|
+
setTimeout(() => resolve(false), 2000);
|
|
453
|
+
});
|
|
454
|
+
writable.write('x');
|
|
455
|
+
writable.write('y');
|
|
456
|
+
const result = await drainEmitted;
|
|
457
|
+
expect(result).toBe(true);
|
|
458
|
+
writable.end();
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// ===================== PassThrough =====================
|
|
463
|
+
await describe('PassThrough', async () => {
|
|
464
|
+
await it('should pass data through unchanged', async () => {
|
|
465
|
+
const pt = new PassThrough();
|
|
466
|
+
const chunks: string[] = [];
|
|
467
|
+
pt.on('data', (chunk: Buffer) => chunks.push(chunk.toString()));
|
|
468
|
+
const done = new Promise<void>((r) => pt.on('end', r));
|
|
469
|
+
pt.write('hello');
|
|
470
|
+
pt.write(' world');
|
|
471
|
+
pt.end();
|
|
472
|
+
await done;
|
|
473
|
+
expect(chunks.join('')).toBe('hello world');
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ===================== Duplex =====================
|
|
478
|
+
await describe('Duplex', async () => {
|
|
479
|
+
await it('should support both read and write', async () => {
|
|
480
|
+
let readCalled = false;
|
|
481
|
+
let writeCalled = false;
|
|
482
|
+
const duplex = new Duplex({
|
|
483
|
+
read() {
|
|
484
|
+
readCalled = true;
|
|
485
|
+
this.push('from-read');
|
|
486
|
+
this.push(null);
|
|
487
|
+
},
|
|
488
|
+
write(_chunk, _enc, cb) {
|
|
489
|
+
writeCalled = true;
|
|
490
|
+
cb();
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
// Write side
|
|
494
|
+
duplex.write('to-write');
|
|
495
|
+
duplex.end();
|
|
496
|
+
// Read side
|
|
497
|
+
const chunks: string[] = [];
|
|
498
|
+
for await (const chunk of duplex) {
|
|
499
|
+
chunks.push(String(chunk));
|
|
500
|
+
}
|
|
501
|
+
expect(readCalled).toBe(true);
|
|
502
|
+
expect(writeCalled).toBe(true);
|
|
503
|
+
expect(chunks).toContain('from-read');
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// ===================== Object mode =====================
|
|
508
|
+
await describe('stream objectMode', async () => {
|
|
509
|
+
await it('should pass objects through Transform', async () => {
|
|
510
|
+
const transform = new Transform({
|
|
511
|
+
objectMode: true,
|
|
512
|
+
transform(obj, _enc, cb) {
|
|
513
|
+
cb(null, { ...obj, transformed: true });
|
|
514
|
+
},
|
|
515
|
+
});
|
|
516
|
+
const results: any[] = [];
|
|
517
|
+
transform.on('data', (obj) => results.push(obj));
|
|
518
|
+
const done = new Promise<void>((r) => transform.on('end', r));
|
|
519
|
+
transform.write({ name: 'test' });
|
|
520
|
+
transform.end();
|
|
521
|
+
await done;
|
|
522
|
+
expect(results.length).toBe(1);
|
|
523
|
+
expect(results[0].name).toBe('test');
|
|
524
|
+
expect(results[0].transformed).toBe(true);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await it('should handle mixed types in objectMode Readable', async () => {
|
|
528
|
+
const items = [42, 'hello', { key: 'value' }, [1, 2, 3], true, null];
|
|
529
|
+
const readable = Readable.from(items.filter(x => x !== null));
|
|
530
|
+
const chunks: unknown[] = [];
|
|
531
|
+
for await (const chunk of readable) {
|
|
532
|
+
chunks.push(chunk);
|
|
533
|
+
}
|
|
534
|
+
expect(chunks.length).toBe(5); // null terminates, so 5 items
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ===================== Stream utility functions =====================
|
|
539
|
+
await describe('stream utility functions', async () => {
|
|
540
|
+
await it('Readable.isDisturbed should return false for undisturbed', async () => {
|
|
541
|
+
const readable = new Readable({ read() {} });
|
|
542
|
+
// isDisturbed may not exist on all platforms — test if available
|
|
543
|
+
if (typeof (Readable as any).isDisturbed === 'function') {
|
|
544
|
+
expect((Readable as any).isDisturbed(readable)).toBe(false);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// ===================== Async iteration =====================
|
|
550
|
+
await describe('stream async iteration', async () => {
|
|
551
|
+
await it('should support for-await-of on Readable', async () => {
|
|
552
|
+
let pushed = false;
|
|
553
|
+
const readable = new Readable({
|
|
554
|
+
read() {
|
|
555
|
+
if (!pushed) {
|
|
556
|
+
pushed = true;
|
|
557
|
+
this.push('chunk1');
|
|
558
|
+
this.push('chunk2');
|
|
559
|
+
this.push(null);
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
let content = '';
|
|
564
|
+
for await (const chunk of readable) {
|
|
565
|
+
content += chunk.toString();
|
|
566
|
+
}
|
|
567
|
+
expect(content).toBe('chunk1chunk2');
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
await it('should handle errors during async iteration', async () => {
|
|
571
|
+
const expectedError = new Error('iteration error');
|
|
572
|
+
let pushed = false;
|
|
573
|
+
const readable = new Readable({
|
|
574
|
+
read() {
|
|
575
|
+
if (!pushed) {
|
|
576
|
+
pushed = true;
|
|
577
|
+
this.push('data');
|
|
578
|
+
setTimeout(() => this.destroy(expectedError), 10);
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
let caught = false;
|
|
583
|
+
try {
|
|
584
|
+
for await (const _chunk of readable) {
|
|
585
|
+
// First chunk should arrive, then error
|
|
586
|
+
}
|
|
587
|
+
} catch {
|
|
588
|
+
caught = true;
|
|
589
|
+
}
|
|
590
|
+
expect(caught).toBe(true);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
};
|