@peerbit/stream 4.4.0-33643d9 → 4.4.0-58d3d09

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,133 +1,81 @@
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";
23
1
  import GenericFIFO from "fast-fifo";
24
2
  import defer from "p-defer";
25
3
 
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
+
26
15
  export interface AbortOptions {
27
16
  signal?: AbortSignal;
28
17
  }
29
18
 
30
- // -----------------------------
31
- // Public API interfaces
32
- // -----------------------------
33
-
34
19
  /**
35
20
  * An iterable that you can push values into.
36
21
  */
37
22
  export interface PushableLanes<T, R = void, N = unknown>
38
23
  extends AsyncGenerator<T, R, N> {
39
24
  /**
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.
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
43
28
  */
44
29
  end(err?: Error): this;
45
30
 
46
31
  /**
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).
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.
49
34
  */
50
35
  push(value: T, lane?: number): this;
51
36
 
52
37
  /**
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.
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.
56
43
  */
57
44
  onEmpty(options?: AbortOptions): Promise<void>;
58
45
 
59
- /** 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
+
60
51
  get readableLength(): number;
61
52
 
62
53
  /**
63
- * 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
64
57
  */
65
58
  getReadableLength(lane?: number): number;
66
59
  }
67
60
 
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
-
74
61
  export interface Options {
75
62
  /**
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.
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.
78
66
  */
79
67
  onEnd?(err?: Error): void;
80
68
 
81
69
  /**
82
- * Number of lanes. Lane 0 is the "fastest"/most preferred lane.
83
- * Default: 1
70
+ * How many lanes, lane 0 is fastest and will drain before lane 1 is consumed
84
71
  */
85
72
  lanes?: number;
86
-
87
73
  /**
88
- * Optional hook invoked on every successful push with the value and lane.
89
- * Useful for metrics/telemetry.
74
+ * Optional hook invoked on every successful push with the value and lane
90
75
  */
91
76
  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;
125
77
  }
126
78
 
127
- // -----------------------------
128
- // Internal queue primitives
129
- // -----------------------------
130
-
131
79
  export interface DoneResult {
132
80
  done: true;
133
81
  }
@@ -144,185 +92,94 @@ export interface Next<T> {
144
92
  }
145
93
 
146
94
  /**
147
- * FIFO that tracks the total readable bytes (`.size`) of queued values.
95
+ * Fifo but with total readableLength counter
148
96
  */
149
- class ByteFifo<T extends { byteLength: number }> extends GenericFIFO<Next<T>> {
150
- size = 0;
151
-
97
+ class Uint8ArrayFifo<T extends { byteLength: number }> extends GenericFIFO<
98
+ Next<T>
99
+ > {
100
+ size: number = 0;
152
101
  push(val: Next<T>): void {
153
- if (val.value) this.size += val.value.byteLength;
102
+ if (val.value) {
103
+ this.size += val.value.byteLength;
104
+ }
154
105
  return super.push(val);
155
106
  }
156
107
 
157
108
  shift(): Next<T> | undefined {
158
109
  const shifted = super.shift();
159
- if (shifted?.value) this.size -= shifted.value.byteLength;
110
+ if (shifted?.value) {
111
+ this.size -= shifted.value.byteLength;
112
+ }
160
113
  return shifted;
161
114
  }
162
115
  }
163
116
 
164
117
  /**
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.
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"
168
122
  */
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);
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();
204
129
  }
205
130
  }
206
131
 
207
- get size(): number {
132
+ get size() {
208
133
  let sum = 0;
209
- for (const lane of this.lanes) sum += lane.size;
134
+ for (const lane of this.lanes) {
135
+ sum += lane.size;
136
+ }
210
137
  return sum;
211
138
  }
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;
139
+ push(val: Next<T>, lane: number) {
140
+ return this.lanes[lane].push(val);
223
141
  }
224
-
225
- /**
226
- * Dequeue the next value by fairness rules.
227
- * Ensures progress even if some schedule slots map to empty lanes.
228
- */
229
142
  shift(): Next<T> | undefined {
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;
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;
237
149
  }
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;
255
150
  }
256
151
  return undefined;
257
152
  }
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
- );
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);
153
+ isEmpty(): boolean {
154
+ for (const lane of this.lanes) {
155
+ if (!lane.isEmpty()) {
156
+ return false;
157
+ }
158
+ }
159
+ return true;
270
160
  }
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;
280
161
  }
281
162
 
282
- // -----------------------------
283
- // Factory
284
- // -----------------------------
285
-
286
163
  export function pushableLanes<T extends { byteLength: number } = Uint8Array>(
287
164
  options: Options = {},
288
165
  ): PushableLanes<T> {
289
166
  return _pushable<Uint8Array, T, PushableLanes<T>>(options);
290
167
  }
291
168
 
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
-
169
+ // Modified from https://github.com/alanshaw/it-pushable
300
170
  function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
301
171
  options?: Options,
302
172
  ): ReturnType {
303
173
  options = options ?? {};
304
174
  let onEnd = options.onEnd;
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
-
175
+ let buffer: Uint8arrayPriorityQueue<PushType> | Uint8ArrayFifo<PushType> =
176
+ new Uint8arrayPriorityQueue<PushType>(
177
+ options.lanes ? { lanes: options.lanes } : undefined,
178
+ );
319
179
  let pushable: any;
320
180
  let onNext: ((next: Next<PushType>, lane: number) => ReturnType) | null;
321
- let ended = false;
322
- let drain = defer<void>();
323
-
324
- const maxBytes = options.maxBufferedBytes;
325
- const overflow: OverflowPolicy = options.overflow ?? "throw";
181
+ let ended: boolean;
182
+ let drain = defer();
326
183
 
327
184
  const getNext = (): NextResult<ValueType> => {
328
185
  const next: Next<PushType> | undefined = buffer.shift();
@@ -330,9 +187,11 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
330
187
  if (next == null) {
331
188
  return { done: true };
332
189
  }
190
+
333
191
  if (next.error != null) {
334
192
  throw next.error;
335
193
  }
194
+
336
195
  return {
337
196
  done: next.done === true,
338
197
  // @ts-expect-error if done is false, value will be present
@@ -345,6 +204,7 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
345
204
  if (!buffer.isEmpty()) {
346
205
  return getNext();
347
206
  }
207
+
348
208
  if (ended) {
349
209
  return { done: true };
350
210
  }
@@ -353,20 +213,23 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
353
213
  onNext = (next: Next<PushType>, lane: number) => {
354
214
  onNext = null;
355
215
  buffer.push(next, lane);
216
+
356
217
  try {
357
218
  resolve(getNext());
358
219
  } catch (err: any) {
359
220
  reject(err);
360
221
  }
222
+
361
223
  return pushable;
362
224
  };
363
225
  });
364
226
  } finally {
365
- // If buffer is empty after this turn, resolve the drain promise (in a microtask)
366
227
  if (buffer.isEmpty()) {
228
+ // settle promise in the microtask queue to give consumers a chance to
229
+ // await after calling .push
367
230
  queueMicrotask(() => {
368
231
  drain.resolve();
369
- drain = defer<void>();
232
+ drain = defer();
370
233
  });
371
234
  }
372
235
  }
@@ -376,69 +239,46 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
376
239
  if (onNext != null) {
377
240
  return onNext(next, lane);
378
241
  }
242
+
379
243
  buffer.push(next, lane);
380
244
  return pushable;
381
245
  };
382
246
 
383
247
  const bufferError = (err: Error): ReturnType => {
384
- // swap to ByteFifo to deliver a single terminal error
385
- buffer = new ByteFifo<PushType>();
248
+ buffer = new Uint8ArrayFifo();
249
+
386
250
  if (onNext != null) {
387
251
  return onNext({ error: err }, 0);
388
252
  }
253
+
389
254
  buffer.push({ error: err });
390
255
  return pushable;
391
256
  };
392
257
 
393
- const totalBufferedBytes = (): number => {
394
- if (isLaneQueue(buffer)) return buffer.size;
395
- return (buffer as ByteFifo<PushType>).size;
396
- };
397
-
398
258
  const push = (value: PushType, lane: number = 0): ReturnType => {
399
259
  if (ended) {
400
- // Ignore pushes after end() for safety (consistent with it-pushable).
401
260
  return pushable;
402
261
  }
403
262
 
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
-
419
263
  const out = bufferNext({ done: false, value }, lane);
420
- options?.onPush?.(
421
- value,
422
- clampLane(lane, isLaneQueue(buffer) ? buffer.lanes.length : 1),
423
- );
264
+ options?.onPush?.(value, lane);
424
265
  return out;
425
266
  };
426
-
427
267
  const end = (err?: Error): ReturnType => {
428
268
  if (ended) return pushable;
429
269
  ended = true;
270
+
430
271
  return err != null ? bufferError(err) : bufferNext({ done: true }, 0);
431
272
  };
432
-
433
273
  const _return = (): DoneResult => {
434
- // Ensure prompt termination
435
- buffer = new ByteFifo<PushType>();
274
+ buffer = new Uint8ArrayFifo();
436
275
  end();
276
+
437
277
  return { done: true };
438
278
  };
439
-
440
279
  const _throw = (err: Error): DoneResult => {
441
280
  end(err);
281
+
442
282
  return { done: true };
443
283
  };
444
284
 
@@ -451,41 +291,45 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
451
291
  throw: _throw,
452
292
  push,
453
293
  end,
454
-
455
294
  get readableLength(): number {
456
- return totalBufferedBytes();
295
+ return buffer.size;
457
296
  },
458
297
 
459
298
  getReadableLength(lane?: number): number {
460
- if (lane == null) return totalBufferedBytes();
461
- if (isLaneQueue(buffer)) {
462
- const idx = clampLane(lane, buffer.lanes.length);
463
- return buffer.lanes[idx].size;
299
+ if (lane == null) {
300
+ return buffer.size;
464
301
  }
465
- // After end/error we swap to a ByteFifo: only "total" makes sense.
466
- return (buffer as ByteFifo<PushType>).size;
467
- },
468
302
 
469
- onEmpty: async (opts?: AbortOptions) => {
470
- const signal = opts?.signal;
471
- 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();
472
311
 
473
- if (buffer.isEmpty()) return;
312
+ if (buffer.isEmpty()) {
313
+ return;
314
+ }
474
315
 
475
316
  let cancel: Promise<void> | undefined;
476
317
  let listener: (() => void) | undefined;
477
318
 
478
319
  if (signal != null) {
479
- cancel = new Promise<void>((_resolve, reject) => {
480
- listener = () => reject(new AbortError());
481
- signal.addEventListener("abort", listener!);
320
+ cancel = new Promise((resolve, reject) => {
321
+ listener = () => {
322
+ reject(new AbortError());
323
+ };
324
+
325
+ signal.addEventListener("abort", listener);
482
326
  });
483
327
  }
484
328
 
485
329
  try {
486
330
  await Promise.race([drain.promise, cancel]);
487
331
  } finally {
488
- if (listener != null) {
332
+ if (listener != null && signal != null) {
489
333
  signal?.removeEventListener("abort", listener);
490
334
  }
491
335
  }
@@ -496,7 +340,6 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
496
340
  return pushable;
497
341
  }
498
342
 
499
- // Wrap with onEnd notifier
500
343
  const _pushable = pushable;
501
344
 
502
345
  pushable = {
@@ -508,36 +351,39 @@ function _pushable<PushType extends Uint8Array, ValueType, ReturnType>(
508
351
  },
509
352
  throw(err: Error) {
510
353
  _pushable.throw(err);
354
+
511
355
  if (onEnd != null) {
512
356
  onEnd(err);
513
357
  onEnd = undefined;
514
358
  }
359
+
515
360
  return { done: true };
516
361
  },
517
362
  return() {
518
363
  _pushable.return();
364
+
519
365
  if (onEnd != null) {
520
366
  onEnd();
521
367
  onEnd = undefined;
522
368
  }
369
+
523
370
  return { done: true };
524
371
  },
525
372
  push,
526
- end(err?: Error) {
373
+ end(err: Error) {
527
374
  _pushable.end(err);
375
+
528
376
  if (onEnd != null) {
529
377
  onEnd(err);
530
378
  onEnd = undefined;
531
379
  }
380
+
532
381
  return pushable;
533
382
  },
534
383
  get readableLength() {
535
384
  return _pushable.readableLength;
536
385
  },
537
- getReadableLength(lane?: number) {
538
- return _pushable.getReadableLength(lane);
539
- },
540
- onEmpty(opts?: AbortOptions) {
386
+ onEmpty: (opts?: AbortOptions) => {
541
387
  return _pushable.onEmpty(opts);
542
388
  },
543
389
  };