@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,398 @@
1
+ // Writable stream — push-based sink with serialized writes and FIFO drain queue.
2
+ //
3
+ // Reference: refs/node/lib/internal/streams/writable.js
4
+ // Reimplemented for GJS using @gjsify/events + microtask scheduling.
5
+
6
+ import { nextTick } from '@gjsify/utils';
7
+ import type { WritableOptions } from 'node:stream';
8
+
9
+ import { Stream_ } from './stream-base.js';
10
+ import { getDefaultHighWaterMark } from './internal/state.js';
11
+ import type { BufferedWrite, ErrCallback } from './internal/types.js';
12
+
13
+ interface WritableInternalState {
14
+ ended: boolean;
15
+ finished: boolean;
16
+ constructed: boolean;
17
+ writing: boolean;
18
+ }
19
+
20
+ /** Object form for `_writev` invocations. */
21
+ interface WriteVChunk { chunk: unknown; encoding: string; }
22
+
23
+ export class Writable_ extends Stream_ {
24
+ // Allow `duplex instanceof Writable` to return true even though Duplex inherits
25
+ // from Readable_ only. Mirrors Node.js: standard prototype-chain check first,
26
+ // then duck-type fallback — but only when checking against Writable itself, not
27
+ // against a user subclass. Because `Writable` is exported as a makeCallable Proxy,
28
+ // `this === Writable_` fails for `obj instanceof Writable`; however the Proxy's
29
+ // default `get` trap forwards `.prototype` straight to the target, so the
30
+ // prototype reference check below correctly recognises both.
31
+ static [Symbol.hasInstance](obj: unknown): boolean {
32
+ const ctor = this as unknown as { prototype?: object };
33
+ if (typeof ctor.prototype !== 'undefined' &&
34
+ Object.prototype.isPrototypeOf.call(ctor.prototype, obj as object)) return true;
35
+ if (ctor.prototype !== Writable_.prototype) return false;
36
+ if (obj === null || obj === undefined) return false;
37
+ return typeof (obj as { writableHighWaterMark?: unknown }).writableHighWaterMark === 'number';
38
+ }
39
+
40
+ writable = true;
41
+ writableHighWaterMark: number;
42
+ writableLength = 0;
43
+ writableObjectMode: boolean;
44
+ writableEnded = false;
45
+ writableFinished = false;
46
+ writableCorked = 0;
47
+ writableNeedDrain = false;
48
+ destroyed = false;
49
+
50
+ /** @internal Stored error from `destroy(err)` so `finished()` can retrieve it. */
51
+ _err?: Error;
52
+
53
+ private _writableState: WritableInternalState = { ended: false, finished: false, constructed: true, writing: false };
54
+ private _corkedBuffer: BufferedWrite[] = [];
55
+ private _writeBuffer: BufferedWrite[] = [];
56
+ private _pendingConstruct: BufferedWrite[] = [];
57
+ private _ending = false;
58
+ private _endCallback?: () => void;
59
+ private _pendingEnd: { chunk?: unknown; encoding?: string; callback?: () => void } | null = null;
60
+ private _writeImpl: ((this: Writable_, chunk: unknown, encoding: string, cb: ErrCallback) => void) | undefined;
61
+ private _writev: ((this: Writable_, chunks: WriteVChunk[], cb: ErrCallback) => void) | undefined;
62
+ private _finalImpl: ((this: Writable_, cb: ErrCallback) => void) | undefined;
63
+ private _destroyImpl: ((this: Writable_, error: Error | null, cb: ErrCallback) => void) | undefined;
64
+ private _constructImpl: ((this: Writable_, cb: ErrCallback) => void) | undefined;
65
+ private _decodeStrings: boolean;
66
+ private _defaultEncoding = 'utf8';
67
+
68
+ constructor(opts?: WritableOptions) {
69
+ super(opts);
70
+ this.writableHighWaterMark = opts?.highWaterMark ?? getDefaultHighWaterMark(opts?.objectMode ?? false);
71
+ this.writableObjectMode = opts?.objectMode ?? false;
72
+ this._decodeStrings = opts?.decodeStrings !== false;
73
+ if (opts?.write) this._writeImpl = opts.write as unknown as (this: Writable_, c: unknown, e: string, cb: ErrCallback) => void;
74
+ if (opts?.writev) this._writev = opts.writev as unknown as (this: Writable_, c: WriteVChunk[], cb: ErrCallback) => void;
75
+ if (opts?.final) this._finalImpl = opts.final as unknown as (this: Writable_, cb: ErrCallback) => void;
76
+ if (opts?.destroy) this._destroyImpl = opts.destroy as unknown as (this: Writable_, e: Error | null, cb: ErrCallback) => void;
77
+ if (opts?.construct) this._constructImpl = opts.construct as unknown as (this: Writable_, cb: ErrCallback) => void;
78
+
79
+ // Call _construct if provided via options or overridden by subclass
80
+ const hasConstruct = this._constructImpl || this._construct !== Writable_.prototype._construct;
81
+ if (hasConstruct) {
82
+ this._writableState.constructed = false;
83
+ nextTick(() => {
84
+ this._construct((err) => {
85
+ this._writableState.constructed = true;
86
+ if (err) {
87
+ this.destroy(err);
88
+ } else {
89
+ this._maybeFlush();
90
+ }
91
+ });
92
+ });
93
+ }
94
+ }
95
+
96
+ _construct(callback: ErrCallback): void {
97
+ if (this._constructImpl) {
98
+ this._constructImpl.call(this, callback);
99
+ } else {
100
+ callback();
101
+ }
102
+ }
103
+
104
+ _write(chunk: unknown, encoding: string, callback: ErrCallback): void {
105
+ if (this._writeImpl) {
106
+ this._writeImpl.call(this, chunk, encoding, callback);
107
+ } else {
108
+ callback();
109
+ }
110
+ }
111
+
112
+ _final(callback: ErrCallback): void {
113
+ if (this._finalImpl) {
114
+ this._finalImpl.call(this, callback);
115
+ } else {
116
+ callback();
117
+ }
118
+ }
119
+
120
+ private _maybeFlush(): void {
121
+ // Flush writes that were buffered while waiting for _construct
122
+ const pending = this._pendingConstruct.splice(0);
123
+ if (pending.length > 0) {
124
+ // First write goes directly, rest get serialized via _writeBuffer
125
+ const [first, ...rest] = pending;
126
+ this._writeBuffer.push(...rest);
127
+ this._doWrite(first.chunk, first.encoding, first.callback);
128
+ }
129
+ if (this._pendingEnd) {
130
+ const { chunk, encoding, callback } = this._pendingEnd;
131
+ this._pendingEnd = null;
132
+ this._doEnd(chunk, encoding, callback);
133
+ }
134
+ }
135
+
136
+ private _doWrite(chunk: unknown, encoding: string, callback: ErrCallback): void {
137
+ this._writableState.writing = true;
138
+ // Track whether the user's _write called its callback synchronously. When it
139
+ // does (e.g. Writable with a synchronous `write` option), Node drains the
140
+ // buffer on the same tick — tests that issue N sync writes expect N sync
141
+ // `_write` dispatches. Deferring via nextTick here broke that expectation.
142
+ let sync = true;
143
+ this._write(chunk, encoding, (err) => {
144
+ this.writableLength -= this.writableObjectMode ? 1 : chunkLen(chunk);
145
+ if (sync) {
146
+ // Synchronous completion: defer the user callback + 'drain' emit to
147
+ // nextTick (Node semantics — user code must not see its own write
148
+ // return before the callback on the same tick), but drain the buffer
149
+ // synchronously so follow-up writes fire on the same tick.
150
+ nextTick(() => {
151
+ if (err) {
152
+ callback(err);
153
+ this.emit('error', err);
154
+ return;
155
+ }
156
+ callback();
157
+ if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
158
+ this.writableNeedDrain = false;
159
+ this.emit('drain');
160
+ }
161
+ });
162
+ if (!err) this._drainWriteBuffer();
163
+ return;
164
+ }
165
+ // Asynchronous completion — full defer.
166
+ if (err) {
167
+ nextTick(() => {
168
+ callback(err);
169
+ this.emit('error', err);
170
+ this._drainWriteBuffer();
171
+ });
172
+ } else {
173
+ nextTick(() => {
174
+ callback();
175
+ if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
176
+ this.writableNeedDrain = false;
177
+ this.emit('drain');
178
+ }
179
+ this._drainWriteBuffer();
180
+ });
181
+ }
182
+ });
183
+ sync = false;
184
+ }
185
+
186
+ private _drainWriteBuffer(): void {
187
+ if (this._writeBuffer.length > 0) {
188
+ const next = this._writeBuffer.shift()!;
189
+ this._doWrite(next.chunk, next.encoding, next.callback);
190
+ } else {
191
+ // Only release the write lock when the buffer is truly empty.
192
+ this._writableState.writing = false;
193
+ this._maybeFinish();
194
+ }
195
+ }
196
+
197
+ private _maybeFinish(): void {
198
+ if (!this._ending || this._writableState.finished || this._writableState.writing || this._writeBuffer.length > 0) return;
199
+ this._ending = false;
200
+
201
+ this._final((err) => {
202
+ this.writableFinished = true;
203
+ this._writableState.finished = true;
204
+ nextTick(() => {
205
+ if (err) {
206
+ this.emit('error', err);
207
+ }
208
+ this.emit('finish');
209
+ nextTick(() => this.emit('close'));
210
+ if (this._endCallback) this._endCallback();
211
+ });
212
+ });
213
+ }
214
+
215
+ write(chunk: unknown, encoding?: string | ErrCallback, callback?: ErrCallback): boolean {
216
+ let cb: ErrCallback;
217
+ let enc: string;
218
+ if (typeof encoding === 'function') {
219
+ cb = encoding;
220
+ enc = this._defaultEncoding;
221
+ } else {
222
+ enc = encoding ?? this._defaultEncoding;
223
+ cb = callback ?? (() => {});
224
+ }
225
+
226
+ // Convert strings to Buffer when decodeStrings is true (default), but not in objectMode
227
+ if (this._decodeStrings && !this.writableObjectMode && typeof chunk === 'string') {
228
+ const BufCtor = (globalThis as { Buffer?: { from: (s: string, e: string) => unknown } }).Buffer;
229
+ if (BufCtor) {
230
+ chunk = BufCtor.from(chunk, enc);
231
+ enc = 'buffer';
232
+ }
233
+ }
234
+ // Set encoding to 'buffer' for Buffer/Uint8Array chunks
235
+ if (typeof chunk !== 'string' && !this.writableObjectMode) {
236
+ const BufCtor = (globalThis as { Buffer?: { isBuffer: (v: unknown) => boolean } }).Buffer;
237
+ if ((BufCtor && BufCtor.isBuffer(chunk)) || chunk instanceof Uint8Array) {
238
+ enc = 'buffer';
239
+ }
240
+ }
241
+
242
+ if (this.writableEnded) {
243
+ const err = new Error('write after end');
244
+ nextTick(() => {
245
+ cb(err);
246
+ this.emit('error', err);
247
+ });
248
+ return false;
249
+ }
250
+
251
+ this.writableLength += this.writableObjectMode ? 1 : chunkLen(chunk);
252
+
253
+ // If corked, buffer the write
254
+ if (this.writableCorked > 0) {
255
+ this._corkedBuffer.push({ chunk, encoding: enc, callback: cb });
256
+ return this.writableLength < this.writableHighWaterMark;
257
+ }
258
+
259
+ // If not yet constructed, buffer writes until construction finishes
260
+ if (!this._writableState.constructed) {
261
+ this._pendingConstruct.push({ chunk, encoding: enc, callback: cb });
262
+ return this.writableLength < this.writableHighWaterMark;
263
+ }
264
+
265
+ // Compute backpressure BEFORE _doWrite (sync transforms may decrement length immediately)
266
+ const belowHWM = this.writableLength < this.writableHighWaterMark;
267
+ if (!belowHWM) {
268
+ this.writableNeedDrain = true;
269
+ }
270
+
271
+ // Serialize writes: only one _write at a time, buffer the rest
272
+ if (this._writableState.writing) {
273
+ this._writeBuffer.push({ chunk, encoding: enc, callback: cb });
274
+ } else {
275
+ this._doWrite(chunk, enc, cb);
276
+ }
277
+
278
+ return belowHWM;
279
+ }
280
+
281
+ private _doEnd(chunk?: unknown, encoding?: string, callback?: () => void): void {
282
+ if (chunk !== undefined && chunk !== null) {
283
+ this.write(chunk, encoding as string);
284
+ }
285
+
286
+ this.writableEnded = true;
287
+ this._writableState.ended = true;
288
+ this._ending = true;
289
+ this._endCallback = callback;
290
+
291
+ // _maybeFinish will call _final once all pending writes have drained
292
+ this._maybeFinish();
293
+ }
294
+
295
+ end(chunk?: unknown | (() => void), encoding?: string | (() => void), callback?: () => void): this {
296
+ if (typeof chunk === 'function') {
297
+ callback = chunk as () => void;
298
+ chunk = undefined;
299
+ }
300
+ if (typeof encoding === 'function') {
301
+ callback = encoding;
302
+ encoding = undefined;
303
+ }
304
+
305
+ // Ignore duplicate end() calls (e.g. from auto-end after half-close)
306
+ if (this.writableEnded) {
307
+ if (callback) nextTick(callback);
308
+ return this;
309
+ }
310
+
311
+ // If not yet constructed, defer end until construction finishes
312
+ if (!this._writableState.constructed) {
313
+ this._pendingEnd = { chunk, encoding: encoding as string, callback };
314
+ return this;
315
+ }
316
+
317
+ this._doEnd(chunk, encoding as string, callback);
318
+
319
+ return this;
320
+ }
321
+
322
+ cork(): void {
323
+ this.writableCorked++;
324
+ }
325
+
326
+ uncork(): void {
327
+ if (this.writableCorked > 0) {
328
+ this.writableCorked--;
329
+ if (this.writableCorked === 0 && this._corkedBuffer.length > 0) {
330
+ this._flushCorkedBuffer();
331
+ }
332
+ }
333
+ }
334
+
335
+ private _flushCorkedBuffer(): void {
336
+ // If _writev is available, flush as a batch
337
+ if (this._writev && this._corkedBuffer.length > 1) {
338
+ const buffered = this._corkedBuffer.splice(0);
339
+ const chunks: WriteVChunk[] = buffered.map(b => ({ chunk: b.chunk, encoding: b.encoding }));
340
+ this._writev.call(this, chunks, (err) => {
341
+ for (const b of buffered) {
342
+ this.writableLength -= this.writableObjectMode ? 1 : chunkLen(b.chunk);
343
+ }
344
+ if (err) {
345
+ for (const b of buffered) b.callback(err);
346
+ this.emit('error', err);
347
+ } else {
348
+ for (const b of buffered) b.callback();
349
+ if (this.writableNeedDrain && this.writableLength <= this.writableHighWaterMark) {
350
+ this.writableNeedDrain = false;
351
+ this.emit('drain');
352
+ }
353
+ }
354
+ });
355
+ } else {
356
+ // Flush one by one via serialized write path
357
+ const buffered = this._corkedBuffer.splice(0);
358
+ if (buffered.length > 0) {
359
+ const [first, ...rest] = buffered;
360
+ this._writeBuffer.push(...rest);
361
+ this._doWrite(first.chunk, first.encoding, first.callback);
362
+ }
363
+ }
364
+ }
365
+
366
+ setDefaultEncoding(encoding: string): this {
367
+ this._defaultEncoding = encoding;
368
+ return this;
369
+ }
370
+
371
+ destroy(error?: Error): this {
372
+ if (this.destroyed) return this;
373
+ this.destroyed = true;
374
+ this.writable = false;
375
+ // Store the error so finished() can retrieve it if called after destroy() but before 'error' fires
376
+ if (error) this._err = error;
377
+
378
+ const cb: ErrCallback = (err) => {
379
+ if (err) nextTick(() => this.emit('error', err));
380
+ nextTick(() => this.emit('close'));
381
+ };
382
+
383
+ if (this._destroyImpl) {
384
+ this._destroyImpl.call(this, error ?? null, cb);
385
+ } else {
386
+ cb(error);
387
+ }
388
+
389
+ return this;
390
+ }
391
+ }
392
+
393
+ /** Length of a stream chunk (string/Buffer/Uint8Array → .length, otherwise 1). */
394
+ function chunkLen(chunk: unknown): number {
395
+ if (chunk == null) return 1;
396
+ const v = (chunk as { length?: unknown }).length;
397
+ return typeof v === 'number' ? v : 1;
398
+ }