@gjsify/stream 0.3.21 → 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
@@ -0,0 +1,108 @@
1
+ // Transform stream — a Duplex whose readable side is fed by `_transform`.
2
+ //
3
+ // Reference: refs/node/lib/internal/streams/transform.js
4
+ // Reimplemented for GJS — direct opts.transform/flush/final assignment matches
5
+ // Node's instance-method override pattern.
6
+
7
+ import { nextTick } from '@gjsify/utils';
8
+ import type { TransformOptions } from 'node:stream';
9
+
10
+ import { Duplex_ } from './duplex.js';
11
+ import type { ErrCallback } from './internal/types.js';
12
+
13
+ type TransformFn = (this: Transform_, chunk: unknown, encoding: string, callback: (error?: Error | null, data?: unknown) => void) => void;
14
+ type FlushFn = (this: Transform_, callback: (error?: Error | null, data?: unknown) => void) => void;
15
+ type FinalFn = (this: Transform_, callback: ErrCallback) => void;
16
+
17
+ interface TransformInstanceMethods {
18
+ _transform: TransformFn;
19
+ _flush: FlushFn;
20
+ _final: FinalFn;
21
+ }
22
+
23
+ export class Transform_ extends Duplex_ {
24
+ constructor(opts?: TransformOptions) {
25
+ // Don't forward transform/flush/final/write — Transform's own method assignments
26
+ // handle those. Passing write/final through would register them in Duplex_'s
27
+ // _writeImpl/_finalImpl and bypass Transform's override.
28
+ super({ ...opts, write: undefined, final: undefined });
29
+ // Direct assignment mirrors Node.js: opts.transform/flush/final overwrite the
30
+ // prototype methods on the instance so `t._transform === opts.transform` holds.
31
+ const self = this as unknown as TransformInstanceMethods;
32
+ if (opts?.transform) self._transform = opts.transform as unknown as TransformFn;
33
+ if (opts?.flush) self._flush = opts.flush as unknown as FlushFn;
34
+ if (opts?.final) self._final = opts.final as unknown as FinalFn;
35
+ }
36
+
37
+ _transform(_chunk: unknown, _encoding: string, _callback: (error?: Error | null, data?: unknown) => void): void {
38
+ // Throw when no implementation was provided (no opts.transform and no subclass override).
39
+ const err = Object.assign(
40
+ new Error('The _transform() method is not implemented'),
41
+ { code: 'ERR_METHOD_NOT_IMPLEMENTED' }
42
+ );
43
+ throw err;
44
+ }
45
+
46
+ _flush(callback: (error?: Error | null, data?: unknown) => void): void {
47
+ callback();
48
+ }
49
+
50
+ _write(chunk: unknown, encoding: string, callback: ErrCallback): void {
51
+ let called = false;
52
+ try {
53
+ this._transform(chunk, encoding, (err, data) => {
54
+ if (called) {
55
+ const e = Object.assign(new Error('Callback called multiple times'), { code: 'ERR_MULTIPLE_CALLBACK' });
56
+ nextTick(() => this.emit('error', e));
57
+ return;
58
+ }
59
+ called = true;
60
+ if (err) {
61
+ callback(err);
62
+ return;
63
+ }
64
+ if (data !== undefined && data !== null) {
65
+ this.push(data);
66
+ }
67
+ callback();
68
+ });
69
+ } catch (err) {
70
+ // ERR_METHOD_NOT_IMPLEMENTED must propagate synchronously (test-stream-transform-constructor-set-methods).
71
+ // User-provided _transform errors are converted to 'error' events.
72
+ const e = err as { code?: string };
73
+ if (e?.code === 'ERR_METHOD_NOT_IMPLEMENTED') throw err;
74
+ callback(err as Error);
75
+ }
76
+ }
77
+
78
+ // Transform's built-in _final: calls _flush then pushes null.
79
+ // This is the default; when the user provides opts.final it is overridden on
80
+ // the instance and _doPrefinishHooks ensures _flush is still called after it.
81
+ _final(callback: ErrCallback): void {
82
+ this._flush((err, data) => {
83
+ if (err) {
84
+ callback(err);
85
+ return;
86
+ }
87
+ if (data !== undefined && data !== null) {
88
+ this.push(data);
89
+ }
90
+ // Signal readable side is done
91
+ this.push(null);
92
+ callback();
93
+ });
94
+ }
95
+
96
+ // When a user-provided _final overrides the prototype method, we still need
97
+ // to call the built-in flush+push-null logic (mirroring Node.js's prefinish).
98
+ protected override _doPrefinishHooks(cb: () => void): void {
99
+ const protoFinal = Transform_.prototype._final;
100
+ if ((this as unknown as TransformInstanceMethods)._final !== protoFinal) {
101
+ // User replaced _final; call the built-in flush+push-null now.
102
+ protoFinal.call(this, cb);
103
+ } else {
104
+ // _final already ran flush+push-null; nothing extra needed.
105
+ cb();
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,156 @@
1
+ // finished + addAbortSignal + is* helpers.
2
+ //
3
+ // Reference: refs/node/lib/internal/streams/end-of-stream.js
4
+ // refs/node/lib/internal/streams/utils.js
5
+ // Reimplemented for GJS using @gjsify/utils microtask scheduling.
6
+
7
+ import { queueMicrotask } from '@gjsify/utils';
8
+ import type { FinishedOptions } from 'node:stream';
9
+
10
+ import { Stream_ } from '../stream-base.js';
11
+ import type { Readable_ } from '../readable.js';
12
+ import type { Writable_ } from '../writable.js';
13
+
14
+ type AnyStream = Stream_ | Readable_ | Writable_;
15
+
16
+ export function finished(stream: AnyStream, callback: (err?: Error | null) => void): () => void;
17
+ export function finished(stream: AnyStream, opts: FinishedOptions, callback: (err?: Error | null) => void): () => void;
18
+ export function finished(stream: AnyStream, optsOrCb: FinishedOptions | ((err?: Error | null) => void), callback?: (err?: Error | null) => void): () => void {
19
+ let cb: (err?: Error | null) => void;
20
+
21
+ if (typeof optsOrCb === 'function') {
22
+ cb = optsOrCb;
23
+ } else {
24
+ // opts not currently consumed — Node uses it for error/readable/writable filters.
25
+ cb = callback!;
26
+ }
27
+
28
+ let called = false;
29
+ function done(err?: Error | null) {
30
+ if (!called) {
31
+ called = true;
32
+ cb(err);
33
+ }
34
+ }
35
+
36
+ const onFinish = () => done();
37
+ const onEnd = () => done();
38
+ const onError = (err: Error) => done(err);
39
+ const onClose = () => {
40
+ const w = stream as Writable_;
41
+ const r = stream as Readable_;
42
+ if (!w.writableFinished && !r.readableEnded) {
43
+ done(new Error('premature close'));
44
+ }
45
+ };
46
+
47
+ stream.on('finish', onFinish);
48
+ stream.on('end', onEnd);
49
+ stream.on('error', onError);
50
+ stream.on('close', onClose);
51
+
52
+ // Check initial state — handle already-finished/destroyed streams
53
+ // Reference: refs/node/lib/internal/streams/end-of-stream.js lines 228-249
54
+ const s = stream as unknown as Record<string, unknown>;
55
+ const isWritableStream = typeof (stream as Writable_).write === 'function';
56
+ const isReadableStream = typeof (stream as Readable_).read === 'function';
57
+ const writableFinished = s.writableFinished === true;
58
+ const readableEnded = s.readableEnded === true;
59
+ const destroyed = s.destroyed === true;
60
+
61
+ if (destroyed) {
62
+ const storedErr = s._err as Error | null | undefined;
63
+ if (storedErr) {
64
+ // Stream was destroyed with an error (may have fired before we registered listener)
65
+ queueMicrotask(() => done(storedErr));
66
+ } else if ((isWritableStream && writableFinished) || (isReadableStream && readableEnded)) {
67
+ // Stream was destroyed after completing normally — treat as success
68
+ queueMicrotask(() => done());
69
+ } else {
70
+ // Stream was destroyed without completing — premature close
71
+ queueMicrotask(() => done(new Error('premature close')));
72
+ }
73
+ } else if (isWritableStream && !isReadableStream && writableFinished) {
74
+ queueMicrotask(() => done());
75
+ } else if (!isWritableStream && isReadableStream && readableEnded) {
76
+ queueMicrotask(() => done());
77
+ } else if (isWritableStream && isReadableStream && writableFinished && readableEnded) {
78
+ queueMicrotask(() => done());
79
+ }
80
+
81
+ return function cleanup() {
82
+ stream.removeListener('finish', onFinish);
83
+ stream.removeListener('end', onEnd);
84
+ stream.removeListener('error', onError);
85
+ stream.removeListener('close', onClose);
86
+ };
87
+ }
88
+
89
+ export function addAbortSignal<T extends AnyStream>(signal: AbortSignal, stream: T): T {
90
+ if (!(signal instanceof AbortSignal)) {
91
+ throw new TypeError('The first argument must be an AbortSignal');
92
+ }
93
+ if (!(stream instanceof Stream_)) {
94
+ throw new TypeError('The second argument must be a Stream');
95
+ }
96
+
97
+ const destroyable = stream as Readable_ | Writable_;
98
+
99
+ if (signal.aborted) {
100
+ destroyable.destroy(new Error('The operation was aborted'));
101
+ } else {
102
+ const onAbort = () => {
103
+ destroyable.destroy(new Error('The operation was aborted'));
104
+ };
105
+ signal.addEventListener('abort', onAbort, { once: true });
106
+ // Cleanup when stream closes
107
+ stream.once('close', () => {
108
+ signal.removeEventListener('abort', onAbort);
109
+ });
110
+ }
111
+
112
+ return stream;
113
+ }
114
+
115
+ // ---- Utility functions ----
116
+
117
+ export function isReadable(stream: unknown): boolean {
118
+ if (stream == null) return false;
119
+ const s = stream as Record<string, unknown>;
120
+ if (typeof s.readable !== 'boolean') return false;
121
+ if (typeof s.read !== 'function') return false;
122
+ if (s.destroyed === true) return false;
123
+ if (s.readableEnded === true) return false;
124
+ return s.readable === true;
125
+ }
126
+
127
+ export function isWritable(stream: unknown): boolean {
128
+ if (stream == null) return false;
129
+ const s = stream as Record<string, unknown>;
130
+ if (typeof s.writable !== 'boolean') return false;
131
+ if (typeof s.write !== 'function') return false;
132
+ if (s.destroyed === true) return false;
133
+ if (s.writableEnded === true) return false;
134
+ return s.writable === true;
135
+ }
136
+
137
+ export function isDestroyed(stream: unknown): boolean {
138
+ if (stream == null) return false;
139
+ return (stream as Record<string, unknown>).destroyed === true;
140
+ }
141
+
142
+ export function isDisturbed(stream: unknown): boolean {
143
+ if (stream == null) return false;
144
+ const s = stream as Record<string, unknown>;
145
+ // A stream is disturbed if data has been read from it
146
+ return s.readableDidRead === true || (s.readableFlowing !== null && s.readableFlowing !== undefined);
147
+ }
148
+
149
+ export function isErrored(stream: unknown): boolean {
150
+ if (stream == null) return false;
151
+ // Check for errored state on either side
152
+ const s = stream as Record<string, unknown>;
153
+ if (s.destroyed === true && typeof s.readable === 'boolean' && s.readable === false) return true;
154
+ if (s.destroyed === true && typeof s.writable === 'boolean' && s.writable === false) return true;
155
+ return false;
156
+ }
@@ -0,0 +1,113 @@
1
+ // Free-function pipe helper used by Stream_.pipe.
2
+ //
3
+ // Reference: refs/node/lib/internal/streams/legacy.js Stream.prototype.pipe.
4
+ // Reimplemented for GJS — extracted so stream-base.ts (Stream_) can stay
5
+ // dependency-free of the per-class files. The Readable_ instance check is
6
+ // resolved lazily at call time, breaking what would otherwise be a top-level
7
+ // import cycle (stream-base → pipe → readable → stream-base).
8
+
9
+ import { _setPipeImpl, type Stream_ } from '../stream-base.js';
10
+ import { Readable_ } from '../readable.js';
11
+ import type { Writable_ } from '../writable.js';
12
+ import type { StreamLike } from '../internal/types.js';
13
+
14
+ /** Tracked pipe destination for unpipe support. */
15
+ export interface PipeState {
16
+ dest: Writable_;
17
+ cleanup: () => void;
18
+ }
19
+
20
+ export function pipe<T extends Writable_>(
21
+ sourceStream: Stream_,
22
+ destination: T,
23
+ options?: { end?: boolean },
24
+ ): T {
25
+ // The source is conceptually Readable-shaped; the legacy Stream signature
26
+ // allows any EventEmitter that emits 'data'/'end'/'close', so we keep the
27
+ // structural cast and only branch on the modern Readable_ check below.
28
+ const source = sourceStream as unknown as Readable_;
29
+ const doEnd = options?.end !== false;
30
+
31
+ // Drain listener is added lazily only when backpressure occurs.
32
+ let drainListenerAdded = false;
33
+ const ondrain = () => {
34
+ drainListenerAdded = false;
35
+ destination.removeListener('drain', ondrain);
36
+ const s = source as StreamLike;
37
+ if (typeof s.resume === 'function') {
38
+ s.resume();
39
+ }
40
+ };
41
+
42
+ const ondata = (chunk: unknown) => {
43
+ if (destination.writable) {
44
+ const s = source as StreamLike;
45
+ if (destination.write(chunk) === false && typeof s.pause === 'function') {
46
+ s.pause();
47
+ if (!drainListenerAdded) {
48
+ drainListenerAdded = true;
49
+ destination.on('drain', ondrain);
50
+ }
51
+ }
52
+ }
53
+ };
54
+
55
+ source.on('data', ondata);
56
+
57
+ let didEnd = false;
58
+
59
+ const onend = () => {
60
+ if (didEnd) return;
61
+ didEnd = true;
62
+ if (doEnd) destination.end();
63
+ };
64
+
65
+ const onclose = () => {
66
+ if (didEnd) return;
67
+ didEnd = true;
68
+ if (doEnd) {
69
+ // Modern Readable streams (Readable_) do NOT destroy dest on source close —
70
+ // only call dest.end/destroy for legacy Stream objects (no Readable_ prototype).
71
+ if (!(source instanceof Readable_)) {
72
+ const d = destination as unknown as { destroy?: () => void };
73
+ if (typeof d.destroy === 'function') {
74
+ d.destroy();
75
+ }
76
+ }
77
+ }
78
+ };
79
+
80
+ if (doEnd) {
81
+ source.on('end', onend);
82
+ source.on('close', onclose);
83
+ }
84
+
85
+ const cleanup = () => {
86
+ source.removeListener('data', ondata);
87
+ if (drainListenerAdded) destination.removeListener('drain', ondrain);
88
+ source.removeListener('end', onend);
89
+ source.removeListener('close', onclose);
90
+ // Self-remove from both end and close
91
+ source.removeListener('end', cleanup);
92
+ source.removeListener('close', cleanup);
93
+ destination.removeListener('close', cleanup);
94
+ };
95
+
96
+ source.on('end', cleanup);
97
+ source.on('close', cleanup);
98
+ destination.on('close', cleanup);
99
+
100
+ // Track piped destinations for unpipe
101
+ if (source instanceof Readable_) {
102
+ source._pipeDests.push({ dest: destination, cleanup });
103
+ source._readableState.pipes.push(destination);
104
+ }
105
+
106
+ destination.emit('pipe', source);
107
+ return destination;
108
+ }
109
+
110
+ // Wire the implementation back into Stream_.prototype.pipe — see _setPipeImpl
111
+ // in ../stream-base.ts for why this is done at module-load time rather than
112
+ // via a static top-level import.
113
+ _setPipeImpl(pipe);
@@ -0,0 +1,59 @@
1
+ // pipeline — chain streams and propagate destroy on error.
2
+ //
3
+ // Reference: refs/node/lib/internal/streams/pipeline.js
4
+ // Reimplemented for GJS — minimal, callback-based variant. The promise form
5
+ // lives under `@gjsify/stream/promises` and wraps this.
6
+
7
+ import type { Stream_ } from '../stream-base.js';
8
+ import type { Writable_ } from '../writable.js';
9
+
10
+ export type PipelineCallback = (err: Error | null) => void;
11
+
12
+ /** A stream that can be destroyed (duck-typed for pipeline). */
13
+ export interface DestroyableStream extends Stream_ {
14
+ destroy?(error?: Error): void;
15
+ }
16
+
17
+ export function pipeline(...args: [...streams: DestroyableStream[], callback: PipelineCallback] | DestroyableStream[]): DestroyableStream {
18
+ const argList = args as Array<DestroyableStream | PipelineCallback>;
19
+ const last = argList[argList.length - 1];
20
+ const callback = typeof last === 'function' ? (argList.pop() as PipelineCallback) : undefined;
21
+ const streams = argList as DestroyableStream[];
22
+
23
+ if (streams.length < 2) {
24
+ throw new Error('pipeline requires at least 2 streams');
25
+ }
26
+
27
+ let error: Error | null = null;
28
+
29
+ function onError(err: Error) {
30
+ if (!error) {
31
+ error = err;
32
+ // Destroy all streams
33
+ for (const stream of streams) {
34
+ if (typeof stream.destroy === 'function') {
35
+ stream.destroy();
36
+ }
37
+ }
38
+ if (callback) callback(err);
39
+ }
40
+ }
41
+
42
+ // Pipe streams together
43
+ let current: Stream_ = streams[0];
44
+ for (let i = 1; i < streams.length; i++) {
45
+ const next = streams[i];
46
+ current.pipe(next as unknown as Writable_);
47
+ current.on('error', onError);
48
+ current = next;
49
+ }
50
+
51
+ // Listen for end on last stream
52
+ const lastStream = streams[streams.length - 1];
53
+ lastStream.on('error', onError);
54
+ lastStream.on('finish', () => {
55
+ if (callback && !error) callback(null);
56
+ });
57
+
58
+ return lastStream;
59
+ }