@gjsify/stream 0.3.20 → 0.4.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.
Files changed (42) hide show
  1. package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
  2. package/lib/esm/consumers/index.js +1 -1
  3. package/lib/esm/duplex.js +1 -0
  4. package/lib/esm/index.js +1 -1
  5. package/lib/esm/internal/state.js +1 -0
  6. package/lib/esm/internal/types.js +0 -0
  7. package/lib/esm/passthrough.js +1 -0
  8. package/lib/esm/promises/index.js +1 -1
  9. package/lib/esm/readable.js +1 -0
  10. package/lib/esm/stream-base.js +1 -0
  11. package/lib/esm/transform.js +1 -0
  12. package/lib/esm/utils/finished.js +1 -0
  13. package/lib/esm/utils/pipe.js +1 -0
  14. package/lib/esm/utils/pipeline.js +1 -0
  15. package/lib/esm/writable.js +1 -0
  16. package/lib/types/duplex.d.ts +42 -0
  17. package/lib/types/index.d.ts +15 -190
  18. package/lib/types/internal/state.d.ts +4 -0
  19. package/lib/types/internal/types.d.ts +21 -0
  20. package/lib/types/passthrough.d.ts +5 -0
  21. package/lib/types/readable.d.ts +73 -0
  22. package/lib/types/stream-base.d.ts +26 -0
  23. package/lib/types/transform.d.ts +11 -0
  24. package/lib/types/utils/finished.d.ts +14 -0
  25. package/lib/types/utils/pipe.d.ts +10 -0
  26. package/lib/types/utils/pipeline.d.ts +7 -0
  27. package/lib/types/writable.d.ts +47 -0
  28. package/package.json +6 -6
  29. package/src/callable.spec.ts +39 -0
  30. package/src/duplex.ts +317 -0
  31. package/src/index.ts +49 -1633
  32. package/src/internal/state.ts +34 -0
  33. package/src/internal/types.ts +31 -0
  34. package/src/passthrough.ts +19 -0
  35. package/src/readable.ts +580 -0
  36. package/src/stream-base.ts +37 -0
  37. package/src/transform.ts +108 -0
  38. package/src/utils/finished.ts +156 -0
  39. package/src/utils/pipe.ts +113 -0
  40. package/src/utils/pipeline.ts +59 -0
  41. package/src/writable.ts +398 -0
  42. package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts CHANGED
@@ -1,1643 +1,54 @@
1
1
  // Reference: Node.js lib/stream.js, lib/internal/streams/*.js
2
- // Reimplemented for GJS using EventEmitter and microtask scheduling
3
-
4
- import { EventEmitter } from '@gjsify/events';
5
- import { nextTick, queueMicrotask } from '@gjsify/utils';
6
- import type { ReadableOptions, WritableOptions, DuplexOptions, TransformOptions, FinishedOptions } from 'node:stream';
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
- /** Validate a named high-water-mark option and throw ERR_INVALID_ARG_VALUE on NaN/non-number. */
29
- function validateHighWaterMark(name: string, value: unknown): void {
30
- if (value === undefined) return;
31
- if (typeof value !== 'number' || Number.isNaN(value)) {
32
- const err = new TypeError(`The value of "${name}" is invalid. Received ${value}`);
33
- (err as any).code = 'ERR_INVALID_ARG_VALUE';
34
- throw err;
35
- }
36
- }
37
-
38
- // ---- Types ----
39
-
40
- /** Base options accepted by the Stream constructor (superset used by subclass options). */
41
- export interface StreamOptions {
42
- highWaterMark?: number;
43
- objectMode?: boolean;
44
- signal?: AbortSignal;
45
- captureRejections?: boolean;
46
- }
47
-
48
- export type { ReadableOptions, WritableOptions, DuplexOptions, TransformOptions, FinishedOptions };
49
-
50
- // ---- Stream base class ----
51
-
52
- /** A stream-like emitter that may have `pause` and `resume` methods (duck-typed). */
53
- interface StreamLike extends EventEmitter {
54
- pause?(): void;
55
- resume?(): void;
56
- }
57
-
58
- /** Tracked pipe destination for unpipe support. */
59
- interface PipeState {
60
- dest: Writable;
61
- cleanup: () => void;
62
- }
63
-
64
- class Stream_ extends EventEmitter {
65
- constructor(opts?: StreamOptions) {
66
- super(opts);
67
- }
68
-
69
- pipe<T extends Writable>(destination: T, options?: { end?: boolean }): T {
70
- const source = this as unknown as Readable;
71
- const doEnd = options?.end !== false;
72
-
73
- // Drain listener is added lazily only when backpressure occurs.
74
- let drainListenerAdded = false;
75
- const ondrain = () => {
76
- drainListenerAdded = false;
77
- destination.removeListener('drain', ondrain);
78
- if (typeof (source as StreamLike).resume === 'function') {
79
- (source as StreamLike).resume!();
80
- }
81
- };
82
-
83
- const ondata = (chunk: unknown) => {
84
- if (destination.writable) {
85
- if (destination.write(chunk) === false && typeof (source as StreamLike).pause === 'function') {
86
- (source as StreamLike).pause!();
87
- if (!drainListenerAdded) {
88
- drainListenerAdded = true;
89
- destination.on('drain', ondrain);
90
- }
91
- }
92
- }
93
- };
94
-
95
- source.on('data', ondata);
96
-
97
- let didEnd = false;
98
-
99
- const onend = () => {
100
- if (didEnd) return;
101
- didEnd = true;
102
- if (doEnd) destination.end();
103
- };
104
-
105
- const onclose = () => {
106
- if (didEnd) return;
107
- didEnd = true;
108
- if (doEnd) {
109
- // Modern Readable streams (Readable_) do NOT destroy dest on source close —
110
- // only call dest.end/destroy for legacy Stream objects (no Readable_ prototype).
111
- if (!(source instanceof Readable) && typeof (destination as any).destroy === 'function') {
112
- (destination as any).destroy();
113
- }
114
- }
115
- };
116
-
117
- if (doEnd) {
118
- source.on('end', onend);
119
- source.on('close', onclose);
120
- }
121
-
122
- const cleanup = () => {
123
- source.removeListener('data', ondata);
124
- if (drainListenerAdded) destination.removeListener('drain', ondrain);
125
- source.removeListener('end', onend);
126
- source.removeListener('close', onclose);
127
- // Self-remove from both end and close
128
- source.removeListener('end', cleanup);
129
- source.removeListener('close', cleanup);
130
- destination.removeListener('close', cleanup);
131
- };
132
-
133
- source.on('end', cleanup);
134
- source.on('close', cleanup);
135
- destination.on('close', cleanup);
136
-
137
- // Track piped destinations for unpipe
138
- if (source instanceof Readable) {
139
- source._pipeDests.push({ dest: destination, cleanup });
140
- (source as any)._readableState.pipes.push(destination);
141
- }
142
-
143
- destination.emit('pipe', source);
144
- return destination;
145
- }
146
- }
147
-
148
- // ---- Readable ----
149
-
150
- class Readable_ extends Stream_ {
151
- readable = true;
152
- readableFlowing: boolean | null = null;
153
- readableLength = 0;
154
- readableHighWaterMark: number;
155
- readableEncoding: string | null;
156
- readableObjectMode: boolean;
157
- readableEnded = false;
158
- readableAborted = false;
159
- destroyed = false;
160
-
161
- /** @internal Tracked pipe destinations for unpipe. */
162
- _pipeDests: PipeState[] = [];
163
-
164
- private _buffer: unknown[] = [];
165
- _readableState = { ended: false, endEmitted: false, reading: false, constructed: true, highWaterMark: 0, objectMode: false, pipes: [] as any[] };
166
- private _readablePending = false;
167
- private _readImpl: ((size: number) => void) | undefined;
168
- private _destroyImpl: ((error: Error | null, cb: (error?: Error | null) => void) => void) | undefined;
169
- private _constructImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
170
-
171
- constructor(opts?: ReadableOptions) {
172
- super(opts);
173
- this.readableHighWaterMark = opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.objectMode ?? false);
174
- this.readableEncoding = opts?.encoding ?? null;
175
- this.readableObjectMode = opts?.objectMode ?? false;
176
- this._readableState.highWaterMark = this.readableHighWaterMark;
177
- this._readableState.objectMode = this.readableObjectMode;
178
- if (opts?.read) this._readImpl = opts.read;
179
- if (opts?.destroy) this._destroyImpl = opts.destroy;
180
- if (opts?.construct) this._constructImpl = opts.construct;
181
-
182
- // Call _construct if provided via options or overridden by subclass
183
- const hasConstruct = this._constructImpl || this._construct !== Readable.prototype._construct;
184
- if (hasConstruct) {
185
- this._readableState.constructed = false;
186
- nextTick(() => {
187
- this._construct((err) => {
188
- this._readableState.constructed = true;
189
- if (err) {
190
- this.destroy(err);
191
- } else {
192
- // If data was requested before construct finished, start reading
193
- if (this.readableFlowing === true) {
194
- this._flow();
195
- }
196
- }
197
- });
198
- });
199
- }
200
- }
201
-
202
- _construct(callback: (error?: Error | null) => void): void {
203
- if (this._constructImpl) {
204
- this._constructImpl.call(this, callback);
205
- } else {
206
- callback();
207
- }
208
- }
209
-
210
- _read(_size: number): void {
211
- if (this._readImpl) {
212
- this._readImpl.call(this, _size);
213
- }
214
- }
215
-
216
- read(size?: number): any {
217
- // Don't read until constructed
218
- if (!this._readableState.constructed) return null;
219
-
220
- if (this._buffer.length === 0) {
221
- if (this._readableState.ended) return null;
222
- this._readableState.reading = true;
223
- this._read(size ?? this.readableHighWaterMark);
224
- this._readableState.reading = false;
225
- }
226
-
227
- if (this._buffer.length === 0) return null;
228
-
229
- if (size === 0) return null;
230
-
231
- if (this.readableObjectMode) {
232
- if (size === undefined) {
233
- const chunk = this._buffer.shift();
234
- this.readableLength -= 1;
235
- if (this._readableState.ended && this._buffer.length === 0 && !this._readableState.endEmitted) {
236
- this._emitEnd();
237
- }
238
- return chunk;
239
- }
240
- // In objectMode, size means number of objects
241
- if (size > this.readableLength) return null;
242
- const chunk = this._buffer.shift();
243
- this.readableLength -= 1;
244
- return chunk;
245
- }
246
-
247
- // Byte mode: compute total buffered bytes
248
- if (size !== undefined && size !== null) {
249
- if (size > this.readableLength) return null;
250
- // Partial read: extract exactly `size` bytes from buffer
251
- return this._readBytes(size);
252
- }
253
-
254
- // Read all buffered data
255
- const result = this._buffer.splice(0);
256
- this.readableLength = 0;
257
- if (this._readableState.ended && this._buffer.length === 0 && !this._readableState.endEmitted) {
258
- this._emitEnd();
259
- }
260
- if (result.length === 1) return result[0];
261
- if (result.length === 0) return null;
262
- // Concatenate: strings with join, buffers with Buffer.concat
263
- if (typeof result[0] === 'string') return result.join('');
264
- const BufCtor = (globalThis as any).Buffer;
265
- return BufCtor?.concat ? BufCtor.concat(result) : result;
266
- }
267
-
268
- /** @internal Extract exactly `size` bytes from the internal buffer. */
269
- private _readBytes(size: number): any {
270
- let collected = 0;
271
- const parts: unknown[] = [];
272
- while (collected < size && this._buffer.length > 0) {
273
- const chunk = this._buffer[0];
274
- const chunkLen = (chunk as any).length ?? 1;
275
- if (collected + chunkLen <= size) {
276
- // Take the whole chunk
277
- parts.push(this._buffer.shift()!);
278
- collected += chunkLen;
279
- this.readableLength -= chunkLen;
280
- } else {
281
- // Split the chunk
282
- const needed = size - collected;
283
- const BufCtor = (globalThis as any).Buffer;
284
- if (BufCtor && BufCtor.isBuffer(chunk)) {
285
- parts.push((chunk as any).slice(0, needed));
286
- this._buffer[0] = (chunk as any).slice(needed);
287
- } else if (typeof chunk === 'string') {
288
- parts.push(chunk.slice(0, needed));
289
- this._buffer[0] = chunk.slice(needed);
290
- } else {
291
- // Uint8Array or similar
292
- parts.push((chunk as Uint8Array).slice(0, needed));
293
- this._buffer[0] = (chunk as Uint8Array).slice(needed);
294
- }
295
- this.readableLength -= needed;
296
- collected += needed;
297
- }
298
- }
299
- if (parts.length === 1) return parts[0];
300
- const BufCtor = (globalThis as any).Buffer;
301
- return BufCtor?.concat ? BufCtor.concat(parts) : parts;
302
- }
303
-
304
- push(chunk: any, encoding?: string): boolean {
305
- if (chunk === null) {
306
- this._readableState.ended = true;
307
- this.readableEnded = true;
308
- if (this._buffer.length === 0 && !this._readableState.endEmitted) {
309
- nextTick(() => this._emitEnd());
310
- }
311
- // Emit 'readable' for listeners waiting on EOF with buffered data
312
- this._scheduleReadable();
313
- return false;
314
- }
315
-
316
- // Validate chunk type for non-objectMode streams (Node.js ERR_INVALID_ARG_TYPE).
317
- // Accept string, Buffer, or any ArrayBufferView — the latter covers Uint8Array
318
- // and TypedArrays from any realm (GJS vs host bundle), avoiding cross-realm
319
- // `instanceof Uint8Array` mismatches that would otherwise reject real Buffers.
320
- if (!this.readableObjectMode) {
321
- const isValid = typeof chunk === 'string' || ArrayBuffer.isView(chunk);
322
- if (!isValid) {
323
- const err = Object.assign(
324
- new TypeError(`Invalid non-string/buffer chunk type: ${typeof chunk}`),
325
- { code: 'ERR_INVALID_ARG_TYPE' }
326
- );
327
- nextTick(() => this.emit('error', err));
328
- return false;
329
- }
330
- }
331
-
332
- this._buffer.push(chunk);
333
- this.readableLength += this.readableObjectMode ? 1 : (chunk.length ?? 1);
334
-
335
- // In flowing mode, schedule draining (unless already flowing)
336
- if (this.readableFlowing && !this._flowing) {
337
- nextTick(() => this._flow());
338
- }
339
-
340
- // In non-flowing mode, emit 'readable' to notify data is available
341
- if (this.readableFlowing !== true) {
342
- this._scheduleReadable();
343
- }
344
-
345
- return this.readableLength < this.readableHighWaterMark;
346
- }
347
-
348
- /** Emit 'end' followed by 'close' (matches Node.js autoDestroy behavior). */
349
- private _emitEnd(): void {
350
- if (this._readableState.endEmitted) return;
351
- this._readableState.endEmitted = true;
352
- this.emit('end');
353
- nextTick(() => this._autoClose());
354
- }
355
-
356
- /** Override in subclasses to suppress automatic 'close' after 'end'. */
357
- protected _autoClose(): void {
358
- this.emit('close');
359
- }
360
-
361
- /** Schedule a single 'readable' event per microtask cycle (deduplicates multiple pushes). */
362
- private _scheduleReadable(): void {
363
- if (this._readablePending || this.listenerCount('readable') === 0) return;
364
- this._readablePending = true;
365
- nextTick(() => {
366
- this._readablePending = false;
367
- if (!this.destroyed) this.emit('readable');
368
- });
369
- }
370
-
371
- on(event: string | symbol, listener: (...args: any[]) => void): this {
372
- super.on(event, listener);
373
- // Attaching a 'data' listener switches to flowing mode (like Node.js)
374
- if (event === 'data' && this.readableFlowing !== false) {
375
- this.resume();
376
- }
377
- // Attaching a 'readable' listener: if data is already buffered, schedule event
378
- if (event === 'readable' && (this._buffer.length > 0 || this._readableState.ended)) {
379
- this._scheduleReadable();
380
- }
381
- return this;
382
- }
383
-
384
- unshift(chunk: any): void {
385
- this._buffer.unshift(chunk);
386
- this.readableLength += this.readableObjectMode ? 1 : (chunk.length ?? 1);
387
- }
388
-
389
- setEncoding(encoding: string): this {
390
- this.readableEncoding = encoding;
391
- return this;
392
- }
393
-
394
- pause(): this {
395
- this.readableFlowing = false;
396
- this.emit('pause');
397
- return this;
398
- }
399
-
400
- resume(): this {
401
- if (this.readableFlowing !== true) {
402
- this.readableFlowing = true;
403
- this.emit('resume');
404
- // Start flowing: drain buffered data and call _read
405
- if (this._readableState.constructed) {
406
- this._flow();
407
- }
408
- }
409
- return this;
410
- }
411
-
412
- private _flowing = false;
413
-
414
- private _flow(): void {
415
- if (this.readableFlowing !== true || this._flowing || this.destroyed) return;
416
- if (!this._readableState.constructed) return;
417
- this._flowing = true;
418
-
419
- try {
420
- // Drain buffered data synchronously (like Node.js flow())
421
- while (this._buffer.length > 0 && this.readableFlowing && !this.destroyed) {
422
- let chunk = this._buffer.shift()!;
423
- this.readableLength -= this.readableObjectMode ? 1 : ((chunk as { length?: number }).length ?? 1);
424
- // Decode to string when setEncoding was called
425
- if (this.readableEncoding && typeof chunk !== 'string') {
426
- const BufCtor = (globalThis as any).Buffer;
427
- if (BufCtor && BufCtor.isBuffer(chunk)) {
428
- chunk = (chunk as any).toString(this.readableEncoding);
429
- } else if (chunk instanceof Uint8Array) {
430
- chunk = new TextDecoder(this.readableEncoding).decode(chunk as Uint8Array);
431
- }
432
- }
433
- this.emit('data', chunk);
434
- }
435
-
436
- if (this.destroyed) return;
437
-
438
- // If ended and buffer drained, emit end
439
- if (this._readableState.ended && this._buffer.length === 0 && !this._readableState.endEmitted) {
440
- nextTick(() => this._emitEnd());
441
- return;
442
- }
443
-
444
- // Call _read to get more data (may push synchronously)
445
- if (!this._readableState.ended && !this._readableState.reading && !this.destroyed) {
446
- this._readableState.reading = true;
447
- this._read(this.readableHighWaterMark);
448
- this._readableState.reading = false;
449
- }
450
- } finally {
451
- this._flowing = false;
452
- }
453
-
454
- // After _read, if new data was pushed, schedule another flow
455
- if (this._buffer.length > 0 && this.readableFlowing && !this.destroyed) {
456
- nextTick(() => this._flow());
457
- }
458
- }
459
-
460
- isPaused(): boolean {
461
- return this.readableFlowing === false;
462
- }
463
-
464
- unpipe(destination?: Writable): this {
465
- if (!destination) {
466
- // Remove all piped destinations
467
- for (const state of this._pipeDests) {
468
- state.cleanup();
469
- state.dest.emit('unpipe', this);
470
- }
471
- this._pipeDests = [];
472
- this._readableState.pipes = [];
473
- this.readableFlowing = false;
474
- } else {
475
- const idx = this._pipeDests.findIndex(s => s.dest === destination);
476
- if (idx !== -1) {
477
- const state = this._pipeDests[idx];
478
- state.cleanup();
479
- this._pipeDests.splice(idx, 1);
480
- const pipeIdx = this._readableState.pipes.indexOf(destination);
481
- if (pipeIdx !== -1) this._readableState.pipes.splice(pipeIdx, 1);
482
- destination.emit('unpipe', this);
483
- if (this._pipeDests.length === 0) {
484
- this.readableFlowing = false;
485
- }
486
- }
487
- }
488
- return this;
489
- }
490
-
491
- _destroy(error: Error | null, callback: (error?: Error | null) => void): void {
492
- if (this._destroyImpl) {
493
- this._destroyImpl.call(this, error, callback);
494
- } else {
495
- callback(error ?? undefined);
496
- }
497
- }
498
-
499
- destroy(error?: Error): this {
500
- if (this.destroyed) return this;
501
- this.destroyed = true;
502
- this.readable = false;
503
- this.readableAborted = !this.readableEnded;
504
- // Store the error so finished() can retrieve it if called after destroy() but before 'error' fires
505
- if (error) (this as any)._err = error;
506
-
507
- const cb = (err?: Error | null) => {
508
- // Emit error and close in separate nextTick calls (matches Node.js behavior)
509
- // so an unhandled error doesn't prevent 'close' from firing
510
- if (err) nextTick(() => this.emit('error', err));
511
- nextTick(() => this.emit('close'));
512
- };
513
-
514
- // Dispatch virtually ONLY when the user overrode _destroy on the instance
515
- // (e.g. tests: `stream._destroy = fn`). Do NOT call a subclass prototype
516
- // _destroy: net.Socket's prototype `_destroy` synchronously cancels in-flight
517
- // Gio I/O and would break tests that call destroy() during pending writes.
518
- // The opts.destroy path still runs via _destroyImpl as before.
519
- if (Object.prototype.hasOwnProperty.call(this, '_destroy')) {
520
- (this as any)._destroy(error ?? null, cb);
521
- } else if (this._destroyImpl) {
522
- this._destroyImpl.call(this, error ?? null, cb);
523
- } else {
524
- cb(error);
525
- }
526
-
527
- return this;
528
- }
529
-
530
- /**
531
- * Converts this Node.js Readable to a Web ReadableStream.
532
- * Used by @hono/node-server to bridge Node.js HTTP → Web Standard Request.
533
- */
534
- static toWeb(nodeReadable: Readable_): ReadableStream<Uint8Array> {
535
- return new ReadableStream({
536
- start(controller) {
537
- nodeReadable.on('data', (chunk: unknown) => {
538
- if (typeof chunk === 'string') {
539
- controller.enqueue(new TextEncoder().encode(chunk));
540
- } else if (chunk instanceof Uint8Array) {
541
- controller.enqueue(chunk);
542
- } else if (chunk && typeof (chunk as any).length === 'number') {
543
- controller.enqueue(new Uint8Array(chunk as any));
544
- }
545
- });
546
- nodeReadable.on('end', () => {
547
- controller.close();
548
- });
549
- nodeReadable.on('error', (err: Error) => {
550
- controller.error(err);
551
- });
552
- },
553
- cancel() {
554
- nodeReadable.destroy();
555
- },
556
- });
557
- }
558
-
559
- /**
560
- * Creates a Node.js Readable from a Web ReadableStream.
561
- */
562
- static fromWeb(webStream: ReadableStream<Uint8Array>, options?: ReadableOptions): Readable_ {
563
- const reader = webStream.getReader();
564
- return new Readable_({
565
- ...options,
566
- read() {
567
- reader.read().then(
568
- ({ done, value }) => {
569
- if (done) {
570
- this.push(null);
571
- } else {
572
- this.push(value);
573
- }
574
- },
575
- (err) => {
576
- this.destroy(err);
577
- },
578
- );
579
- },
580
- destroy(error, callback) {
581
- reader.cancel(error?.message).then(() => callback(null), callback);
582
- },
583
- });
584
- }
585
-
586
- [Symbol.asyncIterator](): AsyncIterableIterator<unknown> {
587
- const readable = this;
588
- const buffer: unknown[] = [];
589
- let done = false;
590
- let error: Error | null = null;
591
- let waitingResolve: ((value: IteratorResult<unknown>) => void) | null = null;
592
- let waitingReject: ((reason: unknown) => void) | null = null;
593
-
594
- readable.on('data', (chunk: unknown) => {
595
- if (waitingResolve) {
596
- const resolve = waitingResolve;
597
- waitingResolve = null;
598
- waitingReject = null;
599
- resolve({ value: chunk, done: false });
600
- } else {
601
- buffer.push(chunk);
602
- }
603
- });
604
-
605
- readable.on('end', () => {
606
- done = true;
607
- if (waitingResolve) {
608
- const resolve = waitingResolve;
609
- waitingResolve = null;
610
- waitingReject = null;
611
- resolve({ value: undefined, done: true });
612
- }
613
- });
614
-
615
- readable.on('error', (err: Error) => {
616
- error = err;
617
- done = true;
618
- if (waitingReject) {
619
- const reject = waitingReject;
620
- waitingResolve = null;
621
- waitingReject = null;
622
- reject(err);
623
- }
624
- });
625
-
626
- return {
627
- next(): Promise<IteratorResult<unknown>> {
628
- if (error) return Promise.reject(error);
629
- if (buffer.length > 0) return Promise.resolve({ value: buffer.shift(), done: false });
630
- if (done) return Promise.resolve({ value: undefined, done: true });
631
- return new Promise((resolve, reject) => {
632
- waitingResolve = resolve;
633
- waitingReject = reject;
634
- });
635
- },
636
- return(): Promise<IteratorResult<unknown>> {
637
- readable.destroy();
638
- return Promise.resolve({ value: undefined, done: true });
639
- },
640
- [Symbol.asyncIterator]() { return this; }
641
- };
642
- }
643
-
644
- static from(iterable: Iterable<unknown> | AsyncIterable<unknown>, opts?: ReadableOptions): Readable {
645
- const readable = new Readable({
646
- objectMode: true,
647
- ...opts,
648
- read() {}
649
- });
650
-
651
- // Buffer, Uint8Array, and strings should be pushed as a single chunk,
652
- // not iterated element-by-element (matching Node.js Readable.from behavior)
653
- if (typeof iterable === 'string' || ArrayBuffer.isView(iterable)) {
654
- readable.push(iterable);
655
- readable.push(null);
656
- return readable;
657
- }
658
-
659
- (async () => {
660
- try {
661
- for await (const chunk of iterable as AsyncIterable<unknown>) {
662
- if (!readable.push(chunk)) {
663
- // Backpressure — wait for drain
664
- await new Promise<void>(resolve => readable.once('drain', resolve));
665
- }
666
- }
667
- readable.push(null);
668
- } catch (err) {
669
- readable.destroy(err as Error);
670
- }
671
- })();
672
-
673
- return readable;
674
- }
675
- }
676
-
677
- // ---- Writable ----
678
-
679
- class Writable_ extends Stream_ {
680
- // Allow `duplex instanceof Writable` to return true even though Duplex inherits
681
- // from Readable_ only. Mirrors Node.js: standard prototype-chain check first,
682
- // then duck-type fallback — but only when checking against Writable itself, not
683
- // against a user subclass. Because `Writable` is exported as a makeCallable Proxy,
684
- // `this === Writable_` fails for `obj instanceof Writable`; however the Proxy's
685
- // default `get` trap forwards `.prototype` straight to the target, so the
686
- // prototype reference check below correctly recognises both.
687
- static [Symbol.hasInstance](obj: any): boolean {
688
- if (typeof (this as any).prototype !== 'undefined' &&
689
- Object.prototype.isPrototypeOf.call((this as any).prototype, obj)) return true;
690
- if ((this as any).prototype !== Writable_.prototype) return false;
691
- return obj !== null && obj !== undefined && typeof obj.writableHighWaterMark === 'number';
692
- }
693
-
694
- writable = true;
695
- writableHighWaterMark: number;
696
- writableLength = 0;
697
- writableObjectMode: boolean;
698
- writableEnded = false;
699
- writableFinished = false;
700
- writableCorked = 0;
701
- writableNeedDrain = false;
702
- destroyed = false;
703
-
704
- private _writableState = { ended: false, finished: false, constructed: true, writing: false };
705
- private _corkedBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
706
- private _writeBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
707
- private _pendingConstruct: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
708
- private _ending = false;
709
- private _endCallback?: () => void;
710
- private _pendingEnd: { chunk?: any; encoding?: string; callback?: () => void } | null = null;
711
- private _writeImpl: ((chunk: any, encoding: string, cb: (error?: Error | null) => void) => void) | undefined;
712
- private _writev: ((chunks: Array<{ chunk: any; encoding: string }>, cb: (error?: Error | null) => void) => void) | undefined;
713
- private _finalImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
714
- private _destroyImpl: ((error: Error | null, cb: (error?: Error | null) => void) => void) | undefined;
715
- private _constructImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
716
- private _decodeStrings: boolean;
717
- private _defaultEncoding = 'utf8';
718
-
719
- constructor(opts?: WritableOptions) {
720
- super(opts);
721
- this.writableHighWaterMark = opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.objectMode ?? false);
722
- this.writableObjectMode = opts?.objectMode ?? false;
723
- this._decodeStrings = opts?.decodeStrings !== false;
724
- if (opts?.write) this._writeImpl = opts.write;
725
- if (opts?.writev) this._writev = opts.writev;
726
- if (opts?.final) this._finalImpl = opts.final;
727
- if (opts?.destroy) this._destroyImpl = opts.destroy;
728
- if (opts?.construct) this._constructImpl = opts.construct;
729
-
730
- // Call _construct if provided via options or overridden by subclass
731
- const hasConstruct = this._constructImpl || this._construct !== Writable.prototype._construct;
732
- if (hasConstruct) {
733
- this._writableState.constructed = false;
734
- nextTick(() => {
735
- this._construct((err) => {
736
- this._writableState.constructed = true;
737
- if (err) {
738
- this.destroy(err);
739
- } else {
740
- this._maybeFlush();
741
- }
742
- });
743
- });
744
- }
745
- }
746
-
747
- _construct(callback: (error?: Error | null) => void): void {
748
- if (this._constructImpl) {
749
- this._constructImpl.call(this, callback);
750
- } else {
751
- callback();
752
- }
753
- }
754
-
755
- _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
756
- if (this._writeImpl) {
757
- this._writeImpl.call(this, chunk, encoding, callback);
758
- } else {
759
- callback();
760
- }
761
- }
762
-
763
- _final(callback: (error?: Error | null) => void): void {
764
- if (this._finalImpl) {
765
- this._finalImpl.call(this, callback);
766
- } else {
767
- callback();
768
- }
769
- }
770
-
771
- private _maybeFlush(): void {
772
- // Flush writes that were buffered while waiting for _construct
773
- const pending = this._pendingConstruct.splice(0);
774
- if (pending.length > 0) {
775
- // First write goes directly, rest get serialized via _writeBuffer
776
- const [first, ...rest] = pending;
777
- this._writeBuffer.push(...rest);
778
- this._doWrite(first.chunk, first.encoding, first.callback);
779
- }
780
- if (this._pendingEnd) {
781
- const { chunk, encoding, callback } = this._pendingEnd;
782
- this._pendingEnd = null;
783
- this._doEnd(chunk, encoding, callback);
784
- }
785
- }
786
-
787
- private _doWrite(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
788
- this._writableState.writing = true;
789
- // Track whether the user's _write called its callback synchronously. When it
790
- // does (e.g. Writable with a synchronous `write` option), Node drains the
791
- // buffer on the same tick — tests that issue N sync writes expect N sync
792
- // `_write` dispatches. Deferring via nextTick here broke that expectation.
793
- let sync = true;
794
- this._write(chunk, encoding, (err) => {
795
- this.writableLength -= this.writableObjectMode ? 1 : (chunk?.length ?? 1);
796
- if (sync) {
797
- // Synchronous completion: defer the user callback + 'drain' emit to
798
- // nextTick (Node semantics — user code must not see its own write
799
- // return before the callback on the same tick), but drain the buffer
800
- // synchronously so follow-up writes fire on the same tick.
801
- nextTick(() => {
802
- if (err) {
803
- callback(err);
804
- this.emit('error', err);
805
- return;
806
- }
807
- callback();
808
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
809
- this.writableNeedDrain = false;
810
- this.emit('drain');
811
- }
812
- });
813
- if (!err) this._drainWriteBuffer();
814
- return;
815
- }
816
- // Asynchronous completion — full defer.
817
- if (err) {
818
- nextTick(() => {
819
- callback(err);
820
- this.emit('error', err);
821
- this._drainWriteBuffer();
822
- });
823
- } else {
824
- nextTick(() => {
825
- callback();
826
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
827
- this.writableNeedDrain = false;
828
- this.emit('drain');
829
- }
830
- this._drainWriteBuffer();
831
- });
832
- }
833
- });
834
- sync = false;
835
- }
836
-
837
- private _drainWriteBuffer(): void {
838
- if (this._writeBuffer.length > 0) {
839
- const next = this._writeBuffer.shift()!;
840
- this._doWrite(next.chunk, next.encoding, next.callback);
841
- } else {
842
- // Only release the write lock when the buffer is truly empty.
843
- this._writableState.writing = false;
844
- this._maybeFinish();
845
- }
846
- }
847
-
848
- private _maybeFinish(): void {
849
- if (!this._ending || this._writableState.finished || this._writableState.writing || this._writeBuffer.length > 0) return;
850
- this._ending = false;
851
-
852
- this._final((err) => {
853
- this.writableFinished = true;
854
- this._writableState.finished = true;
855
- nextTick(() => {
856
- if (err) {
857
- this.emit('error', err);
858
- }
859
- this.emit('finish');
860
- nextTick(() => this.emit('close'));
861
- if (this._endCallback) this._endCallback();
862
- });
863
- });
864
- }
865
-
866
- write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
867
- if (typeof encoding === 'function') {
868
- callback = encoding;
869
- encoding = undefined;
870
- }
871
- if (encoding === undefined) encoding = this._defaultEncoding;
872
- callback = callback || (() => {});
873
-
874
- // Convert strings to Buffer when decodeStrings is true (default), but not in objectMode
875
- if (this._decodeStrings && !this.writableObjectMode && typeof chunk === 'string') {
876
- const BufCtor = (globalThis as any).Buffer;
877
- if (BufCtor) {
878
- chunk = BufCtor.from(chunk, encoding);
879
- encoding = 'buffer';
880
- }
881
- }
882
- // Set encoding to 'buffer' for Buffer/Uint8Array chunks
883
- if (typeof chunk !== 'string' && !this.writableObjectMode) {
884
- const BufCtor = (globalThis as any).Buffer;
885
- if ((BufCtor && BufCtor.isBuffer(chunk)) || chunk instanceof Uint8Array) {
886
- encoding = 'buffer';
887
- }
888
- }
889
-
890
- if (this.writableEnded) {
891
- const err = new Error('write after end');
892
- nextTick(() => {
893
- if (callback) callback(err);
894
- this.emit('error', err);
895
- });
896
- return false;
897
- }
898
-
899
- this.writableLength += this.writableObjectMode ? 1 : (chunk?.length ?? 1);
900
-
901
- // If corked, buffer the write
902
- if (this.writableCorked > 0) {
903
- this._corkedBuffer.push({ chunk, encoding: encoding as string, callback });
904
- return this.writableLength < this.writableHighWaterMark;
905
- }
906
-
907
- // If not yet constructed, buffer writes until construction finishes
908
- if (!this._writableState.constructed) {
909
- this._pendingConstruct.push({ chunk, encoding: encoding as string, callback });
910
- return this.writableLength < this.writableHighWaterMark;
911
- }
912
-
913
- // Compute backpressure BEFORE _doWrite (sync transforms may decrement length immediately)
914
- const belowHWM = this.writableLength < this.writableHighWaterMark;
915
- if (!belowHWM) {
916
- this.writableNeedDrain = true;
917
- }
918
-
919
- // Serialize writes: only one _write at a time, buffer the rest
920
- if (this._writableState.writing) {
921
- this._writeBuffer.push({ chunk, encoding: encoding as string, callback });
922
- } else {
923
- this._doWrite(chunk, encoding as string, callback);
924
- }
925
-
926
- return belowHWM;
927
- }
928
-
929
- private _doEnd(chunk?: any, encoding?: string, callback?: () => void): void {
930
- if (chunk !== undefined && chunk !== null) {
931
- this.write(chunk, encoding as string);
932
- }
933
-
934
- this.writableEnded = true;
935
- this._writableState.ended = true;
936
- this._ending = true;
937
- this._endCallback = callback;
938
-
939
- // _maybeFinish will call _final once all pending writes have drained
940
- this._maybeFinish();
941
- }
942
-
943
- end(chunk?: any, encoding?: string | (() => void), callback?: () => void): this {
944
- if (typeof chunk === 'function') {
945
- callback = chunk;
946
- chunk = undefined;
947
- }
948
- if (typeof encoding === 'function') {
949
- callback = encoding;
950
- encoding = undefined;
951
- }
952
-
953
- // Ignore duplicate end() calls (e.g. from auto-end after half-close)
954
- if (this.writableEnded) {
955
- if (callback) nextTick(callback);
956
- return this;
957
- }
958
-
959
- // If not yet constructed, defer end until construction finishes
960
- if (!this._writableState.constructed) {
961
- this._pendingEnd = { chunk, encoding: encoding as string, callback };
962
- return this;
963
- }
964
-
965
- this._doEnd(chunk, encoding as string, callback);
966
-
967
- return this;
968
- }
969
-
970
- cork(): void {
971
- this.writableCorked++;
972
- }
973
-
974
- uncork(): void {
975
- if (this.writableCorked > 0) {
976
- this.writableCorked--;
977
- if (this.writableCorked === 0 && this._corkedBuffer.length > 0) {
978
- this._flushCorkedBuffer();
979
- }
980
- }
981
- }
982
-
983
- private _flushCorkedBuffer(): void {
984
- // If _writev is available, flush as a batch
985
- if (this._writev && this._corkedBuffer.length > 1) {
986
- const buffered = this._corkedBuffer.splice(0);
987
- const chunks = buffered.map(b => ({ chunk: b.chunk, encoding: b.encoding }));
988
- this._writev.call(this, chunks, (err) => {
989
- for (const b of buffered) {
990
- this.writableLength -= this.writableObjectMode ? 1 : (b.chunk?.length ?? 1);
991
- }
992
- if (err) {
993
- for (const b of buffered) b.callback(err);
994
- this.emit('error', err);
995
- } else {
996
- for (const b of buffered) b.callback();
997
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
998
- this.writableNeedDrain = false;
999
- this.emit('drain');
1000
- }
1001
- }
1002
- });
1003
- } else {
1004
- // Flush one by one via serialized write path
1005
- const buffered = this._corkedBuffer.splice(0);
1006
- if (buffered.length > 0) {
1007
- const [first, ...rest] = buffered;
1008
- this._writeBuffer.push(...rest);
1009
- this._doWrite(first.chunk, first.encoding, first.callback);
1010
- }
1011
- }
1012
- }
1013
-
1014
- setDefaultEncoding(encoding: string): this {
1015
- this._defaultEncoding = encoding;
1016
- return this;
1017
- }
1018
-
1019
- destroy(error?: Error): this {
1020
- if (this.destroyed) return this;
1021
- this.destroyed = true;
1022
- this.writable = false;
1023
- // Store the error so finished() can retrieve it if called after destroy() but before 'error' fires
1024
- if (error) (this as any)._err = error;
1025
-
1026
- const cb = (err?: Error | null) => {
1027
- if (err) nextTick(() => this.emit('error', err));
1028
- nextTick(() => this.emit('close'));
1029
- };
1030
-
1031
- if (this._destroyImpl) {
1032
- this._destroyImpl.call(this, error ?? null, cb);
1033
- } else {
1034
- cb(error);
1035
- }
1036
-
1037
- return this;
1038
- }
1039
- }
1040
-
1041
- // ---- Duplex ----
1042
-
1043
- class Duplex_ extends Readable_ {
1044
- writable = true;
1045
- writableHighWaterMark: number;
1046
- writableLength = 0;
1047
- writableObjectMode: boolean;
1048
- writableEnded = false;
1049
- writableFinished = false;
1050
- writableCorked = 0;
1051
- writableNeedDrain = false;
1052
- allowHalfOpen: boolean;
1053
- private _decodeStrings: boolean;
1054
-
1055
- // Exposed writable-side state (mirrors Node.js _writableState for split HWM tests)
1056
- _writableState = { highWaterMark: 0, objectMode: false };
1057
-
1058
- private _duplexCorkedBuffer: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
1059
- // Write serialization — prevents concurrent write_bytes_async calls (GIO_ERROR_PENDING).
1060
- // Duplex inherits from Readable, not Writable, so it needs its own queue separate from
1061
- // Writable_._doWrite/_drainWriteBuffer.
1062
- private _duplexWriting = false;
1063
- private _duplexWriteQueue: Array<{ chunk: any; encoding: string; callback: (error?: Error | null) => void }> = [];
1064
- private _writeImpl: ((chunk: any, encoding: string, cb: (error?: Error | null) => void) => void) | undefined;
1065
- private _finalImpl: ((cb: (error?: Error | null) => void) => void) | undefined;
1066
- private _defaultEncoding = 'utf8';
1067
- private _pendingWrites = 0;
1068
- private _pendingEndCb: (() => void) | null = null;
1069
-
1070
- constructor(opts?: DuplexOptions) {
1071
- super(opts);
1072
-
1073
- validateHighWaterMark('writableHighWaterMark', opts?.writableHighWaterMark);
1074
- validateHighWaterMark('readableHighWaterMark', opts?.readableHighWaterMark);
1075
-
1076
- // Writable side: highWaterMark (shared) takes priority over writableHighWaterMark.
1077
- this.writableObjectMode = opts?.writableObjectMode ?? opts?.objectMode ?? false;
1078
- this.writableHighWaterMark = opts?.highWaterMark
1079
- ?? opts?.writableHighWaterMark
1080
- ?? getDefaultHighWaterMark(this.writableObjectMode);
1081
- this._writableState.highWaterMark = this.writableHighWaterMark;
1082
- this._writableState.objectMode = this.writableObjectMode;
1083
-
1084
- // Readable side overrides: Readable_ constructor already applied opts.highWaterMark,
1085
- // so only override with readableHighWaterMark when highWaterMark was NOT set.
1086
- if (opts?.highWaterMark === undefined && opts?.readableHighWaterMark !== undefined) {
1087
- this.readableHighWaterMark = opts.readableHighWaterMark;
1088
- this._readableState.highWaterMark = opts.readableHighWaterMark;
1089
- }
1090
- if (opts?.readableObjectMode !== undefined) {
1091
- this.readableObjectMode = opts.readableObjectMode;
1092
- this._readableState.objectMode = opts.readableObjectMode;
1093
- // Re-derive readable HWM for objectMode when neither readableHighWaterMark nor highWaterMark was set.
1094
- if (opts?.readableHighWaterMark === undefined && opts?.highWaterMark === undefined) {
1095
- this.readableHighWaterMark = getDefaultHighWaterMark(opts.readableObjectMode);
1096
- this._readableState.highWaterMark = this.readableHighWaterMark;
1097
- }
1098
- }
1099
-
1100
- this.allowHalfOpen = opts?.allowHalfOpen !== false;
1101
- this._decodeStrings = opts?.decodeStrings !== false;
1102
- if (opts?.write) this._writeImpl = opts.write;
1103
- // writev not yet supported on Duplex
1104
- if (opts?.final) this._finalImpl = opts.final;
1105
-
1106
- // When allowHalfOpen=false, end writable when readable ends
1107
- if (!this.allowHalfOpen) {
1108
- this.once('end', () => {
1109
- if (!this.writableEnded) {
1110
- nextTick(() => this.end());
1111
- }
1112
- });
1113
- }
1114
- }
1115
-
1116
- _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
1117
- if (this._writeImpl) {
1118
- this._writeImpl.call(this, chunk, encoding, callback);
1119
- } else {
1120
- callback();
1121
- }
1122
- }
1123
-
1124
- _final(callback: (error?: Error | null) => void): void {
1125
- if (this._finalImpl) {
1126
- this._finalImpl.call(this, callback);
1127
- } else {
1128
- callback();
1129
- }
1130
- }
1131
-
1132
- override destroy(error?: Error): this {
1133
- if (this.destroyed) return this;
1134
- this.writable = false;
1135
- return super.destroy(error);
1136
- }
1137
-
1138
- write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
1139
- if (typeof encoding === 'function') {
1140
- callback = encoding;
1141
- encoding = undefined;
1142
- }
1143
- if (encoding === undefined) encoding = this._defaultEncoding;
1144
-
1145
- // Convert strings to Buffer when decodeStrings is true (default), but not in objectMode
1146
- if (this._decodeStrings && !this.writableObjectMode && typeof chunk === 'string') {
1147
- const BufCtor = (globalThis as any).Buffer;
1148
- if (BufCtor) {
1149
- chunk = BufCtor.from(chunk, encoding);
1150
- encoding = 'buffer';
1151
- }
1152
- }
1153
- // Set encoding to 'buffer' for Buffer/Uint8Array chunks
1154
- if (typeof chunk !== 'string' && !this.writableObjectMode) {
1155
- const BufCtor = (globalThis as any).Buffer;
1156
- if ((BufCtor && BufCtor.isBuffer(chunk)) || chunk instanceof Uint8Array) {
1157
- encoding = 'buffer';
1158
- }
1159
- }
1160
-
1161
- if (this.writableEnded) {
1162
- const err = new Error('write after end');
1163
- const cb = callback || (() => {});
1164
- nextTick(() => {
1165
- cb(err);
1166
- this.emit('error', err);
1167
- });
1168
- return false;
1169
- }
1170
-
1171
- this.writableLength += this.writableObjectMode ? 1 : (chunk?.length ?? 1);
1172
-
1173
- // If corked, buffer the write
1174
- if (this.writableCorked > 0) {
1175
- this._duplexCorkedBuffer.push({ chunk, encoding: encoding as string, callback: callback || (() => {}) });
1176
- return this.writableLength < this.writableHighWaterMark;
1177
- }
1178
-
1179
- // Compute backpressure BEFORE _write (sync transforms may decrement length immediately)
1180
- const belowHWM = this.writableLength < this.writableHighWaterMark;
1181
- if (!belowHWM) {
1182
- this.writableNeedDrain = true;
1183
- }
1184
-
1185
- const cb = callback || (() => {});
1186
- this._duplexDoWrite(chunk, encoding as string, cb);
1187
-
1188
- return belowHWM;
1189
- }
1190
-
1191
- private _duplexDoWrite(chunk: any, encoding: string, cb: (error?: Error | null) => void): void {
1192
- if (this._duplexWriting) {
1193
- this._duplexWriteQueue.push({ chunk, encoding, callback: cb });
1194
- return;
1195
- }
1196
- this._duplexWriting = true;
1197
- this._duplexStartWrite(chunk, encoding, cb);
1198
- }
1199
-
1200
- // Starts a write assuming _duplexWriting is already true. After the write
1201
- // completes, either start the next queued write (keeping _duplexWriting=true
1202
- // to preserve FIFO order) or clear the flag and emit 'drain'. The 'drain'
1203
- // listener on streamx may synchronously call conn.write() — emitting drain
1204
- // BEFORE the queue is fully processed would let that new write bypass the
1205
- // queue, causing out-of-order bytes on the wire (and, for bittorrent-protocol,
1206
- // desync of piece header vs. piece payload).
1207
- private _duplexStartWrite(chunk: any, encoding: string, cb: (error?: Error | null) => void): void {
1208
- this._pendingWrites++;
1209
- this._write(chunk, encoding, (err) => {
1210
- this._pendingWrites--;
1211
- this.writableLength -= this.writableObjectMode ? 1 : (chunk?.length ?? 1);
1212
- if (err) {
1213
- nextTick(() => {
1214
- cb(err);
1215
- this._duplexWriting = false;
1216
- this.emit('error', err);
1217
- if (this._duplexWriteQueue.length > 0) {
1218
- const next = this._duplexWriteQueue.shift()!;
1219
- this._duplexWriting = true;
1220
- this._duplexStartWrite(next.chunk, next.encoding, next.callback);
1221
- }
1222
- });
1223
- } else {
1224
- nextTick(() => {
1225
- cb();
1226
- if (this._duplexWriteQueue.length > 0) {
1227
- const next = this._duplexWriteQueue.shift()!;
1228
- this._duplexStartWrite(next.chunk, next.encoding, next.callback);
1229
- return;
1230
- }
1231
- this._duplexWriting = false;
1232
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
1233
- this.writableNeedDrain = false;
1234
- this.emit('drain');
1235
- }
1236
- if (this._pendingWrites === 0 && this._pendingEndCb) {
1237
- const endCb = this._pendingEndCb;
1238
- this._pendingEndCb = null;
1239
- endCb();
1240
- }
1241
- });
1242
- }
1243
- });
1244
- }
1245
-
1246
- private _duplexDrainQueue(): void {
1247
- if (this._duplexWriteQueue.length > 0) {
1248
- const next = this._duplexWriteQueue.shift()!;
1249
- this._duplexDoWrite(next.chunk, next.encoding, next.callback);
1250
- }
1251
- }
1252
-
1253
- end(chunk?: any, encoding?: string | (() => void), callback?: () => void): this {
1254
- if (typeof chunk === 'function') {
1255
- callback = chunk;
1256
- chunk = undefined;
1257
- }
1258
- if (typeof encoding === 'function') {
1259
- callback = encoding;
1260
- encoding = undefined;
1261
- }
1262
-
1263
- if (chunk !== undefined && chunk !== null) {
1264
- this.write(chunk, encoding as string);
1265
- }
1266
-
1267
- this.writableEnded = true;
1268
-
1269
- const doFinal = () => {
1270
- this._final((err) => {
1271
- this.writableFinished = true;
1272
- // Allow subclasses (Transform) to run post-final hooks (e.g. flush)
1273
- // before the 'finish' event fires.
1274
- this._doPrefinishHooks(() => {
1275
- nextTick(() => {
1276
- if (err) this.emit('error', err);
1277
- this.emit('finish');
1278
- nextTick(() => this.emit('close'));
1279
- if (callback) callback();
1280
- });
1281
- });
1282
- });
1283
- };
1284
-
1285
- // Wait for all pending writes to complete before calling _final.
1286
- // Transform._write is synchronous (calls user cb in same tick), so _pendingWrites
1287
- // can be 0 even while follow-up writes sit in _duplexWriteQueue. Check the queue
1288
- // and the write-in-flight flag too, otherwise end() fires _final — which for
1289
- // Transform pushes null — before the queued chunks reach _transform.
1290
- if (this._pendingWrites > 0 || this._duplexWriting || this._duplexWriteQueue.length > 0) {
1291
- this._pendingEndCb = doFinal;
1292
- } else {
1293
- doFinal();
1294
- }
1295
-
1296
- return this;
1297
- }
1298
-
1299
- /** Hook for subclasses to run logic between _final and 'finish'. Default: no-op. */
1300
- protected _doPrefinishHooks(cb: () => void): void {
1301
- cb();
1302
- }
1303
-
1304
- cork(): void { this.writableCorked++; }
1305
-
1306
- uncork(): void {
1307
- if (this.writableCorked > 0) {
1308
- this.writableCorked--;
1309
- if (this.writableCorked === 0 && this._duplexCorkedBuffer.length > 0) {
1310
- const buffered = this._duplexCorkedBuffer.splice(0);
1311
- for (const { chunk, encoding, callback } of buffered) {
1312
- this._write(chunk, encoding, (err) => {
1313
- this.writableLength -= this.writableObjectMode ? 1 : (chunk?.length ?? 1);
1314
- if (err) {
1315
- callback(err);
1316
- this.emit('error', err);
1317
- } else {
1318
- callback();
1319
- }
1320
- });
1321
- }
1322
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
1323
- this.writableNeedDrain = false;
1324
- nextTick(() => this.emit('drain'));
1325
- }
1326
- }
1327
- }
1328
- }
1329
-
1330
- setDefaultEncoding(encoding: string): this {
1331
- this._defaultEncoding = encoding;
1332
- return this;
1333
- }
1334
- }
1335
-
1336
- // ---- Transform ----
1337
-
1338
- class Transform_ extends Duplex_ {
1339
- constructor(opts?: TransformOptions) {
1340
- // Don't forward transform/flush/final/write — Transform's own method assignments
1341
- // handle those. Passing write/final through would register them in Duplex_'s
1342
- // _writeImpl/_finalImpl and bypass Transform's override.
1343
- super({ ...opts, write: undefined, final: undefined });
1344
- // Direct assignment mirrors Node.js: opts.transform/flush/final overwrite the
1345
- // prototype methods on the instance so `t._transform === opts.transform` holds.
1346
- if (opts?.transform) (this as any)._transform = opts.transform;
1347
- if (opts?.flush) (this as any)._flush = opts.flush;
1348
- if (opts?.final) (this as any)._final = opts.final;
1349
- }
1350
-
1351
- _transform(_chunk: any, _encoding: string, _callback: (error?: Error | null, data?: any) => void): void {
1352
- // Throw when no implementation was provided (no opts.transform and no subclass override).
1353
- const err = Object.assign(
1354
- new Error('The _transform() method is not implemented'),
1355
- { code: 'ERR_METHOD_NOT_IMPLEMENTED' }
1356
- );
1357
- throw err;
1358
- }
1359
-
1360
- _flush(callback: (error?: Error | null, data?: any) => void): void {
1361
- callback();
1362
- }
1363
-
1364
- _write(chunk: any, encoding: string, callback: (error?: Error | null) => void): void {
1365
- let called = false;
1366
- try {
1367
- this._transform(chunk, encoding, (err, data) => {
1368
- if (called) {
1369
- const e = Object.assign(new Error('Callback called multiple times'), { code: 'ERR_MULTIPLE_CALLBACK' });
1370
- nextTick(() => this.emit('error', e));
1371
- return;
1372
- }
1373
- called = true;
1374
- if (err) {
1375
- callback(err);
1376
- return;
1377
- }
1378
- if (data !== undefined && data !== null) {
1379
- this.push(data);
1380
- }
1381
- callback();
1382
- });
1383
- } catch (err: any) {
1384
- // ERR_METHOD_NOT_IMPLEMENTED must propagate synchronously (test-stream-transform-constructor-set-methods).
1385
- // User-provided _transform errors are converted to 'error' events.
1386
- if (err?.code === 'ERR_METHOD_NOT_IMPLEMENTED') throw err;
1387
- callback(err as Error);
1388
- }
1389
- }
1390
-
1391
- // Transform's built-in _final: calls _flush then pushes null.
1392
- // This is the default; when the user provides opts.final it is overridden on
1393
- // the instance and _doPrefinishHooks ensures _flush is still called after it.
1394
- _final(callback: (error?: Error | null) => void): void {
1395
- this._flush((err, data) => {
1396
- if (err) {
1397
- callback(err);
1398
- return;
1399
- }
1400
- if (data !== undefined && data !== null) {
1401
- this.push(data);
1402
- }
1403
- // Signal readable side is done
1404
- this.push(null);
1405
- callback();
1406
- });
1407
- }
1408
-
1409
- // When a user-provided _final overrides the prototype method, we still need
1410
- // to call the built-in flush+push-null logic (mirroring Node.js's prefinish).
1411
- protected override _doPrefinishHooks(cb: () => void): void {
1412
- const protoFinal = Transform_.prototype._final;
1413
- if ((this as any)._final !== protoFinal) {
1414
- // User replaced _final; call the built-in flush+push-null now.
1415
- protoFinal.call(this, cb);
1416
- } else {
1417
- // _final already ran flush+push-null; nothing extra needed.
1418
- cb();
1419
- }
1420
- }
1421
- }
1422
-
1423
- // ---- PassThrough ----
1424
-
1425
- class PassThrough_ extends Transform_ {
1426
- constructor(opts?: TransformOptions) {
1427
- super({
1428
- ...opts,
1429
- transform(chunk: any, _encoding: string, callback: (error?: Error | null, data?: any) => void) {
1430
- callback(null, chunk);
1431
- }
1432
- });
1433
- }
1434
- }
1435
-
1436
- // ---- pipeline ----
1437
-
1438
- type PipelineCallback = (err: Error | null) => void;
1439
-
1440
- /** A stream that can be destroyed (duck-typed for pipeline). */
1441
- interface DestroyableStream extends Stream {
1442
- destroy?(error?: Error): void;
1443
- }
1444
-
1445
- export function pipeline(...args: [...streams: DestroyableStream[], callback: PipelineCallback] | DestroyableStream[]): DestroyableStream {
1446
- const callback = typeof args[args.length - 1] === 'function' ? args.pop() as PipelineCallback : undefined;
1447
- const streams = args as DestroyableStream[];
1448
-
1449
- if (streams.length < 2) {
1450
- throw new Error('pipeline requires at least 2 streams');
1451
- }
1452
-
1453
- let error: Error | null = null;
1454
-
1455
- function onError(err: Error) {
1456
- if (!error) {
1457
- error = err;
1458
- // Destroy all streams
1459
- for (const stream of streams) {
1460
- if (typeof stream.destroy === 'function') {
1461
- stream.destroy();
1462
- }
1463
- }
1464
- if (callback) callback(err);
1465
- }
1466
- }
1467
-
1468
- // Pipe streams together
1469
- let current: Stream = streams[0];
1470
- for (let i = 1; i < streams.length; i++) {
1471
- const next = streams[i];
1472
- current.pipe(next as unknown as Writable);
1473
- current.on('error', onError);
1474
- current = next;
1475
- }
1476
-
1477
- // Listen for end on last stream
1478
- const last = streams[streams.length - 1];
1479
- last.on('error', onError);
1480
- last.on('finish', () => {
1481
- if (callback && !error) callback(null);
1482
- });
1483
-
1484
- return last;
1485
- }
1486
-
1487
- // ---- finished ----
1488
-
1489
- export function finished(stream: Stream | Readable | Writable, callback: (err?: Error | null) => void): () => void;
1490
- export function finished(stream: Stream | Readable | Writable, opts: FinishedOptions, callback: (err?: Error | null) => void): () => void;
1491
- export function finished(stream: Stream | Readable | Writable, optsOrCb: FinishedOptions | ((err?: Error | null) => void), callback?: (err?: Error | null) => void): () => void {
1492
- let cb: (err?: Error | null) => void;
1493
- let _opts: FinishedOptions = {};
1494
-
1495
- if (typeof optsOrCb === 'function') {
1496
- cb = optsOrCb;
1497
- } else {
1498
- _opts = optsOrCb || {};
1499
- cb = callback!;
1500
- }
1501
-
1502
- let called = false;
1503
- function done(err?: Error | null) {
1504
- if (!called) {
1505
- called = true;
1506
- cb(err);
1507
- }
1508
- }
1509
-
1510
- const onFinish = () => done();
1511
- const onEnd = () => done();
1512
- const onError = (err: Error) => done(err);
1513
- const onClose = () => {
1514
- if (!(stream as Writable).writableFinished && !(stream as Readable).readableEnded) {
1515
- done(new Error('premature close'));
1516
- }
1517
- };
1518
-
1519
- stream.on('finish', onFinish);
1520
- stream.on('end', onEnd);
1521
- stream.on('error', onError);
1522
- stream.on('close', onClose);
1523
-
1524
- // Check initial state — handle already-finished/destroyed streams
1525
- // Reference: refs/node/lib/internal/streams/end-of-stream.js lines 228-249
1526
- const isWritableStream = typeof (stream as Writable).write === 'function';
1527
- const isReadableStream = typeof (stream as Readable).read === 'function';
1528
- const writableFinished = (stream as unknown as Record<string, unknown>).writableFinished === true;
1529
- const readableEnded = (stream as unknown as Record<string, unknown>).readableEnded === true;
1530
- const destroyed = (stream as unknown as Record<string, unknown>).destroyed === true;
1531
-
1532
- if (destroyed) {
1533
- const storedErr = (stream as unknown as Record<string, unknown>)._err as Error | null | undefined;
1534
- if (storedErr) {
1535
- // Stream was destroyed with an error (may have fired before we registered listener)
1536
- queueMicrotask(() => done(storedErr));
1537
- } else if ((isWritableStream && writableFinished) || (isReadableStream && readableEnded)) {
1538
- // Stream was destroyed after completing normally — treat as success
1539
- queueMicrotask(() => done());
1540
- } else {
1541
- // Stream was destroyed without completing — premature close
1542
- queueMicrotask(() => done(new Error('premature close')));
1543
- }
1544
- } else if (isWritableStream && !isReadableStream && writableFinished) {
1545
- queueMicrotask(() => done());
1546
- } else if (!isWritableStream && isReadableStream && readableEnded) {
1547
- queueMicrotask(() => done());
1548
- } else if (isWritableStream && isReadableStream && writableFinished && readableEnded) {
1549
- queueMicrotask(() => done());
1550
- }
1551
-
1552
- return function cleanup() {
1553
- stream.removeListener('finish', onFinish);
1554
- stream.removeListener('end', onEnd);
1555
- stream.removeListener('error', onError);
1556
- stream.removeListener('close', onClose);
1557
- };
1558
- }
1559
-
1560
- // ---- addAbortSignal ----
1561
-
1562
- export function addAbortSignal(signal: AbortSignal, stream: Stream): typeof stream {
1563
- if (!(signal instanceof AbortSignal)) {
1564
- throw new TypeError('The first argument must be an AbortSignal');
1565
- }
1566
- if (!(stream instanceof Stream)) {
1567
- throw new TypeError('The second argument must be a Stream');
1568
- }
1569
-
1570
- if (signal.aborted) {
1571
- (stream as Readable | Writable).destroy(new Error('The operation was aborted'));
1572
- } else {
1573
- const onAbort = () => {
1574
- (stream as Readable | Writable).destroy(new Error('The operation was aborted'));
1575
- };
1576
- signal.addEventListener('abort', onAbort, { once: true });
1577
- // Cleanup when stream closes
1578
- stream.once('close', () => {
1579
- signal.removeEventListener('abort', onAbort);
1580
- });
1581
- }
1582
-
1583
- return stream;
1584
- }
1585
-
1586
- // ---- Utility functions ----
2
+ // Reimplemented for GJS using EventEmitter and microtask scheduling.
3
+ //
4
+ // This file is the public barrel — actual implementations live per-class:
5
+ // - stream-base.ts Stream_ (root EventEmitter + pipe glue)
6
+ // - readable.ts Readable_ (+ Readable_._autoClose protected hook)
7
+ // - writable.ts Writable_ (+ FIFO drain queue)
8
+ // - duplex.ts Duplex_ (Readable_ + writable-half re-implementation)
9
+ // - transform.ts Transform_
10
+ // - passthrough.ts PassThrough_
11
+ // - utils/pipe.ts Stream.pipe() helper
12
+ // - utils/pipeline.ts pipeline()
13
+ // - utils/finished.ts finished(), addAbortSignal(), is{Readable,…}()
14
+ // - internal/state.ts module-singleton defaults + validateHighWaterMark
15
+ // - internal/types.ts shared interfaces
16
+ //
17
+ // Public class names are makeCallable Proxy wrappers around the underscore-suffixed
18
+ // internal classes (`Stream_`, `Readable_`, …) so legacy CJS consumers can do
19
+ // `Stream.call(this)` (npm `send`, `util.inherits(Sub, Stream)`, our own
20
+ // `@gjsify/crypto` `Hash.copy()`). See `./callable.ts` for the rationale.
21
+ //
22
+ // The historical default export shape — the Stream constructor with all classes
23
+ // + helpers attached as static properties — is preserved for `cjs-compat.cjs`.
1587
24
 
1588
- export function isReadable(stream: unknown): boolean {
1589
- if (stream == null) return false;
1590
- const s = stream as Record<string, unknown>;
1591
- if (typeof s.readable !== 'boolean') return false;
1592
- if (typeof s.read !== 'function') return false;
1593
- if (s.destroyed === true) return false;
1594
- if (s.readableEnded === true) return false;
1595
- return (s.readable as boolean) === true;
1596
- }
25
+ import { makeCallable } from './callable.js';
1597
26
 
1598
- export function isWritable(stream: unknown): boolean {
1599
- if (stream == null) return false;
1600
- const s = stream as Record<string, unknown>;
1601
- if (typeof s.writable !== 'boolean') return false;
1602
- if (typeof s.write !== 'function') return false;
1603
- if (s.destroyed === true) return false;
1604
- if (s.writableEnded === true) return false;
1605
- return (s.writable as boolean) === true;
1606
- }
27
+ import { Stream_ } from './stream-base.js';
28
+ import { Readable_ } from './readable.js';
29
+ // Side-effect import: wires Stream_.prototype.pipe pipe() at load time.
30
+ // See ./stream-base.ts (_setPipeImpl) for the late-binding rationale.
31
+ import './utils/pipe.js';
32
+ import { Writable_ } from './writable.js';
33
+ import { Duplex_ } from './duplex.js';
34
+ import { Transform_ } from './transform.js';
35
+ import { PassThrough_ } from './passthrough.js';
36
+ import { pipeline } from './utils/pipeline.js';
37
+ import { finished, addAbortSignal, isReadable, isWritable, isDestroyed, isDisturbed, isErrored } from './utils/finished.js';
38
+ import { getDefaultHighWaterMark, setDefaultHighWaterMark } from './internal/state.js';
1607
39
 
1608
- export function isDestroyed(stream: unknown): boolean {
1609
- if (stream == null) return false;
1610
- return (stream as Record<string, unknown>).destroyed === true;
1611
- }
40
+ // ---- Re-exports of internal helpers ----
1612
41
 
1613
- export function isDisturbed(stream: unknown): boolean {
1614
- if (stream == null) return false;
1615
- const s = stream as Record<string, unknown>;
1616
- // A stream is disturbed if data has been read from it
1617
- return s.readableDidRead === true || (s.readableFlowing !== null && s.readableFlowing !== undefined);
1618
- }
42
+ export { getDefaultHighWaterMark, setDefaultHighWaterMark };
43
+ export { pipeline, finished, addAbortSignal };
44
+ export { isReadable, isWritable, isDestroyed, isDisturbed, isErrored };
1619
45
 
1620
- export function isErrored(stream: unknown): boolean {
1621
- if (stream == null) return false;
1622
- // Check for errored state on either side
1623
- const s = stream as Record<string, unknown>;
1624
- if (s.destroyed === true && typeof s.readable === 'boolean' && s.readable === false) return true;
1625
- if (s.destroyed === true && typeof s.writable === 'boolean' && s.writable === false) return true;
1626
- return false;
1627
- }
46
+ // ---- Type-only re-exports ----
1628
47
 
1629
- // ---- Exports ----
1630
- //
1631
- // The class declarations above use underscore-suffixed internal names
1632
- // (`Stream_`, `Readable_`, etc.) — we wrap each one with `makeCallable` so
1633
- // legacy CJS consumers can do `Stream.call(this)` (npm `send`,
1634
- // `util.inherits(Sub, Stream)`, our own `@gjsify/crypto` `Hash.copy()`).
1635
- // See `./callable.ts` for the rationale and implementation.
1636
- //
1637
- // Public API preserves the historical names. Both value and type positions
1638
- // work because of the `type X = X_` aliases below.
48
+ export type { ReadableOptions, WritableOptions, DuplexOptions, TransformOptions, FinishedOptions } from 'node:stream';
49
+ export type { StreamOptions } from './internal/types.js';
1639
50
 
1640
- import { makeCallable } from './callable.js';
51
+ // ---- Class wrappers (callable for legacy CJS) ----
1641
52
 
1642
53
  export const Stream = makeCallable(Stream_) as typeof Stream_;
1643
54
  export const Readable = makeCallable(Readable_) as typeof Readable_;
@@ -1653,7 +64,12 @@ export type Duplex = Duplex_;
1653
64
  export type Transform = Transform_;
1654
65
  export type PassThrough = PassThrough_;
1655
66
 
1656
- // Default export
67
+ // ---- Default export ----
68
+ //
69
+ // Node returns the Stream constructor with sub-classes + helpers hung off it.
70
+ // `cjs-compat.cjs` resolves `mod.default || mod` to this exact value, so the
71
+ // legacy `util.inherits(Sub, require('stream'))` pattern keeps working.
72
+
1657
73
  const _default = Object.assign(Stream, {
1658
74
  Stream,
1659
75
  Readable,