@gjsify/stream 0.0.4 → 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 -6
- 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 -34693
- 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
package/src/index.spec.ts
CHANGED
|
@@ -1,20 +1,2252 @@
|
|
|
1
1
|
import { describe, it, expect } from '@gjsify/unit';
|
|
2
|
-
import
|
|
2
|
+
import Stream, {
|
|
3
|
+
Readable, Writable, Duplex, Transform, PassThrough,
|
|
4
|
+
pipeline, finished, addAbortSignal,
|
|
5
|
+
isReadable, isWritable,
|
|
6
|
+
getDefaultHighWaterMark, setDefaultHighWaterMark,
|
|
7
|
+
} from 'node:stream';
|
|
8
|
+
|
|
9
|
+
// These are exported from our implementation but not in @types/node's stream module,
|
|
10
|
+
// so we access them via the default export.
|
|
11
|
+
const { isDestroyed, isDisturbed, isErrored } = Stream as any;
|
|
12
|
+
|
|
13
|
+
// Ported from refs/node/test/parallel/test-stream-*.js
|
|
14
|
+
// Original: MIT license, Node.js contributors
|
|
3
15
|
|
|
4
16
|
export default async () => {
|
|
17
|
+
// ==================== Stream base ====================
|
|
5
18
|
|
|
6
|
-
await describe('
|
|
7
|
-
await it('should
|
|
19
|
+
await describe('Stream', async () => {
|
|
20
|
+
await it('should create an instance', async () => {
|
|
8
21
|
const stream = new Stream();
|
|
9
22
|
expect(stream).toBeDefined();
|
|
10
23
|
});
|
|
24
|
+
|
|
25
|
+
await it('should be an EventEmitter', async () => {
|
|
26
|
+
const stream = new Stream();
|
|
27
|
+
expect(typeof stream.on).toBe('function');
|
|
28
|
+
expect(typeof stream.emit).toBe('function');
|
|
29
|
+
expect(typeof stream.removeListener).toBe('function');
|
|
30
|
+
});
|
|
11
31
|
});
|
|
12
32
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
// ==================== Readable ====================
|
|
34
|
+
|
|
35
|
+
await describe('Readable: properties', async () => {
|
|
36
|
+
await it('should create an instance', async () => {
|
|
37
|
+
const readable = new Readable({ read() {} });
|
|
16
38
|
expect(readable).toBeDefined();
|
|
39
|
+
expect(readable.readable).toBeTruthy();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await it('should accept custom highWaterMark', async () => {
|
|
43
|
+
const readable = new Readable({ read() {}, highWaterMark: 42 });
|
|
44
|
+
expect(readable.readableHighWaterMark).toBe(42);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await it('should default readableFlowing to null', async () => {
|
|
48
|
+
const readable = new Readable({ read() {} });
|
|
49
|
+
expect(readable.readableFlowing).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await it('should default readableEnded to false', async () => {
|
|
53
|
+
const readable = new Readable({ read() {} });
|
|
54
|
+
expect(readable.readableEnded).toBeFalsy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await it('should default destroyed to false', async () => {
|
|
58
|
+
const readable = new Readable({ read() {} });
|
|
59
|
+
expect(readable.destroyed).toBeFalsy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await it('should support objectMode', async () => {
|
|
63
|
+
const readable = new Readable({ read() {}, objectMode: true });
|
|
64
|
+
expect(readable.readableObjectMode).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await it('should support setEncoding', async () => {
|
|
68
|
+
const readable = new Readable({ read() {} });
|
|
69
|
+
readable.setEncoding('utf8');
|
|
70
|
+
expect(readable.readableEncoding).toBe('utf8');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await it('should support pause/resume/isPaused', async () => {
|
|
74
|
+
const readable = new Readable({ read() {} });
|
|
75
|
+
readable.pause();
|
|
76
|
+
expect(readable.isPaused()).toBeTruthy();
|
|
77
|
+
readable.resume();
|
|
78
|
+
expect(readable.isPaused()).toBeFalsy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await it('should use getDefaultHighWaterMark when no hwm specified', async () => {
|
|
82
|
+
const readable = new Readable({ read() {} });
|
|
83
|
+
expect(readable.readableHighWaterMark).toBe(getDefaultHighWaterMark(false));
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await describe('Readable: data and end events', async () => {
|
|
88
|
+
await it('should emit data event on push', async () => {
|
|
89
|
+
const readable = new Readable({ read() {} });
|
|
90
|
+
const received = await new Promise<string>((resolve) => {
|
|
91
|
+
readable.on('data', (chunk) => resolve(String(chunk)));
|
|
92
|
+
readable.push('test');
|
|
93
|
+
});
|
|
94
|
+
expect(received).toBe('test');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await it('should emit end when all data consumed after push(null)', async () => {
|
|
98
|
+
const readable = new Readable({
|
|
99
|
+
read() {
|
|
100
|
+
this.push('data');
|
|
101
|
+
this.push(null);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const ended = await new Promise<boolean>((resolve) => {
|
|
105
|
+
readable.on('end', () => resolve(true));
|
|
106
|
+
readable.resume();
|
|
107
|
+
});
|
|
108
|
+
expect(ended).toBeTruthy();
|
|
109
|
+
expect(readable.readableEnded).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await it('push(null) should return false', async () => {
|
|
113
|
+
const readable = new Readable({ read() {} });
|
|
114
|
+
const result = readable.push(null);
|
|
115
|
+
expect(result).toBeFalsy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await it('should emit multiple data events', async () => {
|
|
119
|
+
const readable = new Readable({
|
|
120
|
+
read() {
|
|
121
|
+
this.push('a');
|
|
122
|
+
this.push('b');
|
|
123
|
+
this.push('c');
|
|
124
|
+
this.push(null);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
const chunks: string[] = [];
|
|
128
|
+
await new Promise<void>((resolve) => {
|
|
129
|
+
readable.on('data', (chunk) => chunks.push(String(chunk)));
|
|
130
|
+
readable.on('end', () => resolve());
|
|
131
|
+
});
|
|
132
|
+
expect(chunks.length).toBe(3);
|
|
133
|
+
expect(chunks[0]).toBe('a');
|
|
134
|
+
expect(chunks[1]).toBe('b');
|
|
135
|
+
expect(chunks[2]).toBe('c');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await describe('Readable: destroy', async () => {
|
|
140
|
+
await it('should emit close on destroy', async () => {
|
|
141
|
+
const readable = new Readable({ read() {} });
|
|
142
|
+
const closed = await new Promise<boolean>((resolve) => {
|
|
143
|
+
readable.on('close', () => resolve(true));
|
|
144
|
+
readable.destroy();
|
|
145
|
+
});
|
|
146
|
+
expect(closed).toBeTruthy();
|
|
147
|
+
expect(readable.destroyed).toBeTruthy();
|
|
148
|
+
expect(readable.readable).toBeFalsy();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await it('should emit error on destroy with error', async () => {
|
|
152
|
+
const readable = new Readable({ read() {} });
|
|
153
|
+
const err = await new Promise<Error>((resolve) => {
|
|
154
|
+
readable.on('error', (e) => resolve(e));
|
|
155
|
+
readable.destroy(new Error('test error'));
|
|
156
|
+
});
|
|
157
|
+
expect(err.message).toBe('test error');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await it('should be idempotent on double destroy', async () => {
|
|
161
|
+
const readable = new Readable({ read() {} });
|
|
162
|
+
let closeCount = 0;
|
|
163
|
+
readable.on('close', () => { closeCount++; });
|
|
164
|
+
await new Promise<void>((resolve) => {
|
|
165
|
+
readable.on('close', () => resolve());
|
|
166
|
+
readable.destroy();
|
|
167
|
+
});
|
|
168
|
+
readable.destroy();
|
|
169
|
+
// Give time for any spurious second close
|
|
170
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
171
|
+
expect(closeCount).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await it('should set readableAborted when destroyed before end', async () => {
|
|
175
|
+
const readable = new Readable({ read() {} });
|
|
176
|
+
readable.push('data');
|
|
177
|
+
await new Promise<void>((resolve) => {
|
|
178
|
+
readable.on('close', () => resolve());
|
|
179
|
+
readable.destroy();
|
|
180
|
+
});
|
|
181
|
+
expect(readable.readableAborted).toBeTruthy();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await describe('Readable: _construct', async () => {
|
|
186
|
+
await it('should call _construct before first read', async () => {
|
|
187
|
+
let constructCalled = false;
|
|
188
|
+
const readable = new Readable({
|
|
189
|
+
construct(callback) {
|
|
190
|
+
constructCalled = true;
|
|
191
|
+
callback();
|
|
192
|
+
},
|
|
193
|
+
read() {
|
|
194
|
+
this.push('data');
|
|
195
|
+
this.push(null);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// construct is async, should be called on next tick
|
|
199
|
+
expect(constructCalled).toBeFalsy();
|
|
200
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
201
|
+
expect(constructCalled).toBeTruthy();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await it('should delay flowing until construct completes', async () => {
|
|
205
|
+
const chunks: string[] = [];
|
|
206
|
+
const readable = new Readable({
|
|
207
|
+
construct(callback) {
|
|
208
|
+
setTimeout(() => callback(), 20);
|
|
209
|
+
},
|
|
210
|
+
read() {
|
|
211
|
+
this.push('hello');
|
|
212
|
+
this.push(null);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
readable.on('data', (chunk) => chunks.push(String(chunk)));
|
|
216
|
+
// Data should not flow yet
|
|
217
|
+
expect(chunks.length).toBe(0);
|
|
218
|
+
await new Promise<void>((resolve) => {
|
|
219
|
+
readable.on('end', () => resolve());
|
|
220
|
+
});
|
|
221
|
+
expect(chunks.length).toBe(1);
|
|
222
|
+
expect(chunks[0]).toBe('hello');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await it('should destroy stream if construct errors', async () => {
|
|
226
|
+
const readable = new Readable({
|
|
227
|
+
construct(callback) {
|
|
228
|
+
callback(new Error('construct failed'));
|
|
229
|
+
},
|
|
230
|
+
read() {}
|
|
231
|
+
});
|
|
232
|
+
const err = await new Promise<Error>((resolve) => {
|
|
233
|
+
readable.on('error', (e) => resolve(e));
|
|
234
|
+
});
|
|
235
|
+
expect(err.message).toBe('construct failed');
|
|
236
|
+
expect(readable.destroyed).toBeTruthy();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await describe('Readable.from', async () => {
|
|
241
|
+
await it('should create readable from array', async () => {
|
|
242
|
+
const readable = Readable.from(['a', 'b', 'c']);
|
|
243
|
+
const chunks: unknown[] = [];
|
|
244
|
+
for await (const chunk of readable) {
|
|
245
|
+
chunks.push(chunk);
|
|
246
|
+
}
|
|
247
|
+
expect(chunks.length).toBe(3);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await it('should create readable from async generator', async () => {
|
|
251
|
+
async function* gen() {
|
|
252
|
+
yield 1;
|
|
253
|
+
yield 2;
|
|
254
|
+
yield 3;
|
|
255
|
+
}
|
|
256
|
+
const readable = Readable.from(gen());
|
|
257
|
+
const chunks: unknown[] = [];
|
|
258
|
+
for await (const chunk of readable) {
|
|
259
|
+
chunks.push(chunk);
|
|
260
|
+
}
|
|
261
|
+
expect(chunks.length).toBe(3);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await describe('Readable: async iterator', async () => {
|
|
266
|
+
await it('should iterate over pushed chunks', async () => {
|
|
267
|
+
let i = 0;
|
|
268
|
+
const data = ['chunk1', 'chunk2', null];
|
|
269
|
+
const readable = new Readable({
|
|
270
|
+
read() {
|
|
271
|
+
this.push(data[i++] ?? null);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
const chunks: unknown[] = [];
|
|
275
|
+
for await (const chunk of readable) {
|
|
276
|
+
chunks.push(chunk);
|
|
277
|
+
}
|
|
278
|
+
expect(chunks.length > 0).toBeTruthy();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await describe('Readable: unshift', async () => {
|
|
283
|
+
await it('should put data back at the front of the buffer', async () => {
|
|
284
|
+
const readable = new Readable({ read() {} });
|
|
285
|
+
readable.push('second');
|
|
286
|
+
readable.unshift('first');
|
|
287
|
+
const chunks: string[] = [];
|
|
288
|
+
readable.on('data', (chunk) => chunks.push(String(chunk)));
|
|
289
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
290
|
+
expect(chunks[0]).toBe('first');
|
|
291
|
+
expect(chunks[1]).toBe('second');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await describe('Readable: unpipe', async () => {
|
|
296
|
+
await it('should remove specific piped destination', async () => {
|
|
297
|
+
const readable = new Readable({ read() {} });
|
|
298
|
+
const chunks1: string[] = [];
|
|
299
|
+
const chunks2: string[] = [];
|
|
300
|
+
const w1 = new Writable({ write(chunk, _enc, cb) { chunks1.push(String(chunk)); cb(); } });
|
|
301
|
+
const w2 = new Writable({ write(chunk, _enc, cb) { chunks2.push(String(chunk)); cb(); } });
|
|
302
|
+
|
|
303
|
+
readable.pipe(w1);
|
|
304
|
+
readable.pipe(w2);
|
|
305
|
+
readable.unpipe(w1);
|
|
306
|
+
|
|
307
|
+
readable.push('data');
|
|
308
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
309
|
+
expect(chunks1.length).toBe(0);
|
|
310
|
+
expect(chunks2.length).toBe(1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await it('should emit unpipe event on destination', async () => {
|
|
314
|
+
const readable = new Readable({ read() {} });
|
|
315
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
316
|
+
readable.pipe(writable);
|
|
317
|
+
|
|
318
|
+
const unpiped = await new Promise<boolean>((resolve) => {
|
|
319
|
+
writable.on('unpipe', () => resolve(true));
|
|
320
|
+
readable.unpipe(writable);
|
|
321
|
+
});
|
|
322
|
+
expect(unpiped).toBeTruthy();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await it('should remove all destinations when called without args', async () => {
|
|
326
|
+
const readable = new Readable({ read() {} });
|
|
327
|
+
const w1 = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
328
|
+
const w2 = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
329
|
+
|
|
330
|
+
readable.pipe(w1);
|
|
331
|
+
readable.pipe(w2);
|
|
332
|
+
|
|
333
|
+
let unpipeCount = 0;
|
|
334
|
+
w1.on('unpipe', () => unpipeCount++);
|
|
335
|
+
w2.on('unpipe', () => unpipeCount++);
|
|
336
|
+
readable.unpipe();
|
|
337
|
+
|
|
338
|
+
expect(unpipeCount).toBe(2);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// ==================== Writable ====================
|
|
343
|
+
|
|
344
|
+
await describe('Writable: properties', async () => {
|
|
345
|
+
await it('should create an instance', async () => {
|
|
346
|
+
const writable = new Writable({
|
|
347
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
348
|
+
});
|
|
349
|
+
expect(writable).toBeDefined();
|
|
350
|
+
expect(writable.writable).toBeTruthy();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await it('should default writableEnded to false', async () => {
|
|
354
|
+
const writable = new Writable({
|
|
355
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
356
|
+
});
|
|
357
|
+
expect(writable.writableEnded).toBeFalsy();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
await it('should default destroyed to false', async () => {
|
|
361
|
+
const writable = new Writable({
|
|
362
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
363
|
+
});
|
|
364
|
+
expect(writable.destroyed).toBeFalsy();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await it('should expose writableCorked', async () => {
|
|
368
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
369
|
+
expect(writable.writableCorked).toBe(0);
|
|
370
|
+
writable.cork();
|
|
371
|
+
expect(writable.writableCorked).toBe(1);
|
|
372
|
+
writable.uncork();
|
|
373
|
+
expect(writable.writableCorked).toBe(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
await it('should use getDefaultHighWaterMark when no hwm specified', async () => {
|
|
377
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
378
|
+
expect(writable.writableHighWaterMark).toBe(getDefaultHighWaterMark(false));
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
await describe('Writable: write and end', async () => {
|
|
383
|
+
await it('should call _write with chunk', async () => {
|
|
384
|
+
const chunks: string[] = [];
|
|
385
|
+
const writable = new Writable({
|
|
386
|
+
write(chunk, _encoding, callback) {
|
|
387
|
+
chunks.push(String(chunk));
|
|
388
|
+
callback();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
writable.write('hello');
|
|
392
|
+
// _write is called synchronously, but drain/callback is async
|
|
393
|
+
expect(chunks.length).toBe(1);
|
|
394
|
+
expect(chunks[0]).toBe('hello');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await it('should emit finish on end()', async () => {
|
|
398
|
+
const writable = new Writable({
|
|
399
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
400
|
+
});
|
|
401
|
+
const finished = await new Promise<boolean>((resolve) => {
|
|
402
|
+
writable.on('finish', () => resolve(true));
|
|
403
|
+
writable.end();
|
|
404
|
+
});
|
|
405
|
+
expect(finished).toBeTruthy();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
await it('should set writableEnded after end()', async () => {
|
|
409
|
+
const writable = new Writable({
|
|
410
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
411
|
+
});
|
|
412
|
+
writable.end();
|
|
413
|
+
expect(writable.writableEnded).toBeTruthy();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
await it('should set writableFinished after finish event', async () => {
|
|
417
|
+
const writable = new Writable({
|
|
418
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
419
|
+
});
|
|
420
|
+
await new Promise<void>((resolve) => {
|
|
421
|
+
writable.on('finish', () => resolve());
|
|
422
|
+
writable.end();
|
|
423
|
+
});
|
|
424
|
+
expect(writable.writableFinished).toBeTruthy();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await it('should write chunk passed to end()', async () => {
|
|
428
|
+
const chunks: string[] = [];
|
|
429
|
+
const writable = new Writable({
|
|
430
|
+
write(chunk, _encoding, callback) {
|
|
431
|
+
chunks.push(String(chunk));
|
|
432
|
+
callback();
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
await new Promise<void>((resolve) => {
|
|
436
|
+
writable.on('finish', () => resolve());
|
|
437
|
+
writable.end('final');
|
|
438
|
+
});
|
|
439
|
+
expect(chunks.length).toBe(1);
|
|
440
|
+
expect(chunks[0]).toBe('final');
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await it('should support cork and uncork', async () => {
|
|
444
|
+
const writable = new Writable({
|
|
445
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
446
|
+
});
|
|
447
|
+
writable.cork();
|
|
448
|
+
writable.uncork();
|
|
449
|
+
expect(true).toBeTruthy();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await it('should invoke write callback', async () => {
|
|
453
|
+
const writable = new Writable({
|
|
454
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
455
|
+
});
|
|
456
|
+
const called = await new Promise<boolean>((resolve) => {
|
|
457
|
+
writable.write('data', () => resolve(true));
|
|
458
|
+
});
|
|
459
|
+
expect(called).toBeTruthy();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await describe('Writable: _construct', async () => {
|
|
464
|
+
await it('should call _construct before first write', async () => {
|
|
465
|
+
let constructCalled = false;
|
|
466
|
+
const chunks: string[] = [];
|
|
467
|
+
const writable = new Writable({
|
|
468
|
+
construct(callback) {
|
|
469
|
+
constructCalled = true;
|
|
470
|
+
callback();
|
|
471
|
+
},
|
|
472
|
+
write(chunk, _enc, callback) {
|
|
473
|
+
chunks.push(String(chunk));
|
|
474
|
+
callback();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
expect(constructCalled).toBeFalsy();
|
|
478
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
479
|
+
expect(constructCalled).toBeTruthy();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
await it('should destroy stream if construct errors', async () => {
|
|
483
|
+
const writable = new Writable({
|
|
484
|
+
construct(callback) {
|
|
485
|
+
callback(new Error('construct failed'));
|
|
486
|
+
},
|
|
487
|
+
write(_c, _e, cb) { cb(); }
|
|
488
|
+
});
|
|
489
|
+
const err = await new Promise<Error>((resolve) => {
|
|
490
|
+
writable.on('error', (e) => resolve(e));
|
|
491
|
+
});
|
|
492
|
+
expect(err.message).toBe('construct failed');
|
|
493
|
+
expect(writable.destroyed).toBeTruthy();
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await describe('Writable: cork/uncork buffering', async () => {
|
|
498
|
+
await it('should buffer writes while corked', async () => {
|
|
499
|
+
const chunks: string[] = [];
|
|
500
|
+
const writable = new Writable({
|
|
501
|
+
write(chunk, _enc, callback) {
|
|
502
|
+
chunks.push(String(chunk));
|
|
503
|
+
callback();
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
writable.cork();
|
|
507
|
+
writable.write('a');
|
|
508
|
+
writable.write('b');
|
|
509
|
+
// Writes should be buffered, not yet passed to _write
|
|
510
|
+
expect(chunks.length).toBe(0);
|
|
511
|
+
writable.uncork();
|
|
512
|
+
// After uncork, buffered writes should flush
|
|
513
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
514
|
+
expect(chunks.length).toBe(2);
|
|
515
|
+
expect(chunks[0]).toBe('a');
|
|
516
|
+
expect(chunks[1]).toBe('b');
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await it('should support nested cork/uncork', async () => {
|
|
520
|
+
const chunks: string[] = [];
|
|
521
|
+
const writable = new Writable({
|
|
522
|
+
write(chunk, _enc, callback) {
|
|
523
|
+
chunks.push(String(chunk));
|
|
524
|
+
callback();
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
writable.cork();
|
|
528
|
+
writable.cork();
|
|
529
|
+
writable.write('x');
|
|
530
|
+
writable.uncork();
|
|
531
|
+
// Still corked (count = 1)
|
|
532
|
+
expect(chunks.length).toBe(0);
|
|
533
|
+
writable.uncork();
|
|
534
|
+
// Now fully uncorked
|
|
535
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
536
|
+
expect(chunks.length).toBe(1);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await it('should use _writev for batched writes when available', async () => {
|
|
540
|
+
let writevCalled = false;
|
|
541
|
+
let batchSize = 0;
|
|
542
|
+
const writable = new Writable({
|
|
543
|
+
write(_c, _e, cb) { cb(); },
|
|
544
|
+
writev(chunks, cb) {
|
|
545
|
+
writevCalled = true;
|
|
546
|
+
batchSize = chunks.length;
|
|
547
|
+
cb();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
writable.cork();
|
|
551
|
+
writable.write('a');
|
|
552
|
+
writable.write('b');
|
|
553
|
+
writable.write('c');
|
|
554
|
+
writable.uncork();
|
|
555
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
556
|
+
expect(writevCalled).toBeTruthy();
|
|
557
|
+
expect(batchSize).toBe(3);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await describe('Writable: destroy', async () => {
|
|
562
|
+
await it('should emit close on destroy', async () => {
|
|
563
|
+
const writable = new Writable({
|
|
564
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
565
|
+
});
|
|
566
|
+
const closed = await new Promise<boolean>((resolve) => {
|
|
567
|
+
writable.on('close', () => resolve(true));
|
|
568
|
+
writable.destroy();
|
|
569
|
+
});
|
|
570
|
+
expect(closed).toBeTruthy();
|
|
571
|
+
expect(writable.destroyed).toBeTruthy();
|
|
572
|
+
expect(writable.writable).toBeFalsy();
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
await describe('Writable: write-after-end', async () => {
|
|
577
|
+
await it('should error on write after end', async () => {
|
|
578
|
+
const writable = new Writable({
|
|
579
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
580
|
+
});
|
|
581
|
+
writable.end();
|
|
582
|
+
const errorEmitted = await new Promise<boolean>((resolve) => {
|
|
583
|
+
writable.on('error', () => resolve(true));
|
|
584
|
+
writable.write('after-end');
|
|
585
|
+
});
|
|
586
|
+
expect(errorEmitted).toBeTruthy();
|
|
17
587
|
});
|
|
18
588
|
});
|
|
19
589
|
|
|
20
|
-
|
|
590
|
+
await describe('Writable: _final', async () => {
|
|
591
|
+
await it('should call _final on end', async () => {
|
|
592
|
+
let finalCalled = false;
|
|
593
|
+
const writable = new Writable({
|
|
594
|
+
write(_c, _e, cb) { cb(); },
|
|
595
|
+
final(cb) {
|
|
596
|
+
finalCalled = true;
|
|
597
|
+
cb();
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
await new Promise<void>((resolve) => {
|
|
601
|
+
writable.on('finish', () => resolve());
|
|
602
|
+
writable.end();
|
|
603
|
+
});
|
|
604
|
+
expect(finalCalled).toBeTruthy();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
await it('should emit error if _final fails', async () => {
|
|
608
|
+
const writable = new Writable({
|
|
609
|
+
write(_c, _e, cb) { cb(); },
|
|
610
|
+
final(cb) {
|
|
611
|
+
cb(new Error('final error'));
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
const err = await new Promise<Error>((resolve) => {
|
|
615
|
+
writable.on('error', (e) => resolve(e));
|
|
616
|
+
writable.end();
|
|
617
|
+
});
|
|
618
|
+
expect(err.message).toBe('final error');
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// ==================== Duplex ====================
|
|
623
|
+
|
|
624
|
+
await describe('Duplex', async () => {
|
|
625
|
+
await it('should create an instance', async () => {
|
|
626
|
+
const duplex = new Duplex({
|
|
627
|
+
read() {},
|
|
628
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
629
|
+
});
|
|
630
|
+
expect(duplex).toBeDefined();
|
|
631
|
+
expect(duplex.readable).toBeTruthy();
|
|
632
|
+
expect(duplex.writable).toBeTruthy();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
await it('should accept writes', async () => {
|
|
636
|
+
const received: string[] = [];
|
|
637
|
+
const duplex = new Duplex({
|
|
638
|
+
read() {},
|
|
639
|
+
write(chunk, _encoding, callback) {
|
|
640
|
+
received.push(String(chunk));
|
|
641
|
+
callback();
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
duplex.write('data');
|
|
645
|
+
expect(received.length).toBe(1);
|
|
646
|
+
expect(received[0]).toBe('data');
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await it('should emit finish on end()', async () => {
|
|
650
|
+
const duplex = new Duplex({
|
|
651
|
+
read() {},
|
|
652
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
653
|
+
});
|
|
654
|
+
const finished = await new Promise<boolean>((resolve) => {
|
|
655
|
+
duplex.on('finish', () => resolve(true));
|
|
656
|
+
duplex.end();
|
|
657
|
+
});
|
|
658
|
+
expect(finished).toBeTruthy();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
await it('should support cork/uncork', async () => {
|
|
662
|
+
const chunks: string[] = [];
|
|
663
|
+
const duplex = new Duplex({
|
|
664
|
+
read() {},
|
|
665
|
+
write(chunk, _enc, callback) {
|
|
666
|
+
chunks.push(String(chunk));
|
|
667
|
+
callback();
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
duplex.cork();
|
|
671
|
+
duplex.write('a');
|
|
672
|
+
duplex.write('b');
|
|
673
|
+
expect(chunks.length).toBe(0);
|
|
674
|
+
duplex.uncork();
|
|
675
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
676
|
+
expect(chunks.length).toBe(2);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await it('should expose writableCorked', async () => {
|
|
680
|
+
const duplex = new Duplex({ read() {}, write(_c, _e, cb) { cb(); } });
|
|
681
|
+
expect(duplex.writableCorked).toBe(0);
|
|
682
|
+
duplex.cork();
|
|
683
|
+
expect(duplex.writableCorked).toBe(1);
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// ==================== Transform ====================
|
|
688
|
+
|
|
689
|
+
await describe('Transform', async () => {
|
|
690
|
+
await it('should create an instance', async () => {
|
|
691
|
+
const transform = new Transform({
|
|
692
|
+
transform(chunk, _encoding, callback) { callback(null, chunk); }
|
|
693
|
+
});
|
|
694
|
+
expect(transform).toBeDefined();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
await it('should transform data through _transform', async () => {
|
|
698
|
+
const transform = new Transform({
|
|
699
|
+
transform(chunk, _encoding, callback) {
|
|
700
|
+
callback(null, String(chunk).toUpperCase());
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
const output = await new Promise<string>((resolve) => {
|
|
704
|
+
transform.on('data', (chunk) => resolve(String(chunk)));
|
|
705
|
+
transform.write('hello');
|
|
706
|
+
});
|
|
707
|
+
expect(output).toBe('HELLO');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
await it('should call _flush on end', async () => {
|
|
711
|
+
let flushed = false;
|
|
712
|
+
const transform = new Transform({
|
|
713
|
+
transform(chunk, _encoding, callback) { callback(null, chunk); },
|
|
714
|
+
flush(callback) {
|
|
715
|
+
flushed = true;
|
|
716
|
+
callback();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
transform.on('data', () => {}); // consume
|
|
720
|
+
await new Promise<void>((resolve) => {
|
|
721
|
+
transform.on('finish', () => resolve());
|
|
722
|
+
transform.end();
|
|
723
|
+
});
|
|
724
|
+
expect(flushed).toBeTruthy();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
await it('should emit finish after flush', async () => {
|
|
728
|
+
const transform = new Transform({
|
|
729
|
+
transform(chunk, _encoding, callback) { callback(null, chunk); }
|
|
730
|
+
});
|
|
731
|
+
transform.on('data', () => {});
|
|
732
|
+
const finished = await new Promise<boolean>((resolve) => {
|
|
733
|
+
transform.on('finish', () => resolve(true));
|
|
734
|
+
transform.end('last');
|
|
735
|
+
});
|
|
736
|
+
expect(finished).toBeTruthy();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
await it('should propagate transform errors', async () => {
|
|
740
|
+
const transform = new Transform({
|
|
741
|
+
transform(_chunk, _encoding, callback) {
|
|
742
|
+
callback(new Error('transform error'));
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
const err = await new Promise<Error>((resolve) => {
|
|
746
|
+
transform.on('error', (e) => resolve(e));
|
|
747
|
+
transform.write('data');
|
|
748
|
+
});
|
|
749
|
+
expect(err.message).toBe('transform error');
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
await it('should flush extra data on end', async () => {
|
|
753
|
+
const chunks: string[] = [];
|
|
754
|
+
const transform = new Transform({
|
|
755
|
+
transform(chunk, _encoding, callback) {
|
|
756
|
+
callback(null, chunk);
|
|
757
|
+
},
|
|
758
|
+
flush(callback) {
|
|
759
|
+
this.push('flushed');
|
|
760
|
+
callback();
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
transform.on('data', (chunk) => chunks.push(String(chunk)));
|
|
764
|
+
await new Promise<void>((resolve) => {
|
|
765
|
+
transform.on('end', () => resolve());
|
|
766
|
+
transform.write('regular');
|
|
767
|
+
transform.end();
|
|
768
|
+
});
|
|
769
|
+
expect(chunks).toContain('regular');
|
|
770
|
+
expect(chunks).toContain('flushed');
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// ==================== PassThrough ====================
|
|
775
|
+
|
|
776
|
+
await describe('PassThrough', async () => {
|
|
777
|
+
await it('should create an instance', async () => {
|
|
778
|
+
const pt = new PassThrough();
|
|
779
|
+
expect(pt).toBeDefined();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
await it('should pass data through unchanged', async () => {
|
|
783
|
+
const pt = new PassThrough();
|
|
784
|
+
const output = await new Promise<string>((resolve) => {
|
|
785
|
+
pt.on('data', (chunk) => resolve(String(chunk)));
|
|
786
|
+
pt.write('unchanged');
|
|
787
|
+
});
|
|
788
|
+
expect(output).toBe('unchanged');
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
await it('should emit finish on end', async () => {
|
|
792
|
+
const pt = new PassThrough();
|
|
793
|
+
pt.on('data', () => {}); // consume
|
|
794
|
+
const finished = await new Promise<boolean>((resolve) => {
|
|
795
|
+
pt.on('finish', () => resolve(true));
|
|
796
|
+
pt.end();
|
|
797
|
+
});
|
|
798
|
+
expect(finished).toBeTruthy();
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
await it('should support multiple writes', async () => {
|
|
802
|
+
const pt = new PassThrough();
|
|
803
|
+
const chunks: string[] = [];
|
|
804
|
+
pt.on('data', (chunk) => chunks.push(String(chunk)));
|
|
805
|
+
pt.write('a');
|
|
806
|
+
pt.write('b');
|
|
807
|
+
pt.write('c');
|
|
808
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
809
|
+
expect(chunks.length).toBe(3);
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
// ==================== pipe ====================
|
|
814
|
+
|
|
815
|
+
await describe('Stream.pipe', async () => {
|
|
816
|
+
await it('should emit pipe event on destination', async () => {
|
|
817
|
+
const readable = new Readable({ read() {} });
|
|
818
|
+
const writable = new Writable({
|
|
819
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
820
|
+
});
|
|
821
|
+
const pipeSource = await new Promise<unknown>((resolve) => {
|
|
822
|
+
writable.on('pipe', (src) => resolve(src));
|
|
823
|
+
readable.pipe(writable);
|
|
824
|
+
});
|
|
825
|
+
expect(pipeSource).toBe(readable);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
await it('should return the destination', async () => {
|
|
829
|
+
const readable = new Readable({ read() {} });
|
|
830
|
+
const writable = new Writable({
|
|
831
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
832
|
+
});
|
|
833
|
+
const result = readable.pipe(writable);
|
|
834
|
+
expect(result === writable).toBeTruthy();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
await it('should pipe data and finish', async () => {
|
|
838
|
+
const chunks: string[] = [];
|
|
839
|
+
const readable = new Readable({
|
|
840
|
+
read() {
|
|
841
|
+
this.push('piped');
|
|
842
|
+
this.push(null);
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
const writable = new Writable({
|
|
846
|
+
write(chunk, _encoding, callback) {
|
|
847
|
+
chunks.push(String(chunk));
|
|
848
|
+
callback();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
await new Promise<void>((resolve) => {
|
|
852
|
+
writable.on('finish', () => resolve());
|
|
853
|
+
readable.pipe(writable);
|
|
854
|
+
});
|
|
855
|
+
expect(chunks.length).toBe(1);
|
|
856
|
+
expect(chunks[0]).toBe('piped');
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
await it('should not end destination when end: false', async () => {
|
|
860
|
+
const readable = new Readable({
|
|
861
|
+
read() {
|
|
862
|
+
this.push('data');
|
|
863
|
+
this.push(null);
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
const writable = new Writable({
|
|
867
|
+
write(_c, _e, cb) { cb(); }
|
|
868
|
+
});
|
|
869
|
+
readable.pipe(writable, { end: false });
|
|
870
|
+
await new Promise<void>((resolve) => {
|
|
871
|
+
readable.on('end', () => resolve());
|
|
872
|
+
readable.resume();
|
|
873
|
+
});
|
|
874
|
+
// Writable should NOT be ended
|
|
875
|
+
expect(writable.writableEnded).toBeFalsy();
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
// ==================== pipeline ====================
|
|
880
|
+
|
|
881
|
+
await describe('pipeline', async () => {
|
|
882
|
+
await it('should pipe through a transform', async () => {
|
|
883
|
+
const readable = new Readable({
|
|
884
|
+
read() {
|
|
885
|
+
this.push('hello');
|
|
886
|
+
this.push(null);
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
const transform = new Transform({
|
|
890
|
+
transform(chunk, _encoding, callback) {
|
|
891
|
+
callback(null, String(chunk).toUpperCase());
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
const output: string[] = [];
|
|
895
|
+
const writable = new Writable({
|
|
896
|
+
write(chunk, _encoding, callback) {
|
|
897
|
+
output.push(String(chunk));
|
|
898
|
+
callback();
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
await new Promise<void>((resolve, reject) => {
|
|
903
|
+
pipeline(readable, transform, writable, (err) => {
|
|
904
|
+
if (err) reject(err);
|
|
905
|
+
else resolve();
|
|
906
|
+
});
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
expect(output.length).toBe(1);
|
|
910
|
+
expect(output[0]).toBe('HELLO');
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
await it('should throw with less than 2 streams', async () => {
|
|
914
|
+
expect(() => {
|
|
915
|
+
(pipeline as any)(new Readable({ read() {} }), () => {});
|
|
916
|
+
}).toThrow();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
await it('should callback with error on stream error', async () => {
|
|
920
|
+
const readable = new Readable({
|
|
921
|
+
read() {
|
|
922
|
+
this.destroy(new Error('read error'));
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
926
|
+
|
|
927
|
+
const err = await new Promise<Error>((resolve) => {
|
|
928
|
+
pipeline(readable, writable, (e) => {
|
|
929
|
+
if (e) resolve(e);
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
expect(err.message).toBe('read error');
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// ==================== finished ====================
|
|
937
|
+
|
|
938
|
+
await describe('finished', async () => {
|
|
939
|
+
await it('should callback on writable finish', async () => {
|
|
940
|
+
const writable = new Writable({
|
|
941
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
942
|
+
});
|
|
943
|
+
const done = await new Promise<boolean>((resolve) => {
|
|
944
|
+
finished(writable, () => resolve(true));
|
|
945
|
+
writable.end();
|
|
946
|
+
});
|
|
947
|
+
expect(done).toBeTruthy();
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
await it('should callback on readable end', async () => {
|
|
951
|
+
const readable = new Readable({
|
|
952
|
+
read() {
|
|
953
|
+
this.push(null);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
const done = await new Promise<boolean>((resolve) => {
|
|
957
|
+
finished(readable, () => resolve(true));
|
|
958
|
+
readable.resume();
|
|
959
|
+
});
|
|
960
|
+
expect(done).toBeTruthy();
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
await it('should return cleanup function', async () => {
|
|
964
|
+
const writable = new Writable({
|
|
965
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
966
|
+
});
|
|
967
|
+
const cleanup = finished(writable, () => {});
|
|
968
|
+
expect(typeof cleanup).toBe('function');
|
|
969
|
+
cleanup();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
await it('should callback with error on stream error', async () => {
|
|
973
|
+
const writable = new Writable({
|
|
974
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
975
|
+
});
|
|
976
|
+
const err = await new Promise<Error>((resolve) => {
|
|
977
|
+
finished(writable, (e) => {
|
|
978
|
+
if (e) resolve(e);
|
|
979
|
+
});
|
|
980
|
+
writable.emit('error', new Error('stream error'));
|
|
981
|
+
});
|
|
982
|
+
expect(err.message).toBe('stream error');
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
await it('should detect premature close', async () => {
|
|
986
|
+
const writable = new Writable({
|
|
987
|
+
write(_chunk, _encoding, callback) { callback(); }
|
|
988
|
+
});
|
|
989
|
+
const err = await new Promise<Error>((resolve) => {
|
|
990
|
+
finished(writable, (e) => {
|
|
991
|
+
if (e) resolve(e);
|
|
992
|
+
});
|
|
993
|
+
writable.destroy();
|
|
994
|
+
});
|
|
995
|
+
expect(err.message.toLowerCase()).toBe('premature close');
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// ==================== addAbortSignal ====================
|
|
1000
|
+
await describe('addAbortSignal', async () => {
|
|
1001
|
+
await it('should destroy stream when signal aborts', async () => {
|
|
1002
|
+
const ac = new AbortController();
|
|
1003
|
+
const readable = new Readable({ read() {} });
|
|
1004
|
+
addAbortSignal(ac.signal, readable);
|
|
1005
|
+
|
|
1006
|
+
const errPromise = new Promise<Error>((resolve) => {
|
|
1007
|
+
readable.on('error', (e) => resolve(e));
|
|
1008
|
+
});
|
|
1009
|
+
ac.abort();
|
|
1010
|
+
const err = await errPromise;
|
|
1011
|
+
expect(err.message.toLowerCase().includes('aborted')).toBeTruthy();
|
|
1012
|
+
expect(readable.destroyed).toBeTruthy();
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
await it('should destroy immediately if signal already aborted', async () => {
|
|
1016
|
+
const ac = new AbortController();
|
|
1017
|
+
ac.abort();
|
|
1018
|
+
const readable = new Readable({ read() {} });
|
|
1019
|
+
// Must listen for error to prevent unhandled error crash on Node.js
|
|
1020
|
+
readable.on('error', () => {});
|
|
1021
|
+
addAbortSignal(ac.signal, readable);
|
|
1022
|
+
|
|
1023
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
1024
|
+
expect(readable.destroyed).toBeTruthy();
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
await it('should throw if first arg is not AbortSignal', async () => {
|
|
1028
|
+
expect(() => {
|
|
1029
|
+
addAbortSignal({} as any, new Readable({ read() {} }));
|
|
1030
|
+
}).toThrow();
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
await it('should throw if second arg is not a Stream', async () => {
|
|
1034
|
+
const ac = new AbortController();
|
|
1035
|
+
expect(() => {
|
|
1036
|
+
addAbortSignal(ac.signal, {} as any);
|
|
1037
|
+
}).toThrow();
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
await it('should work with Writable', async () => {
|
|
1041
|
+
const ac = new AbortController();
|
|
1042
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1043
|
+
addAbortSignal(ac.signal, writable);
|
|
1044
|
+
ac.abort();
|
|
1045
|
+
|
|
1046
|
+
await new Promise<void>((resolve) => {
|
|
1047
|
+
writable.on('close', () => resolve());
|
|
1048
|
+
});
|
|
1049
|
+
expect(writable.destroyed).toBeTruthy();
|
|
1050
|
+
});
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// ==================== Utility functions ====================
|
|
1054
|
+
|
|
1055
|
+
await describe('isReadable', async () => {
|
|
1056
|
+
await it('should return true for a readable stream', async () => {
|
|
1057
|
+
const readable = new Readable({ read() {} });
|
|
1058
|
+
expect(isReadable(readable)).toBeTruthy();
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
await it('should return false for a destroyed stream', async () => {
|
|
1062
|
+
const readable = new Readable({ read() {} });
|
|
1063
|
+
readable.destroy();
|
|
1064
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
1065
|
+
expect(isReadable(readable)).toBeFalsy();
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
await it('should return false for an ended stream', async () => {
|
|
1069
|
+
const readable = new Readable({ read() { this.push(null); } });
|
|
1070
|
+
readable.resume();
|
|
1071
|
+
await new Promise<void>((resolve) => readable.on('end', () => resolve()));
|
|
1072
|
+
expect(isReadable(readable)).toBeFalsy();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
await it('should return false for null', async () => {
|
|
1076
|
+
expect(isReadable(null as any)).toBeFalsy();
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
await it('should return false for plain objects', async () => {
|
|
1080
|
+
expect(isReadable({} as any)).toBeFalsy();
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
await describe('isWritable', async () => {
|
|
1085
|
+
await it('should return true for a writable stream', async () => {
|
|
1086
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1087
|
+
expect(isWritable(writable)).toBeTruthy();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
await it('should return false for an ended writable', async () => {
|
|
1091
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1092
|
+
writable.end();
|
|
1093
|
+
expect(isWritable(writable)).toBeFalsy();
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
await it('should return false for null', async () => {
|
|
1097
|
+
expect(isWritable(null as any)).toBeFalsy();
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
await describe('isDestroyed', async () => {
|
|
1102
|
+
await it('should return false for a new stream', async () => {
|
|
1103
|
+
const readable = new Readable({ read() {} });
|
|
1104
|
+
expect(isDestroyed(readable)).toBeFalsy();
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
await it('should return true for a destroyed stream', async () => {
|
|
1108
|
+
const readable = new Readable({ read() {} });
|
|
1109
|
+
readable.destroy();
|
|
1110
|
+
expect(isDestroyed(readable)).toBeTruthy();
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
await it('should return false for null', async () => {
|
|
1114
|
+
expect(isDestroyed(null)).toBeFalsy();
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
await describe('isDisturbed', async () => {
|
|
1119
|
+
await it('should return false for a new stream', async () => {
|
|
1120
|
+
const readable = new Readable({ read() {} });
|
|
1121
|
+
expect(isDisturbed(readable)).toBeFalsy();
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
await it('should return true after data is read', async () => {
|
|
1125
|
+
const readable = new Readable({
|
|
1126
|
+
read() {
|
|
1127
|
+
this.push('data');
|
|
1128
|
+
this.push(null);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
const chunks: unknown[] = [];
|
|
1132
|
+
readable.on('data', (chunk) => chunks.push(chunk));
|
|
1133
|
+
await new Promise<void>((resolve) => readable.on('end', () => resolve()));
|
|
1134
|
+
expect(isDisturbed(readable)).toBeTruthy();
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
await it('should return false for null', async () => {
|
|
1138
|
+
expect(isDisturbed(null)).toBeFalsy();
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
await describe('isErrored', async () => {
|
|
1143
|
+
await it('should return false for a new stream', async () => {
|
|
1144
|
+
const readable = new Readable({ read() {} });
|
|
1145
|
+
expect(isErrored(readable)).toBeFalsy();
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
await it('should return false for null', async () => {
|
|
1149
|
+
expect(isErrored(null)).toBeFalsy();
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// ==================== getDefaultHighWaterMark / setDefaultHighWaterMark ====================
|
|
1154
|
+
|
|
1155
|
+
await describe('getDefaultHighWaterMark / setDefaultHighWaterMark', async () => {
|
|
1156
|
+
await it('should return a positive number for non-object mode by default', async () => {
|
|
1157
|
+
const hwm = getDefaultHighWaterMark(false);
|
|
1158
|
+
expect(typeof hwm).toBe('number');
|
|
1159
|
+
expect(hwm > 0).toBeTruthy();
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
await it('should return 16 for object mode by default', async () => {
|
|
1163
|
+
expect(getDefaultHighWaterMark(true)).toBe(16);
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
await it('should allow setting custom default', async () => {
|
|
1167
|
+
const original = getDefaultHighWaterMark(false);
|
|
1168
|
+
setDefaultHighWaterMark(false, 32768);
|
|
1169
|
+
expect(getDefaultHighWaterMark(false)).toBe(32768);
|
|
1170
|
+
// Restore
|
|
1171
|
+
setDefaultHighWaterMark(false, original);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
await it('should reject invalid values', async () => {
|
|
1175
|
+
expect(() => setDefaultHighWaterMark(false, -1)).toThrow();
|
|
1176
|
+
expect(() => setDefaultHighWaterMark(false, NaN)).toThrow();
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
// ==================== Writable (backpressure & state) ====================
|
|
1181
|
+
|
|
1182
|
+
await describe('Writable (backpressure)', async () => {
|
|
1183
|
+
await it('write should return false when HWM reached', async () => {
|
|
1184
|
+
const writable = new Writable({
|
|
1185
|
+
highWaterMark: 5,
|
|
1186
|
+
write(_chunk, _enc, cb) { setTimeout(cb, 5); },
|
|
1187
|
+
});
|
|
1188
|
+
const result = writable.write('123456'); // > HWM
|
|
1189
|
+
expect(result).toBe(false);
|
|
1190
|
+
writable.destroy();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
await it('write should return true when below HWM', async () => {
|
|
1194
|
+
const writable = new Writable({
|
|
1195
|
+
highWaterMark: 100,
|
|
1196
|
+
write(_chunk, _enc, cb) { cb(); },
|
|
1197
|
+
});
|
|
1198
|
+
const result = writable.write('hi');
|
|
1199
|
+
expect(result).toBe(true);
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
await it('should emit drain after write returns false', async () => {
|
|
1203
|
+
const writable = new Writable({
|
|
1204
|
+
highWaterMark: 1,
|
|
1205
|
+
write(_chunk, _enc, cb) { setTimeout(cb, 5); },
|
|
1206
|
+
});
|
|
1207
|
+
writable.write('x');
|
|
1208
|
+
const drained = await new Promise<boolean>((resolve) => {
|
|
1209
|
+
writable.on('drain', () => resolve(true));
|
|
1210
|
+
setTimeout(() => resolve(false), 2000);
|
|
1211
|
+
});
|
|
1212
|
+
expect(drained).toBe(true);
|
|
1213
|
+
writable.destroy();
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
await it('writableEnded should be true after end()', async () => {
|
|
1217
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1218
|
+
expect(writable.writableEnded).toBe(false);
|
|
1219
|
+
writable.end();
|
|
1220
|
+
expect(writable.writableEnded).toBe(true);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
await it('writableFinished should be true after finish event', async () => {
|
|
1224
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1225
|
+
expect(writable.writableFinished).toBe(false);
|
|
1226
|
+
writable.end();
|
|
1227
|
+
await new Promise<void>((resolve) => writable.on('finish', () => resolve()));
|
|
1228
|
+
expect(writable.writableFinished).toBe(true);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
await it('writableLength should track buffered bytes', async () => {
|
|
1232
|
+
const writable = new Writable({
|
|
1233
|
+
highWaterMark: 100,
|
|
1234
|
+
write(_chunk, _enc, cb) { setTimeout(cb, 50); },
|
|
1235
|
+
});
|
|
1236
|
+
writable.write('abc');
|
|
1237
|
+
expect(writable.writableLength).toBeGreaterThan(0);
|
|
1238
|
+
writable.destroy();
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
// ==================== Transform (objectMode) ====================
|
|
1243
|
+
|
|
1244
|
+
await describe('Transform (objectMode)', async () => {
|
|
1245
|
+
await it('should pass objects through in objectMode', async () => {
|
|
1246
|
+
const transform = new Transform({
|
|
1247
|
+
objectMode: true,
|
|
1248
|
+
transform(chunk, _enc, cb) { cb(null, chunk); },
|
|
1249
|
+
});
|
|
1250
|
+
const obj = { key: 'value' };
|
|
1251
|
+
const result = await new Promise<unknown>((resolve) => {
|
|
1252
|
+
transform.on('data', (chunk) => resolve(chunk));
|
|
1253
|
+
transform.write(obj);
|
|
1254
|
+
});
|
|
1255
|
+
expect(result).toBe(obj);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
await it('should transform objects', async () => {
|
|
1259
|
+
const transform = new Transform({
|
|
1260
|
+
objectMode: true,
|
|
1261
|
+
transform(chunk: any, _enc, cb) {
|
|
1262
|
+
cb(null, { ...chunk, transformed: true });
|
|
1263
|
+
},
|
|
1264
|
+
});
|
|
1265
|
+
const result = await new Promise<any>((resolve) => {
|
|
1266
|
+
transform.on('data', (chunk) => resolve(chunk));
|
|
1267
|
+
transform.write({ id: 1 });
|
|
1268
|
+
});
|
|
1269
|
+
expect(result.id).toBe(1);
|
|
1270
|
+
expect(result.transformed).toBe(true);
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// ==================== Readable (objectMode) ====================
|
|
1275
|
+
|
|
1276
|
+
await describe('Readable (objectMode)', async () => {
|
|
1277
|
+
await it('should read objects in objectMode', async () => {
|
|
1278
|
+
const objects = [{ a: 1 }, { b: 2 }, { c: 3 }];
|
|
1279
|
+
let i = 0;
|
|
1280
|
+
const readable = new Readable({
|
|
1281
|
+
objectMode: true,
|
|
1282
|
+
read() {
|
|
1283
|
+
if (i < objects.length) {
|
|
1284
|
+
this.push(objects[i++]);
|
|
1285
|
+
} else {
|
|
1286
|
+
this.push(null);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
const results: unknown[] = [];
|
|
1291
|
+
readable.on('data', (chunk) => results.push(chunk));
|
|
1292
|
+
await new Promise<void>((resolve) => readable.on('end', () => resolve()));
|
|
1293
|
+
expect(results.length).toBe(3);
|
|
1294
|
+
expect((results[0] as any).a).toBe(1);
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
await it('readableObjectMode should return true', async () => {
|
|
1298
|
+
const readable = new Readable({ objectMode: true, read() {} });
|
|
1299
|
+
expect(readable.readableObjectMode).toBe(true);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
await it('readableObjectMode should return false by default', async () => {
|
|
1303
|
+
const readable = new Readable({ read() {} });
|
|
1304
|
+
expect(readable.readableObjectMode).toBe(false);
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
// ==================== Destroy behavior ====================
|
|
1309
|
+
|
|
1310
|
+
await describe('Stream destroy', async () => {
|
|
1311
|
+
await it('destroy should be idempotent', async () => {
|
|
1312
|
+
const readable = new Readable({ read() {} });
|
|
1313
|
+
readable.destroy();
|
|
1314
|
+
readable.destroy(); // Should not throw
|
|
1315
|
+
expect(readable.destroyed).toBe(true);
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
await it('destroy with error should emit error event', async () => {
|
|
1319
|
+
const readable = new Readable({ read() {} });
|
|
1320
|
+
const err = await new Promise<Error>((resolve) => {
|
|
1321
|
+
readable.on('error', (e) => resolve(e));
|
|
1322
|
+
readable.destroy(new Error('test destroy'));
|
|
1323
|
+
});
|
|
1324
|
+
expect(err.message).toBe('test destroy');
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
await it('destroy should emit close event', async () => {
|
|
1328
|
+
const readable = new Readable({ read() {} });
|
|
1329
|
+
const closed = await new Promise<boolean>((resolve) => {
|
|
1330
|
+
readable.on('close', () => resolve(true));
|
|
1331
|
+
readable.destroy();
|
|
1332
|
+
});
|
|
1333
|
+
expect(closed).toBe(true);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
await it('writable destroy should emit close', async () => {
|
|
1337
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1338
|
+
const closed = await new Promise<boolean>((resolve) => {
|
|
1339
|
+
writable.on('close', () => resolve(true));
|
|
1340
|
+
writable.destroy();
|
|
1341
|
+
});
|
|
1342
|
+
expect(closed).toBe(true);
|
|
1343
|
+
});
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
// ==================== Stream.pipe error handling ====================
|
|
1347
|
+
|
|
1348
|
+
await describe('Stream.pipe (error handling)', async () => {
|
|
1349
|
+
await it('pipe should not destroy writable on readable error by default', async () => {
|
|
1350
|
+
const readable = new Readable({ read() {} });
|
|
1351
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1352
|
+
readable.pipe(writable);
|
|
1353
|
+
readable.on('error', () => {}); // Prevent unhandled error
|
|
1354
|
+
readable.destroy(new Error('source error'));
|
|
1355
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
1356
|
+
// The writable should NOT be destroyed by default
|
|
1357
|
+
// (pipe only calls end() on normal EOF, not on error)
|
|
1358
|
+
expect(writable.destroyed).toBe(false);
|
|
1359
|
+
writable.destroy();
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
await it('unpipe should stop data flow', async () => {
|
|
1363
|
+
const chunks: string[] = [];
|
|
1364
|
+
const readable = new Readable({ read() {} });
|
|
1365
|
+
const writable = new Writable({
|
|
1366
|
+
write(chunk, _e, cb) {
|
|
1367
|
+
chunks.push(String(chunk));
|
|
1368
|
+
cb();
|
|
1369
|
+
}
|
|
1370
|
+
});
|
|
1371
|
+
readable.pipe(writable);
|
|
1372
|
+
readable.push('before');
|
|
1373
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
1374
|
+
readable.unpipe(writable);
|
|
1375
|
+
readable.push('after');
|
|
1376
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
1377
|
+
// 'after' should NOT reach writable
|
|
1378
|
+
expect(chunks).toContain('before');
|
|
1379
|
+
expect(chunks.indexOf('after')).toBe(-1);
|
|
1380
|
+
readable.destroy();
|
|
1381
|
+
writable.destroy();
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
// ==================== stream/promises ====================
|
|
1386
|
+
|
|
1387
|
+
await describe('stream/promises', async () => {
|
|
1388
|
+
await it('pipeline should return a promise', async () => {
|
|
1389
|
+
// Import dynamically to test the sub-module
|
|
1390
|
+
const { pipeline: pipelineP } = await import('node:stream/promises');
|
|
1391
|
+
|
|
1392
|
+
const readable = new Readable({
|
|
1393
|
+
read() {
|
|
1394
|
+
this.push('data');
|
|
1395
|
+
this.push(null);
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
const chunks: string[] = [];
|
|
1399
|
+
const writable = new Writable({
|
|
1400
|
+
write(chunk, _enc, cb) {
|
|
1401
|
+
chunks.push(String(chunk));
|
|
1402
|
+
cb();
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
await pipelineP(readable, writable);
|
|
1407
|
+
expect(chunks.length).toBe(1);
|
|
1408
|
+
expect(chunks[0]).toBe('data');
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
await it('finished should return a promise', async () => {
|
|
1412
|
+
const { finished: finishedP } = await import('node:stream/promises');
|
|
1413
|
+
|
|
1414
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1415
|
+
writable.end();
|
|
1416
|
+
await finishedP(writable);
|
|
1417
|
+
expect(writable.writableFinished).toBeTruthy();
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
// ==================== Readable: readable event ====================
|
|
1422
|
+
// Ported from refs/node-test/parallel/test-stream-readable-event.js
|
|
1423
|
+
// Original: MIT license, Node.js contributors
|
|
1424
|
+
|
|
1425
|
+
await describe('Readable: readable event', async () => {
|
|
1426
|
+
await it('should emit readable when data is available', async () => {
|
|
1427
|
+
const r = new Readable({ read() {} });
|
|
1428
|
+
let readableEmitted = false;
|
|
1429
|
+
r.on('readable', () => { readableEmitted = true; });
|
|
1430
|
+
r.push('data');
|
|
1431
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
1432
|
+
expect(readableEmitted).toBeTruthy();
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
await it('should allow read() in readable handler', async () => {
|
|
1436
|
+
const r = new Readable({ read() {} });
|
|
1437
|
+
const chunks: string[] = [];
|
|
1438
|
+
r.on('readable', () => {
|
|
1439
|
+
let chunk;
|
|
1440
|
+
while ((chunk = r.read()) !== null) {
|
|
1441
|
+
chunks.push(String(chunk));
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
r.push('hello');
|
|
1445
|
+
r.push('world');
|
|
1446
|
+
r.push(null);
|
|
1447
|
+
await new Promise<void>((resolve) => r.on('end', () => resolve()));
|
|
1448
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
1449
|
+
expect(chunks.join('')).toBe('helloworld');
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
await it('should emit readable on push(null) when data buffered', async () => {
|
|
1453
|
+
const r = new Readable({ highWaterMark: 30, read() {} });
|
|
1454
|
+
r.push('small');
|
|
1455
|
+
r.push(null);
|
|
1456
|
+
|
|
1457
|
+
const result = await new Promise<string | null>((resolve) => {
|
|
1458
|
+
r.on('readable', () => {
|
|
1459
|
+
const data = r.read();
|
|
1460
|
+
resolve(data !== null ? String(data) : null);
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
expect(result).toBe('small');
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// ==================== Readable: read() method ====================
|
|
1468
|
+
|
|
1469
|
+
await describe('Readable: read(size)', async () => {
|
|
1470
|
+
await it('should read specific number of bytes', async () => {
|
|
1471
|
+
const r = new Readable({ read() {} });
|
|
1472
|
+
r.push('hello world');
|
|
1473
|
+
const chunk = r.read(5);
|
|
1474
|
+
expect(String(chunk)).toBe('hello');
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
await it('should return null when not enough data', async () => {
|
|
1478
|
+
const r = new Readable({ read() {} });
|
|
1479
|
+
r.push('hi');
|
|
1480
|
+
const chunk = r.read(10);
|
|
1481
|
+
expect(chunk).toBeNull();
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
await it('read(0) should not consume data', async () => {
|
|
1485
|
+
const r = new Readable({ read() {} });
|
|
1486
|
+
r.push('data');
|
|
1487
|
+
const result = r.read(0);
|
|
1488
|
+
expect(result).toBeNull();
|
|
1489
|
+
// Data should still be buffered
|
|
1490
|
+
expect(r.readableLength).toBe(4);
|
|
1491
|
+
});
|
|
1492
|
+
});
|
|
1493
|
+
|
|
1494
|
+
// ==================== Readable.from variants ====================
|
|
1495
|
+
// Ported from refs/node-test/parallel/test-stream-readable-from.js
|
|
1496
|
+
// Original: MIT license, Node.js contributors
|
|
1497
|
+
|
|
1498
|
+
await describe('Readable.from (extended)', async () => {
|
|
1499
|
+
await it('should create readable from sync generator', async () => {
|
|
1500
|
+
function* gen() {
|
|
1501
|
+
yield 'a';
|
|
1502
|
+
yield 'b';
|
|
1503
|
+
yield 'c';
|
|
1504
|
+
}
|
|
1505
|
+
const readable = Readable.from(gen());
|
|
1506
|
+
const chunks: string[] = [];
|
|
1507
|
+
for await (const chunk of readable) {
|
|
1508
|
+
chunks.push(String(chunk));
|
|
1509
|
+
}
|
|
1510
|
+
expect(chunks.length).toBe(3);
|
|
1511
|
+
expect(chunks[0]).toBe('a');
|
|
1512
|
+
expect(chunks[2]).toBe('c');
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
await it('should create readable from iterable (Set)', async () => {
|
|
1516
|
+
const set = new Set([1, 2, 3]);
|
|
1517
|
+
const readable = Readable.from(set);
|
|
1518
|
+
const chunks: unknown[] = [];
|
|
1519
|
+
for await (const chunk of readable) {
|
|
1520
|
+
chunks.push(chunk);
|
|
1521
|
+
}
|
|
1522
|
+
expect(chunks.length).toBe(3);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
await it('should support objectMode by default', async () => {
|
|
1526
|
+
const readable = Readable.from([{ id: 1 }, { id: 2 }]);
|
|
1527
|
+
expect(readable.readableObjectMode).toBeTruthy();
|
|
1528
|
+
const chunks: any[] = [];
|
|
1529
|
+
for await (const chunk of readable) {
|
|
1530
|
+
chunks.push(chunk);
|
|
1531
|
+
}
|
|
1532
|
+
expect(chunks[0].id).toBe(1);
|
|
1533
|
+
expect(chunks[1].id).toBe(2);
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
await it('should create readable from string', async () => {
|
|
1537
|
+
const readable = Readable.from('hello');
|
|
1538
|
+
const chunks: string[] = [];
|
|
1539
|
+
for await (const chunk of readable) {
|
|
1540
|
+
chunks.push(String(chunk));
|
|
1541
|
+
}
|
|
1542
|
+
expect(chunks.join('')).toBe('hello');
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
await it('should create readable from Buffer', async () => {
|
|
1546
|
+
const buf = Buffer.from('test data');
|
|
1547
|
+
const readable = Readable.from(buf);
|
|
1548
|
+
const chunks: unknown[] = [];
|
|
1549
|
+
for await (const chunk of readable) {
|
|
1550
|
+
chunks.push(chunk);
|
|
1551
|
+
}
|
|
1552
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
1553
|
+
});
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
// ==================== Readable: async iterator (extended) ====================
|
|
1557
|
+
// Ported from refs/node-test/parallel/test-stream-readable-async-iterators.js
|
|
1558
|
+
// Original: MIT license, Node.js contributors
|
|
1559
|
+
|
|
1560
|
+
await describe('Readable: async iterator (extended)', async () => {
|
|
1561
|
+
await it('should iterate in objectMode with falsey values', async () => {
|
|
1562
|
+
const readable = new Readable({ objectMode: true, read() {} });
|
|
1563
|
+
readable.push(0);
|
|
1564
|
+
readable.push('');
|
|
1565
|
+
readable.push(false);
|
|
1566
|
+
readable.push(null);
|
|
1567
|
+
|
|
1568
|
+
const values: unknown[] = [];
|
|
1569
|
+
for await (const chunk of readable) {
|
|
1570
|
+
values.push(chunk);
|
|
1571
|
+
}
|
|
1572
|
+
expect(values.length).toBe(3);
|
|
1573
|
+
expect(values[0]).toBe(0);
|
|
1574
|
+
expect(values[1]).toBe('');
|
|
1575
|
+
expect(values[2]).toBe(false);
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
await it('should handle break in for-await loop', async () => {
|
|
1579
|
+
const readable = new Readable({
|
|
1580
|
+
read() {
|
|
1581
|
+
this.push('data');
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
let count = 0;
|
|
1585
|
+
for await (const _chunk of readable) {
|
|
1586
|
+
count++;
|
|
1587
|
+
if (count >= 3) break;
|
|
1588
|
+
}
|
|
1589
|
+
expect(count).toBe(3);
|
|
1590
|
+
expect(readable.destroyed).toBeTruthy();
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
await it('should handle error during async iteration', async () => {
|
|
1594
|
+
const readable = new Readable({
|
|
1595
|
+
read() {
|
|
1596
|
+
this.push('data');
|
|
1597
|
+
this.destroy(new Error('iter error'));
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
let caughtError: Error | null = null;
|
|
1601
|
+
try {
|
|
1602
|
+
for await (const _chunk of readable) {
|
|
1603
|
+
// Should throw
|
|
1604
|
+
}
|
|
1605
|
+
} catch (e) {
|
|
1606
|
+
caughtError = e as Error;
|
|
1607
|
+
}
|
|
1608
|
+
expect(caughtError).toBeDefined();
|
|
1609
|
+
expect(caughtError!.message).toBe('iter error');
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
// ==================== Pipeline (extended) ====================
|
|
1614
|
+
// Ported from refs/node-test/parallel/test-stream-pipeline.js
|
|
1615
|
+
// Original: MIT license, Node.js contributors
|
|
1616
|
+
|
|
1617
|
+
await describe('pipeline (extended)', async () => {
|
|
1618
|
+
await it('should pipe data through multiple transforms', async () => {
|
|
1619
|
+
const readable = new Readable({
|
|
1620
|
+
read() { this.push('hello'); this.push(null); }
|
|
1621
|
+
});
|
|
1622
|
+
const upper = new Transform({
|
|
1623
|
+
transform(chunk, _enc, cb) { cb(null, String(chunk).toUpperCase()); }
|
|
1624
|
+
});
|
|
1625
|
+
const exclaim = new Transform({
|
|
1626
|
+
transform(chunk, _enc, cb) { cb(null, String(chunk) + '!'); }
|
|
1627
|
+
});
|
|
1628
|
+
const output: string[] = [];
|
|
1629
|
+
const writable = new Writable({
|
|
1630
|
+
write(chunk, _enc, cb) { output.push(String(chunk)); cb(); }
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
await new Promise<void>((resolve, reject) => {
|
|
1634
|
+
pipeline(readable, upper, exclaim, writable, (err) => {
|
|
1635
|
+
if (err) reject(err);
|
|
1636
|
+
else resolve();
|
|
1637
|
+
});
|
|
1638
|
+
});
|
|
1639
|
+
expect(output[0]).toBe('HELLO!');
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
await it('should destroy all streams on error in source', async () => {
|
|
1643
|
+
const readable = new Readable({
|
|
1644
|
+
read() { this.destroy(new Error('src err')); }
|
|
1645
|
+
});
|
|
1646
|
+
const transform = new Transform({
|
|
1647
|
+
transform(chunk, _enc, cb) { cb(null, chunk); }
|
|
1648
|
+
});
|
|
1649
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1650
|
+
|
|
1651
|
+
const err = await new Promise<Error>((resolve) => {
|
|
1652
|
+
pipeline(readable, transform, writable, (e) => {
|
|
1653
|
+
if (e) resolve(e);
|
|
1654
|
+
});
|
|
1655
|
+
});
|
|
1656
|
+
expect(err.message).toBe('src err');
|
|
1657
|
+
// All streams should be destroyed
|
|
1658
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
1659
|
+
expect(readable.destroyed).toBeTruthy();
|
|
1660
|
+
expect(transform.destroyed).toBeTruthy();
|
|
1661
|
+
expect(writable.destroyed).toBeTruthy();
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
await it('should destroy all streams on error in middle transform', async () => {
|
|
1665
|
+
const readable = new Readable({
|
|
1666
|
+
read() { this.push('data'); this.push(null); }
|
|
1667
|
+
});
|
|
1668
|
+
const transform = new Transform({
|
|
1669
|
+
transform(_chunk, _enc, cb) { cb(new Error('transform err')); }
|
|
1670
|
+
});
|
|
1671
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1672
|
+
|
|
1673
|
+
const err = await new Promise<Error>((resolve) => {
|
|
1674
|
+
pipeline(readable, transform, writable, (e) => {
|
|
1675
|
+
if (e) resolve(e);
|
|
1676
|
+
});
|
|
1677
|
+
});
|
|
1678
|
+
expect(err.message).toBe('transform err');
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
await it('should complete pipeline even with empty string push', async () => {
|
|
1682
|
+
const readable = new Readable({
|
|
1683
|
+
read() { this.push(''); this.push(null); }
|
|
1684
|
+
});
|
|
1685
|
+
const writable = new Writable({
|
|
1686
|
+
write(chunk, _enc, cb) { cb(); }
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
// Pipeline should complete without error
|
|
1690
|
+
await new Promise<void>((resolve, reject) => {
|
|
1691
|
+
pipeline(readable, writable, (err) => {
|
|
1692
|
+
if (err) reject(err);
|
|
1693
|
+
else resolve();
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
expect(writable.writableFinished).toBeTruthy();
|
|
1697
|
+
});
|
|
1698
|
+
|
|
1699
|
+
await it('pipeline with aborted signal via addAbortSignal should abort', async () => {
|
|
1700
|
+
const ac = new AbortController();
|
|
1701
|
+
const readable = addAbortSignal(ac.signal, new Readable({ read() {} }));
|
|
1702
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
1703
|
+
|
|
1704
|
+
// Must listen for error to prevent unhandled error
|
|
1705
|
+
readable.on('error', () => {});
|
|
1706
|
+
|
|
1707
|
+
const errPromise = new Promise<Error>((resolve) => {
|
|
1708
|
+
pipeline(readable, writable, (err) => {
|
|
1709
|
+
if (err) resolve(err);
|
|
1710
|
+
});
|
|
1711
|
+
});
|
|
1712
|
+
ac.abort();
|
|
1713
|
+
const err = await errPromise;
|
|
1714
|
+
expect(err.message.toLowerCase().includes('abort')).toBeTruthy();
|
|
1715
|
+
});
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
// ==================== Duplex (extended) ====================
|
|
1719
|
+
// Ported from refs/node-test/parallel/test-stream-duplex.js
|
|
1720
|
+
// Original: MIT license, Node.js contributors
|
|
1721
|
+
|
|
1722
|
+
await describe('Duplex (extended)', async () => {
|
|
1723
|
+
await it('should support objectMode on both sides', async () => {
|
|
1724
|
+
const duplex = new Duplex({
|
|
1725
|
+
objectMode: true,
|
|
1726
|
+
read() {},
|
|
1727
|
+
write(_chunk, _enc, cb) { cb(); }
|
|
1728
|
+
});
|
|
1729
|
+
expect(duplex.readableObjectMode).toBeTruthy();
|
|
1730
|
+
expect(duplex.writableObjectMode).toBeTruthy();
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
await it('allowHalfOpen should default to true', async () => {
|
|
1734
|
+
const duplex = new Duplex({ read() {}, write(_c, _e, cb) { cb(); } });
|
|
1735
|
+
expect(duplex.allowHalfOpen).toBeTruthy();
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
await it('should allow reading and writing independently', async () => {
|
|
1739
|
+
const received: string[] = [];
|
|
1740
|
+
const duplex = new Duplex({
|
|
1741
|
+
read() {},
|
|
1742
|
+
write(chunk, _enc, cb) {
|
|
1743
|
+
received.push(String(chunk));
|
|
1744
|
+
cb();
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// Write side
|
|
1749
|
+
duplex.write('hello');
|
|
1750
|
+
expect(received[0]).toBe('hello');
|
|
1751
|
+
|
|
1752
|
+
// Read side
|
|
1753
|
+
duplex.push('world');
|
|
1754
|
+
const readData = await new Promise<string>((resolve) => {
|
|
1755
|
+
duplex.on('data', (chunk) => resolve(String(chunk)));
|
|
1756
|
+
});
|
|
1757
|
+
expect(readData).toBe('world');
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
await it('should not end writable when readable ends (allowHalfOpen=true)', async () => {
|
|
1761
|
+
const duplex = new Duplex({
|
|
1762
|
+
allowHalfOpen: true,
|
|
1763
|
+
read() {},
|
|
1764
|
+
write(_c, _e, cb) { cb(); }
|
|
1765
|
+
});
|
|
1766
|
+
duplex.push(null);
|
|
1767
|
+
duplex.resume();
|
|
1768
|
+
await new Promise<void>((resolve) => duplex.on('end', () => resolve()));
|
|
1769
|
+
// Writable side should still be open
|
|
1770
|
+
expect(duplex.writable).toBeTruthy();
|
|
1771
|
+
duplex.end();
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
await it('should end both when allowHalfOpen=false', async () => {
|
|
1775
|
+
const duplex = new Duplex({
|
|
1776
|
+
allowHalfOpen: false,
|
|
1777
|
+
read() {},
|
|
1778
|
+
write(_c, _e, cb) { cb(); }
|
|
1779
|
+
});
|
|
1780
|
+
duplex.push(null);
|
|
1781
|
+
duplex.resume();
|
|
1782
|
+
|
|
1783
|
+
await new Promise<void>((resolve) => {
|
|
1784
|
+
duplex.on('finish', () => resolve());
|
|
1785
|
+
});
|
|
1786
|
+
expect(duplex.writableEnded).toBeTruthy();
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
await it('should support destroy', async () => {
|
|
1790
|
+
const duplex = new Duplex({
|
|
1791
|
+
read() {},
|
|
1792
|
+
write(_c, _e, cb) { cb(); }
|
|
1793
|
+
});
|
|
1794
|
+
await new Promise<void>((resolve) => {
|
|
1795
|
+
duplex.on('close', () => resolve());
|
|
1796
|
+
duplex.destroy();
|
|
1797
|
+
});
|
|
1798
|
+
expect(duplex.destroyed).toBeTruthy();
|
|
1799
|
+
expect(duplex.readable).toBeFalsy();
|
|
1800
|
+
expect(duplex.writable).toBeFalsy();
|
|
1801
|
+
});
|
|
1802
|
+
});
|
|
1803
|
+
|
|
1804
|
+
// ==================== Transform (extended) ====================
|
|
1805
|
+
|
|
1806
|
+
await describe('Transform (extended)', async () => {
|
|
1807
|
+
await it('should handle objectMode with falsey values', async () => {
|
|
1808
|
+
const transform = new Transform({
|
|
1809
|
+
objectMode: true,
|
|
1810
|
+
transform(chunk, _enc, cb) { cb(null, chunk); }
|
|
1811
|
+
});
|
|
1812
|
+
const results: unknown[] = [];
|
|
1813
|
+
transform.on('data', (chunk) => results.push(chunk));
|
|
1814
|
+
|
|
1815
|
+
transform.write(0);
|
|
1816
|
+
transform.write('');
|
|
1817
|
+
transform.write(false);
|
|
1818
|
+
transform.end();
|
|
1819
|
+
|
|
1820
|
+
await new Promise<void>((resolve) => transform.on('end', () => resolve()));
|
|
1821
|
+
expect(results.length).toBe(3);
|
|
1822
|
+
expect(results[0]).toBe(0);
|
|
1823
|
+
expect(results[1]).toBe('');
|
|
1824
|
+
expect(results[2]).toBe(false);
|
|
1825
|
+
});
|
|
1826
|
+
|
|
1827
|
+
await it('should handle multiple pushes in _transform', async () => {
|
|
1828
|
+
const transform = new Transform({
|
|
1829
|
+
transform(chunk, _enc, cb) {
|
|
1830
|
+
const str = String(chunk);
|
|
1831
|
+
for (const ch of str) {
|
|
1832
|
+
this.push(ch);
|
|
1833
|
+
}
|
|
1834
|
+
cb();
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
const results: string[] = [];
|
|
1838
|
+
transform.on('data', (chunk) => results.push(String(chunk)));
|
|
1839
|
+
transform.write('abc');
|
|
1840
|
+
transform.end();
|
|
1841
|
+
await new Promise<void>((resolve) => transform.on('end', () => resolve()));
|
|
1842
|
+
expect(results.length).toBe(3);
|
|
1843
|
+
expect(results[0]).toBe('a');
|
|
1844
|
+
expect(results[1]).toBe('b');
|
|
1845
|
+
expect(results[2]).toBe('c');
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
await it('should handle async _transform', async () => {
|
|
1849
|
+
const transform = new Transform({
|
|
1850
|
+
transform(chunk, _enc, cb) {
|
|
1851
|
+
setTimeout(() => cb(null, String(chunk).toUpperCase()), 5);
|
|
1852
|
+
}
|
|
1853
|
+
});
|
|
1854
|
+
const results: string[] = [];
|
|
1855
|
+
transform.on('data', (chunk) => results.push(String(chunk)));
|
|
1856
|
+
transform.write('hello');
|
|
1857
|
+
transform.end();
|
|
1858
|
+
await new Promise<void>((resolve) => transform.on('end', () => resolve()));
|
|
1859
|
+
expect(results.length).toBe(1);
|
|
1860
|
+
expect(results[0]).toBe('HELLO');
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
await it('should propagate flush errors', async () => {
|
|
1864
|
+
const transform = new Transform({
|
|
1865
|
+
transform(chunk, _enc, cb) { cb(null, chunk); },
|
|
1866
|
+
flush(cb) { cb(new Error('flush error')); }
|
|
1867
|
+
});
|
|
1868
|
+
transform.on('data', () => {}); // consume
|
|
1869
|
+
const err = await new Promise<Error>((resolve) => {
|
|
1870
|
+
transform.on('error', (e) => resolve(e));
|
|
1871
|
+
transform.end();
|
|
1872
|
+
});
|
|
1873
|
+
expect(err.message).toBe('flush error');
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
// ==================== Writable: _final (extended) ====================
|
|
1878
|
+
// Ported from refs/node-test/parallel/test-stream-writable-final-async.js
|
|
1879
|
+
// Original: MIT license, Node.js contributors
|
|
1880
|
+
|
|
1881
|
+
await describe('Writable: _final (extended)', async () => {
|
|
1882
|
+
await it('should support async _final', async () => {
|
|
1883
|
+
let finalCalled = false;
|
|
1884
|
+
const order: string[] = [];
|
|
1885
|
+
const writable = new Writable({
|
|
1886
|
+
write(_c, _e, cb) { order.push('write'); cb(); },
|
|
1887
|
+
final(cb) {
|
|
1888
|
+
setTimeout(() => {
|
|
1889
|
+
finalCalled = true;
|
|
1890
|
+
order.push('final');
|
|
1891
|
+
cb();
|
|
1892
|
+
}, 10);
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
writable.write('data');
|
|
1896
|
+
writable.end();
|
|
1897
|
+
await new Promise<void>((resolve) => writable.on('finish', () => {
|
|
1898
|
+
order.push('finish');
|
|
1899
|
+
resolve();
|
|
1900
|
+
}));
|
|
1901
|
+
expect(finalCalled).toBeTruthy();
|
|
1902
|
+
expect(order[0]).toBe('write');
|
|
1903
|
+
expect(order[1]).toBe('final');
|
|
1904
|
+
expect(order[2]).toBe('finish');
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
await it('_final should be called after all writes complete', async () => {
|
|
1908
|
+
const order: string[] = [];
|
|
1909
|
+
const writable = new Writable({
|
|
1910
|
+
write(_c, _e, cb) {
|
|
1911
|
+
setTimeout(() => { order.push('write-done'); cb(); }, 5);
|
|
1912
|
+
},
|
|
1913
|
+
final(cb) {
|
|
1914
|
+
order.push('final');
|
|
1915
|
+
cb();
|
|
1916
|
+
}
|
|
1917
|
+
});
|
|
1918
|
+
writable.write('a');
|
|
1919
|
+
writable.write('b');
|
|
1920
|
+
writable.end();
|
|
1921
|
+
await new Promise<void>((resolve) => writable.on('finish', () => resolve()));
|
|
1922
|
+
// Final must come after both writes complete
|
|
1923
|
+
expect(order[order.length - 1]).toBe('final');
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
|
|
1927
|
+
// ==================== Pipe backpressure ====================
|
|
1928
|
+
|
|
1929
|
+
await describe('Pipe backpressure', async () => {
|
|
1930
|
+
await it('should respect writable backpressure during pipe', async () => {
|
|
1931
|
+
let writeCount = 0;
|
|
1932
|
+
const readable = new Readable({
|
|
1933
|
+
read() {
|
|
1934
|
+
this.push('x'.repeat(100));
|
|
1935
|
+
if (++writeCount >= 5) this.push(null);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
const chunks: string[] = [];
|
|
1939
|
+
const writable = new Writable({
|
|
1940
|
+
highWaterMark: 50,
|
|
1941
|
+
write(chunk, _enc, cb) {
|
|
1942
|
+
chunks.push(String(chunk));
|
|
1943
|
+
setTimeout(cb, 5); // Slow consumer
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
await new Promise<void>((resolve) => {
|
|
1948
|
+
writable.on('finish', () => resolve());
|
|
1949
|
+
readable.pipe(writable);
|
|
1950
|
+
});
|
|
1951
|
+
// All data should eventually arrive
|
|
1952
|
+
expect(chunks.length).toBe(5);
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
await it('should emit drain after backpressure resolves', async () => {
|
|
1956
|
+
const writable = new Writable({
|
|
1957
|
+
highWaterMark: 1,
|
|
1958
|
+
write(_chunk, _enc, cb) { setTimeout(cb, 10); }
|
|
1959
|
+
});
|
|
1960
|
+
const result = writable.write('x'.repeat(10));
|
|
1961
|
+
expect(result).toBe(false);
|
|
1962
|
+
|
|
1963
|
+
const drained = await new Promise<boolean>((resolve) => {
|
|
1964
|
+
writable.on('drain', () => resolve(true));
|
|
1965
|
+
setTimeout(() => resolve(false), 2000);
|
|
1966
|
+
});
|
|
1967
|
+
expect(drained).toBeTruthy();
|
|
1968
|
+
writable.destroy();
|
|
1969
|
+
});
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1972
|
+
// ==================== Uint8Array handling ====================
|
|
1973
|
+
// Ported from refs/node-test/parallel/test-stream-uint8array.js
|
|
1974
|
+
// Original: MIT license, Node.js contributors
|
|
1975
|
+
|
|
1976
|
+
await describe('Uint8Array handling', async () => {
|
|
1977
|
+
await it('Writable should accept Uint8Array', async () => {
|
|
1978
|
+
const received: unknown[] = [];
|
|
1979
|
+
const writable = new Writable({
|
|
1980
|
+
write(chunk, _enc, cb) {
|
|
1981
|
+
received.push(chunk);
|
|
1982
|
+
cb();
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
const data = new Uint8Array([1, 2, 3]);
|
|
1986
|
+
writable.write(data);
|
|
1987
|
+
expect(received.length).toBe(1);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
await it('Readable should push Uint8Array', async () => {
|
|
1991
|
+
const data = new Uint8Array([65, 66, 67]); // ABC
|
|
1992
|
+
const readable = new Readable({
|
|
1993
|
+
read() {
|
|
1994
|
+
this.push(data);
|
|
1995
|
+
this.push(null);
|
|
1996
|
+
}
|
|
1997
|
+
});
|
|
1998
|
+
const chunks: unknown[] = [];
|
|
1999
|
+
for await (const chunk of readable) {
|
|
2000
|
+
chunks.push(chunk);
|
|
2001
|
+
}
|
|
2002
|
+
expect(chunks.length).toBe(1);
|
|
2003
|
+
});
|
|
2004
|
+
|
|
2005
|
+
await it('Transform should handle Uint8Array passthrough', async () => {
|
|
2006
|
+
const transform = new Transform({
|
|
2007
|
+
transform(chunk, _enc, cb) { cb(null, chunk); }
|
|
2008
|
+
});
|
|
2009
|
+
const data = new Uint8Array([10, 20, 30]);
|
|
2010
|
+
const result = await new Promise<unknown>((resolve) => {
|
|
2011
|
+
transform.on('data', (chunk) => resolve(chunk));
|
|
2012
|
+
transform.write(data);
|
|
2013
|
+
});
|
|
2014
|
+
expect(result).toBeDefined();
|
|
2015
|
+
});
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
// ==================== Destroy event order ====================
|
|
2019
|
+
// Ported from refs/node-test/parallel/test-stream-destroy-event-order.js
|
|
2020
|
+
// Original: MIT license, Node.js contributors
|
|
2021
|
+
|
|
2022
|
+
await describe('Destroy event order', async () => {
|
|
2023
|
+
await it('error should be emitted before close on Readable', async () => {
|
|
2024
|
+
const order: string[] = [];
|
|
2025
|
+
const readable = new Readable({ read() {} });
|
|
2026
|
+
readable.on('error', () => order.push('error'));
|
|
2027
|
+
readable.on('close', () => order.push('close'));
|
|
2028
|
+
readable.destroy(new Error('test'));
|
|
2029
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
2030
|
+
expect(order[0]).toBe('error');
|
|
2031
|
+
expect(order[1]).toBe('close');
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
await it('error should be emitted before close on Writable', async () => {
|
|
2035
|
+
const order: string[] = [];
|
|
2036
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
2037
|
+
writable.on('error', () => order.push('error'));
|
|
2038
|
+
writable.on('close', () => order.push('close'));
|
|
2039
|
+
writable.destroy(new Error('test'));
|
|
2040
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
2041
|
+
expect(order[0]).toBe('error');
|
|
2042
|
+
expect(order[1]).toBe('close');
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
await it('close should be emitted after finish on normal end', async () => {
|
|
2046
|
+
const order: string[] = [];
|
|
2047
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
2048
|
+
writable.on('finish', () => order.push('finish'));
|
|
2049
|
+
writable.on('close', () => order.push('close'));
|
|
2050
|
+
writable.end();
|
|
2051
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
2052
|
+
expect(order[0]).toBe('finish');
|
|
2053
|
+
expect(order[1]).toBe('close');
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
await it('close should be emitted after end on Readable', async () => {
|
|
2057
|
+
const order: string[] = [];
|
|
2058
|
+
const readable = new Readable({
|
|
2059
|
+
read() { this.push(null); }
|
|
2060
|
+
});
|
|
2061
|
+
readable.on('end', () => order.push('end'));
|
|
2062
|
+
readable.on('close', () => order.push('close'));
|
|
2063
|
+
readable.resume();
|
|
2064
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
2065
|
+
expect(order[0]).toBe('end');
|
|
2066
|
+
expect(order[1]).toBe('close');
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
|
|
2070
|
+
// ==================== Readable: readableLength ====================
|
|
2071
|
+
|
|
2072
|
+
await describe('Readable: readableLength', async () => {
|
|
2073
|
+
await it('should track buffered bytes', async () => {
|
|
2074
|
+
const readable = new Readable({ read() {} });
|
|
2075
|
+
expect(readable.readableLength).toBe(0);
|
|
2076
|
+
readable.push('hello'); // 5 bytes
|
|
2077
|
+
expect(readable.readableLength).toBe(5);
|
|
2078
|
+
});
|
|
2079
|
+
|
|
2080
|
+
await it('should decrease after read()', async () => {
|
|
2081
|
+
const readable = new Readable({ read() {} });
|
|
2082
|
+
readable.push('hello world');
|
|
2083
|
+
expect(readable.readableLength).toBe(11);
|
|
2084
|
+
readable.read(5);
|
|
2085
|
+
expect(readable.readableLength).toBe(6);
|
|
2086
|
+
});
|
|
2087
|
+
|
|
2088
|
+
await it('should count objects as 1 in objectMode', async () => {
|
|
2089
|
+
const readable = new Readable({ objectMode: true, read() {} });
|
|
2090
|
+
readable.push({ a: 1 });
|
|
2091
|
+
readable.push({ b: 2 });
|
|
2092
|
+
expect(readable.readableLength).toBe(2);
|
|
2093
|
+
});
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
// ==================== Writable: encoding ====================
|
|
2097
|
+
|
|
2098
|
+
await describe('Writable: encoding', async () => {
|
|
2099
|
+
await it('should pass encoding to _write when decodeStrings=false', async () => {
|
|
2100
|
+
let receivedEncoding: string = '';
|
|
2101
|
+
const writable = new Writable({
|
|
2102
|
+
decodeStrings: false,
|
|
2103
|
+
write(_chunk, encoding, cb) {
|
|
2104
|
+
receivedEncoding = encoding;
|
|
2105
|
+
cb();
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
writable.write('data', 'utf8');
|
|
2109
|
+
expect(receivedEncoding).toBe('utf8');
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
await it('should default encoding to utf8 when decodeStrings=false', async () => {
|
|
2113
|
+
let receivedEncoding: string = '';
|
|
2114
|
+
const writable = new Writable({
|
|
2115
|
+
decodeStrings: false,
|
|
2116
|
+
write(_chunk, encoding, cb) {
|
|
2117
|
+
receivedEncoding = encoding;
|
|
2118
|
+
cb();
|
|
2119
|
+
}
|
|
2120
|
+
});
|
|
2121
|
+
writable.write('data');
|
|
2122
|
+
expect(receivedEncoding).toBe('utf8');
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
await it('should use buffer encoding for Buffers', async () => {
|
|
2126
|
+
let receivedEncoding: string = '';
|
|
2127
|
+
const writable = new Writable({
|
|
2128
|
+
write(_chunk, encoding, cb) {
|
|
2129
|
+
receivedEncoding = encoding;
|
|
2130
|
+
cb();
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
writable.write(Buffer.from('data'));
|
|
2134
|
+
expect(receivedEncoding).toBe('buffer');
|
|
2135
|
+
});
|
|
2136
|
+
|
|
2137
|
+
await it('with decodeStrings=true (default) encoding should be buffer for strings', async () => {
|
|
2138
|
+
let receivedEncoding: string = '';
|
|
2139
|
+
const writable = new Writable({
|
|
2140
|
+
write(_chunk, encoding, cb) {
|
|
2141
|
+
receivedEncoding = encoding;
|
|
2142
|
+
cb();
|
|
2143
|
+
}
|
|
2144
|
+
});
|
|
2145
|
+
writable.write('data', 'utf8');
|
|
2146
|
+
// When decodeStrings=true, strings are converted to Buffers
|
|
2147
|
+
expect(receivedEncoding).toBe('buffer');
|
|
2148
|
+
});
|
|
2149
|
+
|
|
2150
|
+
await it('setDefaultEncoding should set encoding when decodeStrings=false', async () => {
|
|
2151
|
+
let receivedEncoding: string = '';
|
|
2152
|
+
const writable = new Writable({
|
|
2153
|
+
decodeStrings: false,
|
|
2154
|
+
write(_chunk, encoding, cb) {
|
|
2155
|
+
receivedEncoding = encoding;
|
|
2156
|
+
cb();
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
writable.setDefaultEncoding('ascii');
|
|
2160
|
+
writable.write('data');
|
|
2161
|
+
expect(receivedEncoding).toBe('ascii');
|
|
2162
|
+
});
|
|
2163
|
+
});
|
|
2164
|
+
|
|
2165
|
+
// ==================== finished (extended) ====================
|
|
2166
|
+
|
|
2167
|
+
await describe('finished (extended)', async () => {
|
|
2168
|
+
await it('should work with already-destroyed stream', async () => {
|
|
2169
|
+
const readable = new Readable({ read() {} });
|
|
2170
|
+
readable.destroy();
|
|
2171
|
+
const err = await new Promise<Error | undefined>((resolve) => {
|
|
2172
|
+
finished(readable, (e) => resolve(e || undefined));
|
|
2173
|
+
});
|
|
2174
|
+
expect(err).toBeDefined();
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
await it('should work with already-ended readable', async () => {
|
|
2178
|
+
const readable = new Readable({
|
|
2179
|
+
read() { this.push(null); }
|
|
2180
|
+
});
|
|
2181
|
+
readable.resume();
|
|
2182
|
+
await new Promise<void>((resolve) => readable.on('end', () => resolve()));
|
|
2183
|
+
|
|
2184
|
+
// Stream is already ended
|
|
2185
|
+
await new Promise<void>((resolve) => {
|
|
2186
|
+
finished(readable, () => resolve());
|
|
2187
|
+
});
|
|
2188
|
+
expect(readable.readableEnded).toBeTruthy();
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
await it('should work with already-finished writable', async () => {
|
|
2192
|
+
const writable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
2193
|
+
writable.end();
|
|
2194
|
+
await new Promise<void>((resolve) => writable.on('finish', () => resolve()));
|
|
2195
|
+
|
|
2196
|
+
// Stream is already finished
|
|
2197
|
+
await new Promise<void>((resolve) => {
|
|
2198
|
+
finished(writable, () => resolve());
|
|
2199
|
+
});
|
|
2200
|
+
expect(writable.writableFinished).toBeTruthy();
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
await it('should handle error on Duplex', async () => {
|
|
2204
|
+
const duplex = new Duplex({
|
|
2205
|
+
read() {},
|
|
2206
|
+
write(_c, _e, cb) { cb(); }
|
|
2207
|
+
});
|
|
2208
|
+
const err = await new Promise<Error>((resolve) => {
|
|
2209
|
+
finished(duplex, (e) => { if (e) resolve(e); });
|
|
2210
|
+
duplex.destroy(new Error('duplex error'));
|
|
2211
|
+
});
|
|
2212
|
+
expect(err.message).toBe('duplex error');
|
|
2213
|
+
});
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
// ==================== PassThrough (extended) ====================
|
|
2217
|
+
|
|
2218
|
+
await describe('PassThrough (extended)', async () => {
|
|
2219
|
+
await it('should preserve encoding', async () => {
|
|
2220
|
+
const pt = new PassThrough();
|
|
2221
|
+
pt.setEncoding('utf8');
|
|
2222
|
+
const result = await new Promise<string>((resolve) => {
|
|
2223
|
+
pt.on('data', (chunk) => resolve(chunk));
|
|
2224
|
+
pt.write(Buffer.from('hello'));
|
|
2225
|
+
});
|
|
2226
|
+
expect(typeof result).toBe('string');
|
|
2227
|
+
expect(result).toBe('hello');
|
|
2228
|
+
});
|
|
2229
|
+
|
|
2230
|
+
await it('should be pipeable in a chain', async () => {
|
|
2231
|
+
const pt1 = new PassThrough();
|
|
2232
|
+
const pt2 = new PassThrough();
|
|
2233
|
+
const pt3 = new PassThrough();
|
|
2234
|
+
const chunks: string[] = [];
|
|
2235
|
+
pt3.on('data', (chunk) => chunks.push(String(chunk)));
|
|
2236
|
+
|
|
2237
|
+
pt1.pipe(pt2).pipe(pt3);
|
|
2238
|
+
pt1.write('chained');
|
|
2239
|
+
pt1.end();
|
|
2240
|
+
|
|
2241
|
+
await new Promise<void>((resolve) => pt3.on('end', () => resolve()));
|
|
2242
|
+
expect(chunks.length).toBe(1);
|
|
2243
|
+
expect(chunks[0]).toBe('chained');
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
await it('should handle highWaterMark', async () => {
|
|
2247
|
+
const pt = new PassThrough({ highWaterMark: 2 });
|
|
2248
|
+
const result = pt.write('abc'); // 3 bytes > HWM 2
|
|
2249
|
+
expect(result).toBe(false); // Should signal backpressure
|
|
2250
|
+
});
|
|
2251
|
+
});
|
|
2252
|
+
};
|