@zakkster/lite-stream 1.0.0 → 1.1.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/CHANGELOG.md +155 -0
- package/README.md +119 -63
- package/ROADMAP.md +679 -159
- package/Stream.js +316 -80
- package/llms.txt +101 -18
- package/package.json +3 -3
package/Stream.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @zakkster/lite-stream 1.
|
|
1
|
+
// @zakkster/lite-stream 1.1.1
|
|
2
2
|
//
|
|
3
3
|
// Zero-GC bridge between async iterators and @zakkster/lite-signal. The
|
|
4
4
|
// multi-shot dual of lite-await's fromPromise: project an async source of N
|
|
@@ -20,11 +20,40 @@
|
|
|
20
20
|
// droppedCount. Best for "process every helix page in order, don't miss
|
|
21
21
|
// events".
|
|
22
22
|
//
|
|
23
|
+
// 1.1 added `mode: "latest"`, `filter`, `timeout`, `Symbol.asyncDispose`,
|
|
24
|
+
// multi-waiter FIFO queue, and `overflowCount` on `toAsyncIterable`. See
|
|
25
|
+
// CHANGELOG.md [1.1.0] and llms.txt (toAsyncIterable section) for the full
|
|
26
|
+
// 1.1 story.
|
|
27
|
+
//
|
|
23
28
|
// Copyright (c) 2026 Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
24
29
|
// MIT License
|
|
25
30
|
|
|
26
31
|
import { signal as _signal } from "@zakkster/lite-signal";
|
|
27
32
|
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Errors
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Thrown when a `timeout` deadline elapses before iterator settlement.
|
|
39
|
+
* Introduced in 1.1 alongside `toAsyncIterable`'s `timeout` option.
|
|
40
|
+
* Structurally identical to lite-await's `TimeoutError` so users can
|
|
41
|
+
* duck-check via `e.name === "TimeoutError"` across both packages -- but
|
|
42
|
+
* NOT imported from lite-await, to preserve lite-stream's zero-dep story.
|
|
43
|
+
*
|
|
44
|
+
* A structural twin of lite-await's TimeoutError, deliberately not imported
|
|
45
|
+
* (zero-dep by design), carrying `name === "TimeoutError"` and a `.timeout`
|
|
46
|
+
* field (the elapsed deadline in ms). The duck-check is suite-wide vocabulary;
|
|
47
|
+
* renaming it would be a breaking change everywhere it is consumed.
|
|
48
|
+
*/
|
|
49
|
+
class TimeoutError extends Error {
|
|
50
|
+
constructor(timeoutMs) {
|
|
51
|
+
super("lite-stream: timed out after " + timeoutMs + "ms");
|
|
52
|
+
this.name = "TimeoutError";
|
|
53
|
+
this.timeout = timeoutMs;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
28
57
|
// ---------------------------------------------------------------------------
|
|
29
58
|
// Internal helpers
|
|
30
59
|
// ---------------------------------------------------------------------------
|
|
@@ -118,11 +147,11 @@ function closeIterator(iter) {
|
|
|
118
147
|
* - iterator throws
|
|
119
148
|
* - opts.signal aborts (the iterator's return() is called best-effort)
|
|
120
149
|
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
150
|
+
* Disposing the result signal does NOT stop the pump: the iterator keeps
|
|
151
|
+
* pulling. AbortSignal or natural completion are the only stop mechanisms.
|
|
152
|
+
* A disposed signal's set() is a silent no-op, so the pump never learns of
|
|
153
|
+
* the disposal; an infinite source becomes an unbounded background loop.
|
|
154
|
+
* Pass opts.signal and abort it to stop early. See ROADMAP LS-01.
|
|
126
155
|
*
|
|
127
156
|
* @template T
|
|
128
157
|
* @param {AsyncIterable<T> | AsyncIterator<T> | Iterable<T>} source
|
|
@@ -274,9 +303,11 @@ function fromAsyncIterable(source, opts) {
|
|
|
274
303
|
}
|
|
275
304
|
const v = result.value;
|
|
276
305
|
// Build the new state from lastState (never sig.peek), then
|
|
277
|
-
// commit to both lastState and sig.
|
|
278
|
-
//
|
|
279
|
-
//
|
|
306
|
+
// commit to both lastState and sig. The try/catch around set()
|
|
307
|
+
// is defensive against a peer whose set() throws; on a throw we
|
|
308
|
+
// bail and tear down the iterator rather than pull into the
|
|
309
|
+
// void. Disposal is NOT the trigger -- set() after dispose() is
|
|
310
|
+
// a silent no-op in lite-signal 1.2.2 and 1.5.0 (see LS-01).
|
|
280
311
|
let newState;
|
|
281
312
|
if (mode === "latest") {
|
|
282
313
|
newState = {
|
|
@@ -314,16 +345,22 @@ function fromAsyncIterable(source, opts) {
|
|
|
314
345
|
try {
|
|
315
346
|
sig.set(newState);
|
|
316
347
|
} catch (_e) {
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
348
|
+
// Defensive against a peer signal whose set() throws. This
|
|
349
|
+
// is NOT disposal detection: set() after dispose() is a
|
|
350
|
+
// silent no-op in lite-signal 1.2.2 and 1.5.0 (probe P0),
|
|
351
|
+
// so this branch has never fired against any published
|
|
352
|
+
// peer. If it ever does, tear down without firing onError.
|
|
320
353
|
stopped = true;
|
|
321
354
|
cleanup();
|
|
322
355
|
closeIterator(iter);
|
|
323
356
|
return;
|
|
324
357
|
}
|
|
325
|
-
//
|
|
326
|
-
//
|
|
358
|
+
// pump() is called DIRECTLY here -- no explicit deferral at
|
|
359
|
+
// this site. The stack is already unwound because this
|
|
360
|
+
// continuation runs from a .then() handler, i.e. a fresh
|
|
361
|
+
// microtask. Proven safe: 1,000,000 synchronous values drained
|
|
362
|
+
// in ~80ms with no stack overflow (probe P4, node v26.3.1,
|
|
363
|
+
// 2026-09-01).
|
|
327
364
|
pump();
|
|
328
365
|
},
|
|
329
366
|
function (err) {
|
|
@@ -393,6 +430,8 @@ function makeErrorState(prevState, err, mode) {
|
|
|
393
430
|
* - You want a stop fn instead of an AbortController for cleanup
|
|
394
431
|
*
|
|
395
432
|
* NOTE: this does NOT dispose the target signal. The caller owns its lifetime.
|
|
433
|
+
* Disposing the target signal does NOT stop the pump: call the returned
|
|
434
|
+
* stop fn or abort opts.signal. See ROADMAP LS-01.
|
|
396
435
|
*
|
|
397
436
|
* @template T
|
|
398
437
|
* @param {AsyncIterable<T> | AsyncIterator<T>} source
|
|
@@ -498,31 +537,55 @@ function pipeToSignal(source, target, opts) {
|
|
|
498
537
|
return stop;
|
|
499
538
|
}
|
|
500
539
|
|
|
540
|
+
// ---------------------------------------------------------------------------
|
|
541
|
+
// Symbol.asyncDispose feature detection (Node 20+)
|
|
542
|
+
// ---------------------------------------------------------------------------
|
|
543
|
+
// Cached once at module load. When the runtime lacks Symbol.asyncDispose
|
|
544
|
+
// (Node 18/19), the iterable's disposer property is simply absent;
|
|
545
|
+
// iter.return() is the portable path.
|
|
546
|
+
|
|
547
|
+
const ASYNC_DISPOSE = (typeof Symbol !== "undefined" && Symbol.asyncDispose)
|
|
548
|
+
? Symbol.asyncDispose
|
|
549
|
+
: null;
|
|
550
|
+
|
|
501
551
|
// ---------------------------------------------------------------------------
|
|
502
552
|
// toAsyncIterable -- the reverse direction
|
|
503
553
|
// ---------------------------------------------------------------------------
|
|
504
554
|
|
|
505
555
|
/**
|
|
506
|
-
* Yield signal changes as an async iterable. Each change
|
|
507
|
-
*
|
|
508
|
-
* producer, values queue in an internal bounded buffer; on overflow, the
|
|
509
|
-
* OLDEST queued value is dropped and `droppedCount` is incremented. The
|
|
510
|
-
* dropped count is observable via the iterable's `.droppedCount` property
|
|
511
|
-
* (read after consuming).
|
|
556
|
+
* Yield signal changes as an async iterable. Each change resolves a pending
|
|
557
|
+
* `next()` call. Two backpressure modes (1.1):
|
|
512
558
|
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
559
|
+
* - "buffer" (default; matches 1.0.0): FIFO ring buffer of maxBuffer size;
|
|
560
|
+
* on overflow the OLDEST is dropped and both droppedCount and
|
|
561
|
+
* overflowCount are incremented.
|
|
562
|
+
* - "latest" (1.1): single mutable slot; producer overwrites; every
|
|
563
|
+
* overwrite bumps overflowCount (and droppedCount for compat).
|
|
564
|
+
*
|
|
565
|
+
* 1.1 also adds `filter` (per-value gate; throwing filter rejects the
|
|
566
|
+
* current next() and terminates without a writer surface), `timeout`
|
|
567
|
+
* (overall deadline; rejects with TimeoutError), Symbol.asyncDispose
|
|
568
|
+
* on Node 20+, and a multi-waiter queue that fixes the 1.0.0 latent bug
|
|
569
|
+
* where Promise.all([iter.next(), iter.next()]) silently lost the first
|
|
570
|
+
* resolver.
|
|
571
|
+
*
|
|
572
|
+
* See CHANGELOG.md [1.1.0] and llms.txt (toAsyncIterable section) for the
|
|
573
|
+
* full locked semantics.
|
|
515
574
|
*
|
|
516
575
|
* @template T
|
|
517
576
|
* @param {import("@zakkster/lite-signal").Signal<T> | import("@zakkster/lite-signal").Computed<T>} sig
|
|
518
577
|
* @param {{
|
|
519
|
-
* signal?:
|
|
520
|
-
* emitInitial?: boolean,
|
|
521
|
-
* maxBuffer?: number
|
|
578
|
+
* signal?: AbortSignal,
|
|
579
|
+
* emitInitial?: boolean,
|
|
580
|
+
* maxBuffer?: number,
|
|
581
|
+
* mode?: "latest" | "buffer",
|
|
582
|
+
* filter?: (v: T) => unknown,
|
|
583
|
+
* timeout?: number
|
|
522
584
|
* }} [opts]
|
|
523
|
-
* @returns {AsyncIterable<T> & { readonly droppedCount: number }}
|
|
585
|
+
* @returns {AsyncIterable<T> & { readonly droppedCount: number, readonly overflowCount: number }}
|
|
524
586
|
*/
|
|
525
587
|
function toAsyncIterable(sig, opts) {
|
|
588
|
+
// --- Validation ---
|
|
526
589
|
if (sig === null || sig === undefined || typeof sig.subscribe !== "function" || typeof sig.peek !== "function") {
|
|
527
590
|
throw new TypeError(
|
|
528
591
|
"lite-stream: toAsyncIterable expects a readable lite-signal (Signal/Computed with .peek and .subscribe)"
|
|
@@ -530,101 +593,273 @@ function toAsyncIterable(sig, opts) {
|
|
|
530
593
|
}
|
|
531
594
|
const abortSig = (opts !== undefined && opts !== null) ? opts.signal : undefined;
|
|
532
595
|
const emitInitial = (opts !== undefined && opts !== null && opts.emitInitial === false) ? false : true;
|
|
533
|
-
const
|
|
596
|
+
const filter = (opts !== undefined && opts !== null) ? opts.filter : undefined;
|
|
597
|
+
const timeoutMs = (opts !== undefined && opts !== null) ? opts.timeout : undefined;
|
|
598
|
+
const modeOpt = (opts !== undefined && opts !== null) ? opts.mode : undefined;
|
|
599
|
+
const maxBufferOpt = (opts !== undefined && opts !== null) ? opts.maxBuffer : undefined;
|
|
600
|
+
|
|
601
|
+
// Mode. Default "buffer" preserves 1.0.0 behavior.
|
|
602
|
+
let mode;
|
|
603
|
+
if (modeOpt === undefined || modeOpt === "buffer") {
|
|
604
|
+
mode = "buffer";
|
|
605
|
+
} else if (modeOpt === "latest") {
|
|
606
|
+
mode = "latest";
|
|
607
|
+
} else {
|
|
608
|
+
throw new TypeError(
|
|
609
|
+
"lite-stream: opts.mode must be \"latest\" or \"buffer\" (got " + JSON.stringify(modeOpt) + ")"
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
const isLatestWins = mode === "latest";
|
|
534
613
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
614
|
+
// maxBuffer only meaningful in buffer mode. Combining latest + maxBuffer
|
|
615
|
+
// is user error; silently ignoring one would hide the mistake.
|
|
616
|
+
if (isLatestWins && maxBufferOpt !== undefined) {
|
|
617
|
+
throw new TypeError(
|
|
618
|
+
"lite-stream: opts.maxBuffer is not allowed with mode: \"latest\" -- latest-wins uses a single slot"
|
|
538
619
|
);
|
|
539
620
|
}
|
|
621
|
+
const maxBuffer = maxBufferOpt !== undefined ? maxBufferOpt : 1024;
|
|
622
|
+
if (!isLatestWins) {
|
|
623
|
+
if (typeof maxBuffer !== "number" || !Number.isFinite(maxBuffer) || maxBuffer < 1 || (maxBuffer | 0) !== maxBuffer) {
|
|
624
|
+
throw new RangeError(
|
|
625
|
+
"lite-stream: opts.maxBuffer must be a positive integer (got " + JSON.stringify(maxBuffer) + ")"
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
540
629
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
630
|
+
if (filter !== undefined && typeof filter !== "function") {
|
|
631
|
+
throw new TypeError("lite-stream: opts.filter must be a function");
|
|
632
|
+
}
|
|
633
|
+
if (timeoutMs !== undefined) {
|
|
634
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
635
|
+
throw new RangeError(
|
|
636
|
+
"lite-stream: opts.timeout must be a finite non-negative number (got " + JSON.stringify(timeoutMs) + ")"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// --- State ---
|
|
642
|
+
// Buffer-mode ring (unchanged from 1.0.0 layout); null in latest mode.
|
|
643
|
+
const queue = isLatestWins ? null : new Array(maxBuffer);
|
|
644
|
+
let qHead = 0;
|
|
645
|
+
let qLen = 0;
|
|
646
|
+
// Latest-mode slot.
|
|
647
|
+
let pendingValue;
|
|
648
|
+
let hasPending = false;
|
|
649
|
+
// Overflow. `droppedCount` and `overflowCount` are aliases for the same
|
|
650
|
+
// counter; 1.0.0 exposed `droppedCount`, 1.1 adds `overflowCount` as the
|
|
651
|
+
// mode-neutral vocabulary. Both getters read this variable.
|
|
652
|
+
let overflowCount = 0;
|
|
653
|
+
// Multi-waiter FIFO queue (fixes the 1.0.0 single-slot bug where
|
|
654
|
+
// concurrent .next() calls silently lost the first resolver).
|
|
655
|
+
const waiters = [];
|
|
656
|
+
// Termination
|
|
546
657
|
let done = false;
|
|
658
|
+
let firstNextErr = null; // stashed for the next .next() when no waiter was pending
|
|
659
|
+
// Cleanup handles
|
|
547
660
|
let unsubscribe = null;
|
|
661
|
+
let unsubscribePending = false;
|
|
548
662
|
let abortListener = null;
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
queue[(qHead + qLen) % maxBuffer] = v;
|
|
560
|
-
qLen = (qLen + 1) | 0;
|
|
663
|
+
let timeoutId = null;
|
|
664
|
+
|
|
665
|
+
// Late-binding unsubscribe: needed when a throwing filter (or throwing
|
|
666
|
+
// subscription callback body) fires during the synchronous initial
|
|
667
|
+
// subscribe call, at which point `unsubscribe` is still null. Mirror of
|
|
668
|
+
// lite-await's stop/stopPending pattern.
|
|
669
|
+
function doUnsubscribe() {
|
|
670
|
+
if (unsubscribe !== null) {
|
|
671
|
+
unsubscribe();
|
|
672
|
+
unsubscribe = null;
|
|
561
673
|
} else {
|
|
562
|
-
|
|
563
|
-
queue[qHead] = v;
|
|
564
|
-
qHead = (qHead + 1) % maxBuffer;
|
|
565
|
-
droppedCount = (droppedCount + 1) | 0;
|
|
674
|
+
unsubscribePending = true;
|
|
566
675
|
}
|
|
567
676
|
}
|
|
568
677
|
|
|
569
|
-
function
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
678
|
+
function fullCleanup() {
|
|
679
|
+
doUnsubscribe();
|
|
680
|
+
if (timeoutId !== null) {
|
|
681
|
+
clearTimeout(timeoutId);
|
|
682
|
+
timeoutId = null;
|
|
683
|
+
}
|
|
573
684
|
if (abortListener !== null && abortSig !== undefined && abortSig !== null) {
|
|
574
685
|
abortSig.removeEventListener("abort", abortListener);
|
|
575
686
|
abortListener = null;
|
|
576
687
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
688
|
+
// Release unread values so they can be GC'd before the iterable
|
|
689
|
+
// object itself is. One-time cost per iterable lifetime.
|
|
690
|
+
pendingValue = undefined;
|
|
691
|
+
hasPending = false;
|
|
692
|
+
if (queue !== null) {
|
|
693
|
+
for (let i = 0; i < maxBuffer; i = (i + 1) | 0) queue[i] = undefined;
|
|
694
|
+
qHead = 0;
|
|
695
|
+
qLen = 0;
|
|
581
696
|
}
|
|
582
697
|
}
|
|
583
698
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
699
|
+
// Termination via error: reject all pending waiters, or stash for the
|
|
700
|
+
// next .next() call. Route for timeout and throwing-filter paths.
|
|
701
|
+
function terminateWithError(err) {
|
|
702
|
+
if (done) return;
|
|
703
|
+
done = true;
|
|
704
|
+
fullCleanup();
|
|
705
|
+
if (waiters.length > 0) {
|
|
706
|
+
while (waiters.length > 0) {
|
|
707
|
+
const w = waiters.shift();
|
|
708
|
+
w.reject(err);
|
|
709
|
+
}
|
|
587
710
|
} else {
|
|
588
|
-
|
|
589
|
-
abortSig.addEventListener("abort", abortListener);
|
|
711
|
+
firstNextErr = err;
|
|
590
712
|
}
|
|
591
713
|
}
|
|
592
714
|
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
715
|
+
// Termination via done (abort, natural end via consumer return()): resolve
|
|
716
|
+
// pending waiters with {done: true}. Preserves 1.0.0 abort-is-graceful
|
|
717
|
+
// behavior (mid-iteration abort ends for-await without throw).
|
|
718
|
+
function terminateAsDone() {
|
|
719
|
+
if (done) return;
|
|
720
|
+
done = true;
|
|
721
|
+
fullCleanup();
|
|
722
|
+
while (waiters.length > 0) {
|
|
723
|
+
const w = waiters.shift();
|
|
724
|
+
w.resolve({ value: undefined, done: true });
|
|
725
|
+
}
|
|
602
726
|
}
|
|
603
727
|
|
|
728
|
+
function enqueue(v) {
|
|
729
|
+
if (done) return;
|
|
730
|
+
// Deliver directly to the oldest waiter if any.
|
|
731
|
+
if (waiters.length > 0) {
|
|
732
|
+
const w = waiters.shift();
|
|
733
|
+
w.resolve({ value: v, done: false });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// Otherwise stash in the mode-appropriate structure.
|
|
737
|
+
if (isLatestWins) {
|
|
738
|
+
if (hasPending) overflowCount = (overflowCount + 1) | 0;
|
|
739
|
+
pendingValue = v;
|
|
740
|
+
hasPending = true;
|
|
741
|
+
} else {
|
|
742
|
+
if (qLen < maxBuffer) {
|
|
743
|
+
queue[(qHead + qLen) % maxBuffer] = v;
|
|
744
|
+
qLen = (qLen + 1) | 0;
|
|
745
|
+
} else {
|
|
746
|
+
// Drop oldest.
|
|
747
|
+
queue[qHead] = v;
|
|
748
|
+
qHead = (qHead + 1) % maxBuffer;
|
|
749
|
+
overflowCount = (overflowCount + 1) | 0;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// --- Iterable object ---
|
|
604
755
|
const iterable = {
|
|
605
756
|
[Symbol.asyncIterator]() { return this; },
|
|
606
757
|
next() {
|
|
607
|
-
if (
|
|
758
|
+
if (done) {
|
|
759
|
+
if (firstNextErr !== null) {
|
|
760
|
+
const err = firstNextErr;
|
|
761
|
+
firstNextErr = null;
|
|
762
|
+
return Promise.reject(err);
|
|
763
|
+
}
|
|
764
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
765
|
+
}
|
|
766
|
+
// Value available in slot / ring?
|
|
767
|
+
if (isLatestWins && hasPending) {
|
|
768
|
+
const v = pendingValue;
|
|
769
|
+
pendingValue = undefined;
|
|
770
|
+
hasPending = false;
|
|
771
|
+
return Promise.resolve({ value: v, done: false });
|
|
772
|
+
}
|
|
773
|
+
if (!isLatestWins && qLen > 0) {
|
|
608
774
|
const v = queue[qHead];
|
|
609
775
|
queue[qHead] = undefined;
|
|
610
776
|
qHead = (qHead + 1) % maxBuffer;
|
|
611
777
|
qLen = (qLen - 1) | 0;
|
|
612
778
|
return Promise.resolve({ value: v, done: false });
|
|
613
779
|
}
|
|
614
|
-
|
|
615
|
-
return new Promise(function (resolve) {
|
|
780
|
+
// Nothing available -- queue a waiter.
|
|
781
|
+
return new Promise(function (resolve, reject) {
|
|
782
|
+
waiters.push({ resolve: resolve, reject: reject });
|
|
783
|
+
});
|
|
616
784
|
},
|
|
617
785
|
return(value) {
|
|
618
|
-
|
|
786
|
+
if (done) return Promise.resolve({ value: undefined, done: true });
|
|
787
|
+
terminateAsDone();
|
|
619
788
|
return Promise.resolve({ value: value, done: true });
|
|
620
789
|
},
|
|
621
790
|
throw(err) {
|
|
622
|
-
|
|
791
|
+
if (done) return Promise.reject(err);
|
|
792
|
+
// Consumer-driven throw: pending waiters reject with the error,
|
|
793
|
+
// subsequent next() returns done. Same shape as terminateWithError
|
|
794
|
+
// but without stashing (throw() call site gets its own rejection).
|
|
795
|
+
done = true;
|
|
796
|
+
fullCleanup();
|
|
797
|
+
while (waiters.length > 0) {
|
|
798
|
+
const w = waiters.shift();
|
|
799
|
+
w.reject(err);
|
|
800
|
+
}
|
|
623
801
|
return Promise.reject(err);
|
|
624
802
|
},
|
|
625
|
-
get droppedCount() { return
|
|
803
|
+
get droppedCount() { return overflowCount; },
|
|
804
|
+
get overflowCount() { return overflowCount; }
|
|
626
805
|
};
|
|
627
806
|
|
|
807
|
+
if (ASYNC_DISPOSE !== null) {
|
|
808
|
+
iterable[ASYNC_DISPOSE] = function () { return iterable.return(); };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// --- Pre-aborted signal: mark done, skip all subscription/timer setup.
|
|
812
|
+
// Matches 1.0.0 behavior: first next() resolves with {done: true} rather
|
|
813
|
+
// than rejecting. ---
|
|
814
|
+
if (abortSig !== undefined && abortSig !== null && abortSig.aborted) {
|
|
815
|
+
done = true;
|
|
816
|
+
return iterable;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// --- Timeout setup ---
|
|
820
|
+
if (timeoutMs !== undefined) {
|
|
821
|
+
timeoutId = setTimeout(function () {
|
|
822
|
+
terminateWithError(new TimeoutError(timeoutMs));
|
|
823
|
+
}, timeoutMs);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// --- AbortSignal listener ---
|
|
827
|
+
if (abortSig !== undefined && abortSig !== null) {
|
|
828
|
+
abortListener = function () { terminateAsDone(); };
|
|
829
|
+
abortSig.addEventListener("abort", abortListener);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// --- Subscribe. lite-signal's subscribe fires synchronously with the
|
|
833
|
+
// current value on registration; use a flag to suppress that initial
|
|
834
|
+
// fire if emitInitial is false. Filter (if provided) applies to the
|
|
835
|
+
// initial fire when emitInitial is true. A throwing filter is routed
|
|
836
|
+
// via terminateWithError so the throw never surfaces at the signal
|
|
837
|
+
// writer's .set() call site. ---
|
|
838
|
+
let suppressedInitial = !emitInitial;
|
|
839
|
+
unsubscribe = sig.subscribe(function (v) {
|
|
840
|
+
if (done) return;
|
|
841
|
+
if (suppressedInitial) { suppressedInitial = false; return; }
|
|
842
|
+
if (filter !== undefined) {
|
|
843
|
+
let ok;
|
|
844
|
+
try {
|
|
845
|
+
ok = filter(v);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
terminateWithError(e);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (!ok) return;
|
|
851
|
+
}
|
|
852
|
+
enqueue(v);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// If terminateWithError was called from inside the synchronous initial
|
|
856
|
+
// subscribe fire (throwing filter on the initial value), honor the
|
|
857
|
+
// deferred unsubscribe now that we have the handle.
|
|
858
|
+
if (unsubscribePending && unsubscribe !== null) {
|
|
859
|
+
unsubscribe();
|
|
860
|
+
unsubscribe = null;
|
|
861
|
+
}
|
|
862
|
+
|
|
628
863
|
return iterable;
|
|
629
864
|
}
|
|
630
865
|
|
|
@@ -635,5 +870,6 @@ function toAsyncIterable(sig, opts) {
|
|
|
635
870
|
export {
|
|
636
871
|
fromAsyncIterable,
|
|
637
872
|
pipeToSignal,
|
|
638
|
-
toAsyncIterable
|
|
873
|
+
toAsyncIterable,
|
|
874
|
+
TimeoutError
|
|
639
875
|
};
|