@zakkster/lite-stream 1.0.0

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/Stream.js ADDED
@@ -0,0 +1,639 @@
1
+ // @zakkster/lite-stream 1.0.0
2
+ //
3
+ // Zero-GC bridge between async iterators and @zakkster/lite-signal. The
4
+ // multi-shot dual of lite-await's fromPromise: project an async source of N
5
+ // values (paginated APIs, SSE streams, network frame queues, pubsub topics)
6
+ // into a signal-shaped reactive surface.
7
+ //
8
+ // Three cleanup paths, all structural:
9
+ // 1. Iterator natural completion ({done: true}) -> signal state goes done
10
+ // 2. Iterator throws -> signal state goes error
11
+ // 3. Caller-provided AbortSignal aborts -> iterator.return() called
12
+ //
13
+ // Two modes:
14
+ // - "latest" (default): signal value = most recent yielded value. Cheap;
15
+ // consumer reads sig().value. Best for "show the current frame / latest
16
+ // pubsub message / live cursor position".
17
+ // - "buffer": signal value = bounded ring of recent values, newest
18
+ // last. REQUIRES maxBuffer (no unbounded buffering -- that is a memory
19
+ // bug pretending to be a feature). Overflow drops oldest and increments
20
+ // droppedCount. Best for "process every helix page in order, don't miss
21
+ // events".
22
+ //
23
+ // Copyright (c) 2026 Zahary Shinikchiev <shinikchiev@yahoo.com>
24
+ // MIT License
25
+
26
+ import { signal as _signal } from "@zakkster/lite-signal";
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Internal helpers
30
+ // ---------------------------------------------------------------------------
31
+
32
+ function makeAbortError(signal) {
33
+ if (signal !== undefined && signal !== null && signal.reason !== undefined) {
34
+ return signal.reason;
35
+ }
36
+ if (typeof DOMException !== "undefined") {
37
+ return new DOMException("Aborted", "AbortError");
38
+ }
39
+ const err = new Error("Aborted");
40
+ err.name = "AbortError";
41
+ return err;
42
+ }
43
+
44
+ // Obtain an async iterator from an async iterable. Accepts both forms
45
+ // (iterator-already-iterator, or iterable-needing-Symbol.asyncIterator).
46
+ function toIterator(source) {
47
+ if (source === null || source === undefined) {
48
+ throw new TypeError("lite-stream: source must be an async iterable or iterator");
49
+ }
50
+ // Already an iterator (has next())
51
+ if (typeof source.next === "function") return source;
52
+ // Async iterable (has Symbol.asyncIterator)
53
+ if (typeof source[Symbol.asyncIterator] === "function") {
54
+ return source[Symbol.asyncIterator]();
55
+ }
56
+ // Sync iterable (has Symbol.iterator) -- accepted as a convenience.
57
+ if (typeof source[Symbol.iterator] === "function") {
58
+ return source[Symbol.iterator]();
59
+ }
60
+ throw new TypeError(
61
+ "lite-stream: source must be an async iterable, async iterator, or iterable"
62
+ );
63
+ }
64
+
65
+ // Best-effort iterator close. Async iterators may expose `return()` to signal
66
+ // "no more values will be pulled"; calling it releases resources held by the
67
+ // generator. We swallow any error -- the consumer already moved on.
68
+ function closeIterator(iter) {
69
+ if (iter !== null && typeof iter.return === "function") {
70
+ try {
71
+ const ret = iter.return();
72
+ // `return()` returns a Promise. We don't await it; we just need
73
+ // to give the generator a chance to clean up. Attach a noop catch
74
+ // so an unhandled rejection doesn't blow up the process.
75
+ if (ret !== undefined && typeof ret.then === "function") {
76
+ ret.then(undefined, () => {});
77
+ }
78
+ } catch (_e) { /* swallowed: cleanup is best-effort */ }
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // State shapes (per-mode)
84
+ // ---------------------------------------------------------------------------
85
+ //
86
+ // "latest" mode signal value:
87
+ // { value, count, done, error }
88
+ //
89
+ // "buffer" mode signal value:
90
+ // { values, count, droppedCount, done, error }
91
+ //
92
+ // We allocate ONE state object per signal lifecycle and mutate-in-place via
93
+ // `signal.set(state)`. But lite-signal compares with Object.is; mutating the
94
+ // same object would cause subsequent set()s to be no-ops. So we shallow-clone
95
+ // on every update. That clone is the only per-yield JS-heap allocation in the
96
+ // hot path (besides the iterator's own intrinsic cost).
97
+ //
98
+ // For "buffer" mode, the underlying `values` array is the user-facing snapshot;
99
+ // we allocate a fresh array on overflow and on each set to avoid in-place
100
+ // mutation that would be visible to past observers. The ring buffer beneath
101
+ // the snapshot is a fixed-size Array that we cycle through.
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // fromAsyncIterable
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * Drive a signal from an async iterator. Returns a Signal whose value is a
109
+ * tagged state object reflecting the iterator's lifecycle.
110
+ *
111
+ * In "latest" mode (default), the signal value is the most recent yielded
112
+ * value. In "buffer" mode, the signal value is a bounded array of recent
113
+ * values (newest last); `maxBuffer` is REQUIRED -- unbounded buffering is
114
+ * rejected.
115
+ *
116
+ * Termination on any of:
117
+ * - iterator natural completion ({done: true})
118
+ * - iterator throws
119
+ * - opts.signal aborts (the iterator's return() is called best-effort)
120
+ *
121
+ * To stop the stream without an AbortSignal, you can either:
122
+ * - dispose the returned signal via lite-signal's dispose(sig); pending
123
+ * iterator pulls will see the signal as disposed on next set() (no-op),
124
+ * but the iterator continues until natural completion. PREFER AbortSignal.
125
+ * - pass opts.signal and abort it externally.
126
+ *
127
+ * @template T
128
+ * @param {AsyncIterable<T> | AsyncIterator<T> | Iterable<T>} source
129
+ * @param {{
130
+ * mode?: "latest" | "buffer",
131
+ * maxBuffer?: number,
132
+ * initial?: T,
133
+ * signal?: AbortSignal,
134
+ * onError?: (err: unknown) => void,
135
+ * onDone?: () => void
136
+ * }} [opts]
137
+ * @returns {import("@zakkster/lite-signal").Signal<unknown>}
138
+ */
139
+ function fromAsyncIterable(source, opts) {
140
+ const mode = (opts !== undefined && opts !== null) ? (opts.mode || "latest") : "latest";
141
+ const maxBuffer = (opts !== undefined && opts !== null) ? opts.maxBuffer : undefined;
142
+ const initial = (opts !== undefined && opts !== null) ? opts.initial : undefined;
143
+ const abortSig = (opts !== undefined && opts !== null) ? opts.signal : undefined;
144
+ const onError = (opts !== undefined && opts !== null) ? opts.onError : undefined;
145
+ const onDone = (opts !== undefined && opts !== null) ? opts.onDone : undefined;
146
+
147
+ if (mode !== "latest" && mode !== "buffer") {
148
+ throw new TypeError(
149
+ "lite-stream: opts.mode must be \"latest\" or \"buffer\" (got " + JSON.stringify(mode) + ")"
150
+ );
151
+ }
152
+ if (mode === "buffer") {
153
+ if (typeof maxBuffer !== "number" || !Number.isFinite(maxBuffer) || maxBuffer < 1 || (maxBuffer | 0) !== maxBuffer) {
154
+ throw new RangeError(
155
+ "lite-stream: \"buffer\" mode requires opts.maxBuffer to be a positive integer "
156
+ + "(got " + JSON.stringify(maxBuffer) + "). Unbounded buffering is a memory "
157
+ + "bug pretending to be a feature; pick a deliberate ceiling."
158
+ );
159
+ }
160
+ }
161
+
162
+ // Initial state.
163
+ let state;
164
+ if (mode === "latest") {
165
+ state = { value: initial, count: 0, done: false, error: undefined };
166
+ } else {
167
+ state = { values: [], count: 0, droppedCount: 0, done: false, error: undefined };
168
+ }
169
+ const sig = _signal(state);
170
+ // Track current state locally so we never depend on sig.peek() in the
171
+ // pump's resolve handler. If the consumer disposes the signal between
172
+ // construction and the next pull, sig.peek() returns undefined; reading
173
+ // .count or .droppedCount off undefined would crash the pump and surface
174
+ // as an unhandledRejection. We mutate lastState in lockstep with sig.set()
175
+ // and use a guarded write helper to short-circuit when the signal has
176
+ // been disposed externally.
177
+ let lastState = state;
178
+
179
+ // Pre-aborted: settle synchronously, never start the iterator.
180
+ if (abortSig !== undefined && abortSig !== null && abortSig.aborted) {
181
+ const err = makeAbortError(abortSig);
182
+ const errState = makeErrorState(lastState, err, mode);
183
+ lastState = errState;
184
+ try { sig.set(errState); } catch (_e) {}
185
+ if (onError !== undefined) {
186
+ try { onError(err); } catch (_e) {}
187
+ }
188
+ return sig;
189
+ }
190
+
191
+ // Invalid source -- throw synchronously. Construction-time input errors
192
+ // are a programmer bug; surfacing them via signal state would just defer
193
+ // a crash to a later read.
194
+ const iter = toIterator(source);
195
+
196
+ let stopped = false;
197
+ let abortListener = null;
198
+
199
+ const cleanup = function () {
200
+ if (abortListener !== null && abortSig !== undefined && abortSig !== null) {
201
+ abortSig.removeEventListener("abort", abortListener);
202
+ abortListener = null;
203
+ }
204
+ };
205
+
206
+ const stop = function (cause) {
207
+ if (stopped) return;
208
+ stopped = true;
209
+ cleanup();
210
+ closeIterator(iter);
211
+ // `cause` is null on natural done; an Error otherwise.
212
+ let terminalState;
213
+ if (cause === null) {
214
+ terminalState = makeDoneState(lastState, mode);
215
+ } else {
216
+ terminalState = makeErrorState(lastState, cause, mode);
217
+ }
218
+ lastState = terminalState;
219
+ // sig.set may throw or no-op if the signal was disposed externally;
220
+ // we've already recorded the terminal state in lastState.
221
+ try { sig.set(terminalState); } catch (_e) {}
222
+ if (cause === null) {
223
+ if (onDone !== undefined) {
224
+ try { onDone(); } catch (_e) {}
225
+ }
226
+ } else {
227
+ if (onError !== undefined) {
228
+ try { onError(cause); } catch (_e) {}
229
+ }
230
+ }
231
+ };
232
+
233
+ if (abortSig !== undefined && abortSig !== null) {
234
+ abortListener = function () { stop(makeAbortError(abortSig)); };
235
+ abortSig.addEventListener("abort", abortListener);
236
+ }
237
+
238
+ // Ring buffer for "buffer" mode. Fixed-size, indexed via a head pointer
239
+ // that wraps. On overflow, head advances over the oldest slot. The
240
+ // user-facing `values` snapshot is rebuilt on each yield by reading the
241
+ // ring in order; cheap because maxBuffer is a chosen ceiling.
242
+ let ring = null;
243
+ let ringHead = 0;
244
+ let ringLen = 0;
245
+ if (mode === "buffer") {
246
+ ring = new Array(maxBuffer);
247
+ }
248
+
249
+ // The pump. Pull values until done/error/abort. We use a recursive .then
250
+ // chain rather than for-await so that we can hand a stable handle to the
251
+ // iterator and bail synchronously on stop without leaving a dangling
252
+ // for-await waiter.
253
+ function pump() {
254
+ if (stopped) return;
255
+ let nextPromise;
256
+ try {
257
+ nextPromise = iter.next();
258
+ } catch (err) {
259
+ // Synchronous throw from .next() (rare; mostly sync iterators
260
+ // adapted into the async path).
261
+ stop(err);
262
+ return;
263
+ }
264
+ Promise.resolve(nextPromise).then(
265
+ function (result) {
266
+ if (stopped) return;
267
+ if (result === null || typeof result !== "object") {
268
+ stop(new TypeError("lite-stream: iterator.next() returned non-object"));
269
+ return;
270
+ }
271
+ if (result.done === true) {
272
+ stop(null);
273
+ return;
274
+ }
275
+ const v = result.value;
276
+ // Build the new state from lastState (never sig.peek), then
277
+ // commit to both lastState and sig. If sig.set throws (consumer
278
+ // disposed the signal externally), bail and tear down the
279
+ // iterator so we don't keep pulling into the void.
280
+ let newState;
281
+ if (mode === "latest") {
282
+ newState = {
283
+ value: v,
284
+ count: (lastState.count + 1) | 0,
285
+ done: false,
286
+ error: undefined
287
+ };
288
+ } else {
289
+ // Buffer mode: push into ring; build snapshot array.
290
+ let dropped = lastState.droppedCount;
291
+ if (ringLen < maxBuffer) {
292
+ ring[(ringHead + ringLen) % maxBuffer] = v;
293
+ ringLen = (ringLen + 1) | 0;
294
+ } else {
295
+ // Overwrite at ringHead (oldest); advance head.
296
+ ring[ringHead] = v;
297
+ ringHead = (ringHead + 1) % maxBuffer;
298
+ dropped = (dropped + 1) | 0;
299
+ }
300
+ // Snapshot: a fresh array in newest-last order.
301
+ const values = new Array(ringLen);
302
+ for (let i = 0; i < ringLen; i = (i + 1) | 0) {
303
+ values[i] = ring[(ringHead + i) % maxBuffer];
304
+ }
305
+ newState = {
306
+ values: values,
307
+ count: (lastState.count + 1) | 0,
308
+ droppedCount: dropped,
309
+ done: false,
310
+ error: undefined
311
+ };
312
+ }
313
+ lastState = newState;
314
+ try {
315
+ sig.set(newState);
316
+ } catch (_e) {
317
+ // Signal disposed externally. Tear down quietly without
318
+ // firing onError -- this is consumer-initiated cleanup,
319
+ // not a stream error.
320
+ stopped = true;
321
+ cleanup();
322
+ closeIterator(iter);
323
+ return;
324
+ }
325
+ // Defer the next pull through the microtask queue so back-to-back
326
+ // synchronous yields don't blow the stack.
327
+ pump();
328
+ },
329
+ function (err) {
330
+ stop(err);
331
+ }
332
+ );
333
+ }
334
+
335
+ // Kick off. queueMicrotask to defer the first pull until after the caller
336
+ // can subscribe; we don't want a synchronous yield to fire before the
337
+ // caller's effect() is wired.
338
+ queueMicrotask(pump);
339
+
340
+ return sig;
341
+ }
342
+
343
+ // Build a terminal state object preserving the most recent value/values.
344
+ function makeDoneState(prevState, mode) {
345
+ if (mode === "latest") {
346
+ return {
347
+ value: prevState.value,
348
+ count: prevState.count,
349
+ done: true,
350
+ error: undefined
351
+ };
352
+ }
353
+ return {
354
+ values: prevState.values,
355
+ count: prevState.count,
356
+ droppedCount: prevState.droppedCount,
357
+ done: true,
358
+ error: undefined
359
+ };
360
+ }
361
+
362
+ function makeErrorState(prevState, err, mode) {
363
+ if (mode === "latest") {
364
+ return {
365
+ value: prevState.value,
366
+ count: prevState.count,
367
+ done: true,
368
+ error: err
369
+ };
370
+ }
371
+ return {
372
+ values: prevState.values,
373
+ count: prevState.count,
374
+ droppedCount: prevState.droppedCount,
375
+ done: true,
376
+ error: err
377
+ };
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // pipeToSignal
382
+ // ---------------------------------------------------------------------------
383
+
384
+ /**
385
+ * Lower-level companion to `fromAsyncIterable`: pump an existing signal from
386
+ * an async iterator. The signal's value is replaced directly with each yielded
387
+ * value (no wrapper state). Returns a `stop` function that ends the pump,
388
+ * calls iterator.return(), and removes the abort listener.
389
+ *
390
+ * Use this when:
391
+ * - You already have a signal you want to drive
392
+ * - You don't want the {value, count, done, error} wrapper
393
+ * - You want a stop fn instead of an AbortController for cleanup
394
+ *
395
+ * NOTE: this does NOT dispose the target signal. The caller owns its lifetime.
396
+ *
397
+ * @template T
398
+ * @param {AsyncIterable<T> | AsyncIterator<T>} source
399
+ * @param {import("@zakkster/lite-signal").Signal<T>} target
400
+ * @param {{
401
+ * signal?: AbortSignal,
402
+ * onError?: (err: unknown) => void,
403
+ * onDone?: () => void,
404
+ * transform?: (value: T) => T
405
+ * }} [opts]
406
+ * @returns {() => void} stop fn (idempotent)
407
+ */
408
+ function pipeToSignal(source, target, opts) {
409
+ if (target === null || target === undefined || typeof target.set !== "function") {
410
+ throw new TypeError("lite-stream: pipeToSignal target must be a writable signal");
411
+ }
412
+ const abortSig = (opts !== undefined && opts !== null) ? opts.signal : undefined;
413
+ const onError = (opts !== undefined && opts !== null) ? opts.onError : undefined;
414
+ const onDone = (opts !== undefined && opts !== null) ? opts.onDone : undefined;
415
+ const transform = (opts !== undefined && opts !== null) ? opts.transform : undefined;
416
+
417
+ if (abortSig !== undefined && abortSig !== null && abortSig.aborted) {
418
+ if (onError !== undefined) {
419
+ try { onError(makeAbortError(abortSig)); } catch (_e) {}
420
+ }
421
+ return function noopStop() {};
422
+ }
423
+
424
+ const iter = toIterator(source);
425
+ let stopped = false;
426
+ let abortListener = null;
427
+
428
+ const stop = function () {
429
+ if (stopped) return;
430
+ stopped = true;
431
+ if (abortListener !== null && abortSig !== undefined && abortSig !== null) {
432
+ abortSig.removeEventListener("abort", abortListener);
433
+ abortListener = null;
434
+ }
435
+ closeIterator(iter);
436
+ };
437
+
438
+ if (abortSig !== undefined && abortSig !== null) {
439
+ abortListener = function () {
440
+ const err = makeAbortError(abortSig);
441
+ stop();
442
+ if (onError !== undefined) {
443
+ try { onError(err); } catch (_e) {}
444
+ }
445
+ };
446
+ abortSig.addEventListener("abort", abortListener);
447
+ }
448
+
449
+ function pump() {
450
+ if (stopped) return;
451
+ let nextPromise;
452
+ try { nextPromise = iter.next(); }
453
+ catch (err) {
454
+ stop();
455
+ if (onError !== undefined) {
456
+ try { onError(err); } catch (_e) {}
457
+ }
458
+ return;
459
+ }
460
+ Promise.resolve(nextPromise).then(
461
+ function (result) {
462
+ if (stopped) return;
463
+ if (result === null || typeof result !== "object") {
464
+ stop();
465
+ if (onError !== undefined) {
466
+ try { onError(new TypeError("lite-stream: iterator.next() returned non-object")); } catch (_e) {}
467
+ }
468
+ return;
469
+ }
470
+ if (result.done === true) {
471
+ stop();
472
+ if (onDone !== undefined) {
473
+ try { onDone(); } catch (_e) {}
474
+ }
475
+ return;
476
+ }
477
+ try {
478
+ target.set(transform === undefined ? result.value : transform(result.value));
479
+ } catch (err) {
480
+ stop();
481
+ if (onError !== undefined) {
482
+ try { onError(err); } catch (_e) {}
483
+ }
484
+ return;
485
+ }
486
+ pump();
487
+ },
488
+ function (err) {
489
+ stop();
490
+ if (onError !== undefined) {
491
+ try { onError(err); } catch (_e) {}
492
+ }
493
+ }
494
+ );
495
+ }
496
+
497
+ queueMicrotask(pump);
498
+ return stop;
499
+ }
500
+
501
+ // ---------------------------------------------------------------------------
502
+ // toAsyncIterable -- the reverse direction
503
+ // ---------------------------------------------------------------------------
504
+
505
+ /**
506
+ * Yield signal changes as an async iterable. Each change to the signal
507
+ * resolves a pending `next()` call. If the consumer is slower than the
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).
512
+ *
513
+ * The iterator naturally completes when `opts.signal` aborts; iterator.return()
514
+ * resolves cleanly.
515
+ *
516
+ * @template T
517
+ * @param {import("@zakkster/lite-signal").Signal<T> | import("@zakkster/lite-signal").Computed<T>} sig
518
+ * @param {{
519
+ * signal?: AbortSignal,
520
+ * emitInitial?: boolean, // default true: yield the current value first
521
+ * maxBuffer?: number // default 1024
522
+ * }} [opts]
523
+ * @returns {AsyncIterable<T> & { readonly droppedCount: number }}
524
+ */
525
+ function toAsyncIterable(sig, opts) {
526
+ if (sig === null || sig === undefined || typeof sig.subscribe !== "function" || typeof sig.peek !== "function") {
527
+ throw new TypeError(
528
+ "lite-stream: toAsyncIterable expects a readable lite-signal (Signal/Computed with .peek and .subscribe)"
529
+ );
530
+ }
531
+ const abortSig = (opts !== undefined && opts !== null) ? opts.signal : undefined;
532
+ const emitInitial = (opts !== undefined && opts !== null && opts.emitInitial === false) ? false : true;
533
+ const maxBuffer = (opts !== undefined && opts !== null && opts.maxBuffer !== undefined) ? opts.maxBuffer : 1024;
534
+
535
+ if (typeof maxBuffer !== "number" || !Number.isFinite(maxBuffer) || maxBuffer < 1 || (maxBuffer | 0) !== maxBuffer) {
536
+ throw new RangeError(
537
+ "lite-stream: opts.maxBuffer must be a positive integer (got " + JSON.stringify(maxBuffer) + ")"
538
+ );
539
+ }
540
+
541
+ const queue = new Array(maxBuffer);
542
+ let qHead = 0;
543
+ let qLen = 0;
544
+ let droppedCount = 0;
545
+ let pendingResolve = null;
546
+ let done = false;
547
+ let unsubscribe = null;
548
+ let abortListener = null;
549
+
550
+ function enqueue(v) {
551
+ if (done) return;
552
+ if (pendingResolve !== null) {
553
+ const r = pendingResolve;
554
+ pendingResolve = null;
555
+ r({ value: v, done: false });
556
+ return;
557
+ }
558
+ if (qLen < maxBuffer) {
559
+ queue[(qHead + qLen) % maxBuffer] = v;
560
+ qLen = (qLen + 1) | 0;
561
+ } else {
562
+ // Drop oldest.
563
+ queue[qHead] = v;
564
+ qHead = (qHead + 1) % maxBuffer;
565
+ droppedCount = (droppedCount + 1) | 0;
566
+ }
567
+ }
568
+
569
+ function teardown() {
570
+ if (done) return;
571
+ done = true;
572
+ if (unsubscribe !== null) { unsubscribe(); unsubscribe = null; }
573
+ if (abortListener !== null && abortSig !== undefined && abortSig !== null) {
574
+ abortSig.removeEventListener("abort", abortListener);
575
+ abortListener = null;
576
+ }
577
+ if (pendingResolve !== null) {
578
+ const r = pendingResolve;
579
+ pendingResolve = null;
580
+ r({ value: undefined, done: true });
581
+ }
582
+ }
583
+
584
+ if (abortSig !== undefined && abortSig !== null) {
585
+ if (abortSig.aborted) {
586
+ done = true;
587
+ } else {
588
+ abortListener = function () { teardown(); };
589
+ abortSig.addEventListener("abort", abortListener);
590
+ }
591
+ }
592
+
593
+ // Subscribe to signal changes. lite-signal's subscribe fires once
594
+ // synchronously with the current value when called; we use a flag to
595
+ // suppress that initial fire if emitInitial is false.
596
+ let suppressedInitial = !emitInitial;
597
+ if (!done) {
598
+ unsubscribe = sig.subscribe(function (v) {
599
+ if (suppressedInitial) { suppressedInitial = false; return; }
600
+ enqueue(v);
601
+ });
602
+ }
603
+
604
+ const iterable = {
605
+ [Symbol.asyncIterator]() { return this; },
606
+ next() {
607
+ if (qLen > 0) {
608
+ const v = queue[qHead];
609
+ queue[qHead] = undefined;
610
+ qHead = (qHead + 1) % maxBuffer;
611
+ qLen = (qLen - 1) | 0;
612
+ return Promise.resolve({ value: v, done: false });
613
+ }
614
+ if (done) return Promise.resolve({ value: undefined, done: true });
615
+ return new Promise(function (resolve) { pendingResolve = resolve; });
616
+ },
617
+ return(value) {
618
+ teardown();
619
+ return Promise.resolve({ value: value, done: true });
620
+ },
621
+ throw(err) {
622
+ teardown();
623
+ return Promise.reject(err);
624
+ },
625
+ get droppedCount() { return droppedCount; }
626
+ };
627
+
628
+ return iterable;
629
+ }
630
+
631
+ // ---------------------------------------------------------------------------
632
+ // Exports
633
+ // ---------------------------------------------------------------------------
634
+
635
+ export {
636
+ fromAsyncIterable,
637
+ pipeToSignal,
638
+ toAsyncIterable
639
+ };