@gjsify/stream 0.1.7 → 0.1.9
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/lib/esm/callable.js +4 -0
- package/lib/esm/index.js +172 -57
- package/lib/esm/spec-internals.d.js +0 -0
- package/lib/types/callable.d.ts +1 -0
- package/lib/types/callable.spec.d.ts +2 -0
- package/lib/types/consumers/index.spec.d.ts +2 -0
- package/lib/types/edge-cases.spec.d.ts +2 -0
- package/lib/types/index.d.ts +44 -21
- package/lib/types/index.spec.d.ts +2 -0
- package/lib/types/inheritance.spec.d.ts +2 -0
- package/lib/types/pipe.spec.d.ts +2 -0
- package/lib/types/promises/index.spec.d.ts +2 -0
- package/lib/types/transform.spec.d.ts +2 -0
- package/package.json +7 -7
- package/src/callable.spec.ts +157 -0
- package/src/callable.ts +3 -0
- package/src/consumers/index.spec.ts +2 -2
- package/src/index.ts +237 -61
- package/src/inheritance.spec.ts +315 -0
- package/src/pipe.spec.ts +530 -0
- package/src/spec-internals.d.ts +28 -0
- package/src/test.mts +16 -2
- package/src/transform.spec.ts +520 -0
- package/tsconfig.json +1 -3
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Legacy `.call(this)` + `util.inherits(Sub, Parent)` compatibility tests.
|
|
2
|
+
//
|
|
3
|
+
// Ported from refs/node-test/parallel/test-util-inherits.js
|
|
4
|
+
// Original: MIT license, Node.js contributors
|
|
5
|
+
// Modifications: adapted to @gjsify/unit and extended to cover all six stream
|
|
6
|
+
// classes that are wrapped by `makeCallable` in `./callable.ts`.
|
|
7
|
+
//
|
|
8
|
+
// Regression coverage for the npm `send` (express.static) bug and for our own
|
|
9
|
+
// `@gjsify/crypto` `Hash.copy()` path which both do `Stream.call(this)` /
|
|
10
|
+
// `Transform.call(copy)` on pre-ES2015 CJS-style constructors.
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
13
|
+
import { Stream, Readable, Writable, Duplex, Transform, PassThrough } from 'node:stream';
|
|
14
|
+
import { inherits } from 'node:util';
|
|
15
|
+
|
|
16
|
+
export default async () => {
|
|
17
|
+
await describe('makeCallable: new invocation still works', async () => {
|
|
18
|
+
await it('new Stream() returns a valid instance', async () => {
|
|
19
|
+
const s = new Stream();
|
|
20
|
+
expect(typeof (s as any).pipe).toBe('function');
|
|
21
|
+
expect(s instanceof Stream).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
await it('new Readable() initialises readable state', async () => {
|
|
25
|
+
const r = new Readable({ read() {} });
|
|
26
|
+
expect(r.readable).toBe(true);
|
|
27
|
+
expect(r instanceof Readable).toBe(true);
|
|
28
|
+
expect(r instanceof Stream).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await it('new Transform() initialises both readable + writable state', async () => {
|
|
32
|
+
const t = new Transform({ transform(chunk, _enc, cb) { cb(null, chunk); } });
|
|
33
|
+
expect(t.readable).toBe(true);
|
|
34
|
+
expect(t.writable).toBe(true);
|
|
35
|
+
expect(t instanceof Transform).toBe(true);
|
|
36
|
+
expect(t instanceof Duplex).toBe(true);
|
|
37
|
+
expect(t instanceof Readable).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await it('Stream.prototype is accessible via the wrapper', async () => {
|
|
41
|
+
expect(typeof (Stream as any).prototype).toBe('object');
|
|
42
|
+
expect(typeof (Stream as any).prototype.pipe).toBe('function');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await describe('makeCallable: legacy Stream.call(this) + util.inherits', async () => {
|
|
47
|
+
await it('Stream.call(plainObject) assigns EventEmitter bookkeeping', async () => {
|
|
48
|
+
// Plain object promoted to a Stream via .call()
|
|
49
|
+
const plain: any = Object.create(Stream.prototype);
|
|
50
|
+
(Stream as any).call(plain);
|
|
51
|
+
// EventEmitter state is now present
|
|
52
|
+
plain.on('ping', function(this: any, msg: string) { this.received = msg; });
|
|
53
|
+
plain.emit('ping', 'hi');
|
|
54
|
+
expect(plain.received).toBe('hi');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await it('legacy subclass pattern (function + inherits + .call) works', async () => {
|
|
58
|
+
function SubStream(this: any) {
|
|
59
|
+
(Stream as any).call(this);
|
|
60
|
+
this.foo = 42;
|
|
61
|
+
}
|
|
62
|
+
inherits(SubStream as any, Stream as any);
|
|
63
|
+
(SubStream.prototype as any).greet = function() { return 'hello'; };
|
|
64
|
+
|
|
65
|
+
const s: any = new (SubStream as any)();
|
|
66
|
+
expect(s.foo).toBe(42);
|
|
67
|
+
expect(s.greet()).toBe('hello');
|
|
68
|
+
// Inherited from Stream.prototype
|
|
69
|
+
expect(typeof s.pipe).toBe('function');
|
|
70
|
+
expect(s instanceof Stream).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await it('multi-level inherits (A.call -> B.call -> C) works', async () => {
|
|
74
|
+
function A(this: any) {
|
|
75
|
+
(Stream as any).call(this);
|
|
76
|
+
this._a = 'a';
|
|
77
|
+
}
|
|
78
|
+
inherits(A as any, Stream as any);
|
|
79
|
+
(A.prototype as any).a = function() { return this._a; };
|
|
80
|
+
|
|
81
|
+
function B(this: any) {
|
|
82
|
+
(A as any).call(this);
|
|
83
|
+
this._b = 'b';
|
|
84
|
+
}
|
|
85
|
+
inherits(B as any, A as any);
|
|
86
|
+
(B.prototype as any).b = function() { return this._b; };
|
|
87
|
+
|
|
88
|
+
const b: any = new (B as any)();
|
|
89
|
+
expect(b.a()).toBe('a');
|
|
90
|
+
expect(b.b()).toBe('b');
|
|
91
|
+
expect(b instanceof A).toBe(true);
|
|
92
|
+
expect(b instanceof Stream).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await describe('makeCallable: Readable / Writable / Transform .call(this)', async () => {
|
|
97
|
+
await it('Readable.call(plain) sets up readable state', async () => {
|
|
98
|
+
const r: any = Object.create(Readable.prototype);
|
|
99
|
+
(Readable as any).call(r, { read() {} });
|
|
100
|
+
expect(r.readable).toBe(true);
|
|
101
|
+
expect(typeof r.readableHighWaterMark).toBe('number');
|
|
102
|
+
// pipe is inherited from Stream.prototype through Readable.prototype
|
|
103
|
+
expect(typeof r.pipe).toBe('function');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await it('Writable.call(plain) sets up writable state', async () => {
|
|
107
|
+
const w: any = Object.create(Writable.prototype);
|
|
108
|
+
(Writable as any).call(w, { write(_c: any, _e: any, cb: any) { cb(); } });
|
|
109
|
+
expect(w.writable).toBe(true);
|
|
110
|
+
expect(typeof w.writableHighWaterMark).toBe('number');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await it('Transform.call(plain) sets up both readable and writable state', async () => {
|
|
114
|
+
// This is the exact pattern used by @gjsify/crypto Hash.copy():
|
|
115
|
+
// const copy = Object.create(Hash.prototype);
|
|
116
|
+
// Transform.call(copy);
|
|
117
|
+
const copy: any = Object.create(Transform.prototype);
|
|
118
|
+
(Transform as any).call(copy);
|
|
119
|
+
expect(copy.readable).toBe(true);
|
|
120
|
+
expect(copy.writable).toBe(true);
|
|
121
|
+
// The Transform should be usable after .call() initialization —
|
|
122
|
+
// field initializers from all ancestors ran on copy.
|
|
123
|
+
expect(typeof copy.readableHighWaterMark).toBe('number');
|
|
124
|
+
expect(typeof copy.writableHighWaterMark).toBe('number');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await it('PassThrough.call(plain) inherits the full chain', async () => {
|
|
128
|
+
const pt: any = Object.create(PassThrough.prototype);
|
|
129
|
+
(PassThrough as any).call(pt);
|
|
130
|
+
expect(pt.readable).toBe(true);
|
|
131
|
+
expect(pt.writable).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await describe('makeCallable: integration with util.inherits', async () => {
|
|
136
|
+
await it('inherits(Sub, Readable) + Readable.call(this) yields a working Readable subclass', async () => {
|
|
137
|
+
function SimpleSrc(this: any, items: number[]) {
|
|
138
|
+
(Readable as any).call(this, { objectMode: true });
|
|
139
|
+
this._items = items;
|
|
140
|
+
}
|
|
141
|
+
inherits(SimpleSrc as any, Readable as any);
|
|
142
|
+
(SimpleSrc.prototype as any)._read = function() {
|
|
143
|
+
const item = this._items.shift();
|
|
144
|
+
this.push(item !== undefined ? item : null);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const src: any = new (SimpleSrc as any)([1, 2, 3]);
|
|
148
|
+
const out: number[] = [];
|
|
149
|
+
await new Promise<void>((resolve, reject) => {
|
|
150
|
+
src.on('data', (chunk: number) => out.push(chunk));
|
|
151
|
+
src.on('end', resolve);
|
|
152
|
+
src.on('error', reject);
|
|
153
|
+
});
|
|
154
|
+
expect(out).toEqualArray([1, 2, 3]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
};
|
package/src/callable.ts
ADDED
|
@@ -33,13 +33,13 @@ export default async () => {
|
|
|
33
33
|
await describe('json', async () => {
|
|
34
34
|
await it('should consume stream as JSON', async () => {
|
|
35
35
|
const stream = Readable.from(['{"key":', '"value"}']);
|
|
36
|
-
const result = await json(stream);
|
|
36
|
+
const result = await json(stream) as { key: string };
|
|
37
37
|
expect(result.key).toBe('value');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
await it('should parse JSON array', async () => {
|
|
41
41
|
const stream = Readable.from(['[1,2,3]']);
|
|
42
|
-
const result = await json(stream);
|
|
42
|
+
const result = await json(stream) as number[];
|
|
43
43
|
expect(Array.isArray(result)).toBe(true);
|
|
44
44
|
expect(result.length).toBe(3);
|
|
45
45
|
expect(result[0]).toBe(1);
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,16 @@ export function setDefaultHighWaterMark(objectMode: boolean, value: number): voi
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** Validate a named high-water-mark option and throw ERR_INVALID_ARG_VALUE on NaN/non-number. */
|
|
29
|
+
function validateHighWaterMark(name: string, value: unknown): void {
|
|
30
|
+
if (value === undefined) return;
|
|
31
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
32
|
+
const err = new TypeError(`The value of "${name}" is invalid. Received ${value}`);
|
|
33
|
+
(err as any).code = 'ERR_INVALID_ARG_VALUE';
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
// ---- Types ----
|
|
29
39
|
|
|
30
40
|
/** Base options accepted by the Stream constructor (superset used by subclass options). */
|
|
@@ -48,14 +58,10 @@ interface StreamLike extends EventEmitter {
|
|
|
48
58
|
/** Tracked pipe destination for unpipe support. */
|
|
49
59
|
interface PipeState {
|
|
50
60
|
dest: Writable;
|
|
51
|
-
ondata: (chunk: unknown) => void;
|
|
52
|
-
ondrain: () => void;
|
|
53
|
-
onend: () => void;
|
|
54
61
|
cleanup: () => void;
|
|
55
|
-
doEnd: boolean;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
class Stream_ extends EventEmitter {
|
|
59
65
|
constructor(opts?: StreamOptions) {
|
|
60
66
|
super(opts);
|
|
61
67
|
}
|
|
@@ -64,44 +70,74 @@ export class Stream extends EventEmitter {
|
|
|
64
70
|
const source = this as unknown as Readable;
|
|
65
71
|
const doEnd = options?.end !== false;
|
|
66
72
|
|
|
73
|
+
// Drain listener is added lazily only when backpressure occurs.
|
|
74
|
+
let drainListenerAdded = false;
|
|
75
|
+
const ondrain = () => {
|
|
76
|
+
drainListenerAdded = false;
|
|
77
|
+
destination.removeListener('drain', ondrain);
|
|
78
|
+
if (typeof (source as StreamLike).resume === 'function') {
|
|
79
|
+
(source as StreamLike).resume!();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
67
83
|
const ondata = (chunk: unknown) => {
|
|
68
84
|
if (destination.writable) {
|
|
69
85
|
if (destination.write(chunk) === false && typeof (source as StreamLike).pause === 'function') {
|
|
70
86
|
(source as StreamLike).pause!();
|
|
87
|
+
if (!drainListenerAdded) {
|
|
88
|
+
drainListenerAdded = true;
|
|
89
|
+
destination.on('drain', ondrain);
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
92
|
}
|
|
73
93
|
};
|
|
74
94
|
|
|
75
95
|
source.on('data', ondata);
|
|
76
96
|
|
|
77
|
-
|
|
78
|
-
if (typeof (source as StreamLike).resume === 'function') {
|
|
79
|
-
(source as StreamLike).resume!();
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
destination.on('drain', ondrain);
|
|
97
|
+
let didEnd = false;
|
|
83
98
|
|
|
84
99
|
const onend = () => {
|
|
100
|
+
if (didEnd) return;
|
|
101
|
+
didEnd = true;
|
|
102
|
+
if (doEnd) destination.end();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const onclose = () => {
|
|
106
|
+
if (didEnd) return;
|
|
107
|
+
didEnd = true;
|
|
85
108
|
if (doEnd) {
|
|
86
|
-
|
|
109
|
+
// Modern Readable streams (Readable_) do NOT destroy dest on source close —
|
|
110
|
+
// only call dest.end/destroy for legacy Stream objects (no Readable_ prototype).
|
|
111
|
+
if (!(source instanceof Readable) && typeof (destination as any).destroy === 'function') {
|
|
112
|
+
(destination as any).destroy();
|
|
113
|
+
}
|
|
87
114
|
}
|
|
88
115
|
};
|
|
116
|
+
|
|
89
117
|
if (doEnd) {
|
|
90
118
|
source.on('end', onend);
|
|
119
|
+
source.on('close', onclose);
|
|
91
120
|
}
|
|
92
121
|
|
|
93
122
|
const cleanup = () => {
|
|
94
123
|
source.removeListener('data', ondata);
|
|
95
|
-
destination.removeListener('drain', ondrain);
|
|
124
|
+
if (drainListenerAdded) destination.removeListener('drain', ondrain);
|
|
96
125
|
source.removeListener('end', onend);
|
|
126
|
+
source.removeListener('close', onclose);
|
|
127
|
+
// Self-remove from both end and close
|
|
128
|
+
source.removeListener('end', cleanup);
|
|
129
|
+
source.removeListener('close', cleanup);
|
|
130
|
+
destination.removeListener('close', cleanup);
|
|
97
131
|
};
|
|
98
132
|
|
|
133
|
+
source.on('end', cleanup);
|
|
99
134
|
source.on('close', cleanup);
|
|
100
135
|
destination.on('close', cleanup);
|
|
101
136
|
|
|
102
137
|
// Track piped destinations for unpipe
|
|
103
138
|
if (source instanceof Readable) {
|
|
104
|
-
source._pipeDests.push({ dest: destination,
|
|
139
|
+
source._pipeDests.push({ dest: destination, cleanup });
|
|
140
|
+
(source as any)._readableState.pipes.push(destination);
|
|
105
141
|
}
|
|
106
142
|
|
|
107
143
|
destination.emit('pipe', source);
|
|
@@ -111,7 +147,7 @@ export class Stream extends EventEmitter {
|
|
|
111
147
|
|
|
112
148
|
// ---- Readable ----
|
|
113
149
|
|
|
114
|
-
|
|
150
|
+
class Readable_ extends Stream_ {
|
|
115
151
|
readable = true;
|
|
116
152
|
readableFlowing: boolean | null = null;
|
|
117
153
|
readableLength = 0;
|
|
@@ -126,7 +162,7 @@ export class Readable extends Stream {
|
|
|
126
162
|
_pipeDests: PipeState[] = [];
|
|
127
163
|
|
|
128
164
|
private _buffer: unknown[] = [];
|
|
129
|
-
|
|
165
|
+
_readableState = { ended: false, endEmitted: false, reading: false, constructed: true, highWaterMark: 0, objectMode: false, pipes: [] as any[] };
|
|
130
166
|
private _readablePending = false;
|
|
131
167
|
private _readImpl: ((size: number) => void) | undefined;
|
|
132
168
|
private _destroyImpl: ((error: Error | null, cb: (error?: Error | null) => void) => void) | undefined;
|
|
@@ -137,6 +173,8 @@ export class Readable extends Stream {
|
|
|
137
173
|
this.readableHighWaterMark = opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.objectMode ?? false);
|
|
138
174
|
this.readableEncoding = opts?.encoding ?? null;
|
|
139
175
|
this.readableObjectMode = opts?.objectMode ?? false;
|
|
176
|
+
this._readableState.highWaterMark = this.readableHighWaterMark;
|
|
177
|
+
this._readableState.objectMode = this.readableObjectMode;
|
|
140
178
|
if (opts?.read) this._readImpl = opts.read;
|
|
141
179
|
if (opts?.destroy) this._destroyImpl = opts.destroy;
|
|
142
180
|
if (opts?.construct) this._constructImpl = opts.construct;
|
|
@@ -275,6 +313,22 @@ export class Readable extends Stream {
|
|
|
275
313
|
return false;
|
|
276
314
|
}
|
|
277
315
|
|
|
316
|
+
// Validate chunk type for non-objectMode streams (Node.js ERR_INVALID_ARG_TYPE).
|
|
317
|
+
// Accept string, Buffer, or any ArrayBufferView — the latter covers Uint8Array
|
|
318
|
+
// and TypedArrays from any realm (GJS vs host bundle), avoiding cross-realm
|
|
319
|
+
// `instanceof Uint8Array` mismatches that would otherwise reject real Buffers.
|
|
320
|
+
if (!this.readableObjectMode) {
|
|
321
|
+
const isValid = typeof chunk === 'string' || ArrayBuffer.isView(chunk);
|
|
322
|
+
if (!isValid) {
|
|
323
|
+
const err = Object.assign(
|
|
324
|
+
new TypeError(`Invalid non-string/buffer chunk type: ${typeof chunk}`),
|
|
325
|
+
{ code: 'ERR_INVALID_ARG_TYPE' }
|
|
326
|
+
);
|
|
327
|
+
nextTick(() => this.emit('error', err));
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
278
332
|
this._buffer.push(chunk);
|
|
279
333
|
this.readableLength += this.readableObjectMode ? 1 : (chunk.length ?? 1);
|
|
280
334
|
|
|
@@ -410,6 +464,7 @@ export class Readable extends Stream {
|
|
|
410
464
|
state.dest.emit('unpipe', this);
|
|
411
465
|
}
|
|
412
466
|
this._pipeDests = [];
|
|
467
|
+
this._readableState.pipes = [];
|
|
413
468
|
this.readableFlowing = false;
|
|
414
469
|
} else {
|
|
415
470
|
const idx = this._pipeDests.findIndex(s => s.dest === destination);
|
|
@@ -417,6 +472,8 @@ export class Readable extends Stream {
|
|
|
417
472
|
const state = this._pipeDests[idx];
|
|
418
473
|
state.cleanup();
|
|
419
474
|
this._pipeDests.splice(idx, 1);
|
|
475
|
+
const pipeIdx = this._readableState.pipes.indexOf(destination);
|
|
476
|
+
if (pipeIdx !== -1) this._readableState.pipes.splice(pipeIdx, 1);
|
|
420
477
|
destination.emit('unpipe', this);
|
|
421
478
|
if (this._pipeDests.length === 0) {
|
|
422
479
|
this.readableFlowing = false;
|
|
@@ -426,6 +483,14 @@ export class Readable extends Stream {
|
|
|
426
483
|
return this;
|
|
427
484
|
}
|
|
428
485
|
|
|
486
|
+
_destroy(error: Error | null, callback: (error?: Error | null) => void): void {
|
|
487
|
+
if (this._destroyImpl) {
|
|
488
|
+
this._destroyImpl.call(this, error, callback);
|
|
489
|
+
} else {
|
|
490
|
+
callback(error ?? undefined);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
429
494
|
destroy(error?: Error): this {
|
|
430
495
|
if (this.destroyed) return this;
|
|
431
496
|
this.destroyed = true;
|
|
@@ -439,7 +504,14 @@ export class Readable extends Stream {
|
|
|
439
504
|
nextTick(() => this.emit('close'));
|
|
440
505
|
};
|
|
441
506
|
|
|
442
|
-
|
|
507
|
+
// Dispatch virtually ONLY when the user overrode _destroy on the instance
|
|
508
|
+
// (e.g. tests: `stream._destroy = fn`). Do NOT call a subclass prototype
|
|
509
|
+
// _destroy: net.Socket's prototype `_destroy` synchronously cancels in-flight
|
|
510
|
+
// Gio I/O and would break tests that call destroy() during pending writes.
|
|
511
|
+
// The opts.destroy path still runs via _destroyImpl as before.
|
|
512
|
+
if (Object.prototype.hasOwnProperty.call(this, '_destroy')) {
|
|
513
|
+
(this as any)._destroy(error ?? null, cb);
|
|
514
|
+
} else if (this._destroyImpl) {
|
|
443
515
|
this._destroyImpl.call(this, error ?? null, cb);
|
|
444
516
|
} else {
|
|
445
517
|
cb(error);
|
|
@@ -541,7 +613,21 @@ export class Readable extends Stream {
|
|
|
541
613
|
|
|
542
614
|
// ---- Writable ----
|
|
543
615
|
|
|
544
|
-
|
|
616
|
+
class Writable_ extends Stream_ {
|
|
617
|
+
// Allow `duplex instanceof Writable` to return true even though Duplex inherits
|
|
618
|
+
// from Readable_ only. Mirrors Node.js: standard prototype-chain check first,
|
|
619
|
+
// then duck-type fallback — but only when checking against Writable itself, not
|
|
620
|
+
// against a user subclass. Because `Writable` is exported as a makeCallable Proxy,
|
|
621
|
+
// `this === Writable_` fails for `obj instanceof Writable`; however the Proxy's
|
|
622
|
+
// default `get` trap forwards `.prototype` straight to the target, so the
|
|
623
|
+
// prototype reference check below correctly recognises both.
|
|
624
|
+
static [Symbol.hasInstance](obj: any): boolean {
|
|
625
|
+
if (typeof (this as any).prototype !== 'undefined' &&
|
|
626
|
+
Object.prototype.isPrototypeOf.call((this as any).prototype, obj)) return true;
|
|
627
|
+
if ((this as any).prototype !== Writable_.prototype) return false;
|
|
628
|
+
return obj !== null && obj !== undefined && typeof obj.writableHighWaterMark === 'number';
|
|
629
|
+
}
|
|
630
|
+
|
|
545
631
|
writable = true;
|
|
546
632
|
writableHighWaterMark: number;
|
|
547
633
|
writableLength = 0;
|
|
@@ -649,7 +735,7 @@ export class Writable extends Stream {
|
|
|
649
735
|
} else {
|
|
650
736
|
nextTick(() => {
|
|
651
737
|
callback();
|
|
652
|
-
if (this.writableNeedDrain && this.writableLength
|
|
738
|
+
if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
|
|
653
739
|
this.writableNeedDrain = false;
|
|
654
740
|
this.emit('drain');
|
|
655
741
|
}
|
|
@@ -817,7 +903,7 @@ export class Writable extends Stream {
|
|
|
817
903
|
this.emit('error', err);
|
|
818
904
|
} else {
|
|
819
905
|
for (const b of buffered) b.callback();
|
|
820
|
-
if (this.writableNeedDrain && this.writableLength
|
|
906
|
+
if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
|
|
821
907
|
this.writableNeedDrain = false;
|
|
822
908
|
this.emit('drain');
|
|
823
909
|
}
|
|
@@ -861,7 +947,7 @@ export class Writable extends Stream {
|
|
|
861
947
|
|
|
862
948
|
// ---- Duplex ----
|
|
863
949
|
|
|
864
|
-
|
|
950
|
+
class Duplex_ extends Readable_ {
|
|
865
951
|
writable = true;
|
|
866
952
|
writableHighWaterMark: number;
|
|
867
953
|
writableLength = 0;
|
|
@@ -873,6 +959,9 @@ export class Duplex extends Readable {
|
|
|
873
959
|
allowHalfOpen: boolean;
|
|
874
960
|
private _decodeStrings: boolean;
|
|
875
961
|
|
|
962
|
+
// Exposed writable-side state (mirrors Node.js _writableState for split HWM tests)
|
|
963
|
+
_writableState = { highWaterMark: 0, objectMode: false };
|
|
964
|
+
|
|
876
965
|
private _duplexCorkedBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
|
|
877
966
|
private _writeImpl: ((chunk: any, encoding: string, cb: (error?: Error | null) => void) => void) | undefined;
|
|
878
967
|
private _finalImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
|
|
@@ -882,8 +971,34 @@ export class Duplex extends Readable {
|
|
|
882
971
|
|
|
883
972
|
constructor(opts?: DuplexOptions) {
|
|
884
973
|
super(opts);
|
|
885
|
-
|
|
974
|
+
|
|
975
|
+
validateHighWaterMark('writableHighWaterMark', opts?.writableHighWaterMark);
|
|
976
|
+
validateHighWaterMark('readableHighWaterMark', opts?.readableHighWaterMark);
|
|
977
|
+
|
|
978
|
+
// Writable side: highWaterMark (shared) takes priority over writableHighWaterMark.
|
|
886
979
|
this.writableObjectMode = opts?.writableObjectMode ?? opts?.objectMode ?? false;
|
|
980
|
+
this.writableHighWaterMark = opts?.highWaterMark
|
|
981
|
+
?? opts?.writableHighWaterMark
|
|
982
|
+
?? getDefaultHighWaterMark(this.writableObjectMode);
|
|
983
|
+
this._writableState.highWaterMark = this.writableHighWaterMark;
|
|
984
|
+
this._writableState.objectMode = this.writableObjectMode;
|
|
985
|
+
|
|
986
|
+
// Readable side overrides: Readable_ constructor already applied opts.highWaterMark,
|
|
987
|
+
// so only override with readableHighWaterMark when highWaterMark was NOT set.
|
|
988
|
+
if (opts?.highWaterMark === undefined && opts?.readableHighWaterMark !== undefined) {
|
|
989
|
+
this.readableHighWaterMark = opts.readableHighWaterMark;
|
|
990
|
+
this._readableState.highWaterMark = opts.readableHighWaterMark;
|
|
991
|
+
}
|
|
992
|
+
if (opts?.readableObjectMode !== undefined) {
|
|
993
|
+
this.readableObjectMode = opts.readableObjectMode;
|
|
994
|
+
this._readableState.objectMode = opts.readableObjectMode;
|
|
995
|
+
// Re-derive readable HWM for objectMode when neither readableHighWaterMark nor highWaterMark was set.
|
|
996
|
+
if (opts?.readableHighWaterMark === undefined && opts?.highWaterMark === undefined) {
|
|
997
|
+
this.readableHighWaterMark = getDefaultHighWaterMark(opts.readableObjectMode);
|
|
998
|
+
this._readableState.highWaterMark = this.readableHighWaterMark;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
887
1002
|
this.allowHalfOpen = opts?.allowHalfOpen !== false;
|
|
888
1003
|
this._decodeStrings = opts?.decodeStrings !== false;
|
|
889
1004
|
if (opts?.write) this._writeImpl = opts.write;
|
|
@@ -982,7 +1097,7 @@ export class Duplex extends Readable {
|
|
|
982
1097
|
} else {
|
|
983
1098
|
nextTick(() => {
|
|
984
1099
|
cb();
|
|
985
|
-
if (this.writableNeedDrain && this.writableLength
|
|
1100
|
+
if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
|
|
986
1101
|
this.writableNeedDrain = false;
|
|
987
1102
|
this.emit('drain');
|
|
988
1103
|
}
|
|
@@ -1018,11 +1133,15 @@ export class Duplex extends Readable {
|
|
|
1018
1133
|
const doFinal = () => {
|
|
1019
1134
|
this._final((err) => {
|
|
1020
1135
|
this.writableFinished = true;
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
nextTick(() =>
|
|
1025
|
-
|
|
1136
|
+
// Allow subclasses (Transform) to run post-final hooks (e.g. flush)
|
|
1137
|
+
// before the 'finish' event fires.
|
|
1138
|
+
this._doPrefinishHooks(() => {
|
|
1139
|
+
nextTick(() => {
|
|
1140
|
+
if (err) this.emit('error', err);
|
|
1141
|
+
this.emit('finish');
|
|
1142
|
+
nextTick(() => this.emit('close'));
|
|
1143
|
+
if (callback) callback();
|
|
1144
|
+
});
|
|
1026
1145
|
});
|
|
1027
1146
|
});
|
|
1028
1147
|
};
|
|
@@ -1037,6 +1156,11 @@ export class Duplex extends Readable {
|
|
|
1037
1156
|
return this;
|
|
1038
1157
|
}
|
|
1039
1158
|
|
|
1159
|
+
/** Hook for subclasses to run logic between _final and 'finish'. Default: no-op. */
|
|
1160
|
+
protected _doPrefinishHooks(cb: () => void): void {
|
|
1161
|
+
cb();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1040
1164
|
cork(): void { this.writableCorked++; }
|
|
1041
1165
|
|
|
1042
1166
|
uncork(): void {
|
|
@@ -1055,7 +1179,7 @@ export class Duplex extends Readable {
|
|
|
1055
1179
|
}
|
|
1056
1180
|
});
|
|
1057
1181
|
}
|
|
1058
|
-
if (this.writableNeedDrain && this.writableLength
|
|
1182
|
+
if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
|
|
1059
1183
|
this.writableNeedDrain = false;
|
|
1060
1184
|
nextTick(() => this.emit('drain'));
|
|
1061
1185
|
}
|
|
@@ -1071,48 +1195,62 @@ export class Duplex extends Readable {
|
|
|
1071
1195
|
|
|
1072
1196
|
// ---- Transform ----
|
|
1073
1197
|
|
|
1074
|
-
|
|
1075
|
-
private _transformImpl: ((chunk: any, encoding: string, cb: (error?: Error | null, data?: any) => void) => void) | undefined;
|
|
1076
|
-
private _flushImpl: ((cb: (error?: Error | null, data?: any) => void) => void) | undefined;
|
|
1077
|
-
|
|
1198
|
+
class Transform_ extends Duplex_ {
|
|
1078
1199
|
constructor(opts?: TransformOptions) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
|
|
1200
|
+
// Don't forward transform/flush/final/write — Transform's own method assignments
|
|
1201
|
+
// handle those. Passing write/final through would register them in Duplex_'s
|
|
1202
|
+
// _writeImpl/_finalImpl and bypass Transform's override.
|
|
1203
|
+
super({ ...opts, write: undefined, final: undefined });
|
|
1204
|
+
// Direct assignment mirrors Node.js: opts.transform/flush/final overwrite the
|
|
1205
|
+
// prototype methods on the instance so `t._transform === opts.transform` holds.
|
|
1206
|
+
if (opts?.transform) (this as any)._transform = opts.transform;
|
|
1207
|
+
if (opts?.flush) (this as any)._flush = opts.flush;
|
|
1208
|
+
if (opts?.final) (this as any)._final = opts.final;
|
|
1085
1209
|
}
|
|
1086
1210
|
|
|
1087
|
-
_transform(
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1211
|
+
_transform(_chunk: any, _encoding: string, _callback: (error?: Error | null, data?: any) => void): void {
|
|
1212
|
+
// Throw when no implementation was provided (no opts.transform and no subclass override).
|
|
1213
|
+
const err = Object.assign(
|
|
1214
|
+
new Error('The _transform() method is not implemented'),
|
|
1215
|
+
{ code: 'ERR_METHOD_NOT_IMPLEMENTED' }
|
|
1216
|
+
);
|
|
1217
|
+
throw err;
|
|
1093
1218
|
}
|
|
1094
1219
|
|
|
1095
1220
|
_flush(callback: (error?: Error | null, data?: any) => void): void {
|
|
1096
|
-
|
|
1097
|
-
this._flushImpl.call(this, callback);
|
|
1098
|
-
} else {
|
|
1099
|
-
callback();
|
|
1100
|
-
}
|
|
1221
|
+
callback();
|
|
1101
1222
|
}
|
|
1102
1223
|
|
|
1103
1224
|
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1225
|
+
let called = false;
|
|
1226
|
+
try {
|
|
1227
|
+
this._transform(chunk, encoding, (err, data) => {
|
|
1228
|
+
if (called) {
|
|
1229
|
+
const e = Object.assign(new Error('Callback called multiple times'), { code: 'ERR_MULTIPLE_CALLBACK' });
|
|
1230
|
+
nextTick(() => this.emit('error', e));
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
called = true;
|
|
1234
|
+
if (err) {
|
|
1235
|
+
callback(err);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (data !== undefined && data !== null) {
|
|
1239
|
+
this.push(data);
|
|
1240
|
+
}
|
|
1241
|
+
callback();
|
|
1242
|
+
});
|
|
1243
|
+
} catch (err: any) {
|
|
1244
|
+
// ERR_METHOD_NOT_IMPLEMENTED must propagate synchronously (test-stream-transform-constructor-set-methods).
|
|
1245
|
+
// User-provided _transform errors are converted to 'error' events.
|
|
1246
|
+
if (err?.code === 'ERR_METHOD_NOT_IMPLEMENTED') throw err;
|
|
1247
|
+
callback(err as Error);
|
|
1248
|
+
}
|
|
1114
1249
|
}
|
|
1115
1250
|
|
|
1251
|
+
// Transform's built-in _final: calls _flush then pushes null.
|
|
1252
|
+
// This is the default; when the user provides opts.final it is overridden on
|
|
1253
|
+
// the instance and _doPrefinishHooks ensures _flush is still called after it.
|
|
1116
1254
|
_final(callback: (error?: Error | null) => void): void {
|
|
1117
1255
|
this._flush((err, data) => {
|
|
1118
1256
|
if (err) {
|
|
@@ -1127,11 +1265,24 @@ export class Transform extends Duplex {
|
|
|
1127
1265
|
callback();
|
|
1128
1266
|
});
|
|
1129
1267
|
}
|
|
1268
|
+
|
|
1269
|
+
// When a user-provided _final overrides the prototype method, we still need
|
|
1270
|
+
// to call the built-in flush+push-null logic (mirroring Node.js's prefinish).
|
|
1271
|
+
protected override _doPrefinishHooks(cb: () => void): void {
|
|
1272
|
+
const protoFinal = Transform_.prototype._final;
|
|
1273
|
+
if ((this as any)._final !== protoFinal) {
|
|
1274
|
+
// User replaced _final; call the built-in flush+push-null now.
|
|
1275
|
+
protoFinal.call(this, cb);
|
|
1276
|
+
} else {
|
|
1277
|
+
// _final already ran flush+push-null; nothing extra needed.
|
|
1278
|
+
cb();
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1130
1281
|
}
|
|
1131
1282
|
|
|
1132
1283
|
// ---- PassThrough ----
|
|
1133
1284
|
|
|
1134
|
-
|
|
1285
|
+
class PassThrough_ extends Transform_ {
|
|
1135
1286
|
constructor(opts?: TransformOptions) {
|
|
1136
1287
|
super({
|
|
1137
1288
|
...opts,
|
|
@@ -1326,6 +1477,31 @@ export function isErrored(stream: unknown): boolean {
|
|
|
1326
1477
|
}
|
|
1327
1478
|
|
|
1328
1479
|
// ---- Exports ----
|
|
1480
|
+
//
|
|
1481
|
+
// The class declarations above use underscore-suffixed internal names
|
|
1482
|
+
// (`Stream_`, `Readable_`, etc.) — we wrap each one with `makeCallable` so
|
|
1483
|
+
// legacy CJS consumers can do `Stream.call(this)` (npm `send`,
|
|
1484
|
+
// `util.inherits(Sub, Stream)`, our own `@gjsify/crypto` `Hash.copy()`).
|
|
1485
|
+
// See `./callable.ts` for the rationale and implementation.
|
|
1486
|
+
//
|
|
1487
|
+
// Public API preserves the historical names. Both value and type positions
|
|
1488
|
+
// work because of the `type X = X_` aliases below.
|
|
1489
|
+
|
|
1490
|
+
import { makeCallable } from './callable.js';
|
|
1491
|
+
|
|
1492
|
+
export const Stream = makeCallable(Stream_) as typeof Stream_;
|
|
1493
|
+
export const Readable = makeCallable(Readable_) as typeof Readable_;
|
|
1494
|
+
export const Writable = makeCallable(Writable_) as typeof Writable_;
|
|
1495
|
+
export const Duplex = makeCallable(Duplex_) as typeof Duplex_;
|
|
1496
|
+
export const Transform = makeCallable(Transform_) as typeof Transform_;
|
|
1497
|
+
export const PassThrough = makeCallable(PassThrough_) as typeof PassThrough_;
|
|
1498
|
+
|
|
1499
|
+
export type Stream = Stream_;
|
|
1500
|
+
export type Readable = Readable_;
|
|
1501
|
+
export type Writable = Writable_;
|
|
1502
|
+
export type Duplex = Duplex_;
|
|
1503
|
+
export type Transform = Transform_;
|
|
1504
|
+
export type PassThrough = PassThrough_;
|
|
1329
1505
|
|
|
1330
1506
|
// Default export
|
|
1331
1507
|
const _default = Object.assign(Stream, {
|