MIRROR: javascript for 馃悳's, a tiny runtime with big ambitions
1import { test, testDeep, testThrows, summary } from './helpers.js';
2
3console.log('TransformStream / TransformStreamDefaultController Tests\n');
4
5const READABLE_NO_BACKPRESSURE = { highWaterMark: 1 };
6
7test('TS typeof', typeof TransformStream, 'function');
8test('TS toStringTag', Object.prototype.toString.call(new TransformStream()), '[object TransformStream]');
9
10testThrows('TS requires new', () => TransformStream());
11testThrows('TS rejects readableType', () => new TransformStream({ readableType: 'bytes' }));
12testThrows('TS rejects writableType', () => new TransformStream({ writableType: 'bytes' }));
13
14const ts0 = new TransformStream();
15test('TS readable is ReadableStream', ts0.readable instanceof ReadableStream, true);
16test('TS writable is WritableStream', ts0.writable instanceof WritableStream, true);
17
18testThrows('TSController cannot be constructed', () => new TransformStreamDefaultController());
19test('TSController toStringTag', typeof TransformStreamDefaultController, 'function');
20
21test('TS can be constructed with no transform', true, (() => { new TransformStream({}); return true; })());
22
23let startCtrl = null;
24const ts1 = new TransformStream({ start(c) { startCtrl = c; } });
25test('start called with controller', startCtrl !== null, true);
26test('controller toStringTag', Object.prototype.toString.call(startCtrl), '[object TransformStreamDefaultController]');
27
28async function testIdentity() {
29 const ts = new TransformStream();
30 const writer = ts.writable.getWriter();
31 writer.write('a');
32 const reader = ts.readable.getReader();
33 const result = await reader.read();
34 test('identity value', result.value, 'a');
35 test('identity done', result.done, false);
36}
37
38async function testTransformUppercase() {
39 let c;
40 const ts = new TransformStream({
41 start(controller) { c = controller; },
42 transform(chunk) { c.enqueue(chunk.toUpperCase()); }
43 });
44 const writer = ts.writable.getWriter();
45 writer.write('hello');
46 const reader = ts.readable.getReader();
47 const result = await reader.read();
48 test('uppercase transform', result.value, 'HELLO');
49}
50
51async function testTransformDoubler() {
52 let c;
53 const ts = new TransformStream({
54 start(controller) { c = controller; },
55 transform(chunk) {
56 c.enqueue(chunk.toUpperCase());
57 c.enqueue(chunk.toUpperCase());
58 }
59 });
60 const writer = ts.writable.getWriter();
61 writer.write('x');
62 const reader = ts.readable.getReader();
63 const r1 = await reader.read();
64 const r2 = await reader.read();
65 test('doubler chunk 1', r1.value, 'X');
66 test('doubler chunk 2', r2.value, 'X');
67}
68
69async function testFlush() {
70 let flushed = false;
71 const ts = new TransformStream({
72 transform() {},
73 flush() { flushed = true; }
74 });
75 await ts.writable.getWriter().close();
76 test('flush called on close', flushed, true);
77}
78
79async function testFlushEnqueue() {
80 let c;
81 const ts = new TransformStream({
82 start(controller) { c = controller; },
83 transform() {},
84 flush() { c.enqueue('flushed'); }
85 });
86 const writer = ts.writable.getWriter();
87 writer.write('a');
88 writer.close();
89 const reader = ts.readable.getReader();
90 const r1 = await reader.read();
91 test('flush enqueue value', r1.value, 'flushed');
92}
93
94async function testCloseClosesReadable() {
95 const ts = new TransformStream({ transform() {} });
96 const writer = ts.writable.getWriter();
97 writer.close();
98 await Promise.all([writer.closed, ts.readable.getReader().closed]);
99 test('close propagates to readable', true, true);
100}
101
102async function testTransformError() {
103 const err = new Error('transform boom');
104 const ts = new TransformStream({
105 transform() { throw err; }
106 }, undefined, READABLE_NO_BACKPRESSURE);
107 const writer = ts.writable.getWriter();
108 const reader = ts.readable.getReader();
109 try {
110 await writer.write('a');
111 test('write should reject on transform error', false, true);
112 } catch (e) {
113 test('write rejects with transform error', e, err);
114 }
115 try {
116 await reader.read();
117 test('read should reject on transform error', false, true);
118 } catch (e) {
119 test('read rejects with transform error', e, err);
120 }
121}
122
123async function testFlushError() {
124 const err = new Error('flush boom');
125 const ts = new TransformStream({
126 transform() {},
127 flush() { throw err; }
128 }, undefined, READABLE_NO_BACKPRESSURE);
129 const writer = ts.writable.getWriter();
130 await writer.write('a');
131 try {
132 await writer.close();
133 test('close should reject on flush error', false, true);
134 } catch (e) {
135 test('close rejects with flush error', e, err);
136 }
137}
138
139async function testControllerError() {
140 let ctrl;
141 const ts = new TransformStream({
142 start(c) { ctrl = c; }
143 });
144 ctrl.error(new Error('controller error'));
145 const reader = ts.readable.getReader();
146 try {
147 await reader.read();
148 test('read should reject after controller.error', false, true);
149 } catch (e) {
150 test('controller.error errors readable', e.message, 'controller error');
151 }
152}
153
154async function testControllerTerminate() {
155 let ctrl;
156 const ts = new TransformStream({
157 start(c) { ctrl = c; }
158 });
159 ctrl.terminate();
160 const reader = ts.readable.getReader();
161 const result = await reader.read();
162 test('terminate closes readable', result.done, true);
163}
164
165async function testDesiredSize() {
166 let ctrl;
167 const ts = new TransformStream({
168 start(c) { ctrl = c; }
169 });
170 test('desiredSize initially 0', ctrl.desiredSize, 0);
171}
172
173async function testDefaultHWM() {
174 const ts = new TransformStream();
175 const writer = ts.writable.getWriter();
176 test('writable default HWM is 1', writer.desiredSize, 1);
177}
178
179async function testCustomWritableHWM() {
180 const ts = new TransformStream({}, { highWaterMark: 17 });
181 const writer = ts.writable.getWriter();
182 test('writable custom HWM', writer.desiredSize, 17);
183}
184
185async function testBackpressure() {
186 const ts = new TransformStream(undefined, undefined, { highWaterMark: 0 });
187 const writer = ts.writable.getWriter();
188 const reader = ts.readable.getReader();
189 const readPromise = reader.read();
190 writer.write('a');
191 const result = await readPromise;
192 test('backpressure read value', result.value, 'a');
193 test('backpressure read done', result.done, false);
194}
195
196async function testAsyncTransform() {
197 let c;
198 const ts = new TransformStream({
199 start(controller) { c = controller; },
200 transform(chunk) {
201 return new Promise(resolve => {
202 setTimeout(() => {
203 c.enqueue(chunk.toUpperCase());
204 resolve();
205 }, 10);
206 });
207 }
208 });
209 const writer = ts.writable.getWriter();
210 writer.write('a');
211 const reader = ts.readable.getReader();
212 const result = await reader.read();
213 test('async transform value', result.value, 'A');
214}
215
216async function testStartThrows() {
217 try {
218 new TransformStream({
219 start() { throw new URIError('start thrown'); }
220 });
221 test('TS ctor should throw on start error', false, true);
222 } catch (e) {
223 test('TS ctor throws start error', e instanceof URIError, true);
224 }
225}
226
227async function testCancelCallable() {
228 let cancelled = null;
229 const reason = new Error('cancel reason');
230 const ts = new TransformStream({
231 cancel(r) { cancelled = r; }
232 });
233 await ts.readable.cancel(reason);
234 test('cancel called with reason', cancelled, reason);
235}
236
237async function testAbortCallsCancel() {
238 let aborted = null;
239 const reason = new Error('abort reason');
240 const ts = new TransformStream({
241 cancel(r) { aborted = r; }
242 });
243 await ts.writable.abort(reason);
244 test('abort calls cancel with reason', aborted, reason);
245}
246
247async function testTransformThisBinding() {
248 let c;
249 const ts = new TransformStream({
250 suffix: '-suffix',
251 start(controller) { c = controller; },
252 transform(chunk) { c.enqueue(chunk + this.suffix); },
253 flush() { c.enqueue('flushed' + this.suffix); }
254 });
255 const writer = ts.writable.getWriter();
256 writer.write('a');
257 writer.close();
258 const reader = ts.readable.getReader();
259 const r1 = await reader.read();
260 test('this binding transform', r1.value, 'a-suffix');
261 const r2 = await reader.read();
262 test('this binding flush', r2.value, 'flushed-suffix');
263}
264
265async function testSubclass() {
266 class MyTS extends TransformStream {
267 myMethod() { return 42; }
268 }
269 const ts = new MyTS();
270 test('subclass instanceof', ts instanceof TransformStream, true);
271 test('subclass method', ts.myMethod(), 42);
272 test('subclass readable', ts.readable instanceof ReadableStream, true);
273}
274
275async function testReadableHWM() {
276 let ctrl;
277 new TransformStream({
278 start(c) { ctrl = c; }
279 }, undefined, { highWaterMark: 9 });
280 test('readable custom HWM desiredSize', ctrl.desiredSize, 9);
281}
282
283async function testNegativeHWMThrows() {
284 testThrows('negative writable HWM', () => new TransformStream(undefined, { highWaterMark: -1 }));
285 testThrows('negative readable HWM', () => new TransformStream(undefined, undefined, { highWaterMark: -1 }));
286 testThrows('NaN writable HWM', () => new TransformStream(undefined, { highWaterMark: NaN }));
287 testThrows('NaN readable HWM', () => new TransformStream(undefined, undefined, { highWaterMark: NaN }));
288}
289
290async function testCancelReadableErrorsWritable() {
291 const err = new Error('cancel reason');
292 const ts = new TransformStream();
293 const writer = ts.writable.getWriter();
294 ts.readable.cancel(err);
295 try {
296 await writer.closed;
297 test('writer.closed should reject after cancel', false, true);
298 } catch (e) {
299 test('writer.closed rejects with cancel reason', e, err);
300 }
301}
302
303async function testWritableStrategySize() {
304 let writableSizeCalled = false;
305 let readableSizeCalled = false;
306 const ts = new TransformStream(
307 {
308 transform(chunk, controller) {
309 controller.enqueue(chunk);
310 }
311 },
312 {
313 size() { writableSizeCalled = true; return 1; }
314 },
315 {
316 size() { readableSizeCalled = true; return 1; },
317 highWaterMark: Infinity
318 }
319 );
320 await ts.writable.getWriter().write('x');
321 test('writable size called', writableSizeCalled, true);
322 test('readable size called', readableSizeCalled, true);
323}
324
325async function testPipeThrough() {
326 const rs = new ReadableStream({
327 start(c) { c.enqueue(1); c.enqueue(2); c.enqueue(3); c.close(); }
328 });
329 const ts = new TransformStream({
330 transform(chunk, controller) {
331 controller.enqueue(chunk * 10);
332 }
333 });
334 const result = rs.pipeThrough(ts);
335 test('pipeThrough returns readable', result instanceof ReadableStream, true);
336 const reader = result.getReader();
337 const chunks = [];
338 while (true) {
339 const { value, done } = await reader.read();
340 if (done) break;
341 chunks.push(value);
342 }
343 testDeep('pipeThrough transforms data', chunks, [10, 20, 30]);
344}
345
346async function testEnqueueAfterTerminateThrows() {
347 let threw = false;
348 new TransformStream({
349 start(controller) {
350 controller.terminate();
351 try {
352 controller.enqueue('x');
353 } catch (e) {
354 threw = true;
355 }
356 }
357 });
358 test('enqueue after terminate throws', threw, true);
359}
360
361await testIdentity();
362await testTransformUppercase();
363await testTransformDoubler();
364await testFlush();
365await testFlushEnqueue();
366await testCloseClosesReadable();
367await testTransformError();
368await testFlushError();
369await testControllerError();
370await testControllerTerminate();
371await testDesiredSize();
372await testDefaultHWM();
373await testCustomWritableHWM();
374await testBackpressure();
375await testAsyncTransform();
376await testStartThrows();
377await testCancelCallable();
378await testAbortCallsCancel();
379await testTransformThisBinding();
380await testSubclass();
381await testReadableHWM();
382testNegativeHWMThrows();
383await testCancelReadableErrorsWritable();
384await testWritableStrategySize();
385await testPipeThrough();
386await testEnqueueAfterTerminateThrows();
387
388summary();