@peerbit/stream 4.4.0-780f7ce → 4.4.0-9b0640c

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