@gjsify/stream 0.4.0 → 0.4.3

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/duplex.ts DELETED
@@ -1,317 +0,0 @@
1
- // Duplex stream — Readable + Writable in a single object (independent halves).
2
- //
3
- // Reference: refs/node/lib/internal/streams/duplex.js
4
- // Reimplemented for GJS. Note: extends Readable_ and re-implements the writable
5
- // half here (rather than multiply-inheriting Writable_) — matches Node's lib
6
- // layout, where Duplex.prototype is built by mixing Writable methods into a
7
- // Readable-prototype-chain.
8
-
9
- import { nextTick } from '@gjsify/utils';
10
- import type { DuplexOptions } from 'node:stream';
11
-
12
- import { Readable_ } from './readable.js';
13
- import { getDefaultHighWaterMark, validateHighWaterMark } from './internal/state.js';
14
- import type { BufferedWrite, ErrCallback } from './internal/types.js';
15
-
16
- interface DuplexWritableState {
17
- highWaterMark: number;
18
- objectMode: boolean;
19
- }
20
-
21
- export class Duplex_ extends Readable_ {
22
- writable = true;
23
- writableHighWaterMark: number;
24
- writableLength = 0;
25
- writableObjectMode: boolean;
26
- writableEnded = false;
27
- writableFinished = false;
28
- writableCorked = 0;
29
- writableNeedDrain = false;
30
- allowHalfOpen: boolean;
31
- private _decodeStrings: boolean;
32
-
33
- // Exposed writable-side state (mirrors Node.js _writableState for split HWM tests)
34
- // NOTE: The base Readable_ class declares `_readableState` with a different shape.
35
- // We deliberately only expose the highWaterMark/objectMode pair here — the rest of
36
- // Node's _writableState fields are kept private below.
37
- _writableState: DuplexWritableState = { highWaterMark: 0, objectMode: false };
38
-
39
- private _duplexCorkedBuffer: BufferedWrite[] = [];
40
- // Write serialization — prevents concurrent write_bytes_async calls (GIO_ERROR_PENDING).
41
- // Duplex inherits from Readable, not Writable, so it needs its own queue separate from
42
- // Writable_._doWrite/_drainWriteBuffer.
43
- private _duplexWriting = false;
44
- private _duplexWriteQueue: BufferedWrite[] = [];
45
- private _writeImpl: ((this: Duplex_, chunk: unknown, encoding: string, cb: ErrCallback) => void) | undefined;
46
- private _finalImpl: ((this: Duplex_, cb: ErrCallback) => void) | undefined;
47
- private _defaultEncoding = 'utf8';
48
- private _pendingWrites = 0;
49
- private _pendingEndCb: (() => void) | null = null;
50
-
51
- constructor(opts?: DuplexOptions) {
52
- super(opts);
53
-
54
- validateHighWaterMark('writableHighWaterMark', opts?.writableHighWaterMark);
55
- validateHighWaterMark('readableHighWaterMark', opts?.readableHighWaterMark);
56
-
57
- // Writable side: highWaterMark (shared) takes priority over writableHighWaterMark.
58
- this.writableObjectMode = opts?.writableObjectMode ?? opts?.objectMode ?? false;
59
- this.writableHighWaterMark = opts?.highWaterMark
60
- ?? opts?.writableHighWaterMark
61
- ?? getDefaultHighWaterMark(this.writableObjectMode);
62
- this._writableState.highWaterMark = this.writableHighWaterMark;
63
- this._writableState.objectMode = this.writableObjectMode;
64
-
65
- // Readable side overrides: Readable_ constructor already applied opts.highWaterMark,
66
- // so only override with readableHighWaterMark when highWaterMark was NOT set.
67
- if (opts?.highWaterMark === undefined && opts?.readableHighWaterMark !== undefined) {
68
- this.readableHighWaterMark = opts.readableHighWaterMark;
69
- this._readableState.highWaterMark = opts.readableHighWaterMark;
70
- }
71
- if (opts?.readableObjectMode !== undefined) {
72
- this.readableObjectMode = opts.readableObjectMode;
73
- this._readableState.objectMode = opts.readableObjectMode;
74
- // Re-derive readable HWM for objectMode when neither readableHighWaterMark nor highWaterMark was set.
75
- if (opts?.readableHighWaterMark === undefined && opts?.highWaterMark === undefined) {
76
- this.readableHighWaterMark = getDefaultHighWaterMark(opts.readableObjectMode);
77
- this._readableState.highWaterMark = this.readableHighWaterMark;
78
- }
79
- }
80
-
81
- this.allowHalfOpen = opts?.allowHalfOpen !== false;
82
- this._decodeStrings = opts?.decodeStrings !== false;
83
- if (opts?.write) this._writeImpl = opts.write as unknown as (this: Duplex_, c: unknown, e: string, cb: ErrCallback) => void;
84
- // writev not yet supported on Duplex
85
- if (opts?.final) this._finalImpl = opts.final as unknown as (this: Duplex_, cb: ErrCallback) => void;
86
-
87
- // When allowHalfOpen=false, end writable when readable ends
88
- if (!this.allowHalfOpen) {
89
- this.once('end', () => {
90
- if (!this.writableEnded) {
91
- nextTick(() => this.end());
92
- }
93
- });
94
- }
95
- }
96
-
97
- _write(chunk: unknown, encoding: string, callback: ErrCallback): void {
98
- if (this._writeImpl) {
99
- this._writeImpl.call(this, chunk, encoding, callback);
100
- } else {
101
- callback();
102
- }
103
- }
104
-
105
- _final(callback: ErrCallback): void {
106
- if (this._finalImpl) {
107
- this._finalImpl.call(this, callback);
108
- } else {
109
- callback();
110
- }
111
- }
112
-
113
- override destroy(error?: Error): this {
114
- if (this.destroyed) return this;
115
- this.writable = false;
116
- return super.destroy(error);
117
- }
118
-
119
- write(chunk: unknown, encoding?: string | ErrCallback, callback?: ErrCallback): boolean {
120
- let cb: ErrCallback;
121
- let enc: string;
122
- if (typeof encoding === 'function') {
123
- cb = encoding;
124
- enc = this._defaultEncoding;
125
- } else {
126
- enc = encoding ?? this._defaultEncoding;
127
- cb = callback ?? (() => {});
128
- }
129
-
130
- // Convert strings to Buffer when decodeStrings is true (default), but not in objectMode
131
- if (this._decodeStrings && !this.writableObjectMode && typeof chunk === 'string') {
132
- const BufCtor = (globalThis as { Buffer?: { from: (s: string, e: string) => unknown } }).Buffer;
133
- if (BufCtor) {
134
- chunk = BufCtor.from(chunk, enc);
135
- enc = 'buffer';
136
- }
137
- }
138
- // Set encoding to 'buffer' for Buffer/Uint8Array chunks
139
- if (typeof chunk !== 'string' && !this.writableObjectMode) {
140
- const BufCtor = (globalThis as { Buffer?: { isBuffer: (v: unknown) => boolean } }).Buffer;
141
- if ((BufCtor && BufCtor.isBuffer(chunk)) || chunk instanceof Uint8Array) {
142
- enc = 'buffer';
143
- }
144
- }
145
-
146
- if (this.writableEnded) {
147
- const err = new Error('write after end');
148
- nextTick(() => {
149
- cb(err);
150
- this.emit('error', err);
151
- });
152
- return false;
153
- }
154
-
155
- this.writableLength += this.writableObjectMode ? 1 : chunkLen(chunk);
156
-
157
- // If corked, buffer the write
158
- if (this.writableCorked > 0) {
159
- this._duplexCorkedBuffer.push({ chunk, encoding: enc, callback: cb });
160
- return this.writableLength < this.writableHighWaterMark;
161
- }
162
-
163
- // Compute backpressure BEFORE _write (sync transforms may decrement length immediately)
164
- const belowHWM = this.writableLength < this.writableHighWaterMark;
165
- if (!belowHWM) {
166
- this.writableNeedDrain = true;
167
- }
168
-
169
- this._duplexDoWrite(chunk, enc, cb);
170
-
171
- return belowHWM;
172
- }
173
-
174
- private _duplexDoWrite(chunk: unknown, encoding: string, cb: ErrCallback): void {
175
- if (this._duplexWriting) {
176
- this._duplexWriteQueue.push({ chunk, encoding, callback: cb });
177
- return;
178
- }
179
- this._duplexWriting = true;
180
- this._duplexStartWrite(chunk, encoding, cb);
181
- }
182
-
183
- // Starts a write assuming _duplexWriting is already true. After the write
184
- // completes, either start the next queued write (keeping _duplexWriting=true
185
- // to preserve FIFO order) or clear the flag and emit 'drain'. The 'drain'
186
- // listener on streamx may synchronously call conn.write() — emitting drain
187
- // BEFORE the queue is fully processed would let that new write bypass the
188
- // queue, causing out-of-order bytes on the wire (and, for bittorrent-protocol,
189
- // desync of piece header vs. piece payload).
190
- private _duplexStartWrite(chunk: unknown, encoding: string, cb: ErrCallback): void {
191
- this._pendingWrites++;
192
- this._write(chunk, encoding, (err) => {
193
- this._pendingWrites--;
194
- this.writableLength -= this.writableObjectMode ? 1 : chunkLen(chunk);
195
- if (err) {
196
- nextTick(() => {
197
- cb(err);
198
- this._duplexWriting = false;
199
- this.emit('error', err);
200
- if (this._duplexWriteQueue.length > 0) {
201
- const next = this._duplexWriteQueue.shift()!;
202
- this._duplexWriting = true;
203
- this._duplexStartWrite(next.chunk, next.encoding, next.callback);
204
- }
205
- });
206
- } else {
207
- nextTick(() => {
208
- cb();
209
- if (this._duplexWriteQueue.length > 0) {
210
- const next = this._duplexWriteQueue.shift()!;
211
- this._duplexStartWrite(next.chunk, next.encoding, next.callback);
212
- return;
213
- }
214
- this._duplexWriting = false;
215
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
216
- this.writableNeedDrain = false;
217
- this.emit('drain');
218
- }
219
- if (this._pendingWrites === 0 && this._pendingEndCb) {
220
- const endCb = this._pendingEndCb;
221
- this._pendingEndCb = null;
222
- endCb();
223
- }
224
- });
225
- }
226
- });
227
- }
228
-
229
- end(chunk?: unknown | (() => void), encoding?: string | (() => void), callback?: () => void): this {
230
- if (typeof chunk === 'function') {
231
- callback = chunk as () => void;
232
- chunk = undefined;
233
- }
234
- if (typeof encoding === 'function') {
235
- callback = encoding;
236
- encoding = undefined;
237
- }
238
-
239
- if (chunk !== undefined && chunk !== null) {
240
- this.write(chunk, encoding as string);
241
- }
242
-
243
- this.writableEnded = true;
244
-
245
- const doFinal = () => {
246
- this._final((err) => {
247
- this.writableFinished = true;
248
- // Allow subclasses (Transform) to run post-final hooks (e.g. flush)
249
- // before the 'finish' event fires.
250
- this._doPrefinishHooks(() => {
251
- nextTick(() => {
252
- if (err) this.emit('error', err);
253
- this.emit('finish');
254
- nextTick(() => this.emit('close'));
255
- if (callback) callback();
256
- });
257
- });
258
- });
259
- };
260
-
261
- // Wait for all pending writes to complete before calling _final.
262
- // Transform._write is synchronous (calls user cb in same tick), so _pendingWrites
263
- // can be 0 even while follow-up writes sit in _duplexWriteQueue. Check the queue
264
- // and the write-in-flight flag too, otherwise end() fires _final — which for
265
- // Transform pushes null — before the queued chunks reach _transform.
266
- if (this._pendingWrites > 0 || this._duplexWriting || this._duplexWriteQueue.length > 0) {
267
- this._pendingEndCb = doFinal;
268
- } else {
269
- doFinal();
270
- }
271
-
272
- return this;
273
- }
274
-
275
- /** Hook for subclasses to run logic between _final and 'finish'. Default: no-op. */
276
- protected _doPrefinishHooks(cb: () => void): void {
277
- cb();
278
- }
279
-
280
- cork(): void { this.writableCorked++; }
281
-
282
- uncork(): void {
283
- if (this.writableCorked > 0) {
284
- this.writableCorked--;
285
- if (this.writableCorked === 0 && this._duplexCorkedBuffer.length > 0) {
286
- const buffered = this._duplexCorkedBuffer.splice(0);
287
- for (const { chunk, encoding, callback } of buffered) {
288
- this._write(chunk, encoding, (err) => {
289
- this.writableLength -= this.writableObjectMode ? 1 : chunkLen(chunk);
290
- if (err) {
291
- callback(err);
292
- this.emit('error', err);
293
- } else {
294
- callback();
295
- }
296
- });
297
- }
298
- if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
299
- this.writableNeedDrain = false;
300
- nextTick(() => this.emit('drain'));
301
- }
302
- }
303
- }
304
- }
305
-
306
- setDefaultEncoding(encoding: string): this {
307
- this._defaultEncoding = encoding;
308
- return this;
309
- }
310
- }
311
-
312
- /** Length of a stream chunk (string/Buffer/Uint8Array → .length, otherwise 1). */
313
- function chunkLen(chunk: unknown): number {
314
- if (chunk == null) return 1;
315
- const v = (chunk as { length?: unknown }).length;
316
- return typeof v === 'number' ? v : 1;
317
- }