@gjsify/stream 0.0.4 → 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/src/index.ts CHANGED
@@ -1,7 +1,1350 @@
1
- export * from '@gjsify/deno_std/node/stream';
2
- import stream from '@gjsify/deno_std/node/stream';
1
+ // Reference: Node.js lib/stream.js, lib/internal/streams/*.js
2
+ // Reimplemented for GJS using EventEmitter and microtask scheduling
3
3
 
4
- import type { ReadableOptions } from 'stream';
5
- export type { ReadableOptions };
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
- export default stream;
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;