@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.
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/consumers/index.js +1 -1
- package/lib/esm/duplex.js +1 -0
- package/lib/esm/index.js +1 -1
- package/lib/esm/internal/state.js +1 -0
- package/lib/esm/internal/types.js +0 -0
- package/lib/esm/passthrough.js +1 -0
- package/lib/esm/promises/index.js +1 -1
- package/lib/esm/readable.js +1 -0
- package/lib/esm/stream-base.js +1 -0
- package/lib/esm/transform.js +1 -0
- package/lib/esm/utils/finished.js +1 -0
- package/lib/esm/utils/pipe.js +1 -0
- package/lib/esm/utils/pipeline.js +1 -0
- package/lib/esm/writable.js +1 -0
- package/lib/types/duplex.d.ts +42 -0
- package/lib/types/index.d.ts +15 -190
- package/lib/types/internal/state.d.ts +4 -0
- package/lib/types/internal/types.d.ts +21 -0
- package/lib/types/passthrough.d.ts +5 -0
- package/lib/types/readable.d.ts +73 -0
- package/lib/types/stream-base.d.ts +26 -0
- package/lib/types/transform.d.ts +11 -0
- package/lib/types/utils/finished.d.ts +14 -0
- package/lib/types/utils/pipe.d.ts +10 -0
- package/lib/types/utils/pipeline.d.ts +7 -0
- package/lib/types/writable.d.ts +47 -0
- package/package.json +6 -6
- package/src/callable.spec.ts +39 -0
- package/src/duplex.ts +317 -0
- package/src/index.ts +49 -1633
- package/src/internal/state.ts +34 -0
- package/src/internal/types.ts +31 -0
- package/src/passthrough.ts +19 -0
- package/src/readable.ts +580 -0
- package/src/stream-base.ts +37 -0
- package/src/transform.ts +108 -0
- package/src/utils/finished.ts +156 -0
- package/src/utils/pipe.ts +113 -0
- package/src/utils/pipeline.ts +59 -0
- package/src/writable.ts +398 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/writable.ts
ADDED
|
@@ -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
|
+
}
|