@gjsify/stream 0.0.3 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -2
- package/cjs-compat.cjs +7 -0
- package/lib/esm/consumers/index.js +37 -4
- package/lib/esm/index.js +1096 -4
- package/lib/esm/promises/index.js +28 -4
- package/lib/esm/web/index.js +34 -3
- package/lib/types/consumers/index.d.ts +13 -3
- package/lib/types/index.d.ts +182 -5
- package/lib/types/promises/index.d.ts +8 -3
- package/lib/types/web/index.d.ts +3 -3
- package/package.json +23 -45
- package/src/consumers/index.spec.ts +107 -0
- package/src/consumers/index.ts +39 -3
- package/src/edge-cases.spec.ts +593 -0
- package/src/index.spec.ts +2239 -7
- package/src/index.ts +1348 -5
- package/src/promises/index.spec.ts +140 -0
- package/src/promises/index.ts +31 -3
- package/src/test.mts +5 -2
- package/src/web/index.ts +32 -3
- package/tsconfig.json +21 -9
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/consumers/index.js +0 -6
- package/lib/cjs/index.js +0 -6
- package/lib/cjs/promises/index.js +0 -6
- package/lib/cjs/web/index.js +0 -6
- package/test.gjs.js +0 -34839
- package/test.gjs.mjs +0 -34728
- package/test.gjs.mjs.meta.json +0 -1
- package/test.node.js +0 -1234
- package/test.node.mjs +0 -315
- package/tsconfig.types.json +0 -8
- package/tsconfig.types.tsbuildinfo +0 -1
package/src/index.ts
CHANGED
|
@@ -1,7 +1,1350 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Reference: Node.js lib/stream.js, lib/internal/streams/*.js
|
|
2
|
+
// Reimplemented for GJS using EventEmitter and microtask scheduling
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import { EventEmitter } from '@gjsify/events';
|
|
5
|
+
import { nextTick } from '@gjsify/utils';
|
|
6
|
+
import type { ReadableOptions, WritableOptions, DuplexOptions, TransformOptions, FinishedOptions } from 'node:stream';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
// ---- Default high water marks ----
|
|
9
|
+
|
|
10
|
+
let defaultHighWaterMark = 16384;
|
|
11
|
+
let defaultObjectHighWaterMark = 16;
|
|
12
|
+
|
|
13
|
+
export function getDefaultHighWaterMark(objectMode: boolean): number {
|
|
14
|
+
return objectMode ? defaultObjectHighWaterMark : defaultHighWaterMark;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setDefaultHighWaterMark(objectMode: boolean, value: number): void {
|
|
18
|
+
if (typeof value !== 'number' || value < 0 || Number.isNaN(value)) {
|
|
19
|
+
throw new TypeError(`Invalid highWaterMark: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
if (objectMode) {
|
|
22
|
+
defaultObjectHighWaterMark = value;
|
|
23
|
+
} else {
|
|
24
|
+
defaultHighWaterMark = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---- Types ----
|
|
29
|
+
|
|
30
|
+
/** Base options accepted by the Stream constructor (superset used by subclass options). */
|
|
31
|
+
export interface StreamOptions {
|
|
32
|
+
highWaterMark?: number;
|
|
33
|
+
objectMode?: boolean;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
captureRejections?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type { ReadableOptions, WritableOptions, DuplexOptions, TransformOptions, FinishedOptions };
|
|
39
|
+
|
|
40
|
+
// ---- Stream base class ----
|
|
41
|
+
|
|
42
|
+
/** A stream-like emitter that may have `pause` and `resume` methods (duck-typed). */
|
|
43
|
+
interface StreamLike extends EventEmitter {
|
|
44
|
+
pause?(): void;
|
|
45
|
+
resume?(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Tracked pipe destination for unpipe support. */
|
|
49
|
+
interface PipeState {
|
|
50
|
+
dest: Writable;
|
|
51
|
+
ondata: (chunk: unknown) => void;
|
|
52
|
+
ondrain: () => void;
|
|
53
|
+
onend: () => void;
|
|
54
|
+
cleanup: () => void;
|
|
55
|
+
doEnd: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class Stream extends EventEmitter {
|
|
59
|
+
constructor(opts?: StreamOptions) {
|
|
60
|
+
super(opts);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pipe<T extends Writable>(destination: T, options?: { end?: boolean }): T {
|
|
64
|
+
const source = this as unknown as Readable;
|
|
65
|
+
const doEnd = options?.end !== false;
|
|
66
|
+
|
|
67
|
+
const ondata = (chunk: unknown) => {
|
|
68
|
+
if (destination.writable) {
|
|
69
|
+
if (destination.write(chunk) === false && typeof (source as StreamLike).pause === 'function') {
|
|
70
|
+
(source as StreamLike).pause!();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
source.on('data', ondata);
|
|
76
|
+
|
|
77
|
+
const ondrain = () => {
|
|
78
|
+
if (typeof (source as StreamLike).resume === 'function') {
|
|
79
|
+
(source as StreamLike).resume!();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
destination.on('drain', ondrain);
|
|
83
|
+
|
|
84
|
+
const onend = () => {
|
|
85
|
+
if (doEnd) {
|
|
86
|
+
destination.end();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
if (doEnd) {
|
|
90
|
+
source.on('end', onend);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cleanup = () => {
|
|
94
|
+
source.removeListener('data', ondata);
|
|
95
|
+
destination.removeListener('drain', ondrain);
|
|
96
|
+
source.removeListener('end', onend);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
source.on('close', cleanup);
|
|
100
|
+
destination.on('close', cleanup);
|
|
101
|
+
|
|
102
|
+
// Track piped destinations for unpipe
|
|
103
|
+
if (source instanceof Readable) {
|
|
104
|
+
source._pipeDests.push({ dest: destination, ondata, ondrain, onend, cleanup, doEnd });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
destination.emit('pipe', source);
|
|
108
|
+
return destination;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- Readable ----
|
|
113
|
+
|
|
114
|
+
export class Readable extends Stream {
|
|
115
|
+
readable = true;
|
|
116
|
+
readableFlowing: boolean | null = null;
|
|
117
|
+
readableLength = 0;
|
|
118
|
+
readableHighWaterMark: number;
|
|
119
|
+
readableEncoding: string | null;
|
|
120
|
+
readableObjectMode: boolean;
|
|
121
|
+
readableEnded = false;
|
|
122
|
+
readableAborted = false;
|
|
123
|
+
destroyed = false;
|
|
124
|
+
|
|
125
|
+
/** @internal Tracked pipe destinations for unpipe. */
|
|
126
|
+
_pipeDests: PipeState[] = [];
|
|
127
|
+
|
|
128
|
+
private _buffer: unknown[] = [];
|
|
129
|
+
private _readableState = { ended: false, endEmitted: false, reading: false, constructed: true };
|
|
130
|
+
private _readablePending = false;
|
|
131
|
+
private _readImpl: ((size: number) => void) | undefined;
|
|
132
|
+
private _destroyImpl: ((error: Error | null, cb: (error?: Error | null) => void) => void) | undefined;
|
|
133
|
+
private _constructImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
|
|
134
|
+
|
|
135
|
+
constructor(opts?: ReadableOptions) {
|
|
136
|
+
super(opts);
|
|
137
|
+
this.readableHighWaterMark = opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.objectMode ?? false);
|
|
138
|
+
this.readableEncoding = opts?.encoding ?? null;
|
|
139
|
+
this.readableObjectMode = opts?.objectMode ?? false;
|
|
140
|
+
if (opts?.read) this._readImpl = opts.read;
|
|
141
|
+
if (opts?.destroy) this._destroyImpl = opts.destroy;
|
|
142
|
+
if (opts?.construct) this._constructImpl = opts.construct;
|
|
143
|
+
|
|
144
|
+
// Call _construct if provided via options or overridden by subclass
|
|
145
|
+
const hasConstruct = this._constructImpl || this._construct !== Readable.prototype._construct;
|
|
146
|
+
if (hasConstruct) {
|
|
147
|
+
this._readableState.constructed = false;
|
|
148
|
+
nextTick(() => {
|
|
149
|
+
this._construct((err) => {
|
|
150
|
+
this._readableState.constructed = true;
|
|
151
|
+
if (err) {
|
|
152
|
+
this.destroy(err);
|
|
153
|
+
} else {
|
|
154
|
+
// If data was requested before construct finished, start reading
|
|
155
|
+
if (this.readableFlowing === true) {
|
|
156
|
+
this._flow();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_construct(callback: (error?: Error | null) => void): void {
|
|
165
|
+
if (this._constructImpl) {
|
|
166
|
+
this._constructImpl.call(this, callback);
|
|
167
|
+
} else {
|
|
168
|
+
callback();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_read(_size: number): void {
|
|
173
|
+
if (this._readImpl) {
|
|
174
|
+
this._readImpl.call(this, _size);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
read(size?: number): any {
|
|
179
|
+
// Don't read until constructed
|
|
180
|
+
if (!this._readableState.constructed) return null;
|
|
181
|
+
|
|
182
|
+
if (this._buffer.length === 0) {
|
|
183
|
+
if (this._readableState.ended) return null;
|
|
184
|
+
this._readableState.reading = true;
|
|
185
|
+
this._read(size ?? this.readableHighWaterMark);
|
|
186
|
+
this._readableState.reading = false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (this._buffer.length === 0) return null;
|
|
190
|
+
|
|
191
|
+
if (size === 0) return null;
|
|
192
|
+
|
|
193
|
+
if (this.readableObjectMode) {
|
|
194
|
+
if (size === undefined) {
|
|
195
|
+
const chunk = this._buffer.shift();
|
|
196
|
+
this.readableLength -= 1;
|
|
197
|
+
if (this._readableState.ended && this._buffer.length === 0 && !this._readableState.endEmitted) {
|
|
198
|
+
this._emitEnd();
|
|
199
|
+
}
|
|
200
|
+
return chunk;
|
|
201
|
+
}
|
|
202
|
+
// In objectMode, size means number of objects
|
|
203
|
+
if (size > this.readableLength) return null;
|
|
204
|
+
const chunk = this._buffer.shift();
|
|
205
|
+
this.readableLength -= 1;
|
|
206
|
+
return chunk;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Byte mode: compute total buffered bytes
|
|
210
|
+
if (size !== undefined && size !== null) {
|
|
211
|
+
if (size > this.readableLength) return null;
|
|
212
|
+
// Partial read: extract exactly `size` bytes from buffer
|
|
213
|
+
return this._readBytes(size);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Read all buffered data
|
|
217
|
+
const result = this._buffer.splice(0);
|
|
218
|
+
this.readableLength = 0;
|
|
219
|
+
if (this._readableState.ended && this._buffer.length === 0 && !this._readableState.endEmitted) {
|
|
220
|
+
this._emitEnd();
|
|
221
|
+
}
|
|
222
|
+
if (result.length === 1) return result[0];
|
|
223
|
+
if (result.length === 0) return null;
|
|
224
|
+
// Concatenate: strings with join, buffers with Buffer.concat
|
|
225
|
+
if (typeof result[0] === 'string') return result.join('');
|
|
226
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
227
|
+
return BufCtor?.concat ? BufCtor.concat(result) : result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @internal Extract exactly `size` bytes from the internal buffer. */
|
|
231
|
+
private _readBytes(size: number): any {
|
|
232
|
+
let collected = 0;
|
|
233
|
+
const parts: unknown[] = [];
|
|
234
|
+
while (collected < size && this._buffer.length > 0) {
|
|
235
|
+
const chunk = this._buffer[0];
|
|
236
|
+
const chunkLen = (chunk as any).length ?? 1;
|
|
237
|
+
if (collected + chunkLen <= size) {
|
|
238
|
+
// Take the whole chunk
|
|
239
|
+
parts.push(this._buffer.shift()!);
|
|
240
|
+
collected += chunkLen;
|
|
241
|
+
this.readableLength -= chunkLen;
|
|
242
|
+
} else {
|
|
243
|
+
// Split the chunk
|
|
244
|
+
const needed = size - collected;
|
|
245
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
246
|
+
if (BufCtor && BufCtor.isBuffer(chunk)) {
|
|
247
|
+
parts.push((chunk as any).slice(0, needed));
|
|
248
|
+
this._buffer[0] = (chunk as any).slice(needed);
|
|
249
|
+
} else if (typeof chunk === 'string') {
|
|
250
|
+
parts.push(chunk.slice(0, needed));
|
|
251
|
+
this._buffer[0] = chunk.slice(needed);
|
|
252
|
+
} else {
|
|
253
|
+
// Uint8Array or similar
|
|
254
|
+
parts.push((chunk as Uint8Array).slice(0, needed));
|
|
255
|
+
this._buffer[0] = (chunk as Uint8Array).slice(needed);
|
|
256
|
+
}
|
|
257
|
+
this.readableLength -= needed;
|
|
258
|
+
collected += needed;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (parts.length === 1) return parts[0];
|
|
262
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
263
|
+
return BufCtor?.concat ? BufCtor.concat(parts) : parts;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
push(chunk: any, encoding?: string): boolean {
|
|
267
|
+
if (chunk === null) {
|
|
268
|
+
this._readableState.ended = true;
|
|
269
|
+
this.readableEnded = true;
|
|
270
|
+
if (this._buffer.length === 0 && !this._readableState.endEmitted) {
|
|
271
|
+
nextTick(() => this._emitEnd());
|
|
272
|
+
}
|
|
273
|
+
// Emit 'readable' for listeners waiting on EOF with buffered data
|
|
274
|
+
this._scheduleReadable();
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this._buffer.push(chunk);
|
|
279
|
+
this.readableLength += this.readableObjectMode ? 1 : (chunk.length ?? 1);
|
|
280
|
+
|
|
281
|
+
// In flowing mode, schedule draining (unless already flowing)
|
|
282
|
+
if (this.readableFlowing && !this._flowing) {
|
|
283
|
+
nextTick(() => this._flow());
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// In non-flowing mode, emit 'readable' to notify data is available
|
|
287
|
+
if (this.readableFlowing !== true) {
|
|
288
|
+
this._scheduleReadable();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this.readableLength < this.readableHighWaterMark;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Emit 'end' followed by 'close' (matches Node.js autoDestroy behavior). */
|
|
295
|
+
private _emitEnd(): void {
|
|
296
|
+
if (this._readableState.endEmitted) return;
|
|
297
|
+
this._readableState.endEmitted = true;
|
|
298
|
+
this.emit('end');
|
|
299
|
+
nextTick(() => this.emit('close'));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Schedule a single 'readable' event per microtask cycle (deduplicates multiple pushes). */
|
|
303
|
+
private _scheduleReadable(): void {
|
|
304
|
+
if (this._readablePending || this.listenerCount('readable') === 0) return;
|
|
305
|
+
this._readablePending = true;
|
|
306
|
+
nextTick(() => {
|
|
307
|
+
this._readablePending = false;
|
|
308
|
+
if (!this.destroyed) this.emit('readable');
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
|
313
|
+
super.on(event, listener);
|
|
314
|
+
// Attaching a 'data' listener switches to flowing mode (like Node.js)
|
|
315
|
+
if (event === 'data' && this.readableFlowing !== false) {
|
|
316
|
+
this.resume();
|
|
317
|
+
}
|
|
318
|
+
// Attaching a 'readable' listener: if data is already buffered, schedule event
|
|
319
|
+
if (event === 'readable' && (this._buffer.length > 0 || this._readableState.ended)) {
|
|
320
|
+
this._scheduleReadable();
|
|
321
|
+
}
|
|
322
|
+
return this;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
unshift(chunk: any): void {
|
|
326
|
+
this._buffer.unshift(chunk);
|
|
327
|
+
this.readableLength += this.readableObjectMode ? 1 : (chunk.length ?? 1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
setEncoding(encoding: string): this {
|
|
331
|
+
this.readableEncoding = encoding;
|
|
332
|
+
return this;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
pause(): this {
|
|
336
|
+
this.readableFlowing = false;
|
|
337
|
+
this.emit('pause');
|
|
338
|
+
return this;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
resume(): this {
|
|
342
|
+
if (this.readableFlowing !== true) {
|
|
343
|
+
this.readableFlowing = true;
|
|
344
|
+
this.emit('resume');
|
|
345
|
+
// Start flowing: drain buffered data and call _read
|
|
346
|
+
if (this._readableState.constructed) {
|
|
347
|
+
this._flow();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return this;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private _flowing = false;
|
|
354
|
+
|
|
355
|
+
private _flow(): void {
|
|
356
|
+
if (this.readableFlowing !== true || this._flowing || this.destroyed) return;
|
|
357
|
+
if (!this._readableState.constructed) return;
|
|
358
|
+
this._flowing = true;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
// Drain buffered data synchronously (like Node.js flow())
|
|
362
|
+
while (this._buffer.length > 0 && this.readableFlowing && !this.destroyed) {
|
|
363
|
+
let chunk = this._buffer.shift()!;
|
|
364
|
+
this.readableLength -= this.readableObjectMode ? 1 : ((chunk as { length?: number }).length ?? 1);
|
|
365
|
+
// Decode to string when setEncoding was called
|
|
366
|
+
if (this.readableEncoding && typeof chunk !== 'string') {
|
|
367
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
368
|
+
if (BufCtor && BufCtor.isBuffer(chunk)) {
|
|
369
|
+
chunk = (chunk as any).toString(this.readableEncoding);
|
|
370
|
+
} else if (chunk instanceof Uint8Array) {
|
|
371
|
+
chunk = new TextDecoder(this.readableEncoding).decode(chunk as Uint8Array);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
this.emit('data', chunk);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (this.destroyed) return;
|
|
378
|
+
|
|
379
|
+
// If ended and buffer drained, emit end
|
|
380
|
+
if (this._readableState.ended && this._buffer.length === 0 && !this._readableState.endEmitted) {
|
|
381
|
+
nextTick(() => this._emitEnd());
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Call _read to get more data (may push synchronously)
|
|
386
|
+
if (!this._readableState.ended && !this._readableState.reading && !this.destroyed) {
|
|
387
|
+
this._readableState.reading = true;
|
|
388
|
+
this._read(this.readableHighWaterMark);
|
|
389
|
+
this._readableState.reading = false;
|
|
390
|
+
}
|
|
391
|
+
} finally {
|
|
392
|
+
this._flowing = false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// After _read, if new data was pushed, schedule another flow
|
|
396
|
+
if (this._buffer.length > 0 && this.readableFlowing && !this.destroyed) {
|
|
397
|
+
nextTick(() => this._flow());
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
isPaused(): boolean {
|
|
402
|
+
return this.readableFlowing === false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
unpipe(destination?: Writable): this {
|
|
406
|
+
if (!destination) {
|
|
407
|
+
// Remove all piped destinations
|
|
408
|
+
for (const state of this._pipeDests) {
|
|
409
|
+
state.cleanup();
|
|
410
|
+
state.dest.emit('unpipe', this);
|
|
411
|
+
}
|
|
412
|
+
this._pipeDests = [];
|
|
413
|
+
this.readableFlowing = false;
|
|
414
|
+
} else {
|
|
415
|
+
const idx = this._pipeDests.findIndex(s => s.dest === destination);
|
|
416
|
+
if (idx !== -1) {
|
|
417
|
+
const state = this._pipeDests[idx];
|
|
418
|
+
state.cleanup();
|
|
419
|
+
this._pipeDests.splice(idx, 1);
|
|
420
|
+
destination.emit('unpipe', this);
|
|
421
|
+
if (this._pipeDests.length === 0) {
|
|
422
|
+
this.readableFlowing = false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
destroy(error?: Error): this {
|
|
430
|
+
if (this.destroyed) return this;
|
|
431
|
+
this.destroyed = true;
|
|
432
|
+
this.readable = false;
|
|
433
|
+
this.readableAborted = !this.readableEnded;
|
|
434
|
+
|
|
435
|
+
const cb = (err?: Error | null) => {
|
|
436
|
+
// Emit error and close in separate nextTick calls (matches Node.js behavior)
|
|
437
|
+
// so an unhandled error doesn't prevent 'close' from firing
|
|
438
|
+
if (err) nextTick(() => this.emit('error', err));
|
|
439
|
+
nextTick(() => this.emit('close'));
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (this._destroyImpl) {
|
|
443
|
+
this._destroyImpl.call(this, error ?? null, cb);
|
|
444
|
+
} else {
|
|
445
|
+
cb(error);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<unknown> {
|
|
452
|
+
const readable = this;
|
|
453
|
+
const buffer: unknown[] = [];
|
|
454
|
+
let done = false;
|
|
455
|
+
let error: Error | null = null;
|
|
456
|
+
let waitingResolve: ((value: IteratorResult<unknown>) => void) | null = null;
|
|
457
|
+
let waitingReject: ((reason: unknown) => void) | null = null;
|
|
458
|
+
|
|
459
|
+
readable.on('data', (chunk: unknown) => {
|
|
460
|
+
if (waitingResolve) {
|
|
461
|
+
const resolve = waitingResolve;
|
|
462
|
+
waitingResolve = null;
|
|
463
|
+
waitingReject = null;
|
|
464
|
+
resolve({ value: chunk, done: false });
|
|
465
|
+
} else {
|
|
466
|
+
buffer.push(chunk);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
readable.on('end', () => {
|
|
471
|
+
done = true;
|
|
472
|
+
if (waitingResolve) {
|
|
473
|
+
const resolve = waitingResolve;
|
|
474
|
+
waitingResolve = null;
|
|
475
|
+
waitingReject = null;
|
|
476
|
+
resolve({ value: undefined, done: true });
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
readable.on('error', (err: Error) => {
|
|
481
|
+
error = err;
|
|
482
|
+
done = true;
|
|
483
|
+
if (waitingReject) {
|
|
484
|
+
const reject = waitingReject;
|
|
485
|
+
waitingResolve = null;
|
|
486
|
+
waitingReject = null;
|
|
487
|
+
reject(err);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
next(): Promise<IteratorResult<unknown>> {
|
|
493
|
+
if (error) return Promise.reject(error);
|
|
494
|
+
if (buffer.length > 0) return Promise.resolve({ value: buffer.shift(), done: false });
|
|
495
|
+
if (done) return Promise.resolve({ value: undefined, done: true });
|
|
496
|
+
return new Promise((resolve, reject) => {
|
|
497
|
+
waitingResolve = resolve;
|
|
498
|
+
waitingReject = reject;
|
|
499
|
+
});
|
|
500
|
+
},
|
|
501
|
+
return(): Promise<IteratorResult<unknown>> {
|
|
502
|
+
readable.destroy();
|
|
503
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
504
|
+
},
|
|
505
|
+
[Symbol.asyncIterator]() { return this; }
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
static from(iterable: Iterable<unknown> | AsyncIterable<unknown>, opts?: ReadableOptions): Readable {
|
|
510
|
+
const readable = new Readable({
|
|
511
|
+
objectMode: true,
|
|
512
|
+
...opts,
|
|
513
|
+
read() {}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Buffer, Uint8Array, and strings should be pushed as a single chunk,
|
|
517
|
+
// not iterated element-by-element (matching Node.js Readable.from behavior)
|
|
518
|
+
if (typeof iterable === 'string' || ArrayBuffer.isView(iterable)) {
|
|
519
|
+
readable.push(iterable);
|
|
520
|
+
readable.push(null);
|
|
521
|
+
return readable;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
(async () => {
|
|
525
|
+
try {
|
|
526
|
+
for await (const chunk of iterable as AsyncIterable<unknown>) {
|
|
527
|
+
if (!readable.push(chunk)) {
|
|
528
|
+
// Backpressure — wait for drain
|
|
529
|
+
await new Promise<void>(resolve => readable.once('drain', resolve));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
readable.push(null);
|
|
533
|
+
} catch (err) {
|
|
534
|
+
readable.destroy(err as Error);
|
|
535
|
+
}
|
|
536
|
+
})();
|
|
537
|
+
|
|
538
|
+
return readable;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ---- Writable ----
|
|
543
|
+
|
|
544
|
+
export class Writable extends Stream {
|
|
545
|
+
writable = true;
|
|
546
|
+
writableHighWaterMark: number;
|
|
547
|
+
writableLength = 0;
|
|
548
|
+
writableObjectMode: boolean;
|
|
549
|
+
writableEnded = false;
|
|
550
|
+
writableFinished = false;
|
|
551
|
+
writableCorked = 0;
|
|
552
|
+
writableNeedDrain = false;
|
|
553
|
+
destroyed = false;
|
|
554
|
+
|
|
555
|
+
private _writableState = { ended: false, finished: false, constructed: true, writing: false };
|
|
556
|
+
private _corkedBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
|
|
557
|
+
private _writeBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
|
|
558
|
+
private _pendingConstruct: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
|
|
559
|
+
private _ending = false;
|
|
560
|
+
private _endCallback?: () => void;
|
|
561
|
+
private _pendingEnd: { chunk?: any; encoding?: string; callback?: () => void } | null = null;
|
|
562
|
+
private _writeImpl: ((chunk: any, encoding: string, cb: (error?: Error | null) => void) => void) | undefined;
|
|
563
|
+
private _writev: ((chunks: Array<{ chunk: any; encoding: string }>, cb: (error?: Error | null) => void) => void) | undefined;
|
|
564
|
+
private _finalImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
|
|
565
|
+
private _destroyImpl: ((error: Error | null, cb: (error?: Error | null) => void) => void) | undefined;
|
|
566
|
+
private _constructImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
|
|
567
|
+
private _decodeStrings: boolean;
|
|
568
|
+
private _defaultEncoding = 'utf8';
|
|
569
|
+
|
|
570
|
+
constructor(opts?: WritableOptions) {
|
|
571
|
+
super(opts);
|
|
572
|
+
this.writableHighWaterMark = opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.objectMode ?? false);
|
|
573
|
+
this.writableObjectMode = opts?.objectMode ?? false;
|
|
574
|
+
this._decodeStrings = opts?.decodeStrings !== false;
|
|
575
|
+
if (opts?.write) this._writeImpl = opts.write;
|
|
576
|
+
if (opts?.writev) this._writev = opts.writev;
|
|
577
|
+
if (opts?.final) this._finalImpl = opts.final;
|
|
578
|
+
if (opts?.destroy) this._destroyImpl = opts.destroy;
|
|
579
|
+
if (opts?.construct) this._constructImpl = opts.construct;
|
|
580
|
+
|
|
581
|
+
// Call _construct if provided via options or overridden by subclass
|
|
582
|
+
const hasConstruct = this._constructImpl || this._construct !== Writable.prototype._construct;
|
|
583
|
+
if (hasConstruct) {
|
|
584
|
+
this._writableState.constructed = false;
|
|
585
|
+
nextTick(() => {
|
|
586
|
+
this._construct((err) => {
|
|
587
|
+
this._writableState.constructed = true;
|
|
588
|
+
if (err) {
|
|
589
|
+
this.destroy(err);
|
|
590
|
+
} else {
|
|
591
|
+
this._maybeFlush();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
_construct(callback: (error?: Error | null) => void): void {
|
|
599
|
+
if (this._constructImpl) {
|
|
600
|
+
this._constructImpl.call(this, callback);
|
|
601
|
+
} else {
|
|
602
|
+
callback();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
|
|
607
|
+
if (this._writeImpl) {
|
|
608
|
+
this._writeImpl.call(this, chunk, encoding, callback);
|
|
609
|
+
} else {
|
|
610
|
+
callback();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
_final(callback: (error?: Error | null) => void): void {
|
|
615
|
+
if (this._finalImpl) {
|
|
616
|
+
this._finalImpl.call(this, callback);
|
|
617
|
+
} else {
|
|
618
|
+
callback();
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private _maybeFlush(): void {
|
|
623
|
+
// Flush writes that were buffered while waiting for _construct
|
|
624
|
+
const pending = this._pendingConstruct.splice(0);
|
|
625
|
+
if (pending.length > 0) {
|
|
626
|
+
// First write goes directly, rest get serialized via _writeBuffer
|
|
627
|
+
const [first, ...rest] = pending;
|
|
628
|
+
this._writeBuffer.push(...rest);
|
|
629
|
+
this._doWrite(first.chunk, first.encoding, first.callback);
|
|
630
|
+
}
|
|
631
|
+
if (this._pendingEnd) {
|
|
632
|
+
const { chunk, encoding, callback } = this._pendingEnd;
|
|
633
|
+
this._pendingEnd = null;
|
|
634
|
+
this._doEnd(chunk, encoding, callback);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private _doWrite(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
|
|
639
|
+
this._writableState.writing = true;
|
|
640
|
+
this._write(chunk, encoding, (err) => {
|
|
641
|
+
this._writableState.writing = false;
|
|
642
|
+
this.writableLength -= this.writableObjectMode ? 1 : (chunk?.length ?? 1);
|
|
643
|
+
if (err) {
|
|
644
|
+
nextTick(() => {
|
|
645
|
+
callback(err);
|
|
646
|
+
this.emit('error', err);
|
|
647
|
+
this._drainWriteBuffer();
|
|
648
|
+
});
|
|
649
|
+
} else {
|
|
650
|
+
nextTick(() => {
|
|
651
|
+
callback();
|
|
652
|
+
if (this.writableNeedDrain && this.writableLength < this.writableHighWaterMark) {
|
|
653
|
+
this.writableNeedDrain = false;
|
|
654
|
+
this.emit('drain');
|
|
655
|
+
}
|
|
656
|
+
this._drainWriteBuffer();
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private _drainWriteBuffer(): void {
|
|
663
|
+
if (this._writeBuffer.length > 0) {
|
|
664
|
+
const next = this._writeBuffer.shift()!;
|
|
665
|
+
this._doWrite(next.chunk, next.encoding, next.callback);
|
|
666
|
+
} else {
|
|
667
|
+
this._maybeFinish();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
private _maybeFinish(): void {
|
|
672
|
+
if (!this._ending || this._writableState.finished || this._writableState.writing || this._writeBuffer.length > 0) return;
|
|
673
|
+
this._ending = false;
|
|
674
|
+
|
|
675
|
+
this._final((err) => {
|
|
676
|
+
this.writableFinished = true;
|
|
677
|
+
this._writableState.finished = true;
|
|
678
|
+
nextTick(() => {
|
|
679
|
+
if (err) {
|
|
680
|
+
this.emit('error', err);
|
|
681
|
+
}
|
|
682
|
+
this.emit('finish');
|
|
683
|
+
nextTick(() => this.emit('close'));
|
|
684
|
+
if (this._endCallback) this._endCallback();
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
|
|
690
|
+
if (typeof encoding === 'function') {
|
|
691
|
+
callback = encoding;
|
|
692
|
+
encoding = undefined;
|
|
693
|
+
}
|
|
694
|
+
if (encoding === undefined) encoding = this._defaultEncoding;
|
|
695
|
+
callback = callback || (() => {});
|
|
696
|
+
|
|
697
|
+
// Convert strings to Buffer when decodeStrings is true (default), but not in objectMode
|
|
698
|
+
if (this._decodeStrings && !this.writableObjectMode && typeof chunk === 'string') {
|
|
699
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
700
|
+
if (BufCtor) {
|
|
701
|
+
chunk = BufCtor.from(chunk, encoding);
|
|
702
|
+
encoding = 'buffer';
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Set encoding to 'buffer' for Buffer/Uint8Array chunks
|
|
706
|
+
if (typeof chunk !== 'string' && !this.writableObjectMode) {
|
|
707
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
708
|
+
if ((BufCtor && BufCtor.isBuffer(chunk)) || chunk instanceof Uint8Array) {
|
|
709
|
+
encoding = 'buffer';
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (this.writableEnded) {
|
|
714
|
+
const err = new Error('write after end');
|
|
715
|
+
nextTick(() => {
|
|
716
|
+
if (callback) callback(err);
|
|
717
|
+
this.emit('error', err);
|
|
718
|
+
});
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
this.writableLength += this.writableObjectMode ? 1 : (chunk?.length ?? 1);
|
|
723
|
+
|
|
724
|
+
// If corked, buffer the write
|
|
725
|
+
if (this.writableCorked > 0) {
|
|
726
|
+
this._corkedBuffer.push({ chunk, encoding: encoding as string, callback });
|
|
727
|
+
return this.writableLength < this.writableHighWaterMark;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// If not yet constructed, buffer writes until construction finishes
|
|
731
|
+
if (!this._writableState.constructed) {
|
|
732
|
+
this._pendingConstruct.push({ chunk, encoding: encoding as string, callback });
|
|
733
|
+
return this.writableLength < this.writableHighWaterMark;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Compute backpressure BEFORE _doWrite (sync transforms may decrement length immediately)
|
|
737
|
+
const belowHWM = this.writableLength < this.writableHighWaterMark;
|
|
738
|
+
if (!belowHWM) {
|
|
739
|
+
this.writableNeedDrain = true;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Serialize writes: only one _write at a time, buffer the rest
|
|
743
|
+
if (this._writableState.writing) {
|
|
744
|
+
this._writeBuffer.push({ chunk, encoding: encoding as string, callback });
|
|
745
|
+
} else {
|
|
746
|
+
this._doWrite(chunk, encoding as string, callback);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return belowHWM;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private _doEnd(chunk?: any, encoding?: string, callback?: () => void): void {
|
|
753
|
+
if (chunk !== undefined && chunk !== null) {
|
|
754
|
+
this.write(chunk, encoding as string);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
this.writableEnded = true;
|
|
758
|
+
this._writableState.ended = true;
|
|
759
|
+
this._ending = true;
|
|
760
|
+
this._endCallback = callback;
|
|
761
|
+
|
|
762
|
+
// _maybeFinish will call _final once all pending writes have drained
|
|
763
|
+
this._maybeFinish();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
end(chunk?: any, encoding?: string | (() => void), callback?: () => void): this {
|
|
767
|
+
if (typeof chunk === 'function') {
|
|
768
|
+
callback = chunk;
|
|
769
|
+
chunk = undefined;
|
|
770
|
+
}
|
|
771
|
+
if (typeof encoding === 'function') {
|
|
772
|
+
callback = encoding;
|
|
773
|
+
encoding = undefined;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Ignore duplicate end() calls (e.g. from auto-end after half-close)
|
|
777
|
+
if (this.writableEnded) {
|
|
778
|
+
if (callback) nextTick(callback);
|
|
779
|
+
return this;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// If not yet constructed, defer end until construction finishes
|
|
783
|
+
if (!this._writableState.constructed) {
|
|
784
|
+
this._pendingEnd = { chunk, encoding: encoding as string, callback };
|
|
785
|
+
return this;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this._doEnd(chunk, encoding as string, callback);
|
|
789
|
+
|
|
790
|
+
return this;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
cork(): void {
|
|
794
|
+
this.writableCorked++;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
uncork(): void {
|
|
798
|
+
if (this.writableCorked > 0) {
|
|
799
|
+
this.writableCorked--;
|
|
800
|
+
if (this.writableCorked === 0 && this._corkedBuffer.length > 0) {
|
|
801
|
+
this._flushCorkedBuffer();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private _flushCorkedBuffer(): void {
|
|
807
|
+
// If _writev is available, flush as a batch
|
|
808
|
+
if (this._writev && this._corkedBuffer.length > 1) {
|
|
809
|
+
const buffered = this._corkedBuffer.splice(0);
|
|
810
|
+
const chunks = buffered.map(b => ({ chunk: b.chunk, encoding: b.encoding }));
|
|
811
|
+
this._writev.call(this, chunks, (err) => {
|
|
812
|
+
for (const b of buffered) {
|
|
813
|
+
this.writableLength -= this.writableObjectMode ? 1 : (b.chunk?.length ?? 1);
|
|
814
|
+
}
|
|
815
|
+
if (err) {
|
|
816
|
+
for (const b of buffered) b.callback(err);
|
|
817
|
+
this.emit('error', err);
|
|
818
|
+
} else {
|
|
819
|
+
for (const b of buffered) b.callback();
|
|
820
|
+
if (this.writableNeedDrain && this.writableLength < this.writableHighWaterMark) {
|
|
821
|
+
this.writableNeedDrain = false;
|
|
822
|
+
this.emit('drain');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
} else {
|
|
827
|
+
// Flush one by one via serialized write path
|
|
828
|
+
const buffered = this._corkedBuffer.splice(0);
|
|
829
|
+
if (buffered.length > 0) {
|
|
830
|
+
const [first, ...rest] = buffered;
|
|
831
|
+
this._writeBuffer.push(...rest);
|
|
832
|
+
this._doWrite(first.chunk, first.encoding, first.callback);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
setDefaultEncoding(encoding: string): this {
|
|
838
|
+
this._defaultEncoding = encoding;
|
|
839
|
+
return this;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
destroy(error?: Error): this {
|
|
843
|
+
if (this.destroyed) return this;
|
|
844
|
+
this.destroyed = true;
|
|
845
|
+
this.writable = false;
|
|
846
|
+
|
|
847
|
+
const cb = (err?: Error | null) => {
|
|
848
|
+
if (err) nextTick(() => this.emit('error', err));
|
|
849
|
+
nextTick(() => this.emit('close'));
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
if (this._destroyImpl) {
|
|
853
|
+
this._destroyImpl.call(this, error ?? null, cb);
|
|
854
|
+
} else {
|
|
855
|
+
cb(error);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ---- Duplex ----
|
|
863
|
+
|
|
864
|
+
export class Duplex extends Readable {
|
|
865
|
+
writable = true;
|
|
866
|
+
writableHighWaterMark: number;
|
|
867
|
+
writableLength = 0;
|
|
868
|
+
writableObjectMode: boolean;
|
|
869
|
+
writableEnded = false;
|
|
870
|
+
writableFinished = false;
|
|
871
|
+
writableCorked = 0;
|
|
872
|
+
writableNeedDrain = false;
|
|
873
|
+
allowHalfOpen: boolean;
|
|
874
|
+
private _decodeStrings: boolean;
|
|
875
|
+
|
|
876
|
+
private _duplexCorkedBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
|
|
877
|
+
private _writeImpl: ((chunk: any, encoding: string, cb: (error?: Error | null) => void) => void) | undefined;
|
|
878
|
+
private _finalImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
|
|
879
|
+
private _defaultEncoding = 'utf8';
|
|
880
|
+
private _pendingWrites = 0;
|
|
881
|
+
private _pendingEndCb: (() => void) | null = null;
|
|
882
|
+
|
|
883
|
+
constructor(opts?: DuplexOptions) {
|
|
884
|
+
super(opts);
|
|
885
|
+
this.writableHighWaterMark = opts?.writableHighWaterMark ?? opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.writableObjectMode ?? opts?.objectMode ?? false);
|
|
886
|
+
this.writableObjectMode = opts?.writableObjectMode ?? opts?.objectMode ?? false;
|
|
887
|
+
this.allowHalfOpen = opts?.allowHalfOpen !== false;
|
|
888
|
+
this._decodeStrings = opts?.decodeStrings !== false;
|
|
889
|
+
if (opts?.write) this._writeImpl = opts.write;
|
|
890
|
+
// writev not yet supported on Duplex
|
|
891
|
+
if (opts?.final) this._finalImpl = opts.final;
|
|
892
|
+
|
|
893
|
+
// When allowHalfOpen=false, end writable when readable ends
|
|
894
|
+
if (!this.allowHalfOpen) {
|
|
895
|
+
this.once('end', () => {
|
|
896
|
+
if (!this.writableEnded) {
|
|
897
|
+
nextTick(() => this.end());
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
|
|
904
|
+
if (this._writeImpl) {
|
|
905
|
+
this._writeImpl.call(this, chunk, encoding, callback);
|
|
906
|
+
} else {
|
|
907
|
+
callback();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
_final(callback: (error?: Error | null) => void): void {
|
|
912
|
+
if (this._finalImpl) {
|
|
913
|
+
this._finalImpl.call(this, callback);
|
|
914
|
+
} else {
|
|
915
|
+
callback();
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
override destroy(error?: Error): this {
|
|
920
|
+
if (this.destroyed) return this;
|
|
921
|
+
this.writable = false;
|
|
922
|
+
return super.destroy(error);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
|
|
926
|
+
if (typeof encoding === 'function') {
|
|
927
|
+
callback = encoding;
|
|
928
|
+
encoding = undefined;
|
|
929
|
+
}
|
|
930
|
+
if (encoding === undefined) encoding = this._defaultEncoding;
|
|
931
|
+
|
|
932
|
+
// Convert strings to Buffer when decodeStrings is true (default), but not in objectMode
|
|
933
|
+
if (this._decodeStrings && !this.writableObjectMode && typeof chunk === 'string') {
|
|
934
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
935
|
+
if (BufCtor) {
|
|
936
|
+
chunk = BufCtor.from(chunk, encoding);
|
|
937
|
+
encoding = 'buffer';
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Set encoding to 'buffer' for Buffer/Uint8Array chunks
|
|
941
|
+
if (typeof chunk !== 'string' && !this.writableObjectMode) {
|
|
942
|
+
const BufCtor = (globalThis as any).Buffer;
|
|
943
|
+
if ((BufCtor && BufCtor.isBuffer(chunk)) || chunk instanceof Uint8Array) {
|
|
944
|
+
encoding = 'buffer';
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (this.writableEnded) {
|
|
949
|
+
const err = new Error('write after end');
|
|
950
|
+
const cb = callback || (() => {});
|
|
951
|
+
nextTick(() => {
|
|
952
|
+
cb(err);
|
|
953
|
+
this.emit('error', err);
|
|
954
|
+
});
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
this.writableLength += this.writableObjectMode ? 1 : (chunk?.length ?? 1);
|
|
959
|
+
|
|
960
|
+
// If corked, buffer the write
|
|
961
|
+
if (this.writableCorked > 0) {
|
|
962
|
+
this._duplexCorkedBuffer.push({ chunk, encoding: encoding as string, callback: callback || (() => {}) });
|
|
963
|
+
return this.writableLength < this.writableHighWaterMark;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Compute backpressure BEFORE _write (sync transforms may decrement length immediately)
|
|
967
|
+
const belowHWM = this.writableLength < this.writableHighWaterMark;
|
|
968
|
+
if (!belowHWM) {
|
|
969
|
+
this.writableNeedDrain = true;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const cb = callback || (() => {});
|
|
973
|
+
this._pendingWrites++;
|
|
974
|
+
this._write(chunk, encoding as string, (err) => {
|
|
975
|
+
this._pendingWrites--;
|
|
976
|
+
this.writableLength -= this.writableObjectMode ? 1 : (chunk?.length ?? 1);
|
|
977
|
+
if (err) {
|
|
978
|
+
nextTick(() => {
|
|
979
|
+
cb(err);
|
|
980
|
+
this.emit('error', err);
|
|
981
|
+
});
|
|
982
|
+
} else {
|
|
983
|
+
nextTick(() => {
|
|
984
|
+
cb();
|
|
985
|
+
if (this.writableNeedDrain && this.writableLength < this.writableHighWaterMark) {
|
|
986
|
+
this.writableNeedDrain = false;
|
|
987
|
+
this.emit('drain');
|
|
988
|
+
}
|
|
989
|
+
// If end() is waiting for pending writes to complete, trigger it now
|
|
990
|
+
if (this._pendingWrites === 0 && this._pendingEndCb) {
|
|
991
|
+
const endCb = this._pendingEndCb;
|
|
992
|
+
this._pendingEndCb = null;
|
|
993
|
+
endCb();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
return belowHWM;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
end(chunk?: any, encoding?: string | (() => void), callback?: () => void): this {
|
|
1003
|
+
if (typeof chunk === 'function') {
|
|
1004
|
+
callback = chunk;
|
|
1005
|
+
chunk = undefined;
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof encoding === 'function') {
|
|
1008
|
+
callback = encoding;
|
|
1009
|
+
encoding = undefined;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (chunk !== undefined && chunk !== null) {
|
|
1013
|
+
this.write(chunk, encoding as string);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
this.writableEnded = true;
|
|
1017
|
+
|
|
1018
|
+
const doFinal = () => {
|
|
1019
|
+
this._final((err) => {
|
|
1020
|
+
this.writableFinished = true;
|
|
1021
|
+
nextTick(() => {
|
|
1022
|
+
if (err) this.emit('error', err);
|
|
1023
|
+
this.emit('finish');
|
|
1024
|
+
nextTick(() => this.emit('close'));
|
|
1025
|
+
if (callback) callback();
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// Wait for all pending writes to complete before calling _final
|
|
1031
|
+
if (this._pendingWrites > 0) {
|
|
1032
|
+
this._pendingEndCb = doFinal;
|
|
1033
|
+
} else {
|
|
1034
|
+
doFinal();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return this;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
cork(): void { this.writableCorked++; }
|
|
1041
|
+
|
|
1042
|
+
uncork(): void {
|
|
1043
|
+
if (this.writableCorked > 0) {
|
|
1044
|
+
this.writableCorked--;
|
|
1045
|
+
if (this.writableCorked === 0 && this._duplexCorkedBuffer.length > 0) {
|
|
1046
|
+
const buffered = this._duplexCorkedBuffer.splice(0);
|
|
1047
|
+
for (const { chunk, encoding, callback } of buffered) {
|
|
1048
|
+
this._write(chunk, encoding, (err) => {
|
|
1049
|
+
this.writableLength -= this.writableObjectMode ? 1 : (chunk?.length ?? 1);
|
|
1050
|
+
if (err) {
|
|
1051
|
+
callback(err);
|
|
1052
|
+
this.emit('error', err);
|
|
1053
|
+
} else {
|
|
1054
|
+
callback();
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
if (this.writableNeedDrain && this.writableLength < this.writableHighWaterMark) {
|
|
1059
|
+
this.writableNeedDrain = false;
|
|
1060
|
+
nextTick(() => this.emit('drain'));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
setDefaultEncoding(encoding: string): this {
|
|
1067
|
+
this._defaultEncoding = encoding;
|
|
1068
|
+
return this;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ---- Transform ----
|
|
1073
|
+
|
|
1074
|
+
export class Transform extends Duplex {
|
|
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
|
+
|
|
1078
|
+
constructor(opts?: TransformOptions) {
|
|
1079
|
+
super({
|
|
1080
|
+
...opts,
|
|
1081
|
+
write: undefined, // Override write to use transform
|
|
1082
|
+
});
|
|
1083
|
+
if (opts?.transform) this._transformImpl = opts.transform;
|
|
1084
|
+
if (opts?.flush) this._flushImpl = opts.flush;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
_transform(chunk: any, encoding: string, callback: (error?: Error | null, data?: any) => void): void {
|
|
1088
|
+
if (this._transformImpl) {
|
|
1089
|
+
this._transformImpl.call(this, chunk, encoding, callback);
|
|
1090
|
+
} else {
|
|
1091
|
+
callback(null, chunk);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
_flush(callback: (error?: Error | null, data?: any) => void): void {
|
|
1096
|
+
if (this._flushImpl) {
|
|
1097
|
+
this._flushImpl.call(this, callback);
|
|
1098
|
+
} else {
|
|
1099
|
+
callback();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
_write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
|
|
1104
|
+
this._transform(chunk, encoding, (err, data) => {
|
|
1105
|
+
if (err) {
|
|
1106
|
+
callback(err);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
if (data !== undefined && data !== null) {
|
|
1110
|
+
this.push(data);
|
|
1111
|
+
}
|
|
1112
|
+
callback();
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
_final(callback: (error?: Error | null) => void): void {
|
|
1117
|
+
this._flush((err, data) => {
|
|
1118
|
+
if (err) {
|
|
1119
|
+
callback(err);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (data !== undefined && data !== null) {
|
|
1123
|
+
this.push(data);
|
|
1124
|
+
}
|
|
1125
|
+
// Signal readable side is done
|
|
1126
|
+
this.push(null);
|
|
1127
|
+
callback();
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// ---- PassThrough ----
|
|
1133
|
+
|
|
1134
|
+
export class PassThrough extends Transform {
|
|
1135
|
+
constructor(opts?: TransformOptions) {
|
|
1136
|
+
super({
|
|
1137
|
+
...opts,
|
|
1138
|
+
transform(chunk: any, _encoding: string, callback: (error?: Error | null, data?: any) => void) {
|
|
1139
|
+
callback(null, chunk);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// ---- pipeline ----
|
|
1146
|
+
|
|
1147
|
+
type PipelineCallback = (err: Error | null) => void;
|
|
1148
|
+
|
|
1149
|
+
/** A stream that can be destroyed (duck-typed for pipeline). */
|
|
1150
|
+
interface DestroyableStream extends Stream {
|
|
1151
|
+
destroy?(error?: Error): void;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export function pipeline(...args: [...streams: DestroyableStream[], callback: PipelineCallback] | DestroyableStream[]): DestroyableStream {
|
|
1155
|
+
const callback = typeof args[args.length - 1] === 'function' ? args.pop() as PipelineCallback : undefined;
|
|
1156
|
+
const streams = args as DestroyableStream[];
|
|
1157
|
+
|
|
1158
|
+
if (streams.length < 2) {
|
|
1159
|
+
throw new Error('pipeline requires at least 2 streams');
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
let error: Error | null = null;
|
|
1163
|
+
|
|
1164
|
+
function onError(err: Error) {
|
|
1165
|
+
if (!error) {
|
|
1166
|
+
error = err;
|
|
1167
|
+
// Destroy all streams
|
|
1168
|
+
for (const stream of streams) {
|
|
1169
|
+
if (typeof stream.destroy === 'function') {
|
|
1170
|
+
stream.destroy();
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (callback) callback(err);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Pipe streams together
|
|
1178
|
+
let current: Stream = streams[0];
|
|
1179
|
+
for (let i = 1; i < streams.length; i++) {
|
|
1180
|
+
const next = streams[i];
|
|
1181
|
+
current.pipe(next as unknown as Writable);
|
|
1182
|
+
current.on('error', onError);
|
|
1183
|
+
current = next;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Listen for end on last stream
|
|
1187
|
+
const last = streams[streams.length - 1];
|
|
1188
|
+
last.on('error', onError);
|
|
1189
|
+
last.on('finish', () => {
|
|
1190
|
+
if (callback && !error) callback(null);
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
return last;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ---- finished ----
|
|
1197
|
+
|
|
1198
|
+
export function finished(stream: Stream | Readable | Writable, callback: (err?: Error | null) => void): () => void;
|
|
1199
|
+
export function finished(stream: Stream | Readable | Writable, opts: FinishedOptions, callback: (err?: Error | null) => void): () => void;
|
|
1200
|
+
export function finished(stream: Stream | Readable | Writable, optsOrCb: FinishedOptions | ((err?: Error | null) => void), callback?: (err?: Error | null) => void): () => void {
|
|
1201
|
+
let cb: (err?: Error | null) => void;
|
|
1202
|
+
let _opts: FinishedOptions = {};
|
|
1203
|
+
|
|
1204
|
+
if (typeof optsOrCb === 'function') {
|
|
1205
|
+
cb = optsOrCb;
|
|
1206
|
+
} else {
|
|
1207
|
+
_opts = optsOrCb || {};
|
|
1208
|
+
cb = callback!;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
let called = false;
|
|
1212
|
+
function done(err?: Error | null) {
|
|
1213
|
+
if (!called) {
|
|
1214
|
+
called = true;
|
|
1215
|
+
cb(err);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const onFinish = () => done();
|
|
1220
|
+
const onEnd = () => done();
|
|
1221
|
+
const onError = (err: Error) => done(err);
|
|
1222
|
+
const onClose = () => {
|
|
1223
|
+
if (!(stream as Writable).writableFinished && !(stream as Readable).readableEnded) {
|
|
1224
|
+
done(new Error('premature close'));
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
stream.on('finish', onFinish);
|
|
1229
|
+
stream.on('end', onEnd);
|
|
1230
|
+
stream.on('error', onError);
|
|
1231
|
+
stream.on('close', onClose);
|
|
1232
|
+
|
|
1233
|
+
// Check initial state — handle already-finished/destroyed streams
|
|
1234
|
+
// Reference: refs/node/lib/internal/streams/end-of-stream.js lines 228-249
|
|
1235
|
+
const isWritableStream = typeof (stream as Writable).write === 'function';
|
|
1236
|
+
const isReadableStream = typeof (stream as Readable).read === 'function';
|
|
1237
|
+
const writableFinished = (stream as unknown as Record<string, unknown>).writableFinished === true;
|
|
1238
|
+
const readableEnded = (stream as unknown as Record<string, unknown>).readableEnded === true;
|
|
1239
|
+
const destroyed = (stream as unknown as Record<string, unknown>).destroyed === true;
|
|
1240
|
+
|
|
1241
|
+
if (destroyed) {
|
|
1242
|
+
queueMicrotask(() => done(((stream as unknown as Record<string, unknown>)._err as Error | null) || null));
|
|
1243
|
+
} else if (isWritableStream && !isReadableStream && writableFinished) {
|
|
1244
|
+
queueMicrotask(() => done());
|
|
1245
|
+
} else if (!isWritableStream && isReadableStream && readableEnded) {
|
|
1246
|
+
queueMicrotask(() => done());
|
|
1247
|
+
} else if (isWritableStream && isReadableStream && writableFinished && readableEnded) {
|
|
1248
|
+
queueMicrotask(() => done());
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
return function cleanup() {
|
|
1252
|
+
stream.removeListener('finish', onFinish);
|
|
1253
|
+
stream.removeListener('end', onEnd);
|
|
1254
|
+
stream.removeListener('error', onError);
|
|
1255
|
+
stream.removeListener('close', onClose);
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// ---- addAbortSignal ----
|
|
1260
|
+
|
|
1261
|
+
export function addAbortSignal(signal: AbortSignal, stream: Stream): typeof stream {
|
|
1262
|
+
if (!(signal instanceof AbortSignal)) {
|
|
1263
|
+
throw new TypeError('The first argument must be an AbortSignal');
|
|
1264
|
+
}
|
|
1265
|
+
if (!(stream instanceof Stream)) {
|
|
1266
|
+
throw new TypeError('The second argument must be a Stream');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (signal.aborted) {
|
|
1270
|
+
(stream as Readable | Writable).destroy(new Error('The operation was aborted'));
|
|
1271
|
+
} else {
|
|
1272
|
+
const onAbort = () => {
|
|
1273
|
+
(stream as Readable | Writable).destroy(new Error('The operation was aborted'));
|
|
1274
|
+
};
|
|
1275
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1276
|
+
// Cleanup when stream closes
|
|
1277
|
+
stream.once('close', () => {
|
|
1278
|
+
signal.removeEventListener('abort', onAbort);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
return stream;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// ---- Utility functions ----
|
|
1286
|
+
|
|
1287
|
+
export function isReadable(stream: unknown): boolean {
|
|
1288
|
+
if (stream == null) return false;
|
|
1289
|
+
const s = stream as Record<string, unknown>;
|
|
1290
|
+
if (typeof s.readable !== 'boolean') return false;
|
|
1291
|
+
if (typeof s.read !== 'function') return false;
|
|
1292
|
+
if (s.destroyed === true) return false;
|
|
1293
|
+
if (s.readableEnded === true) return false;
|
|
1294
|
+
return (s.readable as boolean) === true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
export function isWritable(stream: unknown): boolean {
|
|
1298
|
+
if (stream == null) return false;
|
|
1299
|
+
const s = stream as Record<string, unknown>;
|
|
1300
|
+
if (typeof s.writable !== 'boolean') return false;
|
|
1301
|
+
if (typeof s.write !== 'function') return false;
|
|
1302
|
+
if (s.destroyed === true) return false;
|
|
1303
|
+
if (s.writableEnded === true) return false;
|
|
1304
|
+
return (s.writable as boolean) === true;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
export function isDestroyed(stream: unknown): boolean {
|
|
1308
|
+
if (stream == null) return false;
|
|
1309
|
+
return (stream as Record<string, unknown>).destroyed === true;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export function isDisturbed(stream: unknown): boolean {
|
|
1313
|
+
if (stream == null) return false;
|
|
1314
|
+
const s = stream as Record<string, unknown>;
|
|
1315
|
+
// A stream is disturbed if data has been read from it
|
|
1316
|
+
return s.readableDidRead === true || (s.readableFlowing !== null && s.readableFlowing !== undefined);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
export function isErrored(stream: unknown): boolean {
|
|
1320
|
+
if (stream == null) return false;
|
|
1321
|
+
// Check for errored state on either side
|
|
1322
|
+
const s = stream as Record<string, unknown>;
|
|
1323
|
+
if (s.destroyed === true && typeof s.readable === 'boolean' && s.readable === false) return true;
|
|
1324
|
+
if (s.destroyed === true && typeof s.writable === 'boolean' && s.writable === false) return true;
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ---- Exports ----
|
|
1329
|
+
|
|
1330
|
+
// Default export
|
|
1331
|
+
const _default = Object.assign(Stream, {
|
|
1332
|
+
Stream,
|
|
1333
|
+
Readable,
|
|
1334
|
+
Writable,
|
|
1335
|
+
Duplex,
|
|
1336
|
+
Transform,
|
|
1337
|
+
PassThrough,
|
|
1338
|
+
pipeline,
|
|
1339
|
+
finished,
|
|
1340
|
+
addAbortSignal,
|
|
1341
|
+
isReadable,
|
|
1342
|
+
isWritable,
|
|
1343
|
+
isDestroyed,
|
|
1344
|
+
isDisturbed,
|
|
1345
|
+
isErrored,
|
|
1346
|
+
getDefaultHighWaterMark,
|
|
1347
|
+
setDefaultHighWaterMark,
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
export default _default;
|