@peerbit/stream 4.4.0 → 4.4.1-4a6b62b
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/dist/src/index.d.ts +10 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +79 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/pushable-lanes.d.ts +48 -27
- package/dist/src/pushable-lanes.d.ts.map +1 -1
- package/dist/src/pushable-lanes.js +180 -66
- package/dist/src/pushable-lanes.js.map +1 -1
- package/package.json +94 -94
- package/src/index.ts +108 -11
- package/src/pushable-lanes.ts +281 -127
package/src/pushable-lanes.ts
CHANGED
|
@@ -1,81 +1,133 @@
|
|
|
1
|
+
// A multi-lane async pushable with starvation-free scheduling.
|
|
2
|
+
// Inspired by it-pushable (MIT) and extended for multi-lane fairness.
|
|
3
|
+
//
|
|
4
|
+
// Features:
|
|
5
|
+
// - Async iterator you can .push() into
|
|
6
|
+
// - N lanes (priorities) with starvation-free scheduling (Weighted Round-Robin)
|
|
7
|
+
// - Optional strict priority mode for legacy behavior
|
|
8
|
+
// - Optional high-water mark (bytes) with overflow policy
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// const p = pushableLanes<Uint8Array>({ lanes: 2 }); // default fairness = 'wrr'
|
|
12
|
+
// p.push(new Uint8Array([1]), 1); // slower lane
|
|
13
|
+
// p.push(new Uint8Array([0]), 0); // faster lane
|
|
14
|
+
// for await (const chunk of p) { ... }
|
|
15
|
+
//
|
|
16
|
+
// // Backpressure example (throw if > 8MB buffered):
|
|
17
|
+
// const q = pushableLanes<Uint8Array>({ lanes: 3, maxBufferedBytes: 8 * 1024 * 1024, overflow: 'throw' });
|
|
18
|
+
//
|
|
19
|
+
// Notes:
|
|
20
|
+
// - T must have a .byteLength number property (e.g. Uint8Array).
|
|
21
|
+
// - Lane indices are 0..(lanes-1). Defaults to lane 0.
|
|
22
|
+
import { AbortError } from "@peerbit/time";
|
|
1
23
|
import GenericFIFO from "fast-fifo";
|
|
2
24
|
import defer from "p-defer";
|
|
3
25
|
|
|
4
|
-
export class AbortError extends Error {
|
|
5
|
-
type: string;
|
|
6
|
-
code: string;
|
|
7
|
-
|
|
8
|
-
constructor(message?: string, code?: string) {
|
|
9
|
-
super(message ?? "The operation was aborted");
|
|
10
|
-
this.type = "aborted";
|
|
11
|
-
this.code = code ?? "ABORT_ERR";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
26
|
export interface AbortOptions {
|
|
16
27
|
signal?: AbortSignal;
|
|
17
28
|
}
|
|
18
29
|
|
|
30
|
+
// -----------------------------
|
|
31
|
+
// Public API interfaces
|
|
32
|
+
// -----------------------------
|
|
33
|
+
|
|
19
34
|
/**
|
|
20
35
|
* An iterable that you can push values into.
|
|
21
36
|
*/
|
|
22
37
|
export interface PushableLanes<T, R = void, N = unknown>
|
|
23
38
|
extends AsyncGenerator<T, R, N> {
|
|
24
39
|
/**
|
|
25
|
-
* End the iterable after all values in the buffer (if any) have been yielded.
|
|
26
|
-
* error is passed the buffer is cleared immediately and the next
|
|
27
|
-
* throw the passed error
|
|
40
|
+
* End the iterable after all values in the buffer (if any) have been yielded.
|
|
41
|
+
* If an error is passed, the buffer is cleared immediately and the next
|
|
42
|
+
* iteration will throw the passed error.
|
|
28
43
|
*/
|
|
29
44
|
end(err?: Error): this;
|
|
30
45
|
|
|
31
46
|
/**
|
|
32
|
-
* Push a value into the iterable. Values are yielded
|
|
33
|
-
*
|
|
47
|
+
* Push a value into the iterable. Values are yielded in a lane-aware order.
|
|
48
|
+
* Values not yet consumed are buffered. Optional `lane` is 0-based (default 0).
|
|
34
49
|
*/
|
|
35
50
|
push(value: T, lane?: number): this;
|
|
36
51
|
|
|
37
52
|
/**
|
|
38
|
-
*
|
|
39
|
-
* this
|
|
40
|
-
*
|
|
41
|
-
* If an AbortSignal is passed as an option and that signal aborts, it only
|
|
42
|
-
* causes the returned promise to reject - it does not end the pushable.
|
|
53
|
+
* Resolves when the underlying buffer becomes empty (no queued data).
|
|
54
|
+
* If an AbortSignal is given and it aborts, only this promise rejects;
|
|
55
|
+
* the pushable itself is not ended.
|
|
43
56
|
*/
|
|
44
57
|
onEmpty(options?: AbortOptions): Promise<void>;
|
|
45
58
|
|
|
46
|
-
/**
|
|
47
|
-
* This property contains the total number of bytes in the queue ready to be read.
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
|
|
59
|
+
/** Total number of bytes buffered (across all lanes). */
|
|
51
60
|
get readableLength(): number;
|
|
52
61
|
|
|
53
62
|
/**
|
|
54
|
-
* Get readable length for specific lane
|
|
55
|
-
* @param lane
|
|
56
|
-
* @returns readable length for the lane
|
|
63
|
+
* Get readable length for a specific lane (bytes) or total when `lane` is undefined.
|
|
57
64
|
*/
|
|
58
65
|
getReadableLength(lane?: number): number;
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
/** How to distribute turns between lanes. */
|
|
69
|
+
export type FairnessMode = "priority" | "wrr";
|
|
70
|
+
|
|
71
|
+
/** What to do when buffer would exceed `maxBufferedBytes`. */
|
|
72
|
+
export type OverflowPolicy = "throw" | "drop-newest";
|
|
73
|
+
|
|
61
74
|
export interface Options {
|
|
62
75
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* passed the error as a parameter.
|
|
76
|
+
* Called after *all* values have been yielded from the iterator (including buffered values).
|
|
77
|
+
* If the iterator is ended with an error it will receive the error.
|
|
66
78
|
*/
|
|
67
79
|
onEnd?(err?: Error): void;
|
|
68
80
|
|
|
69
81
|
/**
|
|
70
|
-
*
|
|
82
|
+
* Number of lanes. Lane 0 is the "fastest"/most preferred lane.
|
|
83
|
+
* Default: 1
|
|
71
84
|
*/
|
|
72
85
|
lanes?: number;
|
|
86
|
+
|
|
73
87
|
/**
|
|
74
|
-
* Optional hook invoked on every successful push with the value and lane
|
|
88
|
+
* Optional hook invoked on every successful push with the value and lane.
|
|
89
|
+
* Useful for metrics/telemetry.
|
|
75
90
|
*/
|
|
76
91
|
onPush?(value: { byteLength: number }, lane: number): void;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Fairness mode:
|
|
95
|
+
* - 'priority': strict priority (original behavior).
|
|
96
|
+
* - 'wrr': weighted round-robin (starvation-free).
|
|
97
|
+
* Default: 'wrr'
|
|
98
|
+
*/
|
|
99
|
+
fairness?: FairnessMode;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Weights per lane if fairness === 'wrr'. Larger weight = more service.
|
|
103
|
+
* Must have length === lanes and each weight >= 1.
|
|
104
|
+
* If omitted, weights are auto-generated from `bias`.
|
|
105
|
+
*/
|
|
106
|
+
weights?: number[];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Bias factor for auto-generated weights when fairness === 'wrr'.
|
|
110
|
+
* For lanes L, weight[i] = floor(bias^(L-1-i)) with a minimum of 1.
|
|
111
|
+
* Default: 2 (e.g., lanes=4 -> [8,4,2,1])
|
|
112
|
+
*/
|
|
113
|
+
bias?: number;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Optional high-water mark in **bytes** across all lanes.
|
|
117
|
+
* If a `push` would exceed this many buffered bytes:
|
|
118
|
+
* - overflow: 'throw' -> throw an Error (default policy)
|
|
119
|
+
* - overflow: 'drop-newest' -> silently drop this pushed item
|
|
120
|
+
*/
|
|
121
|
+
maxBufferedBytes?: number;
|
|
122
|
+
|
|
123
|
+
/** Overflow policy when `maxBufferedBytes` would be exceeded. Default: 'throw' */
|
|
124
|
+
overflow?: OverflowPolicy;
|
|
77
125
|
}
|
|
78
126
|
|
|
127
|
+
// -----------------------------
|
|
128
|
+
// Internal queue primitives
|
|
129
|
+
// -----------------------------
|
|
130
|
+
|
|
79
131
|
export interface DoneResult {
|
|
80
132
|
done: true;
|
|
81
133
|
}
|
|
@@ -92,94 +144,185 @@ export interface Next<T> {
|
|
|
92
144
|
}
|
|
93
145
|
|
|
94
146
|
/**
|
|
95
|
-
*
|
|
147
|
+
* FIFO that tracks the total readable bytes (`.size`) of queued values.
|
|
96
148
|
*/
|
|
97
|
-
class
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
size: number = 0;
|
|
149
|
+
class ByteFifo<T extends { byteLength: number }> extends GenericFIFO<Next<T>> {
|
|
150
|
+
size = 0;
|
|
151
|
+
|
|
101
152
|
push(val: Next<T>): void {
|
|
102
|
-
if (val.value)
|
|
103
|
-
this.size += val.value.byteLength;
|
|
104
|
-
}
|
|
153
|
+
if (val.value) this.size += val.value.byteLength;
|
|
105
154
|
return super.push(val);
|
|
106
155
|
}
|
|
107
156
|
|
|
108
157
|
shift(): Next<T> | undefined {
|
|
109
158
|
const shifted = super.shift();
|
|
110
|
-
if (shifted?.value)
|
|
111
|
-
this.size -= shifted.value.byteLength;
|
|
112
|
-
}
|
|
159
|
+
if (shifted?.value) this.size -= shifted.value.byteLength;
|
|
113
160
|
return shifted;
|
|
114
161
|
}
|
|
115
162
|
}
|
|
116
163
|
|
|
117
164
|
/**
|
|
118
|
-
* A queue
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* so no lane get really "stuck"
|
|
165
|
+
* A multi-lane queue with configurable fairness.
|
|
166
|
+
* - 'priority': probe lanes in order 0..L-1 each shift.
|
|
167
|
+
* - 'wrr': service lanes according to weights in a repeating schedule.
|
|
122
168
|
*/
|
|
123
|
-
class
|
|
124
|
-
lanes:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
169
|
+
class LaneQueue<T extends { byteLength: number }> {
|
|
170
|
+
public readonly lanes: ByteFifo<T>[];
|
|
171
|
+
private readonly mode: FairnessMode;
|
|
172
|
+
private readonly schedule: number[]; // WRR: repeated lane indices per weight
|
|
173
|
+
private cursor = 0;
|
|
174
|
+
|
|
175
|
+
constructor(init: {
|
|
176
|
+
lanes: number;
|
|
177
|
+
fairness?: FairnessMode;
|
|
178
|
+
weights?: number[];
|
|
179
|
+
bias?: number;
|
|
180
|
+
}) {
|
|
181
|
+
const L = Math.max(1, init.lanes | 0);
|
|
182
|
+
this.mode = init.fairness ?? "wrr";
|
|
183
|
+
this.lanes = Array.from({ length: L }, () => new ByteFifo<T>());
|
|
184
|
+
|
|
185
|
+
if (this.mode === "wrr") {
|
|
186
|
+
const bias = init.bias ?? 2;
|
|
187
|
+
const auto = Array.from({ length: L }, (_, i) =>
|
|
188
|
+
Math.max(1, Math.floor(Math.pow(bias, L - 1 - i))),
|
|
189
|
+
);
|
|
190
|
+
const w = normalizeWeights(init.weights ?? auto, L);
|
|
191
|
+
|
|
192
|
+
// Build a simple round-robin schedule by repeating lanes according to weight.
|
|
193
|
+
this.schedule = [];
|
|
194
|
+
for (let i = 0; i < L; i++) {
|
|
195
|
+
for (let k = 0; k < w[i]; k++) this.schedule.push(i);
|
|
196
|
+
}
|
|
197
|
+
// Edge case: if all weights collapsed to zero (shouldn't), fall back to priority.
|
|
198
|
+
if (this.schedule.length === 0) {
|
|
199
|
+
this.schedule = Array.from({ length: L }, (_, i) => i);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// strict priority
|
|
203
|
+
this.schedule = Array.from({ length: L }, (_, i) => i);
|
|
129
204
|
}
|
|
130
205
|
}
|
|
131
206
|
|
|
132
|
-
get size() {
|
|
207
|
+
get size(): number {
|
|
133
208
|
let sum = 0;
|
|
134
|
-
for (const lane of this.lanes)
|
|
135
|
-
sum += lane.size;
|
|
136
|
-
}
|
|
209
|
+
for (const lane of this.lanes) sum += lane.size;
|
|
137
210
|
return sum;
|
|
138
211
|
}
|
|
139
|
-
|
|
140
|
-
|
|
212
|
+
|
|
213
|
+
/** Enqueue a value into a specific lane. */
|
|
214
|
+
push(val: Next<T>, lane: number): void {
|
|
215
|
+
const idx = clampLane(lane, this.lanes.length);
|
|
216
|
+
this.lanes[idx].push(val);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** True if all lanes are empty. */
|
|
220
|
+
isEmpty(): boolean {
|
|
221
|
+
for (const lane of this.lanes) if (!lane.isEmpty()) return false;
|
|
222
|
+
return true;
|
|
141
223
|
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Dequeue the next value by fairness rules.
|
|
227
|
+
* Ensures progress even if some schedule slots map to empty lanes.
|
|
228
|
+
*/
|
|
142
229
|
shift(): Next<T> | undefined {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
230
|
+
if (this.isEmpty()) return undefined;
|
|
231
|
+
|
|
232
|
+
if (this.mode === "priority") {
|
|
233
|
+
// strict priority: always scan from lane 0
|
|
234
|
+
for (let i = 0; i < this.lanes.length; i++) {
|
|
235
|
+
const item = this.lanes[i].shift();
|
|
236
|
+
if (item) return item;
|
|
149
237
|
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// WRR mode: use rotating schedule
|
|
242
|
+
const L = this.schedule.length;
|
|
243
|
+
for (let probes = 0; probes < L; probes++) {
|
|
244
|
+
const laneIdx = this.schedule[this.cursor];
|
|
245
|
+
this.cursor = (this.cursor + 1) % L;
|
|
246
|
+
|
|
247
|
+
const item = this.lanes[laneIdx].shift();
|
|
248
|
+
if (item) return item;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// (very unlikely) nothing was found despite size>0 – linear scan fallback
|
|
252
|
+
for (let i = 0; i < this.lanes.length; i++) {
|
|
253
|
+
const item = this.lanes[i].shift();
|
|
254
|
+
if (item) return item;
|
|
150
255
|
}
|
|
151
256
|
return undefined;
|
|
152
257
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeWeights(weights: number[], lanes: number): number[] {
|
|
261
|
+
if (weights.length !== lanes) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`weights length (${weights.length}) must equal lanes (${lanes})`,
|
|
264
|
+
);
|
|
160
265
|
}
|
|
266
|
+
const w = weights.map((x) => (x && x > 0 ? Math.floor(x) : 0));
|
|
267
|
+
if (w.every((x) => x === 0)) {
|
|
268
|
+
// ensure at least 1 for all lanes to retain progress guarantees
|
|
269
|
+
return Array.from({ length: lanes }, () => 1);
|
|
270
|
+
}
|
|
271
|
+
return w;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function clampLane(lane: number, lanes: number): number {
|
|
275
|
+
if (!Number.isFinite(lane)) return 0;
|
|
276
|
+
lane = lane | 0;
|
|
277
|
+
if (lane < 0) return 0;
|
|
278
|
+
if (lane >= lanes) return lanes - 1;
|
|
279
|
+
return lane;
|
|
161
280
|
}
|
|
162
281
|
|
|
282
|
+
// -----------------------------
|
|
283
|
+
// Factory
|
|
284
|
+
// -----------------------------
|
|
285
|
+
|
|
163
286
|
export function pushableLanes<T extends { byteLength: number } = Uint8Array>(
|
|
164
287
|
options: Options = {},
|
|
165
288
|
): PushableLanes<T> {
|
|
166
289
|
return _pushable<Uint8Array, T, PushableLanes<T>>(options);
|
|
167
290
|
}
|
|
168
291
|
|
|
169
|
-
//
|
|
292
|
+
// -----------------------------
|
|
293
|
+
// Core implementation
|
|
294
|
+
// -----------------------------
|
|
295
|
+
// Based on it-pushable, adapted to multi-lane buffered queues with fairness.
|
|
296
|
+
// Important invariants:
|
|
297
|
+
// - We resolve the internal "drain" promise whenever the buffer *becomes empty*.
|
|
298
|
+
// - After end(err), the iterator finishes; push() becomes a no-op.
|
|
299
|
+
|
|
170
300
|
function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
171
301
|
options?: Options,
|
|
172
302
|
): ReturnType {
|
|
173
303
|
options = options ?? {};
|
|
174
304
|
let onEnd = options.onEnd;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
305
|
+
|
|
306
|
+
// Main buffer: multi-lane with fairness
|
|
307
|
+
let buffer: LaneQueue<PushType> | ByteFifo<PushType> =
|
|
308
|
+
new LaneQueue<PushType>({
|
|
309
|
+
lanes: options.lanes ?? 1,
|
|
310
|
+
fairness: options.fairness ?? "wrr",
|
|
311
|
+
weights: options.weights,
|
|
312
|
+
bias: options.bias ?? 2,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// After end(err) we may swap buffer to a simple ByteFifo to deliver the terminal signal/error.
|
|
316
|
+
const isLaneQueue = (buffer: any): buffer is LaneQueue<PushType> =>
|
|
317
|
+
buffer instanceof LaneQueue;
|
|
318
|
+
|
|
179
319
|
let pushable: any;
|
|
180
320
|
let onNext: ((next: Next<PushType>, lane: number) => ReturnType) | null;
|
|
181
|
-
let ended
|
|
182
|
-
let drain = defer();
|
|
321
|
+
let ended = false;
|
|
322
|
+
let drain = defer<void>();
|
|
323
|
+
|
|
324
|
+
const maxBytes = options.maxBufferedBytes;
|
|
325
|
+
const overflow: OverflowPolicy = options.overflow ?? "throw";
|
|
183
326
|
|
|
184
327
|
const getNext = (): NextResult<ValueType> => {
|
|
185
328
|
const next: Next<PushType> | undefined = buffer.shift();
|
|
@@ -187,11 +330,9 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
187
330
|
if (next == null) {
|
|
188
331
|
return { done: true };
|
|
189
332
|
}
|
|
190
|
-
|
|
191
333
|
if (next.error != null) {
|
|
192
334
|
throw next.error;
|
|
193
335
|
}
|
|
194
|
-
|
|
195
336
|
return {
|
|
196
337
|
done: next.done === true,
|
|
197
338
|
// @ts-expect-error if done is false, value will be present
|
|
@@ -204,7 +345,6 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
204
345
|
if (!buffer.isEmpty()) {
|
|
205
346
|
return getNext();
|
|
206
347
|
}
|
|
207
|
-
|
|
208
348
|
if (ended) {
|
|
209
349
|
return { done: true };
|
|
210
350
|
}
|
|
@@ -213,23 +353,20 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
213
353
|
onNext = (next: Next<PushType>, lane: number) => {
|
|
214
354
|
onNext = null;
|
|
215
355
|
buffer.push(next, lane);
|
|
216
|
-
|
|
217
356
|
try {
|
|
218
357
|
resolve(getNext());
|
|
219
358
|
} catch (err: any) {
|
|
220
359
|
reject(err);
|
|
221
360
|
}
|
|
222
|
-
|
|
223
361
|
return pushable;
|
|
224
362
|
};
|
|
225
363
|
});
|
|
226
364
|
} finally {
|
|
365
|
+
// If buffer is empty after this turn, resolve the drain promise (in a microtask)
|
|
227
366
|
if (buffer.isEmpty()) {
|
|
228
|
-
// settle promise in the microtask queue to give consumers a chance to
|
|
229
|
-
// await after calling .push
|
|
230
367
|
queueMicrotask(() => {
|
|
231
368
|
drain.resolve();
|
|
232
|
-
drain = defer();
|
|
369
|
+
drain = defer<void>();
|
|
233
370
|
});
|
|
234
371
|
}
|
|
235
372
|
}
|
|
@@ -239,46 +376,69 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
239
376
|
if (onNext != null) {
|
|
240
377
|
return onNext(next, lane);
|
|
241
378
|
}
|
|
242
|
-
|
|
243
379
|
buffer.push(next, lane);
|
|
244
380
|
return pushable;
|
|
245
381
|
};
|
|
246
382
|
|
|
247
383
|
const bufferError = (err: Error): ReturnType => {
|
|
248
|
-
|
|
249
|
-
|
|
384
|
+
// swap to ByteFifo to deliver a single terminal error
|
|
385
|
+
buffer = new ByteFifo<PushType>();
|
|
250
386
|
if (onNext != null) {
|
|
251
387
|
return onNext({ error: err }, 0);
|
|
252
388
|
}
|
|
253
|
-
|
|
254
389
|
buffer.push({ error: err });
|
|
255
390
|
return pushable;
|
|
256
391
|
};
|
|
257
392
|
|
|
393
|
+
const totalBufferedBytes = (): number => {
|
|
394
|
+
if (isLaneQueue(buffer)) return buffer.size;
|
|
395
|
+
return (buffer as ByteFifo<PushType>).size;
|
|
396
|
+
};
|
|
397
|
+
|
|
258
398
|
const push = (value: PushType, lane: number = 0): ReturnType => {
|
|
259
399
|
if (ended) {
|
|
400
|
+
// Ignore pushes after end() for safety (consistent with it-pushable).
|
|
260
401
|
return pushable;
|
|
261
402
|
}
|
|
262
403
|
|
|
404
|
+
// Simple backpressure: enforce HWM if configured
|
|
405
|
+
if (maxBytes != null && maxBytes > 0) {
|
|
406
|
+
const wouldBe = totalBufferedBytes() + value.byteLength;
|
|
407
|
+
if (wouldBe > maxBytes) {
|
|
408
|
+
if (overflow === "drop-newest") {
|
|
409
|
+
// silently drop this item
|
|
410
|
+
return pushable;
|
|
411
|
+
}
|
|
412
|
+
// default 'throw'
|
|
413
|
+
throw new Error(
|
|
414
|
+
`pushableLanes buffer overflow: ${wouldBe} bytes > maxBufferedBytes=${maxBytes}`,
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
263
419
|
const out = bufferNext({ done: false, value }, lane);
|
|
264
|
-
options?.onPush?.(
|
|
420
|
+
options?.onPush?.(
|
|
421
|
+
value,
|
|
422
|
+
clampLane(lane, isLaneQueue(buffer) ? buffer.lanes.length : 1),
|
|
423
|
+
);
|
|
265
424
|
return out;
|
|
266
425
|
};
|
|
426
|
+
|
|
267
427
|
const end = (err?: Error): ReturnType => {
|
|
268
428
|
if (ended) return pushable;
|
|
269
429
|
ended = true;
|
|
270
|
-
|
|
271
430
|
return err != null ? bufferError(err) : bufferNext({ done: true }, 0);
|
|
272
431
|
};
|
|
432
|
+
|
|
273
433
|
const _return = (): DoneResult => {
|
|
274
|
-
|
|
434
|
+
// Ensure prompt termination
|
|
435
|
+
buffer = new ByteFifo<PushType>();
|
|
275
436
|
end();
|
|
276
|
-
|
|
277
437
|
return { done: true };
|
|
278
438
|
};
|
|
439
|
+
|
|
279
440
|
const _throw = (err: Error): DoneResult => {
|
|
280
441
|
end(err);
|
|
281
|
-
|
|
282
442
|
return { done: true };
|
|
283
443
|
};
|
|
284
444
|
|
|
@@ -291,45 +451,41 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
291
451
|
throw: _throw,
|
|
292
452
|
push,
|
|
293
453
|
end,
|
|
454
|
+
|
|
294
455
|
get readableLength(): number {
|
|
295
|
-
return
|
|
456
|
+
return totalBufferedBytes();
|
|
296
457
|
},
|
|
297
458
|
|
|
298
459
|
getReadableLength(lane?: number): number {
|
|
299
|
-
if (lane == null)
|
|
300
|
-
|
|
460
|
+
if (lane == null) return totalBufferedBytes();
|
|
461
|
+
if (isLaneQueue(buffer)) {
|
|
462
|
+
const idx = clampLane(lane, buffer.lanes.length);
|
|
463
|
+
return buffer.lanes[idx].size;
|
|
301
464
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return buffer.lanes[lane].size;
|
|
305
|
-
}
|
|
306
|
-
return buffer.size; // we can only arrive here if we are "done" or "err" or "end" where we reasign the buffer to a simple one and put 1 message into it
|
|
465
|
+
// After end/error we swap to a ByteFifo: only "total" makes sense.
|
|
466
|
+
return (buffer as ByteFifo<PushType>).size;
|
|
307
467
|
},
|
|
308
|
-
onEmpty: async (options?: AbortOptions) => {
|
|
309
|
-
const signal = options?.signal;
|
|
310
|
-
signal?.throwIfAborted();
|
|
311
468
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
469
|
+
onEmpty: async (opts?: AbortOptions) => {
|
|
470
|
+
const signal = opts?.signal;
|
|
471
|
+
signal?.throwIfAborted?.();
|
|
472
|
+
|
|
473
|
+
if (buffer.isEmpty()) return;
|
|
315
474
|
|
|
316
475
|
let cancel: Promise<void> | undefined;
|
|
317
476
|
let listener: (() => void) | undefined;
|
|
318
477
|
|
|
319
478
|
if (signal != null) {
|
|
320
|
-
cancel = new Promise((
|
|
321
|
-
listener = () =>
|
|
322
|
-
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
signal.addEventListener("abort", listener);
|
|
479
|
+
cancel = new Promise<void>((_resolve, reject) => {
|
|
480
|
+
listener = () => reject(new AbortError());
|
|
481
|
+
signal.addEventListener("abort", listener!);
|
|
326
482
|
});
|
|
327
483
|
}
|
|
328
484
|
|
|
329
485
|
try {
|
|
330
486
|
await Promise.race([drain.promise, cancel]);
|
|
331
487
|
} finally {
|
|
332
|
-
if (listener != null
|
|
488
|
+
if (listener != null) {
|
|
333
489
|
signal?.removeEventListener("abort", listener);
|
|
334
490
|
}
|
|
335
491
|
}
|
|
@@ -340,6 +496,7 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
340
496
|
return pushable;
|
|
341
497
|
}
|
|
342
498
|
|
|
499
|
+
// Wrap with onEnd notifier
|
|
343
500
|
const _pushable = pushable;
|
|
344
501
|
|
|
345
502
|
pushable = {
|
|
@@ -351,39 +508,36 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
|
|
|
351
508
|
},
|
|
352
509
|
throw(err: Error) {
|
|
353
510
|
_pushable.throw(err);
|
|
354
|
-
|
|
355
511
|
if (onEnd != null) {
|
|
356
512
|
onEnd(err);
|
|
357
513
|
onEnd = undefined;
|
|
358
514
|
}
|
|
359
|
-
|
|
360
515
|
return { done: true };
|
|
361
516
|
},
|
|
362
517
|
return() {
|
|
363
518
|
_pushable.return();
|
|
364
|
-
|
|
365
519
|
if (onEnd != null) {
|
|
366
520
|
onEnd();
|
|
367
521
|
onEnd = undefined;
|
|
368
522
|
}
|
|
369
|
-
|
|
370
523
|
return { done: true };
|
|
371
524
|
},
|
|
372
525
|
push,
|
|
373
|
-
end(err
|
|
526
|
+
end(err?: Error) {
|
|
374
527
|
_pushable.end(err);
|
|
375
|
-
|
|
376
528
|
if (onEnd != null) {
|
|
377
529
|
onEnd(err);
|
|
378
530
|
onEnd = undefined;
|
|
379
531
|
}
|
|
380
|
-
|
|
381
532
|
return pushable;
|
|
382
533
|
},
|
|
383
534
|
get readableLength() {
|
|
384
535
|
return _pushable.readableLength;
|
|
385
536
|
},
|
|
386
|
-
|
|
537
|
+
getReadableLength(lane?: number) {
|
|
538
|
+
return _pushable.getReadableLength(lane);
|
|
539
|
+
},
|
|
540
|
+
onEmpty(opts?: AbortOptions) {
|
|
387
541
|
return _pushable.onEmpty(opts);
|
|
388
542
|
},
|
|
389
543
|
};
|