experimental-fast-webstreams 0.0.1
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/README.md +484 -0
- package/package.json +45 -0
- package/src/byob-reader.js +25 -0
- package/src/controller.js +286 -0
- package/src/index.js +7 -0
- package/src/materialize.js +34 -0
- package/src/natives.js +12 -0
- package/src/patch.js +45 -0
- package/src/pipe-to.js +263 -0
- package/src/readable.js +771 -0
- package/src/reader.js +348 -0
- package/src/transform.js +431 -0
- package/src/utils.js +37 -0
- package/src/writable.js +939 -0
- package/src/writer.js +297 -0
- package/types/index.d.ts +51 -0
package/src/reader.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FastReadableStreamDefaultReader
|
|
3
|
+
* Bridges reader.read() to Node Readable consumption with sync fast path (Tier 1).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { unwrapError } from './controller.js';
|
|
7
|
+
import { kLock, kNodeReadable, noop } from './utils.js';
|
|
8
|
+
|
|
9
|
+
// Cached done result — avoids allocating { value: undefined, done: true } + Promise per stream end
|
|
10
|
+
const DONE_RESULT = { value: undefined, done: true };
|
|
11
|
+
const DONE_PROMISE = Promise.resolve(DONE_RESULT);
|
|
12
|
+
|
|
13
|
+
function _resolveReadResult(value, done) {
|
|
14
|
+
if (done) return DONE_PROMISE;
|
|
15
|
+
return Promise.resolve({ value, done: false });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class FastReadableStreamDefaultReader {
|
|
19
|
+
#stream;
|
|
20
|
+
#nodeReadable;
|
|
21
|
+
#closedPromise;
|
|
22
|
+
#closedResolve;
|
|
23
|
+
#closedReject;
|
|
24
|
+
#closedSettled = false;
|
|
25
|
+
#released = false;
|
|
26
|
+
#pendingReads = []; // Track pending reads for releaseLock
|
|
27
|
+
|
|
28
|
+
constructor(stream) {
|
|
29
|
+
if (stream[kLock]) {
|
|
30
|
+
throw new TypeError('ReadableStream is already locked');
|
|
31
|
+
}
|
|
32
|
+
this.#stream = stream;
|
|
33
|
+
this.#nodeReadable = stream[kNodeReadable];
|
|
34
|
+
stream[kLock] = this;
|
|
35
|
+
|
|
36
|
+
this.#closedPromise = new Promise((resolve, reject) => {
|
|
37
|
+
this.#closedResolve = resolve;
|
|
38
|
+
this.#closedReject = reject;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Use stream-level state for initial closed/errored checks (preserves error identity)
|
|
42
|
+
if (stream._errored) {
|
|
43
|
+
this.#settleClose(false, stream._storedError);
|
|
44
|
+
} else if (stream._closed) {
|
|
45
|
+
this.#settleClose(true);
|
|
46
|
+
} else {
|
|
47
|
+
const nodeReadable = this.#nodeReadable;
|
|
48
|
+
|
|
49
|
+
// Check Node-level state as fallback
|
|
50
|
+
if (nodeReadable.destroyed) {
|
|
51
|
+
if (nodeReadable.errored) {
|
|
52
|
+
this.#settleClose(false, unwrapError(nodeReadable.errored));
|
|
53
|
+
} else {
|
|
54
|
+
this.#settleClose(true);
|
|
55
|
+
}
|
|
56
|
+
} else if (nodeReadable.readableEnded) {
|
|
57
|
+
this.#settleClose(true);
|
|
58
|
+
} else {
|
|
59
|
+
// Listen for close events
|
|
60
|
+
const onEnd = () => {
|
|
61
|
+
cleanup();
|
|
62
|
+
stream._closed = true;
|
|
63
|
+
this.#settleClose(true);
|
|
64
|
+
};
|
|
65
|
+
const onError = (err) => {
|
|
66
|
+
cleanup();
|
|
67
|
+
// Use stream-level error if available (preserves identity)
|
|
68
|
+
if (stream._errored) {
|
|
69
|
+
this.#settleClose(false, stream._storedError);
|
|
70
|
+
} else {
|
|
71
|
+
this.#settleClose(false, unwrapError(err));
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const onClose = () => {
|
|
75
|
+
cleanup();
|
|
76
|
+
if (stream._errored) {
|
|
77
|
+
this.#settleClose(false, stream._storedError);
|
|
78
|
+
} else if (nodeReadable.errored) {
|
|
79
|
+
this.#settleClose(false, unwrapError(nodeReadable.errored));
|
|
80
|
+
} else {
|
|
81
|
+
stream._closed = true;
|
|
82
|
+
this.#settleClose(true);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const cleanup = () => {
|
|
86
|
+
nodeReadable.removeListener('end', onEnd);
|
|
87
|
+
nodeReadable.removeListener('error', onError);
|
|
88
|
+
nodeReadable.removeListener('close', onClose);
|
|
89
|
+
};
|
|
90
|
+
nodeReadable.on('end', onEnd);
|
|
91
|
+
nodeReadable.on('error', onError);
|
|
92
|
+
nodeReadable.on('close', onClose);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#settleClose(success, err) {
|
|
98
|
+
if (this.#closedSettled || this.#released) return;
|
|
99
|
+
this.#closedSettled = true;
|
|
100
|
+
if (success) {
|
|
101
|
+
this.#closedResolve(undefined);
|
|
102
|
+
} else {
|
|
103
|
+
this.#closedReject(err);
|
|
104
|
+
this.#closedPromise.catch(noop);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
read() {
|
|
109
|
+
if (this.#released) {
|
|
110
|
+
return Promise.reject(new TypeError('Reader has been released'));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const stream = this.#stream;
|
|
114
|
+
const nodeReadable = this.#nodeReadable;
|
|
115
|
+
|
|
116
|
+
// Check stream-level error first (preserves error identity for falsy errors)
|
|
117
|
+
if (stream._errored) {
|
|
118
|
+
return Promise.reject(stream._storedError);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if the stream has errored (Node-level fallback)
|
|
122
|
+
if (nodeReadable.errored) {
|
|
123
|
+
return Promise.reject(unwrapError(nodeReadable.errored));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if destroyed (closed)
|
|
127
|
+
if (nodeReadable.destroyed) {
|
|
128
|
+
return _resolveReadResult(undefined, true);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Tier 1: sync fast path — data already in buffer
|
|
132
|
+
const chunk = nodeReadable.read();
|
|
133
|
+
if (chunk !== null) {
|
|
134
|
+
// Notify transform controller that data was consumed (may clear backpressure)
|
|
135
|
+
if (stream._onPull) stream._onPull();
|
|
136
|
+
return _resolveReadResult(chunk, false);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check if ended
|
|
140
|
+
if (nodeReadable.readableEnded) {
|
|
141
|
+
return _resolveReadResult(undefined, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Data not available yet — wait for 'readable', 'end', or 'close'
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
// Track this pending read for releaseLock
|
|
147
|
+
const entry = { reject: null, cleanup: null };
|
|
148
|
+
this.#pendingReads.push(entry);
|
|
149
|
+
|
|
150
|
+
// Notify transform controller that there's a pending read (clears backpressure)
|
|
151
|
+
// Must be called AFTER pushing to pendingReads so _pendingReadCount() is accurate
|
|
152
|
+
if (stream._onPull) stream._onPull();
|
|
153
|
+
|
|
154
|
+
const onReadable = () => {
|
|
155
|
+
cleanup();
|
|
156
|
+
// Detach from pendingReads BEFORE read() to prevent _errorReadRequests
|
|
157
|
+
// from rejecting this entry if read() triggers a pull that errors.
|
|
158
|
+
removePending();
|
|
159
|
+
const data = nodeReadable.read();
|
|
160
|
+
if (data !== null) {
|
|
161
|
+
if (stream._onPull) stream._onPull();
|
|
162
|
+
resolve({ value: data, done: false });
|
|
163
|
+
} else if (nodeReadable.readableEnded || nodeReadable.destroyed) {
|
|
164
|
+
resolve(DONE_RESULT);
|
|
165
|
+
} else {
|
|
166
|
+
// No data yet, re-register
|
|
167
|
+
this.#pendingReads.push(entry);
|
|
168
|
+
nodeReadable.once('readable', onReadable);
|
|
169
|
+
nodeReadable.once('end', onEnd);
|
|
170
|
+
nodeReadable.once('error', onError);
|
|
171
|
+
nodeReadable.once('close', onClose);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const onEnd = () => {
|
|
175
|
+
cleanup();
|
|
176
|
+
removePending();
|
|
177
|
+
resolve(DONE_RESULT);
|
|
178
|
+
};
|
|
179
|
+
const onError = (err) => {
|
|
180
|
+
cleanup();
|
|
181
|
+
removePending();
|
|
182
|
+
if (stream._errored) {
|
|
183
|
+
reject(stream._storedError);
|
|
184
|
+
} else {
|
|
185
|
+
reject(unwrapError(err));
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const onClose = () => {
|
|
189
|
+
cleanup();
|
|
190
|
+
removePending();
|
|
191
|
+
if (stream._errored) {
|
|
192
|
+
reject(stream._storedError);
|
|
193
|
+
} else if (nodeReadable.errored) {
|
|
194
|
+
reject(unwrapError(nodeReadable.errored));
|
|
195
|
+
} else {
|
|
196
|
+
resolve(DONE_RESULT);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const cleanup = () => {
|
|
200
|
+
nodeReadable.removeListener('readable', onReadable);
|
|
201
|
+
nodeReadable.removeListener('end', onEnd);
|
|
202
|
+
nodeReadable.removeListener('error', onError);
|
|
203
|
+
nodeReadable.removeListener('close', onClose);
|
|
204
|
+
};
|
|
205
|
+
const removePending = () => {
|
|
206
|
+
const idx = this.#pendingReads.indexOf(entry);
|
|
207
|
+
if (idx !== -1) this.#pendingReads.splice(idx, 1);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
entry.reject = reject;
|
|
211
|
+
entry.cleanup = cleanup;
|
|
212
|
+
|
|
213
|
+
nodeReadable.once('readable', onReadable);
|
|
214
|
+
nodeReadable.once('end', onEnd);
|
|
215
|
+
nodeReadable.once('error', onError);
|
|
216
|
+
nodeReadable.once('close', onClose);
|
|
217
|
+
|
|
218
|
+
// Trigger _read() to request data (pull).
|
|
219
|
+
if (nodeReadable._readableState.reading) {
|
|
220
|
+
nodeReadable._readableState.reading = false;
|
|
221
|
+
}
|
|
222
|
+
nodeReadable.read(0);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Internal sync read — returns { value, done } directly, or null if data isn't
|
|
228
|
+
* available (caller should fall back to async read()). Used by specPipeTo to
|
|
229
|
+
* avoid Promise allocation when data is buffered.
|
|
230
|
+
*/
|
|
231
|
+
_readSync() {
|
|
232
|
+
if (this.#released) return null;
|
|
233
|
+
const stream = this.#stream;
|
|
234
|
+
if (stream._errored) return null; // Let async read() handle error rejection
|
|
235
|
+
const nodeReadable = this.#nodeReadable;
|
|
236
|
+
if (nodeReadable.errored || nodeReadable.destroyed) return null;
|
|
237
|
+
const chunk = nodeReadable.read();
|
|
238
|
+
if (chunk !== null) {
|
|
239
|
+
if (stream._onPull) stream._onPull();
|
|
240
|
+
return { value: chunk, done: false };
|
|
241
|
+
}
|
|
242
|
+
if (nodeReadable.readableEnded) return DONE_RESULT;
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
cancel(reason) {
|
|
247
|
+
if (this.#released) {
|
|
248
|
+
return Promise.reject(new TypeError('Reader has been released'));
|
|
249
|
+
}
|
|
250
|
+
// Call the stream's internal cancel (bypasses lock check, calls underlyingSource.cancel)
|
|
251
|
+
return this.#stream._cancelInternal(reason);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Called by controller.error() to synchronously settle the closedPromise.
|
|
256
|
+
* Must be called BEFORE _errorReadRequests so closed rejects before reads.
|
|
257
|
+
*/
|
|
258
|
+
_settleClosedFromError(error) {
|
|
259
|
+
if (!this.#closedSettled && !this.#released) {
|
|
260
|
+
this.#closedSettled = true;
|
|
261
|
+
this.#closedReject(error);
|
|
262
|
+
this.#closedPromise.catch(noop);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Called by controller.error() to synchronously reject all pending read requests.
|
|
268
|
+
* Per spec: ReadableStreamError rejects all read requests before releaseLock.
|
|
269
|
+
*/
|
|
270
|
+
_errorReadRequests(error) {
|
|
271
|
+
for (const entry of this.#pendingReads) {
|
|
272
|
+
if (entry.cleanup) entry.cleanup();
|
|
273
|
+
if (entry.reject) entry.reject(error);
|
|
274
|
+
}
|
|
275
|
+
this.#pendingReads = [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns the number of pending read requests. Used by transform controller
|
|
280
|
+
* to compute accurate desiredSize (pending reads consume enqueued chunks).
|
|
281
|
+
*/
|
|
282
|
+
_pendingReadCount() {
|
|
283
|
+
return this.#pendingReads.length;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Called by stream._cancelInternal() to resolve closedPromise synchronously.
|
|
288
|
+
* Per spec: cancel sets stream state to "closed" and resolves reader.closedPromise
|
|
289
|
+
* before calling the cancel algorithm.
|
|
290
|
+
*/
|
|
291
|
+
_resolveClosedFromCancel() {
|
|
292
|
+
if (!this.#closedSettled && !this.#released) {
|
|
293
|
+
this.#closedResolve(undefined);
|
|
294
|
+
this.#closedSettled = true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
releaseLock() {
|
|
299
|
+
if (!this.#stream) return;
|
|
300
|
+
if (!this.#released) {
|
|
301
|
+
this.#released = true;
|
|
302
|
+
const stream = this.#stream;
|
|
303
|
+
|
|
304
|
+
// Reject all pending read requests
|
|
305
|
+
const releasedError = new TypeError('Reader was released');
|
|
306
|
+
for (const entry of this.#pendingReads) {
|
|
307
|
+
if (entry.cleanup) entry.cleanup();
|
|
308
|
+
if (entry.reject) entry.reject(releasedError);
|
|
309
|
+
}
|
|
310
|
+
this.#pendingReads = [];
|
|
311
|
+
|
|
312
|
+
if (!this.#closedSettled) {
|
|
313
|
+
// Per spec: if state is "readable", reject existing promise (preserve identity).
|
|
314
|
+
// If state is "closed" or "errored", the promise should already be settled
|
|
315
|
+
// from events. But if it hasn't settled yet, settle it first.
|
|
316
|
+
if (stream._closed) {
|
|
317
|
+
// Stream closed (e.g., via cancel) — resolve, then replace
|
|
318
|
+
this.#closedResolve(undefined);
|
|
319
|
+
this.#closedSettled = true;
|
|
320
|
+
this.#closedPromise = Promise.reject(releasedError);
|
|
321
|
+
this.#closedPromise.catch(noop);
|
|
322
|
+
} else if (stream._errored) {
|
|
323
|
+
// Stream errored — reject with stored error, then replace
|
|
324
|
+
this.#closedReject(stream._storedError);
|
|
325
|
+
this.#closedPromise.catch(noop);
|
|
326
|
+
this.#closedSettled = true;
|
|
327
|
+
this.#closedPromise = Promise.reject(releasedError);
|
|
328
|
+
this.#closedPromise.catch(noop);
|
|
329
|
+
} else {
|
|
330
|
+
// Stream still readable — reject existing promise, preserve identity
|
|
331
|
+
this.#closedReject(releasedError);
|
|
332
|
+
this.#closedPromise.catch(noop);
|
|
333
|
+
this.#closedSettled = true;
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// Already settled — per spec: replace with new rejected promise
|
|
337
|
+
this.#closedPromise = Promise.reject(releasedError);
|
|
338
|
+
this.#closedPromise.catch(noop);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
stream[kLock] = null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
get closed() {
|
|
346
|
+
return this.#closedPromise;
|
|
347
|
+
}
|
|
348
|
+
}
|