@gjsify/stream 0.4.0 → 0.4.3

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.
@@ -1,520 +0,0 @@
1
- // Transform stream test suite.
2
- //
3
- // Ported from:
4
- // refs/node-test/parallel/test-stream-transform-constructor-set-methods.js
5
- // refs/node-test/parallel/test-stream-transform-flush-data.js
6
- // refs/node-test/parallel/test-stream-transform-objectmode-falsey-value.js
7
- // refs/node-test/parallel/test-stream-transform-split-highwatermark.js
8
- // refs/node-test/parallel/test-stream-transform-split-objectmode.js
9
- // refs/node-test/parallel/test-stream-transform-hwm0.js
10
- // refs/node-test/parallel/test-stream-transform-callback-twice.js
11
- // refs/node-test/parallel/test-stream-transform-destroy.js
12
- // refs/node-test/parallel/test-stream-transform-final.js
13
- // refs/node-test/parallel/test-stream-transform-final-sync.js
14
- // Original: MIT license, Node.js contributors
15
- // Modifications: adapted to @gjsify/unit, async/await, removed common/mustCall.
16
-
17
- import { describe, it, expect } from '@gjsify/unit';
18
- import { Transform, Readable, Writable, PassThrough } from 'node:stream';
19
-
20
- export default async () => {
21
-
22
- // -------------------------------------------------------------------------
23
- // Constructor: set methods via options
24
- // -------------------------------------------------------------------------
25
- await describe('Transform: constructor options set _transform/_flush/_final', async () => {
26
- await it('throws ERR_METHOD_NOT_IMPLEMENTED when _transform is not set', async () => {
27
- const t = new Transform();
28
- let threw = false;
29
- let errCode = '';
30
- try {
31
- t.end(Buffer.from('blerg'));
32
- } catch (e: any) {
33
- threw = true;
34
- errCode = e.code;
35
- }
36
- expect(threw).toBe(true);
37
- expect(errCode).toBe('ERR_METHOD_NOT_IMPLEMENTED');
38
- });
39
-
40
- await it('options.transform / flush / final are assigned to instance', async () => {
41
- const _transform = (_chunk: any, _enc: any, next: () => void) => next();
42
- const _flush = (next: () => void) => next();
43
- const _final = (next: () => void) => next();
44
- const t = new Transform({ transform: _transform, flush: _flush, final: _final });
45
- expect(t._transform).toBe(_transform);
46
- expect(t._flush).toBe(_flush);
47
- expect(t._final).toBe(_final);
48
- });
49
-
50
- await it('end + resume works after options.transform is set', async () => {
51
- await new Promise<void>((resolve) => {
52
- const t = new Transform({
53
- transform(_chunk, _enc, next) { next(); },
54
- flush(next) { next(); },
55
- final(next) { next(); },
56
- });
57
- t.on('finish', resolve);
58
- t.end(Buffer.from('blerg'));
59
- t.resume();
60
- });
61
- });
62
- });
63
-
64
- // -------------------------------------------------------------------------
65
- // Flush can push data
66
- // -------------------------------------------------------------------------
67
- await describe('Transform: flush can push data', async () => {
68
- await it('data pushed in _flush is received as a data event', async () => {
69
- const expected = 'asdf';
70
- const t = new Transform({
71
- transform(_d, _e, n) { n(); },
72
- flush(n) { n(null, expected); },
73
- });
74
-
75
- const received: string[] = [];
76
- await new Promise<void>((resolve, reject) => {
77
- t.on('data', (chunk: Buffer) => received.push(chunk.toString()));
78
- t.on('end', resolve);
79
- t.on('error', reject);
80
- t.end(Buffer.from('blerg'));
81
- });
82
- expect(received).toEqualArray([expected]);
83
- });
84
- });
85
-
86
- // -------------------------------------------------------------------------
87
- // ObjectMode: falsy values pass through
88
- // -------------------------------------------------------------------------
89
- await describe('Transform: objectMode passes falsy values', async () => {
90
- await it('pipes -1, 0, 1..10 through objectMode PassThrough streams', async () => {
91
- const expected = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
92
- const src = new PassThrough({ objectMode: true });
93
- const tx = new PassThrough({ objectMode: true });
94
- const dest = new PassThrough({ objectMode: true });
95
-
96
- const results: number[] = [];
97
- src.pipe(tx).pipe(dest);
98
-
99
- await new Promise<void>((resolve, reject) => {
100
- dest.on('data', (x: number) => results.push(x));
101
- dest.on('end', () => {
102
- try {
103
- expect(results).toEqualArray(expected);
104
- resolve();
105
- } catch (e) {
106
- reject(e);
107
- }
108
- });
109
- dest.on('error', reject);
110
-
111
- // Write all values then end
112
- for (const v of expected) {
113
- src.write(v);
114
- }
115
- src.end();
116
- });
117
- });
118
-
119
- await it('objectMode PassThrough passes null-equivalent numbers without blocking', async () => {
120
- const pt = new PassThrough({ objectMode: true });
121
- const out: number[] = [];
122
- await new Promise<void>((resolve, reject) => {
123
- pt.on('data', (v: number) => out.push(v));
124
- pt.on('end', resolve);
125
- pt.on('error', reject);
126
- pt.write(0);
127
- pt.write(-1);
128
- pt.write(false as any);
129
- pt.end();
130
- });
131
- expect(out).toEqualArray([0, -1, false]);
132
- });
133
- });
134
-
135
- // -------------------------------------------------------------------------
136
- // Split highWaterMark options
137
- // -------------------------------------------------------------------------
138
- await describe('Transform: readableHighWaterMark / writableHighWaterMark options', async () => {
139
- await it('readableHighWaterMark sets _readableState.highWaterMark independently', async () => {
140
- const t = new Transform({
141
- readableHighWaterMark: 666,
142
- transform(_c, _e, cb) { cb(); },
143
- });
144
- expect(t._readableState.highWaterMark).toBe(666);
145
- });
146
-
147
- await it('writableHighWaterMark sets _writableState.highWaterMark independently', async () => {
148
- const t = new Transform({
149
- writableHighWaterMark: 777,
150
- transform(_c, _e, cb) { cb(); },
151
- });
152
- expect(t._writableState.highWaterMark).toBe(777);
153
- });
154
-
155
- await it('both readable and writable HWM can be set independently', async () => {
156
- const t = new Transform({
157
- readableHighWaterMark: 666,
158
- writableHighWaterMark: 777,
159
- transform(_c, _e, cb) { cb(); },
160
- });
161
- expect(t._readableState.highWaterMark).toBe(666);
162
- expect(t._writableState.highWaterMark).toBe(777);
163
- });
164
-
165
- await it('highWaterMark overrides both readable and writable HWM', async () => {
166
- const t = new Transform({
167
- highWaterMark: 555,
168
- readableHighWaterMark: 666,
169
- writableHighWaterMark: 777,
170
- transform(_c, _e, cb) { cb(); },
171
- });
172
- expect(t._readableState.highWaterMark).toBe(555);
173
- expect(t._writableState.highWaterMark).toBe(555);
174
- });
175
-
176
- await it('NaN readableHighWaterMark throws ERR_INVALID_ARG_VALUE', async () => {
177
- let threw = false;
178
- let code = '';
179
- try {
180
- new Transform({ readableHighWaterMark: NaN, transform(_c, _e, cb) { cb(); } });
181
- } catch (e: any) {
182
- threw = true;
183
- code = e.code;
184
- }
185
- expect(threw).toBe(true);
186
- expect(code).toBe('ERR_INVALID_ARG_VALUE');
187
- });
188
-
189
- await it('NaN writableHighWaterMark throws ERR_INVALID_ARG_VALUE', async () => {
190
- let threw = false;
191
- let code = '';
192
- try {
193
- new Transform({ writableHighWaterMark: NaN, transform(_c, _e, cb) { cb(); } });
194
- } catch (e: any) {
195
- threw = true;
196
- code = e.code;
197
- }
198
- expect(threw).toBe(true);
199
- expect(code).toBe('ERR_INVALID_ARG_VALUE');
200
- });
201
-
202
- await it('Readable ignores readableHighWaterMark option (uses default)', async () => {
203
- const DEFAULT = new Readable({ read() {} })._readableState.highWaterMark;
204
- // readableHighWaterMark is a Duplex/Transform-only option; passing it
205
- // to Readable should be silently ignored. Cast to bypass strict types.
206
- const r = new Readable({ readableHighWaterMark: 666, read() {} } as any);
207
- expect(r._readableState.highWaterMark).toBe(DEFAULT);
208
- });
209
-
210
- await it('Writable ignores writableHighWaterMark option (uses default)', async () => {
211
- const DEFAULT = new Writable({ write(_c, _e, cb) { cb(); } })._writableState.highWaterMark;
212
- // writableHighWaterMark is a Duplex/Transform-only option.
213
- const w = new Writable({ writableHighWaterMark: 777, write(_c, _e, cb) { cb(); } } as any);
214
- expect(w._writableState.highWaterMark).toBe(DEFAULT);
215
- });
216
- });
217
-
218
- // -------------------------------------------------------------------------
219
- // Split objectMode: readableObjectMode / writableObjectMode
220
- // -------------------------------------------------------------------------
221
- await describe('Transform: readableObjectMode / writableObjectMode', async () => {
222
- await it('readableObjectMode enables objectMode only on readable side', async () => {
223
- const parser = new Transform({
224
- readableObjectMode: true,
225
- transform(chunk, _enc, callback) {
226
- callback(null, { val: chunk[0] });
227
- },
228
- });
229
- expect(parser._readableState.objectMode).toBe(true);
230
- expect(parser._writableState.objectMode).toBe(false);
231
- // readableHWM is object-mode default (16)
232
- expect(parser.readableHighWaterMark).toBe(16);
233
- });
234
-
235
- await it('writableObjectMode enables objectMode only on writable side', async () => {
236
- const serializer = new Transform({
237
- writableObjectMode: true,
238
- transform(obj: any, _enc, callback) {
239
- callback(null, Buffer.from([obj.val]));
240
- },
241
- });
242
- expect(serializer._readableState.objectMode).toBe(false);
243
- expect(serializer._writableState.objectMode).toBe(true);
244
- // writableHWM is object-mode default (16)
245
- expect(serializer.writableHighWaterMark).toBe(16);
246
- });
247
-
248
- await it('readableObjectMode: parsed objects flow through', async () => {
249
- const parser = new Transform({
250
- readableObjectMode: true,
251
- transform(chunk, _enc, callback) {
252
- callback(null, { val: chunk[0] });
253
- },
254
- });
255
- const results: any[] = [];
256
- await new Promise<void>((resolve, reject) => {
257
- parser.on('data', (obj: any) => results.push(obj));
258
- parser.on('end', resolve);
259
- parser.on('error', reject);
260
- parser.end(Buffer.from([42]));
261
- });
262
- expect(results.length).toBe(1);
263
- expect(results[0].val).toBe(42);
264
- });
265
- });
266
-
267
- // -------------------------------------------------------------------------
268
- // highWaterMark: 0 — backpressure and drain
269
- // -------------------------------------------------------------------------
270
- await describe('Transform: highWaterMark 0', async () => {
271
- await it('write returns false when hwm is 0 (objectMode)', async () => {
272
- const t = new Transform({
273
- objectMode: true,
274
- highWaterMark: 0,
275
- transform(chunk, _enc, callback) {
276
- Promise.resolve().then(() => callback(null, chunk));
277
- },
278
- });
279
- const ret = t.write(1);
280
- expect(ret).toBe(false);
281
- // consume to avoid hanging
282
- t.resume();
283
- t.end();
284
- await new Promise<void>((resolve) => t.on('finish', resolve));
285
- });
286
-
287
- await it('drain event fires after hwm-0 backpressure resolves', async () => {
288
- const t = new Transform({
289
- objectMode: true,
290
- highWaterMark: 0,
291
- transform(chunk, _enc, callback) {
292
- Promise.resolve().then(() => callback(null, chunk));
293
- },
294
- });
295
- t.write(1); // returns false, triggers drain eventually
296
- await new Promise<void>((resolve) => {
297
- t.on('drain', () => {
298
- t.end();
299
- resolve();
300
- });
301
- t.resume(); // put in flowing mode so data is consumed
302
- });
303
- });
304
- });
305
-
306
- // -------------------------------------------------------------------------
307
- // Callback called twice — ERR_MULTIPLE_CALLBACK
308
- // -------------------------------------------------------------------------
309
- await describe('Transform: callback called twice', async () => {
310
- await it('emits error with ERR_MULTIPLE_CALLBACK when callback called twice', async () => {
311
- const stream = new Transform({
312
- transform(_chunk, _enc, cb) { cb(); cb(); },
313
- });
314
- await new Promise<void>((resolve) => {
315
- stream.on('error', (err: any) => {
316
- expect(err.code).toBe('ERR_MULTIPLE_CALLBACK');
317
- resolve();
318
- });
319
- stream.write('foo');
320
- });
321
- });
322
- });
323
-
324
- // -------------------------------------------------------------------------
325
- // destroy()
326
- // -------------------------------------------------------------------------
327
- await describe('Transform: destroy()', async () => {
328
- await it('destroy() emits close, not end or finish', async () => {
329
- await new Promise<void>((resolve) => {
330
- const transform = new Transform({ transform(_c, _e, cb) {} });
331
- transform.resume();
332
- let endFired = false;
333
- let finishFired = false;
334
- transform.on('end', () => { endFired = true; });
335
- transform.on('finish', () => { finishFired = true; });
336
- transform.on('close', () => {
337
- expect(endFired).toBe(false);
338
- expect(finishFired).toBe(false);
339
- resolve();
340
- });
341
- transform.destroy();
342
- });
343
- });
344
-
345
- await it('destroy(err) emits error then close', async () => {
346
- await new Promise<void>((resolve) => {
347
- const transform = new Transform({ transform(_c, _e, cb) {} });
348
- transform.resume();
349
- const expected = new Error('kaboom');
350
- let errorReceived: Error | null = null;
351
- transform.on('end', () => { throw new Error('end should not fire'); });
352
- transform.on('finish', () => { throw new Error('finish should not fire'); });
353
- transform.on('error', (err) => { errorReceived = err; });
354
- transform.on('close', () => {
355
- expect(errorReceived).toBe(expected);
356
- resolve();
357
- });
358
- transform.destroy(expected);
359
- });
360
- });
361
-
362
- await it('custom _destroy is called with the error', async () => {
363
- await new Promise<void>((resolve) => {
364
- const expected = new Error('kaboom');
365
- let destroyCalled = false;
366
- const transform = new Transform({ transform(_c, _e, cb) {} });
367
- transform._destroy = function(err, cb) {
368
- destroyCalled = true;
369
- expect(err).toBe(expected);
370
- cb(err);
371
- };
372
- transform.on('error', () => {});
373
- transform.on('close', () => {
374
- expect(destroyCalled).toBe(true);
375
- resolve();
376
- });
377
- transform.destroy(expected);
378
- });
379
- });
380
-
381
- await it('custom _destroy swallowing error suppresses error event', async () => {
382
- await new Promise<void>((resolve) => {
383
- const expected = new Error('kaboom');
384
- let errorFired = false;
385
- const transform = new Transform({
386
- transform(_c, _e, cb) {},
387
- destroy(_err, cb) { cb(); }, // swallow the error
388
- });
389
- transform.resume();
390
- transform.on('end', () => { throw new Error('no end event'); });
391
- transform.on('finish', () => { throw new Error('no finish event'); });
392
- transform.on('error', () => { errorFired = true; });
393
- transform.on('close', () => {
394
- expect(errorFired).toBe(false);
395
- resolve();
396
- });
397
- transform.destroy(expected);
398
- });
399
- });
400
-
401
- await it('_destroy with null error calls cb() cleanly', async () => {
402
- await new Promise<void>((resolve) => {
403
- let destroyCalled = false;
404
- const transform = new Transform({ transform(_c, _e, cb) {} });
405
- transform._destroy = function(err, cb) {
406
- destroyCalled = true;
407
- expect(err).toBe(null);
408
- cb();
409
- };
410
- transform.on('close', () => {
411
- expect(destroyCalled).toBe(true);
412
- resolve();
413
- });
414
- transform.destroy();
415
- });
416
- });
417
- });
418
-
419
- // -------------------------------------------------------------------------
420
- // final / flush ordering (async final)
421
- // -------------------------------------------------------------------------
422
- await describe('Transform: final/flush ordering (async final)', async () => {
423
- await it('data, final, flush and end events fire in the correct order', async () => {
424
- const order: string[] = [];
425
-
426
- await new Promise<void>((resolve, reject) => {
427
- const t = new Transform({
428
- objectMode: true,
429
- transform(chunk, _enc, next) {
430
- order.push(`transform:${chunk}`);
431
- this.push(chunk);
432
- Promise.resolve().then(() => { next(); });
433
- },
434
- final(done) {
435
- order.push('final:start');
436
- setTimeout(() => {
437
- order.push('final:done');
438
- done();
439
- }, 10);
440
- },
441
- flush(done) {
442
- order.push('flush:start');
443
- Promise.resolve().then(() => {
444
- order.push('flush:done');
445
- done();
446
- });
447
- },
448
- });
449
-
450
- t.on('data', (d: any) => order.push(`data:${d}`));
451
- t.on('finish', () => order.push('finish'));
452
- t.on('end', () => {
453
- order.push('end');
454
- try {
455
- // All transforms must have fired before final
456
- expect(order.indexOf('final:start')).toBeGreaterThan(order.indexOf('transform:3'));
457
- // final:done before flush:start
458
- expect(order.indexOf('flush:start')).toBeGreaterThan(order.indexOf('final:done'));
459
- // finish before end
460
- expect(order.indexOf('finish')).toBeLessThan(order.indexOf('end'));
461
- resolve();
462
- } catch (e) {
463
- reject(e);
464
- }
465
- });
466
- t.on('error', reject);
467
-
468
- t.write(1);
469
- t.write(2);
470
- t.end(3);
471
- });
472
- });
473
- });
474
-
475
- // -------------------------------------------------------------------------
476
- // final / flush ordering (sync final)
477
- // -------------------------------------------------------------------------
478
- await describe('Transform: final/flush ordering (sync final)', async () => {
479
- await it('sync final completes before flush', async () => {
480
- const order: string[] = [];
481
-
482
- await new Promise<void>((resolve, reject) => {
483
- const t = new Transform({
484
- objectMode: true,
485
- transform(chunk, _enc, next) {
486
- order.push(`transform:${chunk}`);
487
- this.push(chunk);
488
- Promise.resolve().then(() => { next(); });
489
- },
490
- final(done) {
491
- order.push('final');
492
- done(); // synchronous completion
493
- },
494
- flush(done) {
495
- order.push('flush');
496
- Promise.resolve().then(() => { done(); });
497
- },
498
- });
499
-
500
- t.on('data', (d: any) => order.push(`data:${d}`));
501
- t.on('finish', () => order.push('finish'));
502
- t.on('end', () => {
503
- order.push('end');
504
- try {
505
- expect(order.indexOf('final')).toBeGreaterThan(order.indexOf('transform:2'));
506
- expect(order.indexOf('flush')).toBeGreaterThan(order.indexOf('final'));
507
- expect(order.indexOf('finish')).toBeLessThan(order.indexOf('end'));
508
- resolve();
509
- } catch (e) {
510
- reject(e);
511
- }
512
- });
513
- t.on('error', reject);
514
-
515
- t.write(1);
516
- t.end(2);
517
- });
518
- });
519
- });
520
- };
package/src/transform.ts DELETED
@@ -1,108 +0,0 @@
1
- // Transform stream — a Duplex whose readable side is fed by `_transform`.
2
- //
3
- // Reference: refs/node/lib/internal/streams/transform.js
4
- // Reimplemented for GJS — direct opts.transform/flush/final assignment matches
5
- // Node's instance-method override pattern.
6
-
7
- import { nextTick } from '@gjsify/utils';
8
- import type { TransformOptions } from 'node:stream';
9
-
10
- import { Duplex_ } from './duplex.js';
11
- import type { ErrCallback } from './internal/types.js';
12
-
13
- type TransformFn = (this: Transform_, chunk: unknown, encoding: string, callback: (error?: Error | null, data?: unknown) => void) => void;
14
- type FlushFn = (this: Transform_, callback: (error?: Error | null, data?: unknown) => void) => void;
15
- type FinalFn = (this: Transform_, callback: ErrCallback) => void;
16
-
17
- interface TransformInstanceMethods {
18
- _transform: TransformFn;
19
- _flush: FlushFn;
20
- _final: FinalFn;
21
- }
22
-
23
- export class Transform_ extends Duplex_ {
24
- constructor(opts?: TransformOptions) {
25
- // Don't forward transform/flush/final/write — Transform's own method assignments
26
- // handle those. Passing write/final through would register them in Duplex_'s
27
- // _writeImpl/_finalImpl and bypass Transform's override.
28
- super({ ...opts, write: undefined, final: undefined });
29
- // Direct assignment mirrors Node.js: opts.transform/flush/final overwrite the
30
- // prototype methods on the instance so `t._transform === opts.transform` holds.
31
- const self = this as unknown as TransformInstanceMethods;
32
- if (opts?.transform) self._transform = opts.transform as unknown as TransformFn;
33
- if (opts?.flush) self._flush = opts.flush as unknown as FlushFn;
34
- if (opts?.final) self._final = opts.final as unknown as FinalFn;
35
- }
36
-
37
- _transform(_chunk: unknown, _encoding: string, _callback: (error?: Error | null, data?: unknown) => void): void {
38
- // Throw when no implementation was provided (no opts.transform and no subclass override).
39
- const err = Object.assign(
40
- new Error('The _transform() method is not implemented'),
41
- { code: 'ERR_METHOD_NOT_IMPLEMENTED' }
42
- );
43
- throw err;
44
- }
45
-
46
- _flush(callback: (error?: Error | null, data?: unknown) => void): void {
47
- callback();
48
- }
49
-
50
- _write(chunk: unknown, encoding: string, callback: ErrCallback): void {
51
- let called = false;
52
- try {
53
- this._transform(chunk, encoding, (err, data) => {
54
- if (called) {
55
- const e = Object.assign(new Error('Callback called multiple times'), { code: 'ERR_MULTIPLE_CALLBACK' });
56
- nextTick(() => this.emit('error', e));
57
- return;
58
- }
59
- called = true;
60
- if (err) {
61
- callback(err);
62
- return;
63
- }
64
- if (data !== undefined && data !== null) {
65
- this.push(data);
66
- }
67
- callback();
68
- });
69
- } catch (err) {
70
- // ERR_METHOD_NOT_IMPLEMENTED must propagate synchronously (test-stream-transform-constructor-set-methods).
71
- // User-provided _transform errors are converted to 'error' events.
72
- const e = err as { code?: string };
73
- if (e?.code === 'ERR_METHOD_NOT_IMPLEMENTED') throw err;
74
- callback(err as Error);
75
- }
76
- }
77
-
78
- // Transform's built-in _final: calls _flush then pushes null.
79
- // This is the default; when the user provides opts.final it is overridden on
80
- // the instance and _doPrefinishHooks ensures _flush is still called after it.
81
- _final(callback: ErrCallback): void {
82
- this._flush((err, data) => {
83
- if (err) {
84
- callback(err);
85
- return;
86
- }
87
- if (data !== undefined && data !== null) {
88
- this.push(data);
89
- }
90
- // Signal readable side is done
91
- this.push(null);
92
- callback();
93
- });
94
- }
95
-
96
- // When a user-provided _final overrides the prototype method, we still need
97
- // to call the built-in flush+push-null logic (mirroring Node.js's prefinish).
98
- protected override _doPrefinishHooks(cb: () => void): void {
99
- const protoFinal = Transform_.prototype._final;
100
- if ((this as unknown as TransformInstanceMethods)._final !== protoFinal) {
101
- // User replaced _final; call the built-in flush+push-null now.
102
- protoFinal.call(this, cb);
103
- } else {
104
- // _final already ran flush+push-null; nothing extra needed.
105
- cb();
106
- }
107
- }
108
- }