@gjsify/web-streams 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 +30 -0
- package/lib/esm/index.js +121 -0
- package/lib/esm/queuing-strategies.js +56 -0
- package/lib/esm/readable-stream.js +1064 -0
- package/lib/esm/text-decoder-stream.js +126 -0
- package/lib/esm/text-encoder-stream.js +46 -0
- package/lib/esm/transform-stream.js +336 -0
- package/lib/esm/util.js +161 -0
- package/lib/esm/writable-stream.js +676 -0
- package/lib/types/index.d.ts +77 -0
- package/lib/types/queuing-strategies.d.ts +18 -0
- package/lib/types/readable-stream.d.ts +61 -0
- package/lib/types/text-decoder-stream.d.ts +16 -0
- package/lib/types/text-encoder-stream.d.ts +15 -0
- package/lib/types/transform-stream.d.ts +21 -0
- package/lib/types/util.d.ts +40 -0
- package/lib/types/writable-stream.d.ts +49 -0
- package/package.json +44 -0
- package/src/index.spec.ts +2043 -0
- package/src/index.ts +131 -0
- package/src/queuing-strategies.ts +67 -0
- package/src/readable-stream.ts +1337 -0
- package/src/test.mts +6 -0
- package/src/text-decoder-stream.ts +183 -0
- package/src/text-encoder-stream.ts +62 -0
- package/src/transform-stream.ts +410 -0
- package/src/util.ts +170 -0
- package/src/writable-stream.ts +773 -0
- package/tsconfig.json +32 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,2043 @@
|
|
|
1
|
+
// Tests for WHATWG Streams API
|
|
2
|
+
// Ported from refs/wpt/streams/ and refs/deno/tests/unit/streams_test.ts
|
|
3
|
+
// Original: 3-Clause BSD license (WPT), MIT license (Deno)
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
6
|
+
import {
|
|
7
|
+
WritableStream,
|
|
8
|
+
ReadableStream,
|
|
9
|
+
TransformStream,
|
|
10
|
+
ByteLengthQueuingStrategy,
|
|
11
|
+
CountQueuingStrategy,
|
|
12
|
+
TextEncoderStream,
|
|
13
|
+
TextDecoderStream,
|
|
14
|
+
} from 'node:stream/web';
|
|
15
|
+
|
|
16
|
+
export default async () => {
|
|
17
|
+
|
|
18
|
+
// ==================== ByteLengthQueuingStrategy ====================
|
|
19
|
+
|
|
20
|
+
await describe('ByteLengthQueuingStrategy', async () => {
|
|
21
|
+
await it('should be a constructor', async () => {
|
|
22
|
+
expect(typeof ByteLengthQueuingStrategy).toBe('function');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await it('should require init object', async () => {
|
|
26
|
+
let threw = false;
|
|
27
|
+
try { new ByteLengthQueuingStrategy(null as any); } catch { threw = true; }
|
|
28
|
+
expect(threw).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await it('should have highWaterMark', async () => {
|
|
32
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 1024 });
|
|
33
|
+
expect(strategy.highWaterMark).toBe(1024);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await it('should have size function returning byteLength', async () => {
|
|
37
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 1 });
|
|
38
|
+
const chunk = new Uint8Array(42);
|
|
39
|
+
expect(strategy.size(chunk)).toBe(42);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await it('should coerce highWaterMark to number', async () => {
|
|
43
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: '10' as any });
|
|
44
|
+
expect(strategy.highWaterMark).toBe(10);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await it('should handle highWaterMark of 0', async () => {
|
|
48
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 0 });
|
|
49
|
+
expect(strategy.highWaterMark).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await it('should return byteLength for ArrayBuffer', async () => {
|
|
53
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 1 });
|
|
54
|
+
const buf = new ArrayBuffer(64);
|
|
55
|
+
expect(strategy.size(buf as any)).toBe(64);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await it('should return byteLength for DataView', async () => {
|
|
59
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 1 });
|
|
60
|
+
const dv = new DataView(new ArrayBuffer(16));
|
|
61
|
+
expect(strategy.size(dv as any)).toBe(16);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await it('size function name should be "size"', async () => {
|
|
65
|
+
const strategy = new ByteLengthQueuingStrategy({ highWaterMark: 1 });
|
|
66
|
+
expect(strategy.size.name).toBe('size');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ==================== CountQueuingStrategy ====================
|
|
71
|
+
|
|
72
|
+
await describe('CountQueuingStrategy', async () => {
|
|
73
|
+
await it('should be a constructor', async () => {
|
|
74
|
+
expect(typeof CountQueuingStrategy).toBe('function');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await it('should have highWaterMark', async () => {
|
|
78
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: 5 });
|
|
79
|
+
expect(strategy.highWaterMark).toBe(5);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await it('should have size function returning 1', async () => {
|
|
83
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: 1 });
|
|
84
|
+
expect(strategy.size('anything')).toBe(1);
|
|
85
|
+
expect(strategy.size(42)).toBe(1);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await it('should coerce highWaterMark to number', async () => {
|
|
89
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: '3' as any });
|
|
90
|
+
expect(strategy.highWaterMark).toBe(3);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await it('should handle highWaterMark of 0', async () => {
|
|
94
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: 0 });
|
|
95
|
+
expect(strategy.highWaterMark).toBe(0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await it('size should return 1 for undefined', async () => {
|
|
99
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: 1 });
|
|
100
|
+
expect(strategy.size(undefined)).toBe(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await it('size should return 1 for null', async () => {
|
|
104
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: 1 });
|
|
105
|
+
expect(strategy.size(null)).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await it('size function name should be "size"', async () => {
|
|
109
|
+
const strategy = new CountQueuingStrategy({ highWaterMark: 1 });
|
|
110
|
+
expect(strategy.size.name).toBe('size');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await it('should require init object', async () => {
|
|
114
|
+
let threw = false;
|
|
115
|
+
try { new CountQueuingStrategy(null as any); } catch { threw = true; }
|
|
116
|
+
expect(threw).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ==================== WritableStream ====================
|
|
121
|
+
|
|
122
|
+
await describe('WritableStream', async () => {
|
|
123
|
+
await it('should be a constructor', async () => {
|
|
124
|
+
expect(typeof WritableStream).toBe('function');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await it('should be constructable with no arguments', async () => {
|
|
128
|
+
const ws = new WritableStream();
|
|
129
|
+
expect(ws).toBeDefined();
|
|
130
|
+
expect(ws.locked).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await it('should accept an underlying sink', async () => {
|
|
134
|
+
const chunks: string[] = [];
|
|
135
|
+
const ws = new WritableStream({
|
|
136
|
+
write(chunk: string) {
|
|
137
|
+
chunks.push(chunk);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
expect(ws).toBeDefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
await it('should write and close', async () => {
|
|
144
|
+
const chunks: string[] = [];
|
|
145
|
+
const ws = new WritableStream({
|
|
146
|
+
write(chunk: string) { chunks.push(chunk); },
|
|
147
|
+
});
|
|
148
|
+
const writer = ws.getWriter();
|
|
149
|
+
await writer.write('hello');
|
|
150
|
+
await writer.write('world');
|
|
151
|
+
await writer.close();
|
|
152
|
+
expect(chunks.length).toBe(2);
|
|
153
|
+
expect(chunks[0]).toBe('hello');
|
|
154
|
+
expect(chunks[1]).toBe('world');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await it('should be locked when writer is acquired', async () => {
|
|
158
|
+
const ws = new WritableStream();
|
|
159
|
+
expect(ws.locked).toBe(false);
|
|
160
|
+
const writer = ws.getWriter();
|
|
161
|
+
expect(ws.locked).toBe(true);
|
|
162
|
+
writer.releaseLock();
|
|
163
|
+
expect(ws.locked).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await it('should throw when getting second writer', async () => {
|
|
167
|
+
const ws = new WritableStream();
|
|
168
|
+
ws.getWriter();
|
|
169
|
+
let threw = false;
|
|
170
|
+
try { ws.getWriter(); } catch { threw = true; }
|
|
171
|
+
expect(threw).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await it('should abort', async () => {
|
|
175
|
+
let abortReason: any;
|
|
176
|
+
const ws = new WritableStream({
|
|
177
|
+
abort(reason: any) { abortReason = reason; },
|
|
178
|
+
});
|
|
179
|
+
await ws.abort('test reason');
|
|
180
|
+
expect(abortReason).toBe('test reason');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await it('should support backpressure via desiredSize', async () => {
|
|
184
|
+
const ws = new WritableStream({
|
|
185
|
+
write() {
|
|
186
|
+
return new Promise((resolve) => setTimeout(resolve, 10));
|
|
187
|
+
},
|
|
188
|
+
}, new CountQueuingStrategy({ highWaterMark: 2 }));
|
|
189
|
+
const writer = ws.getWriter();
|
|
190
|
+
expect(writer.desiredSize).toBe(2);
|
|
191
|
+
writer.write('a');
|
|
192
|
+
expect(writer.desiredSize).toBe(1);
|
|
193
|
+
writer.write('b');
|
|
194
|
+
expect(writer.desiredSize).toBe(0);
|
|
195
|
+
await writer.close();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await it('should handle write errors', async () => {
|
|
199
|
+
const ws = new WritableStream({
|
|
200
|
+
write() { throw new Error('write failed'); },
|
|
201
|
+
});
|
|
202
|
+
const writer = ws.getWriter();
|
|
203
|
+
let error: any;
|
|
204
|
+
try {
|
|
205
|
+
await writer.write('data');
|
|
206
|
+
} catch (e) {
|
|
207
|
+
error = e;
|
|
208
|
+
}
|
|
209
|
+
expect(error).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await it('writer.closed should resolve on close', async () => {
|
|
213
|
+
const ws = new WritableStream();
|
|
214
|
+
const writer = ws.getWriter();
|
|
215
|
+
const closedPromise = writer.closed;
|
|
216
|
+
await writer.close();
|
|
217
|
+
await closedPromise;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await it('writer.ready should resolve when writable', async () => {
|
|
221
|
+
const ws = new WritableStream();
|
|
222
|
+
const writer = ws.getWriter();
|
|
223
|
+
await writer.ready;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await it('should have correct Symbol.toStringTag', async () => {
|
|
227
|
+
const ws = new WritableStream();
|
|
228
|
+
// Check the polyfill has it (native may vary)
|
|
229
|
+
expect(typeof ws).toBe('object');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
await it('should reject abort on locked stream', async () => {
|
|
233
|
+
const ws = new WritableStream();
|
|
234
|
+
ws.getWriter();
|
|
235
|
+
let threw = false;
|
|
236
|
+
try { await ws.abort(); } catch { threw = true; }
|
|
237
|
+
expect(threw).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await it('should reject close on locked stream', async () => {
|
|
241
|
+
const ws = new WritableStream();
|
|
242
|
+
ws.getWriter();
|
|
243
|
+
let threw = false;
|
|
244
|
+
try { await ws.close(); } catch { threw = true; }
|
|
245
|
+
expect(threw).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
await it('should call start callback on construction', async () => {
|
|
249
|
+
let startCalled = false;
|
|
250
|
+
const ws = new WritableStream({
|
|
251
|
+
start() { startCalled = true; },
|
|
252
|
+
});
|
|
253
|
+
expect(startCalled).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await it('should call close callback', async () => {
|
|
257
|
+
let closeCalled = false;
|
|
258
|
+
const ws = new WritableStream({
|
|
259
|
+
close() { closeCalled = true; },
|
|
260
|
+
});
|
|
261
|
+
const writer = ws.getWriter();
|
|
262
|
+
await writer.close();
|
|
263
|
+
expect(closeCalled).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
await it('should write various types of chunks', async () => {
|
|
267
|
+
const chunks: unknown[] = [];
|
|
268
|
+
const ws = new WritableStream({
|
|
269
|
+
write(chunk: unknown) { chunks.push(chunk); },
|
|
270
|
+
});
|
|
271
|
+
const writer = ws.getWriter();
|
|
272
|
+
await writer.write(42);
|
|
273
|
+
await writer.write(null);
|
|
274
|
+
await writer.write(true);
|
|
275
|
+
await writer.write({ key: 'val' });
|
|
276
|
+
await writer.close();
|
|
277
|
+
expect(chunks.length).toBe(4);
|
|
278
|
+
expect(chunks[0]).toBe(42);
|
|
279
|
+
expect(chunks[1]).toBeNull();
|
|
280
|
+
expect(chunks[2]).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await it('should handle async write function', async () => {
|
|
284
|
+
const chunks: string[] = [];
|
|
285
|
+
const ws = new WritableStream({
|
|
286
|
+
async write(chunk: string) {
|
|
287
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
288
|
+
chunks.push(chunk);
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
const writer = ws.getWriter();
|
|
292
|
+
await writer.write('async1');
|
|
293
|
+
await writer.write('async2');
|
|
294
|
+
await writer.close();
|
|
295
|
+
expect(chunks.length).toBe(2);
|
|
296
|
+
expect(chunks[0]).toBe('async1');
|
|
297
|
+
expect(chunks[1]).toBe('async2');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
await it('writer.abort should put stream in errored state', async () => {
|
|
301
|
+
const ws = new WritableStream();
|
|
302
|
+
const writer = ws.getWriter();
|
|
303
|
+
await writer.abort('reason');
|
|
304
|
+
let threw = false;
|
|
305
|
+
try {
|
|
306
|
+
await writer.write('after abort');
|
|
307
|
+
} catch {
|
|
308
|
+
threw = true;
|
|
309
|
+
}
|
|
310
|
+
expect(threw).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await it('abort without reason should still abort', async () => {
|
|
314
|
+
let abortCalled = false;
|
|
315
|
+
const ws = new WritableStream({
|
|
316
|
+
abort() { abortCalled = true; },
|
|
317
|
+
});
|
|
318
|
+
await ws.abort();
|
|
319
|
+
expect(abortCalled).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
await it('should accept ByteLengthQueuingStrategy', async () => {
|
|
323
|
+
const ws = new WritableStream({
|
|
324
|
+
write() {},
|
|
325
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: 1024 }));
|
|
326
|
+
const writer = ws.getWriter();
|
|
327
|
+
await writer.ready;
|
|
328
|
+
await writer.close();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await it('writer desiredSize should be null after close', async () => {
|
|
332
|
+
const ws = new WritableStream();
|
|
333
|
+
const writer = ws.getWriter();
|
|
334
|
+
await writer.close();
|
|
335
|
+
expect(writer.desiredSize).toBe(0);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ==================== WritableStreamDefaultWriter ====================
|
|
340
|
+
|
|
341
|
+
await describe('WritableStreamDefaultWriter', async () => {
|
|
342
|
+
await it('should have write, close, abort, releaseLock methods', async () => {
|
|
343
|
+
const ws = new WritableStream();
|
|
344
|
+
const writer = ws.getWriter();
|
|
345
|
+
expect(typeof writer.write).toBe('function');
|
|
346
|
+
expect(typeof writer.close).toBe('function');
|
|
347
|
+
expect(typeof writer.abort).toBe('function');
|
|
348
|
+
expect(typeof writer.releaseLock).toBe('function');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await it('should have closed and ready promises', async () => {
|
|
352
|
+
const ws = new WritableStream();
|
|
353
|
+
const writer = ws.getWriter();
|
|
354
|
+
expect(writer.closed).toBeDefined();
|
|
355
|
+
expect(writer.ready).toBeDefined();
|
|
356
|
+
expect(typeof writer.closed.then).toBe('function');
|
|
357
|
+
expect(typeof writer.ready.then).toBe('function');
|
|
358
|
+
await writer.close();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await it('releaseLock should make stream unlocked', async () => {
|
|
362
|
+
const ws = new WritableStream();
|
|
363
|
+
const writer = ws.getWriter();
|
|
364
|
+
expect(ws.locked).toBe(true);
|
|
365
|
+
writer.releaseLock();
|
|
366
|
+
expect(ws.locked).toBe(false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await it('releaseLock should be idempotent after release', async () => {
|
|
370
|
+
const ws = new WritableStream();
|
|
371
|
+
const writer = ws.getWriter();
|
|
372
|
+
writer.releaseLock();
|
|
373
|
+
// Calling again should not throw
|
|
374
|
+
writer.releaseLock();
|
|
375
|
+
expect(ws.locked).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
await it('desiredSize should reflect queue state', async () => {
|
|
379
|
+
const ws = new WritableStream({
|
|
380
|
+
write() {
|
|
381
|
+
return new Promise((resolve) => setTimeout(resolve, 50));
|
|
382
|
+
},
|
|
383
|
+
}, new CountQueuingStrategy({ highWaterMark: 3 }));
|
|
384
|
+
const writer = ws.getWriter();
|
|
385
|
+
expect(writer.desiredSize).toBe(3);
|
|
386
|
+
await writer.close();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await it('ready should resolve immediately when no backpressure', async () => {
|
|
390
|
+
const ws = new WritableStream({}, new CountQueuingStrategy({ highWaterMark: 10 }));
|
|
391
|
+
const writer = ws.getWriter();
|
|
392
|
+
// Should resolve without delay
|
|
393
|
+
await writer.ready;
|
|
394
|
+
await writer.close();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await it('closed should reject after abort', async () => {
|
|
398
|
+
const ws = new WritableStream();
|
|
399
|
+
const writer = ws.getWriter();
|
|
400
|
+
await writer.abort('test');
|
|
401
|
+
let threw = false;
|
|
402
|
+
try {
|
|
403
|
+
await writer.closed;
|
|
404
|
+
} catch {
|
|
405
|
+
threw = true;
|
|
406
|
+
}
|
|
407
|
+
expect(threw).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await it('write after close should reject', async () => {
|
|
411
|
+
const ws = new WritableStream();
|
|
412
|
+
const writer = ws.getWriter();
|
|
413
|
+
await writer.close();
|
|
414
|
+
let threw = false;
|
|
415
|
+
try {
|
|
416
|
+
await writer.write('after close');
|
|
417
|
+
} catch {
|
|
418
|
+
threw = true;
|
|
419
|
+
}
|
|
420
|
+
expect(threw).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
await it('close after close should reject', async () => {
|
|
424
|
+
const ws = new WritableStream();
|
|
425
|
+
const writer = ws.getWriter();
|
|
426
|
+
await writer.close();
|
|
427
|
+
let threw = false;
|
|
428
|
+
try {
|
|
429
|
+
await writer.close();
|
|
430
|
+
} catch {
|
|
431
|
+
threw = true;
|
|
432
|
+
}
|
|
433
|
+
expect(threw).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
await it('can acquire new writer after releaseLock', async () => {
|
|
437
|
+
const ws = new WritableStream();
|
|
438
|
+
const writer1 = ws.getWriter();
|
|
439
|
+
writer1.releaseLock();
|
|
440
|
+
const writer2 = ws.getWriter();
|
|
441
|
+
expect(ws.locked).toBe(true);
|
|
442
|
+
await writer2.close();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// ==================== ReadableStream ====================
|
|
447
|
+
|
|
448
|
+
await describe('ReadableStream', async () => {
|
|
449
|
+
await it('should be a constructor', async () => {
|
|
450
|
+
expect(typeof ReadableStream).toBe('function');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await it('should be constructable with no arguments', async () => {
|
|
454
|
+
const rs = new ReadableStream();
|
|
455
|
+
expect(rs).toBeDefined();
|
|
456
|
+
expect(rs.locked).toBe(false);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await it('should accept an underlying source', async () => {
|
|
460
|
+
const rs = new ReadableStream({
|
|
461
|
+
start(controller: any) {
|
|
462
|
+
controller.enqueue('hello');
|
|
463
|
+
controller.close();
|
|
464
|
+
},
|
|
465
|
+
});
|
|
466
|
+
expect(rs).toBeDefined();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
await it('should read chunks from source', async () => {
|
|
470
|
+
const rs = new ReadableStream({
|
|
471
|
+
start(controller: any) {
|
|
472
|
+
controller.enqueue('a');
|
|
473
|
+
controller.enqueue('b');
|
|
474
|
+
controller.close();
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
const reader = rs.getReader();
|
|
478
|
+
const r1 = await reader.read();
|
|
479
|
+
expect(r1.done).toBe(false);
|
|
480
|
+
expect(r1.value).toBe('a');
|
|
481
|
+
const r2 = await reader.read();
|
|
482
|
+
expect(r2.done).toBe(false);
|
|
483
|
+
expect(r2.value).toBe('b');
|
|
484
|
+
const r3 = await reader.read();
|
|
485
|
+
expect(r3.done).toBe(true);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
await it('should be locked when reader is acquired', async () => {
|
|
489
|
+
const rs = new ReadableStream();
|
|
490
|
+
expect(rs.locked).toBe(false);
|
|
491
|
+
const reader = rs.getReader();
|
|
492
|
+
expect(rs.locked).toBe(true);
|
|
493
|
+
reader.releaseLock();
|
|
494
|
+
expect(rs.locked).toBe(false);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await it('should cancel', async () => {
|
|
498
|
+
let cancelReason: any;
|
|
499
|
+
const rs = new ReadableStream({
|
|
500
|
+
cancel(reason: any) { cancelReason = reason; },
|
|
501
|
+
});
|
|
502
|
+
await rs.cancel('test');
|
|
503
|
+
expect(cancelReason).toBe('test');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
await it('should support tee', async () => {
|
|
507
|
+
const rs = new ReadableStream({
|
|
508
|
+
start(controller: any) {
|
|
509
|
+
controller.enqueue(1);
|
|
510
|
+
controller.enqueue(2);
|
|
511
|
+
controller.close();
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
const [branch1, branch2] = rs.tee();
|
|
515
|
+
const reader1 = branch1.getReader();
|
|
516
|
+
const reader2 = branch2.getReader();
|
|
517
|
+
|
|
518
|
+
const r1 = await reader1.read();
|
|
519
|
+
expect(r1.value).toBe(1);
|
|
520
|
+
const r2 = await reader2.read();
|
|
521
|
+
expect(r2.value).toBe(1);
|
|
522
|
+
|
|
523
|
+
const r3 = await reader1.read();
|
|
524
|
+
expect(r3.value).toBe(2);
|
|
525
|
+
const r4 = await reader2.read();
|
|
526
|
+
expect(r4.value).toBe(2);
|
|
527
|
+
|
|
528
|
+
const r5 = await reader1.read();
|
|
529
|
+
expect(r5.done).toBe(true);
|
|
530
|
+
const r6 = await reader2.read();
|
|
531
|
+
expect(r6.done).toBe(true);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
await it('should support async iteration', async () => {
|
|
535
|
+
const rs = new ReadableStream({
|
|
536
|
+
start(controller: any) {
|
|
537
|
+
controller.enqueue('x');
|
|
538
|
+
controller.enqueue('y');
|
|
539
|
+
controller.close();
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
const collected: string[] = [];
|
|
543
|
+
for await (const chunk of rs) {
|
|
544
|
+
collected.push(chunk as string);
|
|
545
|
+
}
|
|
546
|
+
expect(collected.length).toBe(2);
|
|
547
|
+
expect(collected[0]).toBe('x');
|
|
548
|
+
expect(collected[1]).toBe('y');
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
await it('should pipeTo a WritableStream', async () => {
|
|
552
|
+
const chunks: string[] = [];
|
|
553
|
+
const rs = new ReadableStream({
|
|
554
|
+
start(controller: any) {
|
|
555
|
+
controller.enqueue('hello');
|
|
556
|
+
controller.enqueue('world');
|
|
557
|
+
controller.close();
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
const ws = new WritableStream({
|
|
561
|
+
write(chunk: string) { chunks.push(chunk); },
|
|
562
|
+
});
|
|
563
|
+
await rs.pipeTo(ws);
|
|
564
|
+
expect(chunks.length).toBe(2);
|
|
565
|
+
expect(chunks[0]).toBe('hello');
|
|
566
|
+
expect(chunks[1]).toBe('world');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
await it('should support ReadableStream.from with array', async () => {
|
|
570
|
+
const rs = ReadableStream.from(['a', 'b', 'c']);
|
|
571
|
+
const reader = rs.getReader();
|
|
572
|
+
const r1 = await reader.read();
|
|
573
|
+
expect(r1.value).toBe('a');
|
|
574
|
+
const r2 = await reader.read();
|
|
575
|
+
expect(r2.value).toBe('b');
|
|
576
|
+
const r3 = await reader.read();
|
|
577
|
+
expect(r3.value).toBe('c');
|
|
578
|
+
const r4 = await reader.read();
|
|
579
|
+
expect(r4.done).toBe(true);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
await it('should support ReadableStream.from with async generator', async () => {
|
|
583
|
+
async function* gen() {
|
|
584
|
+
yield 1;
|
|
585
|
+
yield 2;
|
|
586
|
+
yield 3;
|
|
587
|
+
}
|
|
588
|
+
const rs = ReadableStream.from(gen());
|
|
589
|
+
const reader = rs.getReader();
|
|
590
|
+
const r1 = await reader.read();
|
|
591
|
+
expect(r1.value).toBe(1);
|
|
592
|
+
const r2 = await reader.read();
|
|
593
|
+
expect(r2.value).toBe(2);
|
|
594
|
+
const r3 = await reader.read();
|
|
595
|
+
expect(r3.value).toBe(3);
|
|
596
|
+
const r4 = await reader.read();
|
|
597
|
+
expect(r4.done).toBe(true);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
await it('reader.closed should resolve on stream close', async () => {
|
|
601
|
+
const rs = new ReadableStream({
|
|
602
|
+
start(controller: any) { controller.close(); },
|
|
603
|
+
});
|
|
604
|
+
const reader = rs.getReader();
|
|
605
|
+
await reader.closed;
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
await it('should handle pull-based source', async () => {
|
|
609
|
+
let callCount = 0;
|
|
610
|
+
const rs = new ReadableStream({
|
|
611
|
+
pull(controller: any) {
|
|
612
|
+
callCount++;
|
|
613
|
+
if (callCount <= 3) {
|
|
614
|
+
controller.enqueue(callCount);
|
|
615
|
+
} else {
|
|
616
|
+
controller.close();
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
const reader = rs.getReader();
|
|
621
|
+
const r1 = await reader.read();
|
|
622
|
+
expect(r1.value).toBe(1);
|
|
623
|
+
const r2 = await reader.read();
|
|
624
|
+
expect(r2.value).toBe(2);
|
|
625
|
+
const r3 = await reader.read();
|
|
626
|
+
expect(r3.value).toBe(3);
|
|
627
|
+
const r4 = await reader.read();
|
|
628
|
+
expect(r4.done).toBe(true);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
await it('should throw when getting second reader', async () => {
|
|
632
|
+
const rs = new ReadableStream();
|
|
633
|
+
rs.getReader();
|
|
634
|
+
let threw = false;
|
|
635
|
+
try { rs.getReader(); } catch { threw = true; }
|
|
636
|
+
expect(threw).toBe(true);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
await it('should reject cancel on locked stream', async () => {
|
|
640
|
+
const rs = new ReadableStream();
|
|
641
|
+
rs.getReader();
|
|
642
|
+
let threw = false;
|
|
643
|
+
try { await rs.cancel(); } catch { threw = true; }
|
|
644
|
+
expect(threw).toBe(true);
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
await it('tee should lock the original stream', async () => {
|
|
648
|
+
const rs = new ReadableStream({
|
|
649
|
+
start(controller: any) { controller.close(); },
|
|
650
|
+
});
|
|
651
|
+
rs.tee();
|
|
652
|
+
expect(rs.locked).toBe(true);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
await it('should read single chunk and close', async () => {
|
|
656
|
+
const rs = new ReadableStream({
|
|
657
|
+
start(controller: any) {
|
|
658
|
+
controller.enqueue('only');
|
|
659
|
+
controller.close();
|
|
660
|
+
},
|
|
661
|
+
});
|
|
662
|
+
const reader = rs.getReader();
|
|
663
|
+
const r1 = await reader.read();
|
|
664
|
+
expect(r1.done).toBe(false);
|
|
665
|
+
expect(r1.value).toBe('only');
|
|
666
|
+
const r2 = await reader.read();
|
|
667
|
+
expect(r2.done).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
await it('should read an immediately closed stream', async () => {
|
|
671
|
+
const rs = new ReadableStream({
|
|
672
|
+
start(controller: any) { controller.close(); },
|
|
673
|
+
});
|
|
674
|
+
const reader = rs.getReader();
|
|
675
|
+
const r1 = await reader.read();
|
|
676
|
+
expect(r1.done).toBe(true);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
await it('cancel should resolve the cancel promise', async () => {
|
|
680
|
+
const rs = new ReadableStream({
|
|
681
|
+
start() {},
|
|
682
|
+
});
|
|
683
|
+
// cancel should resolve without error
|
|
684
|
+
await rs.cancel();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
await it('cancel should pass reason to underlying source cancel', async () => {
|
|
688
|
+
let receivedReason: any;
|
|
689
|
+
const rs = new ReadableStream({
|
|
690
|
+
cancel(reason: any) { receivedReason = reason; },
|
|
691
|
+
});
|
|
692
|
+
const err = new Error('cancel reason');
|
|
693
|
+
await rs.cancel(err);
|
|
694
|
+
expect(receivedReason).toBe(err);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
await it('pipeTo should close writable on readable end', async () => {
|
|
698
|
+
let writableClosed = false;
|
|
699
|
+
const rs = new ReadableStream({
|
|
700
|
+
start(controller: any) {
|
|
701
|
+
controller.enqueue('data');
|
|
702
|
+
controller.close();
|
|
703
|
+
},
|
|
704
|
+
});
|
|
705
|
+
const ws = new WritableStream({
|
|
706
|
+
write() {},
|
|
707
|
+
close() { writableClosed = true; },
|
|
708
|
+
});
|
|
709
|
+
await rs.pipeTo(ws);
|
|
710
|
+
expect(writableClosed).toBe(true);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
await it('pipeTo should propagate readable error to writable', async () => {
|
|
714
|
+
const rs = new ReadableStream({
|
|
715
|
+
start(controller: any) {
|
|
716
|
+
controller.error(new Error('source error'));
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
const ws = new WritableStream();
|
|
720
|
+
let threw = false;
|
|
721
|
+
try {
|
|
722
|
+
await rs.pipeTo(ws);
|
|
723
|
+
} catch {
|
|
724
|
+
threw = true;
|
|
725
|
+
}
|
|
726
|
+
expect(threw).toBe(true);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
await it('pipeThrough should chain transforms', async () => {
|
|
730
|
+
const rs = new ReadableStream({
|
|
731
|
+
start(controller: any) {
|
|
732
|
+
controller.enqueue('hello');
|
|
733
|
+
controller.close();
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const upper = new TransformStream({
|
|
738
|
+
transform(chunk: string, controller: any) {
|
|
739
|
+
controller.enqueue(chunk.toUpperCase());
|
|
740
|
+
},
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const exclaim = new TransformStream({
|
|
744
|
+
transform(chunk: string, controller: any) {
|
|
745
|
+
controller.enqueue(chunk + '!');
|
|
746
|
+
},
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
const result = rs.pipeThrough(upper).pipeThrough(exclaim);
|
|
750
|
+
const reader = result.getReader();
|
|
751
|
+
const r1 = await reader.read();
|
|
752
|
+
expect(r1.value).toBe('HELLO!');
|
|
753
|
+
const r2 = await reader.read();
|
|
754
|
+
expect(r2.done).toBe(true);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
await it('ReadableStream.from with sync generator', async () => {
|
|
758
|
+
function* gen() {
|
|
759
|
+
yield 'a';
|
|
760
|
+
yield 'b';
|
|
761
|
+
}
|
|
762
|
+
const rs = ReadableStream.from(gen());
|
|
763
|
+
const reader = rs.getReader();
|
|
764
|
+
const r1 = await reader.read();
|
|
765
|
+
expect(r1.value).toBe('a');
|
|
766
|
+
const r2 = await reader.read();
|
|
767
|
+
expect(r2.value).toBe('b');
|
|
768
|
+
const r3 = await reader.read();
|
|
769
|
+
expect(r3.done).toBe(true);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
await it('should enqueue typed arrays', async () => {
|
|
773
|
+
const rs = new ReadableStream({
|
|
774
|
+
start(controller: any) {
|
|
775
|
+
controller.enqueue(new Uint8Array([1, 2, 3]));
|
|
776
|
+
controller.close();
|
|
777
|
+
},
|
|
778
|
+
});
|
|
779
|
+
const reader = rs.getReader();
|
|
780
|
+
const r1 = await reader.read();
|
|
781
|
+
expect(r1.done).toBe(false);
|
|
782
|
+
expect(r1.value.length).toBe(3);
|
|
783
|
+
expect(r1.value[0]).toBe(1);
|
|
784
|
+
expect(r1.value[2]).toBe(3);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
await it('should support pull returning a promise', async () => {
|
|
788
|
+
let n = 0;
|
|
789
|
+
const rs = new ReadableStream({
|
|
790
|
+
async pull(controller: any) {
|
|
791
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
792
|
+
n++;
|
|
793
|
+
if (n <= 2) {
|
|
794
|
+
controller.enqueue(n);
|
|
795
|
+
} else {
|
|
796
|
+
controller.close();
|
|
797
|
+
}
|
|
798
|
+
},
|
|
799
|
+
});
|
|
800
|
+
const reader = rs.getReader();
|
|
801
|
+
const r1 = await reader.read();
|
|
802
|
+
expect(r1.value).toBe(1);
|
|
803
|
+
const r2 = await reader.read();
|
|
804
|
+
expect(r2.value).toBe(2);
|
|
805
|
+
const r3 = await reader.read();
|
|
806
|
+
expect(r3.done).toBe(true);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
await it('tee both branches should be independent', async () => {
|
|
810
|
+
const rs = new ReadableStream({
|
|
811
|
+
start(controller: any) {
|
|
812
|
+
controller.enqueue('a');
|
|
813
|
+
controller.enqueue('b');
|
|
814
|
+
controller.enqueue('c');
|
|
815
|
+
controller.close();
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
const [b1, b2] = rs.tee();
|
|
819
|
+
|
|
820
|
+
// Read all from branch1
|
|
821
|
+
const r1: string[] = [];
|
|
822
|
+
const reader1 = b1.getReader();
|
|
823
|
+
let result = await reader1.read();
|
|
824
|
+
while (!result.done) {
|
|
825
|
+
r1.push(result.value as string);
|
|
826
|
+
result = await reader1.read();
|
|
827
|
+
}
|
|
828
|
+
expect(r1.length).toBe(3);
|
|
829
|
+
expect(r1[0]).toBe('a');
|
|
830
|
+
|
|
831
|
+
// Branch 2 should still be fully readable
|
|
832
|
+
const r2: string[] = [];
|
|
833
|
+
const reader2 = b2.getReader();
|
|
834
|
+
result = await reader2.read();
|
|
835
|
+
while (!result.done) {
|
|
836
|
+
r2.push(result.value as string);
|
|
837
|
+
result = await reader2.read();
|
|
838
|
+
}
|
|
839
|
+
expect(r2.length).toBe(3);
|
|
840
|
+
expect(r2[2]).toBe('c');
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
await it('pipeTo with preventClose should not close writable', async () => {
|
|
844
|
+
let closeCalled = false;
|
|
845
|
+
const rs = new ReadableStream({
|
|
846
|
+
start(controller: any) {
|
|
847
|
+
controller.enqueue('data');
|
|
848
|
+
controller.close();
|
|
849
|
+
},
|
|
850
|
+
});
|
|
851
|
+
const ws = new WritableStream({
|
|
852
|
+
write() {},
|
|
853
|
+
close() { closeCalled = true; },
|
|
854
|
+
});
|
|
855
|
+
await rs.pipeTo(ws, { preventClose: true });
|
|
856
|
+
expect(closeCalled).toBe(false);
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// ==================== ReadableStreamDefaultReader ====================
|
|
861
|
+
|
|
862
|
+
await describe('ReadableStreamDefaultReader', async () => {
|
|
863
|
+
await it('should have read, releaseLock, cancel methods', async () => {
|
|
864
|
+
const rs = new ReadableStream();
|
|
865
|
+
const reader = rs.getReader();
|
|
866
|
+
expect(typeof reader.read).toBe('function');
|
|
867
|
+
expect(typeof reader.releaseLock).toBe('function');
|
|
868
|
+
expect(typeof reader.cancel).toBe('function');
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
await it('should have closed promise', async () => {
|
|
872
|
+
const rs = new ReadableStream({
|
|
873
|
+
start(controller: any) { controller.close(); },
|
|
874
|
+
});
|
|
875
|
+
const reader = rs.getReader();
|
|
876
|
+
expect(reader.closed).toBeDefined();
|
|
877
|
+
expect(typeof reader.closed.then).toBe('function');
|
|
878
|
+
await reader.closed;
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
await it('read should return { value, done } objects', async () => {
|
|
882
|
+
const rs = new ReadableStream({
|
|
883
|
+
start(controller: any) {
|
|
884
|
+
controller.enqueue(42);
|
|
885
|
+
controller.close();
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
const reader = rs.getReader();
|
|
889
|
+
const result = await reader.read();
|
|
890
|
+
expect(result.done).toBe(false);
|
|
891
|
+
expect(result.value).toBe(42);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
await it('read after stream end should return done: true', async () => {
|
|
895
|
+
const rs = new ReadableStream({
|
|
896
|
+
start(controller: any) { controller.close(); },
|
|
897
|
+
});
|
|
898
|
+
const reader = rs.getReader();
|
|
899
|
+
const r1 = await reader.read();
|
|
900
|
+
expect(r1.done).toBe(true);
|
|
901
|
+
// Reading again should still return done
|
|
902
|
+
const r2 = await reader.read();
|
|
903
|
+
expect(r2.done).toBe(true);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
await it('cancel should cancel the underlying stream', async () => {
|
|
907
|
+
let cancelCalled = false;
|
|
908
|
+
const rs = new ReadableStream({
|
|
909
|
+
cancel() { cancelCalled = true; },
|
|
910
|
+
});
|
|
911
|
+
const reader = rs.getReader();
|
|
912
|
+
await reader.cancel();
|
|
913
|
+
expect(cancelCalled).toBe(true);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
await it('cancel should pass reason to underlying source', async () => {
|
|
917
|
+
let receivedReason: any;
|
|
918
|
+
const rs = new ReadableStream({
|
|
919
|
+
cancel(reason: any) { receivedReason = reason; },
|
|
920
|
+
});
|
|
921
|
+
const reader = rs.getReader();
|
|
922
|
+
await reader.cancel('reader cancel');
|
|
923
|
+
expect(receivedReason).toBe('reader cancel');
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
await it('releaseLock should unlock the stream', async () => {
|
|
927
|
+
const rs = new ReadableStream({
|
|
928
|
+
start(controller: any) { controller.close(); },
|
|
929
|
+
});
|
|
930
|
+
const reader = rs.getReader();
|
|
931
|
+
expect(rs.locked).toBe(true);
|
|
932
|
+
reader.releaseLock();
|
|
933
|
+
expect(rs.locked).toBe(false);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
await it('should be able to re-acquire reader after release', async () => {
|
|
937
|
+
const rs = new ReadableStream({
|
|
938
|
+
start(controller: any) {
|
|
939
|
+
controller.enqueue('data');
|
|
940
|
+
controller.close();
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
const reader1 = rs.getReader();
|
|
944
|
+
reader1.releaseLock();
|
|
945
|
+
const reader2 = rs.getReader();
|
|
946
|
+
const result = await reader2.read();
|
|
947
|
+
expect(result.value).toBe('data');
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
await it('closed should resolve when stream closes', async () => {
|
|
951
|
+
const rs = new ReadableStream({
|
|
952
|
+
start(controller: any) {
|
|
953
|
+
controller.enqueue('x');
|
|
954
|
+
controller.close();
|
|
955
|
+
},
|
|
956
|
+
});
|
|
957
|
+
const reader = rs.getReader();
|
|
958
|
+
// Drain the stream
|
|
959
|
+
await reader.read();
|
|
960
|
+
await reader.read();
|
|
961
|
+
await reader.closed;
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
await it('closed should reject when stream errors', async () => {
|
|
965
|
+
const rs = new ReadableStream({
|
|
966
|
+
start(controller: any) {
|
|
967
|
+
controller.error(new Error('boom'));
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
const reader = rs.getReader();
|
|
971
|
+
let threw = false;
|
|
972
|
+
try {
|
|
973
|
+
await reader.closed;
|
|
974
|
+
} catch {
|
|
975
|
+
threw = true;
|
|
976
|
+
}
|
|
977
|
+
expect(threw).toBe(true);
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
await it('read should reject on errored stream', async () => {
|
|
981
|
+
const rs = new ReadableStream({
|
|
982
|
+
start(controller: any) {
|
|
983
|
+
controller.error(new Error('read error'));
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
const reader = rs.getReader();
|
|
987
|
+
let threw = false;
|
|
988
|
+
try {
|
|
989
|
+
await reader.read();
|
|
990
|
+
} catch {
|
|
991
|
+
threw = true;
|
|
992
|
+
}
|
|
993
|
+
expect(threw).toBe(true);
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
// ==================== TransformStream ====================
|
|
998
|
+
|
|
999
|
+
await describe('TransformStream', async () => {
|
|
1000
|
+
await it('should be a constructor', async () => {
|
|
1001
|
+
expect(typeof TransformStream).toBe('function');
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
await it('should be constructable with no arguments', async () => {
|
|
1005
|
+
const ts = new TransformStream();
|
|
1006
|
+
expect(ts).toBeDefined();
|
|
1007
|
+
expect(ts.readable).toBeDefined();
|
|
1008
|
+
expect(ts.writable).toBeDefined();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
await it('should pass through chunks by default', async () => {
|
|
1012
|
+
const ts = new TransformStream();
|
|
1013
|
+
const writer = ts.writable.getWriter();
|
|
1014
|
+
const reader = ts.readable.getReader();
|
|
1015
|
+
|
|
1016
|
+
writer.write('hello');
|
|
1017
|
+
writer.close();
|
|
1018
|
+
|
|
1019
|
+
const r1 = await reader.read();
|
|
1020
|
+
expect(r1.value).toBe('hello');
|
|
1021
|
+
const r2 = await reader.read();
|
|
1022
|
+
expect(r2.done).toBe(true);
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
await it('should transform chunks', async () => {
|
|
1026
|
+
const ts = new TransformStream({
|
|
1027
|
+
transform(chunk: string, controller: any) {
|
|
1028
|
+
controller.enqueue(chunk.toUpperCase());
|
|
1029
|
+
},
|
|
1030
|
+
});
|
|
1031
|
+
const writer = ts.writable.getWriter();
|
|
1032
|
+
const reader = ts.readable.getReader();
|
|
1033
|
+
|
|
1034
|
+
writer.write('hello');
|
|
1035
|
+
writer.write('world');
|
|
1036
|
+
writer.close();
|
|
1037
|
+
|
|
1038
|
+
const r1 = await reader.read();
|
|
1039
|
+
expect(r1.value).toBe('HELLO');
|
|
1040
|
+
const r2 = await reader.read();
|
|
1041
|
+
expect(r2.value).toBe('WORLD');
|
|
1042
|
+
const r3 = await reader.read();
|
|
1043
|
+
expect(r3.done).toBe(true);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
await it('should support flush', async () => {
|
|
1047
|
+
let flushed = false;
|
|
1048
|
+
const ts = new TransformStream({
|
|
1049
|
+
flush() { flushed = true; },
|
|
1050
|
+
});
|
|
1051
|
+
const writer = ts.writable.getWriter();
|
|
1052
|
+
const reader = ts.readable.getReader();
|
|
1053
|
+
|
|
1054
|
+
writer.close();
|
|
1055
|
+
// Read to drain the stream
|
|
1056
|
+
await reader.read();
|
|
1057
|
+
expect(flushed).toBe(true);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
await it('should work with pipeThrough', async () => {
|
|
1061
|
+
const rs = new ReadableStream({
|
|
1062
|
+
start(controller: any) {
|
|
1063
|
+
controller.enqueue(1);
|
|
1064
|
+
controller.enqueue(2);
|
|
1065
|
+
controller.enqueue(3);
|
|
1066
|
+
controller.close();
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const doubled = rs.pipeThrough(new TransformStream({
|
|
1071
|
+
transform(chunk: number, controller: any) {
|
|
1072
|
+
controller.enqueue(chunk * 2);
|
|
1073
|
+
},
|
|
1074
|
+
}));
|
|
1075
|
+
|
|
1076
|
+
const reader = doubled.getReader();
|
|
1077
|
+
const r1 = await reader.read();
|
|
1078
|
+
expect(r1.value).toBe(2);
|
|
1079
|
+
const r2 = await reader.read();
|
|
1080
|
+
expect(r2.value).toBe(4);
|
|
1081
|
+
const r3 = await reader.read();
|
|
1082
|
+
expect(r3.value).toBe(6);
|
|
1083
|
+
const r4 = await reader.read();
|
|
1084
|
+
expect(r4.done).toBe(true);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
await it('should support start callback', async () => {
|
|
1088
|
+
let startCalled = false;
|
|
1089
|
+
const ts = new TransformStream({
|
|
1090
|
+
start() { startCalled = true; },
|
|
1091
|
+
});
|
|
1092
|
+
expect(startCalled).toBe(true);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
await it('should propagate transform errors to readable', async () => {
|
|
1096
|
+
const ts = new TransformStream({
|
|
1097
|
+
transform() { throw new Error('transform error'); },
|
|
1098
|
+
});
|
|
1099
|
+
const writer = ts.writable.getWriter();
|
|
1100
|
+
const reader = ts.readable.getReader();
|
|
1101
|
+
|
|
1102
|
+
// Write triggers the transform — error propagates to reader
|
|
1103
|
+
writer.write('data').catch(() => {});
|
|
1104
|
+
let readError: any;
|
|
1105
|
+
try {
|
|
1106
|
+
await reader.read();
|
|
1107
|
+
} catch (e) {
|
|
1108
|
+
readError = e;
|
|
1109
|
+
}
|
|
1110
|
+
expect(readError).toBeDefined();
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
await it('controller.desiredSize should reflect backpressure', async () => {
|
|
1114
|
+
let savedController: any;
|
|
1115
|
+
const ts = new TransformStream({
|
|
1116
|
+
start(controller: any) { savedController = controller; },
|
|
1117
|
+
transform(chunk: any, controller: any) { controller.enqueue(chunk); },
|
|
1118
|
+
});
|
|
1119
|
+
// desiredSize should be available
|
|
1120
|
+
expect(typeof savedController.desiredSize).toBe('number');
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
await it('controller.terminate should close readable', async () => {
|
|
1124
|
+
let savedController: any;
|
|
1125
|
+
const ts = new TransformStream({
|
|
1126
|
+
start(controller: any) { savedController = controller; },
|
|
1127
|
+
});
|
|
1128
|
+
const reader = ts.readable.getReader();
|
|
1129
|
+
savedController.terminate();
|
|
1130
|
+
const result = await reader.read();
|
|
1131
|
+
expect(result.done).toBe(true);
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
await it('should pass multiple chunks unchanged with identity transform', async () => {
|
|
1135
|
+
const ts = new TransformStream();
|
|
1136
|
+
const writer = ts.writable.getWriter();
|
|
1137
|
+
const reader = ts.readable.getReader();
|
|
1138
|
+
|
|
1139
|
+
writer.write(1);
|
|
1140
|
+
writer.write('two');
|
|
1141
|
+
writer.write(null);
|
|
1142
|
+
writer.close();
|
|
1143
|
+
|
|
1144
|
+
const r1 = await reader.read();
|
|
1145
|
+
expect(r1.value).toBe(1);
|
|
1146
|
+
const r2 = await reader.read();
|
|
1147
|
+
expect(r2.value).toBe('two');
|
|
1148
|
+
const r3 = await reader.read();
|
|
1149
|
+
expect(r3.value).toBeNull();
|
|
1150
|
+
const r4 = await reader.read();
|
|
1151
|
+
expect(r4.done).toBe(true);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
await it('transform can enqueue multiple chunks per input', async () => {
|
|
1155
|
+
const ts = new TransformStream({
|
|
1156
|
+
transform(chunk: string, controller: any) {
|
|
1157
|
+
for (const ch of chunk) {
|
|
1158
|
+
controller.enqueue(ch);
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
});
|
|
1162
|
+
const writer = ts.writable.getWriter();
|
|
1163
|
+
const reader = ts.readable.getReader();
|
|
1164
|
+
|
|
1165
|
+
writer.write('abc');
|
|
1166
|
+
writer.close();
|
|
1167
|
+
|
|
1168
|
+
const r1 = await reader.read();
|
|
1169
|
+
expect(r1.value).toBe('a');
|
|
1170
|
+
const r2 = await reader.read();
|
|
1171
|
+
expect(r2.value).toBe('b');
|
|
1172
|
+
const r3 = await reader.read();
|
|
1173
|
+
expect(r3.value).toBe('c');
|
|
1174
|
+
const r4 = await reader.read();
|
|
1175
|
+
expect(r4.done).toBe(true);
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
await it('transform can enqueue zero chunks (filter)', async () => {
|
|
1179
|
+
const ts = new TransformStream({
|
|
1180
|
+
transform(chunk: number, controller: any) {
|
|
1181
|
+
if (chunk % 2 === 0) {
|
|
1182
|
+
controller.enqueue(chunk);
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
});
|
|
1186
|
+
const writer = ts.writable.getWriter();
|
|
1187
|
+
const reader = ts.readable.getReader();
|
|
1188
|
+
|
|
1189
|
+
writer.write(1);
|
|
1190
|
+
writer.write(2);
|
|
1191
|
+
writer.write(3);
|
|
1192
|
+
writer.write(4);
|
|
1193
|
+
writer.close();
|
|
1194
|
+
|
|
1195
|
+
const r1 = await reader.read();
|
|
1196
|
+
expect(r1.value).toBe(2);
|
|
1197
|
+
const r2 = await reader.read();
|
|
1198
|
+
expect(r2.value).toBe(4);
|
|
1199
|
+
const r3 = await reader.read();
|
|
1200
|
+
expect(r3.done).toBe(true);
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
await it('flush can enqueue final chunks', async () => {
|
|
1204
|
+
const ts = new TransformStream({
|
|
1205
|
+
transform(chunk: string, controller: any) {
|
|
1206
|
+
controller.enqueue(chunk);
|
|
1207
|
+
},
|
|
1208
|
+
flush(controller: any) {
|
|
1209
|
+
controller.enqueue('END');
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
const writer = ts.writable.getWriter();
|
|
1213
|
+
const reader = ts.readable.getReader();
|
|
1214
|
+
|
|
1215
|
+
writer.write('data');
|
|
1216
|
+
writer.close();
|
|
1217
|
+
|
|
1218
|
+
const r1 = await reader.read();
|
|
1219
|
+
expect(r1.value).toBe('data');
|
|
1220
|
+
const r2 = await reader.read();
|
|
1221
|
+
expect(r2.value).toBe('END');
|
|
1222
|
+
const r3 = await reader.read();
|
|
1223
|
+
expect(r3.done).toBe(true);
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
await it('controller.error should error both sides', async () => {
|
|
1227
|
+
let savedController: any;
|
|
1228
|
+
const ts = new TransformStream({
|
|
1229
|
+
start(controller: any) { savedController = controller; },
|
|
1230
|
+
});
|
|
1231
|
+
const writer = ts.writable.getWriter();
|
|
1232
|
+
const reader = ts.readable.getReader();
|
|
1233
|
+
|
|
1234
|
+
savedController.error(new Error('controller error'));
|
|
1235
|
+
|
|
1236
|
+
let readThrew = false;
|
|
1237
|
+
try { await reader.read(); } catch { readThrew = true; }
|
|
1238
|
+
expect(readThrew).toBe(true);
|
|
1239
|
+
|
|
1240
|
+
let writeThrew = false;
|
|
1241
|
+
try { await writer.write('data'); } catch { writeThrew = true; }
|
|
1242
|
+
expect(writeThrew).toBe(true);
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
await it('readable and writable should be correct types', async () => {
|
|
1246
|
+
const ts = new TransformStream();
|
|
1247
|
+
expect(ts.readable.locked).toBe(false);
|
|
1248
|
+
expect(ts.writable.locked).toBe(false);
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
await it('should accept writableStrategy highWaterMark', async () => {
|
|
1252
|
+
const ts = new TransformStream({}, { highWaterMark: 5 });
|
|
1253
|
+
const writer = ts.writable.getWriter();
|
|
1254
|
+
expect(writer.desiredSize).toBe(5);
|
|
1255
|
+
await writer.close();
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
await it('should accept readableStrategy highWaterMark', async () => {
|
|
1259
|
+
const ts = new TransformStream({}, {}, { highWaterMark: 10 });
|
|
1260
|
+
const reader = ts.readable.getReader();
|
|
1261
|
+
// Just verify it doesn't throw
|
|
1262
|
+
reader.releaseLock();
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
await it('flush error should error the readable', async () => {
|
|
1266
|
+
const ts = new TransformStream({
|
|
1267
|
+
flush() { throw new Error('flush error'); },
|
|
1268
|
+
});
|
|
1269
|
+
const writer = ts.writable.getWriter();
|
|
1270
|
+
const reader = ts.readable.getReader();
|
|
1271
|
+
|
|
1272
|
+
writer.close().catch(() => {});
|
|
1273
|
+
let threw = false;
|
|
1274
|
+
try {
|
|
1275
|
+
await reader.read();
|
|
1276
|
+
} catch {
|
|
1277
|
+
threw = true;
|
|
1278
|
+
}
|
|
1279
|
+
expect(threw).toBe(true);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
await it('async transform should work', async () => {
|
|
1283
|
+
const ts = new TransformStream({
|
|
1284
|
+
async transform(chunk: number, controller: any) {
|
|
1285
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
1286
|
+
controller.enqueue(chunk * 10);
|
|
1287
|
+
},
|
|
1288
|
+
});
|
|
1289
|
+
const writer = ts.writable.getWriter();
|
|
1290
|
+
const reader = ts.readable.getReader();
|
|
1291
|
+
|
|
1292
|
+
writer.write(1);
|
|
1293
|
+
writer.write(2);
|
|
1294
|
+
writer.close();
|
|
1295
|
+
|
|
1296
|
+
const r1 = await reader.read();
|
|
1297
|
+
expect(r1.value).toBe(10);
|
|
1298
|
+
const r2 = await reader.read();
|
|
1299
|
+
expect(r2.value).toBe(20);
|
|
1300
|
+
const r3 = await reader.read();
|
|
1301
|
+
expect(r3.done).toBe(true);
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
// ==================== Error handling ====================
|
|
1306
|
+
|
|
1307
|
+
await describe('Error handling', async () => {
|
|
1308
|
+
await it('writing to a closed writable should reject', async () => {
|
|
1309
|
+
const ws = new WritableStream();
|
|
1310
|
+
const writer = ws.getWriter();
|
|
1311
|
+
await writer.close();
|
|
1312
|
+
let threw = false;
|
|
1313
|
+
try { await writer.write('data'); } catch { threw = true; }
|
|
1314
|
+
expect(threw).toBe(true);
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
await it('reading from an errored readable should reject', async () => {
|
|
1318
|
+
const err = new Error('stream error');
|
|
1319
|
+
const rs = new ReadableStream({
|
|
1320
|
+
start(controller: any) { controller.error(err); },
|
|
1321
|
+
});
|
|
1322
|
+
const reader = rs.getReader();
|
|
1323
|
+
let threw = false;
|
|
1324
|
+
try { await reader.read(); } catch { threw = true; }
|
|
1325
|
+
expect(threw).toBe(true);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
await it('writer closed promise should reject on error', async () => {
|
|
1329
|
+
const ws = new WritableStream({
|
|
1330
|
+
write() { throw new Error('write err'); },
|
|
1331
|
+
});
|
|
1332
|
+
const writer = ws.getWriter();
|
|
1333
|
+
writer.write('trigger').catch(() => {});
|
|
1334
|
+
let threw = false;
|
|
1335
|
+
try { await writer.closed; } catch { threw = true; }
|
|
1336
|
+
expect(threw).toBe(true);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
await it('reader closed promise should reject on error', async () => {
|
|
1340
|
+
const rs = new ReadableStream({
|
|
1341
|
+
start(controller: any) { controller.error(new Error('err')); },
|
|
1342
|
+
});
|
|
1343
|
+
const reader = rs.getReader();
|
|
1344
|
+
let threw = false;
|
|
1345
|
+
try { await reader.closed; } catch { threw = true; }
|
|
1346
|
+
expect(threw).toBe(true);
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
await it('abort on already errored stream should resolve', async () => {
|
|
1350
|
+
const ws = new WritableStream({
|
|
1351
|
+
write() { throw new Error('fail'); },
|
|
1352
|
+
});
|
|
1353
|
+
const writer = ws.getWriter();
|
|
1354
|
+
try { await writer.write('data'); } catch { /* expected */ }
|
|
1355
|
+
// abort after error should not throw
|
|
1356
|
+
await writer.abort();
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
await it('pipeTo should reject when writable errors during pipe', async () => {
|
|
1360
|
+
const rs = new ReadableStream({
|
|
1361
|
+
start(controller: any) {
|
|
1362
|
+
controller.enqueue('chunk1');
|
|
1363
|
+
controller.enqueue('chunk2');
|
|
1364
|
+
controller.close();
|
|
1365
|
+
},
|
|
1366
|
+
});
|
|
1367
|
+
const ws = new WritableStream({
|
|
1368
|
+
write() { throw new Error('write during pipe'); },
|
|
1369
|
+
});
|
|
1370
|
+
let threw = false;
|
|
1371
|
+
try { await rs.pipeTo(ws); } catch { threw = true; }
|
|
1372
|
+
expect(threw).toBe(true);
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
await it('controller.error during start should error the readable', async () => {
|
|
1376
|
+
const rs = new ReadableStream({
|
|
1377
|
+
start(controller: any) {
|
|
1378
|
+
controller.error(new TypeError('start error'));
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
const reader = rs.getReader();
|
|
1382
|
+
let threw = false;
|
|
1383
|
+
let errorType = '';
|
|
1384
|
+
try {
|
|
1385
|
+
await reader.read();
|
|
1386
|
+
} catch (e: any) {
|
|
1387
|
+
threw = true;
|
|
1388
|
+
errorType = e?.constructor?.name || '';
|
|
1389
|
+
}
|
|
1390
|
+
expect(threw).toBe(true);
|
|
1391
|
+
expect(errorType).toBe('TypeError');
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
await it('abort reason should be available in writer.closed rejection', async () => {
|
|
1395
|
+
const ws = new WritableStream();
|
|
1396
|
+
const writer = ws.getWriter();
|
|
1397
|
+
const reason = new Error('abort reason');
|
|
1398
|
+
writer.abort(reason);
|
|
1399
|
+
let caughtReason: any;
|
|
1400
|
+
try { await writer.closed; } catch (e) { caughtReason = e; }
|
|
1401
|
+
expect(caughtReason).toBeDefined();
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
// ==================== Helpers for stream tests ====================
|
|
1406
|
+
// Native Web Streams use backpressure, so writes and reads must run
|
|
1407
|
+
// concurrently. These helpers collect all output from a readable side
|
|
1408
|
+
// while a write function feeds the writable side.
|
|
1409
|
+
|
|
1410
|
+
async function collectEncoderOutput(
|
|
1411
|
+
stream: InstanceType<typeof TextEncoderStream>,
|
|
1412
|
+
writeFn: (writer: WritableStreamDefaultWriter<string>) => Promise<void>,
|
|
1413
|
+
): Promise<Uint8Array> {
|
|
1414
|
+
const writer = stream.writable.getWriter();
|
|
1415
|
+
const reader = stream.readable.getReader();
|
|
1416
|
+
|
|
1417
|
+
const chunks: Uint8Array[] = [];
|
|
1418
|
+
const [, ] = await Promise.all([
|
|
1419
|
+
writeFn(writer),
|
|
1420
|
+
(async () => {
|
|
1421
|
+
while (true) {
|
|
1422
|
+
const { value, done } = await reader.read();
|
|
1423
|
+
if (done) break;
|
|
1424
|
+
chunks.push(value);
|
|
1425
|
+
}
|
|
1426
|
+
})(),
|
|
1427
|
+
]);
|
|
1428
|
+
const result = new Uint8Array(chunks.reduce((a, c) => a + c.byteLength, 0));
|
|
1429
|
+
let offset = 0;
|
|
1430
|
+
for (const chunk of chunks) {
|
|
1431
|
+
result.set(chunk, offset);
|
|
1432
|
+
offset += chunk.byteLength;
|
|
1433
|
+
}
|
|
1434
|
+
return result;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
async function collectDecoderOutput(
|
|
1438
|
+
stream: InstanceType<typeof TextDecoderStream>,
|
|
1439
|
+
writeFn: (writer: WritableStreamDefaultWriter<BufferSource>) => Promise<void>,
|
|
1440
|
+
): Promise<string> {
|
|
1441
|
+
const writer = stream.writable.getWriter();
|
|
1442
|
+
const reader = stream.readable.getReader();
|
|
1443
|
+
|
|
1444
|
+
const chunks: string[] = [];
|
|
1445
|
+
await Promise.all([
|
|
1446
|
+
writeFn(writer),
|
|
1447
|
+
(async () => {
|
|
1448
|
+
while (true) {
|
|
1449
|
+
const { value, done } = await reader.read();
|
|
1450
|
+
if (done) break;
|
|
1451
|
+
chunks.push(value);
|
|
1452
|
+
}
|
|
1453
|
+
})(),
|
|
1454
|
+
]);
|
|
1455
|
+
return chunks.join('');
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// ==================== TextEncoderStream ====================
|
|
1459
|
+
|
|
1460
|
+
await describe('TextEncoderStream', async () => {
|
|
1461
|
+
await it('should be a constructor', async () => {
|
|
1462
|
+
expect(typeof TextEncoderStream).toBe('function');
|
|
1463
|
+
const stream = new TextEncoderStream();
|
|
1464
|
+
expect(stream).toBeDefined();
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
await it('encoding should be utf-8', async () => {
|
|
1468
|
+
const stream = new TextEncoderStream();
|
|
1469
|
+
expect(stream.encoding).toBe('utf-8');
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
await it('should have readable and writable', async () => {
|
|
1473
|
+
const stream = new TextEncoderStream();
|
|
1474
|
+
expect(stream.readable).toBeDefined();
|
|
1475
|
+
expect(stream.writable).toBeDefined();
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
await it('should encode ASCII string to bytes', async () => {
|
|
1479
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1480
|
+
await w.write('hello');
|
|
1481
|
+
await w.close();
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// 'hello' = [104, 101, 108, 108, 111]
|
|
1485
|
+
expect(result.length).toBe(5);
|
|
1486
|
+
expect(result[0]).toBe(104); // 'h'
|
|
1487
|
+
expect(result[1]).toBe(101); // 'e'
|
|
1488
|
+
expect(result[2]).toBe(108); // 'l'
|
|
1489
|
+
expect(result[3]).toBe(108); // 'l'
|
|
1490
|
+
expect(result[4]).toBe(111); // 'o'
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
await it('should encode multi-byte UTF-8 characters', async () => {
|
|
1494
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1495
|
+
await w.write('€');
|
|
1496
|
+
await w.close();
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// '€' (U+20AC) in UTF-8: [0xE2, 0x82, 0xAC]
|
|
1500
|
+
expect(result.length).toBe(3);
|
|
1501
|
+
expect(result[0]).toBe(0xE2);
|
|
1502
|
+
expect(result[1]).toBe(0x82);
|
|
1503
|
+
expect(result[2]).toBe(0xAC);
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
await it('should handle multiple chunks', async () => {
|
|
1507
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1508
|
+
await w.write('ab');
|
|
1509
|
+
await w.write('cd');
|
|
1510
|
+
await w.close();
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// 'abcd'
|
|
1514
|
+
expect(result.length).toBe(4);
|
|
1515
|
+
expect(result[0]).toBe(97); // 'a'
|
|
1516
|
+
expect(result[3]).toBe(100); // 'd'
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
await it('should skip empty string chunks', async () => {
|
|
1520
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1521
|
+
await w.write('');
|
|
1522
|
+
await w.write('x');
|
|
1523
|
+
await w.close();
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
expect(result.length).toBe(1);
|
|
1527
|
+
expect(result[0]).toBe(120); // 'x'
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
await it('should handle surrogate pairs split across chunks', async () => {
|
|
1531
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1532
|
+
// U+1F600 (😀) as surrogate pair: \uD83D\uDE00, split across writes
|
|
1533
|
+
await w.write('\uD83D');
|
|
1534
|
+
await w.write('\uDE00');
|
|
1535
|
+
await w.close();
|
|
1536
|
+
});
|
|
1537
|
+
|
|
1538
|
+
// U+1F600 in UTF-8: [0xF0, 0x9F, 0x98, 0x80]
|
|
1539
|
+
expect(result[0]).toBe(0xF0);
|
|
1540
|
+
expect(result[1]).toBe(0x9F);
|
|
1541
|
+
expect(result[2]).toBe(0x98);
|
|
1542
|
+
expect(result[3]).toBe(0x80);
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
await it('should emit U+FFFD for unpaired high surrogate at end', async () => {
|
|
1546
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1547
|
+
await w.write('\uD83D');
|
|
1548
|
+
await w.close();
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
// U+FFFD in UTF-8: [0xEF, 0xBF, 0xBD]
|
|
1552
|
+
expect(result[0]).toBe(0xEF);
|
|
1553
|
+
expect(result[1]).toBe(0xBF);
|
|
1554
|
+
expect(result[2]).toBe(0xBD);
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
await it('should encode 2-byte UTF-8 characters', async () => {
|
|
1558
|
+
// U+00E9 (é) is 2-byte UTF-8: [0xC3, 0xA9]
|
|
1559
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1560
|
+
await w.write('é');
|
|
1561
|
+
await w.close();
|
|
1562
|
+
});
|
|
1563
|
+
expect(result.length).toBe(2);
|
|
1564
|
+
expect(result[0]).toBe(0xC3);
|
|
1565
|
+
expect(result[1]).toBe(0xA9);
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
await it('should encode 4-byte UTF-8 characters', async () => {
|
|
1569
|
+
// U+1F600 (😀) in UTF-8: [0xF0, 0x9F, 0x98, 0x80]
|
|
1570
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1571
|
+
await w.write('😀');
|
|
1572
|
+
await w.close();
|
|
1573
|
+
});
|
|
1574
|
+
expect(result.length).toBe(4);
|
|
1575
|
+
expect(result[0]).toBe(0xF0);
|
|
1576
|
+
expect(result[1]).toBe(0x9F);
|
|
1577
|
+
expect(result[2]).toBe(0x98);
|
|
1578
|
+
expect(result[3]).toBe(0x80);
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
await it('should encode CJK characters', async () => {
|
|
1582
|
+
// U+4E16 (世) in UTF-8: [0xE4, 0xB8, 0x96]
|
|
1583
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1584
|
+
await w.write('世');
|
|
1585
|
+
await w.close();
|
|
1586
|
+
});
|
|
1587
|
+
expect(result.length).toBe(3);
|
|
1588
|
+
expect(result[0]).toBe(0xE4);
|
|
1589
|
+
expect(result[1]).toBe(0xB8);
|
|
1590
|
+
expect(result[2]).toBe(0x96);
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
await it('should encode mixed ASCII and multi-byte', async () => {
|
|
1594
|
+
const result = await collectEncoderOutput(new TextEncoderStream(), async (w) => {
|
|
1595
|
+
await w.write('A€B');
|
|
1596
|
+
await w.close();
|
|
1597
|
+
});
|
|
1598
|
+
// 'A' (1 byte) + '€' (3 bytes) + 'B' (1 byte) = 5 bytes
|
|
1599
|
+
expect(result.length).toBe(5);
|
|
1600
|
+
expect(result[0]).toBe(65); // 'A'
|
|
1601
|
+
expect(result[4]).toBe(66); // 'B'
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
await it('should produce Uint8Array output', async () => {
|
|
1605
|
+
const stream = new TextEncoderStream();
|
|
1606
|
+
const writer = stream.writable.getWriter();
|
|
1607
|
+
const reader = stream.readable.getReader();
|
|
1608
|
+
|
|
1609
|
+
const readPromise = reader.read();
|
|
1610
|
+
await writer.write('a');
|
|
1611
|
+
await writer.close();
|
|
1612
|
+
const { value } = await readPromise;
|
|
1613
|
+
expect(value instanceof Uint8Array).toBe(true);
|
|
1614
|
+
});
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
// ==================== TextDecoderStream ====================
|
|
1618
|
+
|
|
1619
|
+
await describe('TextDecoderStream', async () => {
|
|
1620
|
+
await it('should be a constructor', async () => {
|
|
1621
|
+
expect(typeof TextDecoderStream).toBe('function');
|
|
1622
|
+
const stream = new TextDecoderStream();
|
|
1623
|
+
expect(stream).toBeDefined();
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
await it('encoding should default to utf-8', async () => {
|
|
1627
|
+
const stream = new TextDecoderStream();
|
|
1628
|
+
expect(stream.encoding).toBe('utf-8');
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
await it('fatal should default to false', async () => {
|
|
1632
|
+
const stream = new TextDecoderStream();
|
|
1633
|
+
expect(stream.fatal).toBe(false);
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
await it('ignoreBOM should default to false', async () => {
|
|
1637
|
+
const stream = new TextDecoderStream();
|
|
1638
|
+
expect(stream.ignoreBOM).toBe(false);
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
await it('should accept fatal option', async () => {
|
|
1642
|
+
const stream = new TextDecoderStream('utf-8', { fatal: true });
|
|
1643
|
+
expect(stream.fatal).toBe(true);
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
await it('should have readable and writable', async () => {
|
|
1647
|
+
const stream = new TextDecoderStream();
|
|
1648
|
+
expect(stream.readable).toBeDefined();
|
|
1649
|
+
expect(stream.writable).toBeDefined();
|
|
1650
|
+
});
|
|
1651
|
+
|
|
1652
|
+
await it('should decode ASCII bytes to string', async () => {
|
|
1653
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1654
|
+
await w.write(new Uint8Array([104, 101, 108, 108, 111]));
|
|
1655
|
+
await w.close();
|
|
1656
|
+
});
|
|
1657
|
+
expect(result).toBe('hello');
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
await it('should decode multi-byte UTF-8', async () => {
|
|
1661
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1662
|
+
await w.write(new Uint8Array([0xE2, 0x82, 0xAC]));
|
|
1663
|
+
await w.close();
|
|
1664
|
+
});
|
|
1665
|
+
expect(result).toBe('€');
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
await it('should handle multi-byte sequence split across chunks', async () => {
|
|
1669
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1670
|
+
// '€' in UTF-8: [0xE2, 0x82, 0xAC] — split across two writes
|
|
1671
|
+
await w.write(new Uint8Array([0xE2]));
|
|
1672
|
+
await w.write(new Uint8Array([0x82, 0xAC]));
|
|
1673
|
+
await w.close();
|
|
1674
|
+
});
|
|
1675
|
+
expect(result).toBe('€');
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
await it('should handle 4-byte sequence split across chunks', async () => {
|
|
1679
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1680
|
+
// U+1F600 (😀) in UTF-8: [0xF0, 0x9F, 0x98, 0x80]
|
|
1681
|
+
await w.write(new Uint8Array([0xF0, 0x9F]));
|
|
1682
|
+
await w.write(new Uint8Array([0x98, 0x80]));
|
|
1683
|
+
await w.close();
|
|
1684
|
+
});
|
|
1685
|
+
expect(result).toBe('😀');
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
await it('should handle multiple chunks', async () => {
|
|
1689
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1690
|
+
await w.write(new Uint8Array([72, 101])); // 'He'
|
|
1691
|
+
await w.write(new Uint8Array([108, 108, 111])); // 'llo'
|
|
1692
|
+
await w.close();
|
|
1693
|
+
});
|
|
1694
|
+
expect(result).toBe('Hello');
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
await it('should accept ArrayBuffer chunks', async () => {
|
|
1698
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1699
|
+
await w.write(new Uint8Array([79, 75]).buffer); // 'OK'
|
|
1700
|
+
await w.close();
|
|
1701
|
+
});
|
|
1702
|
+
expect(result).toBe('OK');
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
await it('should decode 2-byte UTF-8 characters', async () => {
|
|
1706
|
+
// U+00E9 (é): [0xC3, 0xA9]
|
|
1707
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1708
|
+
await w.write(new Uint8Array([0xC3, 0xA9]));
|
|
1709
|
+
await w.close();
|
|
1710
|
+
});
|
|
1711
|
+
expect(result).toBe('é');
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
await it('should decode 2-byte sequence split across chunks', async () => {
|
|
1715
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1716
|
+
await w.write(new Uint8Array([0xC3]));
|
|
1717
|
+
await w.write(new Uint8Array([0xA9]));
|
|
1718
|
+
await w.close();
|
|
1719
|
+
});
|
|
1720
|
+
expect(result).toBe('é');
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1723
|
+
await it('should decode 4-byte sequence split byte-by-byte', async () => {
|
|
1724
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1725
|
+
// U+1F600 byte by byte
|
|
1726
|
+
await w.write(new Uint8Array([0xF0]));
|
|
1727
|
+
await w.write(new Uint8Array([0x9F]));
|
|
1728
|
+
await w.write(new Uint8Array([0x98]));
|
|
1729
|
+
await w.write(new Uint8Array([0x80]));
|
|
1730
|
+
await w.close();
|
|
1731
|
+
});
|
|
1732
|
+
expect(result).toBe('😀');
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
await it('should decode mixed ASCII and multi-byte', async () => {
|
|
1736
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1737
|
+
// 'Aé' = [0x41, 0xC3, 0xA9]
|
|
1738
|
+
await w.write(new Uint8Array([0x41, 0xC3, 0xA9]));
|
|
1739
|
+
await w.close();
|
|
1740
|
+
});
|
|
1741
|
+
expect(result).toBe('Aé');
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
await it('should decode CJK characters', async () => {
|
|
1745
|
+
// U+4E16 (世): [0xE4, 0xB8, 0x96]
|
|
1746
|
+
// U+754C (界): [0xE7, 0x95, 0x8C]
|
|
1747
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1748
|
+
await w.write(new Uint8Array([0xE4, 0xB8, 0x96, 0xE7, 0x95, 0x8C]));
|
|
1749
|
+
await w.close();
|
|
1750
|
+
});
|
|
1751
|
+
expect(result).toBe('世界');
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
await it('should handle empty chunks', async () => {
|
|
1755
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1756
|
+
await w.write(new Uint8Array([]));
|
|
1757
|
+
await w.write(new Uint8Array([72, 105])); // 'Hi'
|
|
1758
|
+
await w.close();
|
|
1759
|
+
});
|
|
1760
|
+
expect(result).toBe('Hi');
|
|
1761
|
+
});
|
|
1762
|
+
|
|
1763
|
+
await it('should decode 3-byte sequence split at each byte', async () => {
|
|
1764
|
+
// '€' = [0xE2, 0x82, 0xAC] — each byte in separate write
|
|
1765
|
+
const result = await collectDecoderOutput(new TextDecoderStream(), async (w) => {
|
|
1766
|
+
await w.write(new Uint8Array([0xE2]));
|
|
1767
|
+
await w.write(new Uint8Array([0x82]));
|
|
1768
|
+
await w.write(new Uint8Array([0xAC]));
|
|
1769
|
+
await w.close();
|
|
1770
|
+
});
|
|
1771
|
+
expect(result).toBe('€');
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
await it('should produce string output', async () => {
|
|
1775
|
+
const stream = new TextDecoderStream();
|
|
1776
|
+
const writer = stream.writable.getWriter();
|
|
1777
|
+
const reader = stream.readable.getReader();
|
|
1778
|
+
|
|
1779
|
+
const readPromise = reader.read();
|
|
1780
|
+
await writer.write(new Uint8Array([65])); // 'A'
|
|
1781
|
+
await writer.close();
|
|
1782
|
+
const { value } = await readPromise;
|
|
1783
|
+
expect(typeof value).toBe('string');
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
// ==================== TextEncoderStream + TextDecoderStream round-trip ====================
|
|
1788
|
+
|
|
1789
|
+
await describe('TextEncoderStream + TextDecoderStream round-trip', async () => {
|
|
1790
|
+
await it('should round-trip ASCII text', async () => {
|
|
1791
|
+
const encoder = new TextEncoderStream();
|
|
1792
|
+
const decoder = new TextDecoderStream();
|
|
1793
|
+
|
|
1794
|
+
const pipePromise = encoder.readable.pipeTo(decoder.writable);
|
|
1795
|
+
const writer = encoder.writable.getWriter();
|
|
1796
|
+
const reader = decoder.readable.getReader();
|
|
1797
|
+
|
|
1798
|
+
const chunks: string[] = [];
|
|
1799
|
+
const [, ] = await Promise.all([
|
|
1800
|
+
(async () => {
|
|
1801
|
+
await writer.write('Hello, World!');
|
|
1802
|
+
await writer.close();
|
|
1803
|
+
})(),
|
|
1804
|
+
(async () => {
|
|
1805
|
+
while (true) {
|
|
1806
|
+
const { value, done } = await reader.read();
|
|
1807
|
+
if (done) break;
|
|
1808
|
+
chunks.push(value);
|
|
1809
|
+
}
|
|
1810
|
+
})(),
|
|
1811
|
+
]);
|
|
1812
|
+
await pipePromise;
|
|
1813
|
+
expect(chunks.join('')).toBe('Hello, World!');
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
await it('should round-trip Unicode text', async () => {
|
|
1817
|
+
const encoder = new TextEncoderStream();
|
|
1818
|
+
const decoder = new TextDecoderStream();
|
|
1819
|
+
|
|
1820
|
+
const pipePromise = encoder.readable.pipeTo(decoder.writable);
|
|
1821
|
+
const writer = encoder.writable.getWriter();
|
|
1822
|
+
const reader = decoder.readable.getReader();
|
|
1823
|
+
|
|
1824
|
+
const chunks: string[] = [];
|
|
1825
|
+
await Promise.all([
|
|
1826
|
+
(async () => {
|
|
1827
|
+
await writer.write('Héllo 世界 😀');
|
|
1828
|
+
await writer.close();
|
|
1829
|
+
})(),
|
|
1830
|
+
(async () => {
|
|
1831
|
+
while (true) {
|
|
1832
|
+
const { value, done } = await reader.read();
|
|
1833
|
+
if (done) break;
|
|
1834
|
+
chunks.push(value);
|
|
1835
|
+
}
|
|
1836
|
+
})(),
|
|
1837
|
+
]);
|
|
1838
|
+
await pipePromise;
|
|
1839
|
+
expect(chunks.join('')).toBe('Héllo 世界 😀');
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
await it('should round-trip with surrogate pair split across chunks', async () => {
|
|
1843
|
+
const encoder = new TextEncoderStream();
|
|
1844
|
+
const decoder = new TextDecoderStream();
|
|
1845
|
+
|
|
1846
|
+
const pipePromise = encoder.readable.pipeTo(decoder.writable);
|
|
1847
|
+
const writer = encoder.writable.getWriter();
|
|
1848
|
+
const reader = decoder.readable.getReader();
|
|
1849
|
+
|
|
1850
|
+
const chunks: string[] = [];
|
|
1851
|
+
await Promise.all([
|
|
1852
|
+
(async () => {
|
|
1853
|
+
await writer.write('A\uD83D');
|
|
1854
|
+
await writer.write('\uDE00B');
|
|
1855
|
+
await writer.close();
|
|
1856
|
+
})(),
|
|
1857
|
+
(async () => {
|
|
1858
|
+
while (true) {
|
|
1859
|
+
const { value, done } = await reader.read();
|
|
1860
|
+
if (done) break;
|
|
1861
|
+
chunks.push(value);
|
|
1862
|
+
}
|
|
1863
|
+
})(),
|
|
1864
|
+
]);
|
|
1865
|
+
await pipePromise;
|
|
1866
|
+
expect(chunks.join('')).toBe('A😀B');
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1869
|
+
await it('should round-trip multiple separate writes', async () => {
|
|
1870
|
+
const encoder = new TextEncoderStream();
|
|
1871
|
+
const decoder = new TextDecoderStream();
|
|
1872
|
+
|
|
1873
|
+
const pipePromise = encoder.readable.pipeTo(decoder.writable);
|
|
1874
|
+
const writer = encoder.writable.getWriter();
|
|
1875
|
+
const reader = decoder.readable.getReader();
|
|
1876
|
+
|
|
1877
|
+
const chunks: string[] = [];
|
|
1878
|
+
await Promise.all([
|
|
1879
|
+
(async () => {
|
|
1880
|
+
await writer.write('one');
|
|
1881
|
+
await writer.write(' ');
|
|
1882
|
+
await writer.write('two');
|
|
1883
|
+
await writer.close();
|
|
1884
|
+
})(),
|
|
1885
|
+
(async () => {
|
|
1886
|
+
while (true) {
|
|
1887
|
+
const { value, done } = await reader.read();
|
|
1888
|
+
if (done) break;
|
|
1889
|
+
chunks.push(value);
|
|
1890
|
+
}
|
|
1891
|
+
})(),
|
|
1892
|
+
]);
|
|
1893
|
+
await pipePromise;
|
|
1894
|
+
expect(chunks.join('')).toBe('one two');
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
await it('should round-trip empty string', async () => {
|
|
1898
|
+
const encoder = new TextEncoderStream();
|
|
1899
|
+
const decoder = new TextDecoderStream();
|
|
1900
|
+
|
|
1901
|
+
const pipePromise = encoder.readable.pipeTo(decoder.writable);
|
|
1902
|
+
const writer = encoder.writable.getWriter();
|
|
1903
|
+
const reader = decoder.readable.getReader();
|
|
1904
|
+
|
|
1905
|
+
const chunks: string[] = [];
|
|
1906
|
+
await Promise.all([
|
|
1907
|
+
(async () => {
|
|
1908
|
+
await writer.write('');
|
|
1909
|
+
await writer.write('ok');
|
|
1910
|
+
await writer.close();
|
|
1911
|
+
})(),
|
|
1912
|
+
(async () => {
|
|
1913
|
+
while (true) {
|
|
1914
|
+
const { value, done } = await reader.read();
|
|
1915
|
+
if (done) break;
|
|
1916
|
+
chunks.push(value);
|
|
1917
|
+
}
|
|
1918
|
+
})(),
|
|
1919
|
+
]);
|
|
1920
|
+
await pipePromise;
|
|
1921
|
+
expect(chunks.join('')).toBe('ok');
|
|
1922
|
+
});
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
// ==================== Integration: piping and chaining ====================
|
|
1926
|
+
|
|
1927
|
+
await describe('Stream integration', async () => {
|
|
1928
|
+
await it('readable pipeTo writable collects all data', async () => {
|
|
1929
|
+
const data = [10, 20, 30, 40, 50];
|
|
1930
|
+
const rs = new ReadableStream({
|
|
1931
|
+
start(controller: any) {
|
|
1932
|
+
for (const d of data) controller.enqueue(d);
|
|
1933
|
+
controller.close();
|
|
1934
|
+
},
|
|
1935
|
+
});
|
|
1936
|
+
const collected: number[] = [];
|
|
1937
|
+
const ws = new WritableStream({
|
|
1938
|
+
write(chunk: number) { collected.push(chunk); },
|
|
1939
|
+
});
|
|
1940
|
+
await rs.pipeTo(ws);
|
|
1941
|
+
expect(collected.length).toBe(5);
|
|
1942
|
+
expect(collected[0]).toBe(10);
|
|
1943
|
+
expect(collected[4]).toBe(50);
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
await it('chained pipeThrough transforms', async () => {
|
|
1947
|
+
const rs = new ReadableStream({
|
|
1948
|
+
start(controller: any) {
|
|
1949
|
+
controller.enqueue(5);
|
|
1950
|
+
controller.close();
|
|
1951
|
+
},
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// First: multiply by 2
|
|
1955
|
+
const double = new TransformStream({
|
|
1956
|
+
transform(chunk: number, ctrl: any) { ctrl.enqueue(chunk * 2); },
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1959
|
+
// Second: add 1
|
|
1960
|
+
const addOne = new TransformStream({
|
|
1961
|
+
transform(chunk: number, ctrl: any) { ctrl.enqueue(chunk + 1); },
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
// Third: convert to string
|
|
1965
|
+
const toString = new TransformStream({
|
|
1966
|
+
transform(chunk: number, ctrl: any) { ctrl.enqueue(`value:${chunk}`); },
|
|
1967
|
+
});
|
|
1968
|
+
|
|
1969
|
+
const result = rs.pipeThrough(double).pipeThrough(addOne).pipeThrough(toString);
|
|
1970
|
+
const reader = result.getReader();
|
|
1971
|
+
const r = await reader.read();
|
|
1972
|
+
expect(r.value).toBe('value:11'); // (5*2)+1 = 11
|
|
1973
|
+
});
|
|
1974
|
+
|
|
1975
|
+
await it('ReadableStream.from piped through transform', async () => {
|
|
1976
|
+
const rs = ReadableStream.from([1, 2, 3]);
|
|
1977
|
+
const ts = new TransformStream({
|
|
1978
|
+
transform(chunk: number, ctrl: any) { ctrl.enqueue(chunk * 100); },
|
|
1979
|
+
});
|
|
1980
|
+
const result = rs.pipeThrough(ts);
|
|
1981
|
+
const reader = result.getReader();
|
|
1982
|
+
const r1 = await reader.read();
|
|
1983
|
+
expect(r1.value).toBe(100);
|
|
1984
|
+
const r2 = await reader.read();
|
|
1985
|
+
expect(r2.value).toBe(200);
|
|
1986
|
+
const r3 = await reader.read();
|
|
1987
|
+
expect(r3.value).toBe(300);
|
|
1988
|
+
const r4 = await reader.read();
|
|
1989
|
+
expect(r4.done).toBe(true);
|
|
1990
|
+
});
|
|
1991
|
+
|
|
1992
|
+
await it('tee and pipeTo both branches independently', async () => {
|
|
1993
|
+
const rs = new ReadableStream({
|
|
1994
|
+
start(controller: any) {
|
|
1995
|
+
controller.enqueue('x');
|
|
1996
|
+
controller.enqueue('y');
|
|
1997
|
+
controller.close();
|
|
1998
|
+
},
|
|
1999
|
+
});
|
|
2000
|
+
const [b1, b2] = rs.tee();
|
|
2001
|
+
const c1: string[] = [];
|
|
2002
|
+
const c2: string[] = [];
|
|
2003
|
+
const ws1 = new WritableStream({ write(ch: string) { c1.push(ch); } });
|
|
2004
|
+
const ws2 = new WritableStream({ write(ch: string) { c2.push(ch); } });
|
|
2005
|
+
await Promise.all([b1.pipeTo(ws1), b2.pipeTo(ws2)]);
|
|
2006
|
+
expect(c1.length).toBe(2);
|
|
2007
|
+
expect(c2.length).toBe(2);
|
|
2008
|
+
expect(c1[0]).toBe('x');
|
|
2009
|
+
expect(c2[1]).toBe('y');
|
|
2010
|
+
});
|
|
2011
|
+
|
|
2012
|
+
await it('CountQueuingStrategy with ReadableStream', async () => {
|
|
2013
|
+
const rs = new ReadableStream({
|
|
2014
|
+
start(controller: any) {
|
|
2015
|
+
controller.enqueue('a');
|
|
2016
|
+
controller.enqueue('b');
|
|
2017
|
+
controller.close();
|
|
2018
|
+
},
|
|
2019
|
+
}, new CountQueuingStrategy({ highWaterMark: 2 }));
|
|
2020
|
+
const reader = rs.getReader();
|
|
2021
|
+
const r1 = await reader.read();
|
|
2022
|
+
expect(r1.value).toBe('a');
|
|
2023
|
+
const r2 = await reader.read();
|
|
2024
|
+
expect(r2.value).toBe('b');
|
|
2025
|
+
const r3 = await reader.read();
|
|
2026
|
+
expect(r3.done).toBe(true);
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
await it('ByteLengthQueuingStrategy with WritableStream', async () => {
|
|
2030
|
+
const chunks: Uint8Array[] = [];
|
|
2031
|
+
const ws = new WritableStream({
|
|
2032
|
+
write(chunk: Uint8Array) { chunks.push(chunk); },
|
|
2033
|
+
}, new ByteLengthQueuingStrategy({ highWaterMark: 1024 }));
|
|
2034
|
+
const writer = ws.getWriter();
|
|
2035
|
+
await writer.write(new Uint8Array([1, 2, 3]));
|
|
2036
|
+
await writer.write(new Uint8Array([4, 5]));
|
|
2037
|
+
await writer.close();
|
|
2038
|
+
expect(chunks.length).toBe(2);
|
|
2039
|
+
expect(chunks[0].length).toBe(3);
|
|
2040
|
+
expect(chunks[1].length).toBe(2);
|
|
2041
|
+
});
|
|
2042
|
+
});
|
|
2043
|
+
};
|