@johnhenry/iteration 0.0.0 → 0.0.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.
Files changed (49) hide show
  1. package/dist/async-channel.d.ts +24 -4
  2. package/dist/async-channel.d.ts.map +1 -1
  3. package/dist/async-channel.js +41 -38
  4. package/dist/async-channel.js.map +1 -1
  5. package/dist/combine.d.ts +146 -0
  6. package/dist/combine.d.ts.map +1 -0
  7. package/dist/combine.js +353 -0
  8. package/dist/combine.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/internal.d.ts +66 -0
  14. package/dist/internal.d.ts.map +1 -0
  15. package/dist/internal.js +134 -0
  16. package/dist/internal.js.map +1 -0
  17. package/dist/iterator-tools.d.ts.map +1 -1
  18. package/dist/iterator-tools.js +38 -18
  19. package/dist/iterator-tools.js.map +1 -1
  20. package/dist/itertools.d.ts.map +1 -1
  21. package/dist/itertools.js +42 -24
  22. package/dist/itertools.js.map +1 -1
  23. package/dist/tee.d.ts +24 -2
  24. package/dist/tee.d.ts.map +1 -1
  25. package/dist/tee.js +175 -2
  26. package/dist/tee.js.map +1 -1
  27. package/dist/time.d.ts +69 -0
  28. package/dist/time.d.ts.map +1 -0
  29. package/dist/time.js +234 -0
  30. package/dist/time.js.map +1 -0
  31. package/dist/transduce.d.ts +49 -0
  32. package/dist/transduce.d.ts.map +1 -1
  33. package/dist/transduce.js +27 -1
  34. package/dist/transduce.js.map +1 -1
  35. package/dist/transducers.d.ts.map +1 -1
  36. package/dist/transducers.js +32 -14
  37. package/dist/transducers.js.map +1 -1
  38. package/package.json +10 -2
  39. package/readme.md +304 -14
  40. package/src/async-channel.ts +63 -47
  41. package/src/combine.ts +441 -0
  42. package/src/index.ts +2 -0
  43. package/src/internal.ts +164 -0
  44. package/src/iterator-tools.ts +35 -20
  45. package/src/itertools.ts +36 -23
  46. package/src/tee.ts +202 -2
  47. package/src/time.ts +262 -0
  48. package/src/transduce.ts +91 -1
  49. package/src/transducers.ts +34 -19
package/readme.md CHANGED
@@ -21,6 +21,23 @@ Requires **Node.js 22.12+** (or an equivalent Iterator-Helpers-capable engine).
21
21
  > [`johnhenry/math`](https://github.com/johnhenry/math) monorepo, restarted at `0.0.0` under the
22
22
  > new scoped name — the version number is fresh, the code and its lineage are not.
23
23
 
24
+ ## What's new in 0.0.1
25
+
26
+ Additions for carrying **live values and events**. Every existing export keeps
27
+ its signature and default behaviour.
28
+
29
+ - **Combinators** (`@johnhenry/iteration/combine`): `mergeAsync`,
30
+ `combineLatestAsync`, `withLatestFromAsync`, `latestAsync`, `sampleAsync`.
31
+ - **Time operators** (`@johnhenry/iteration/time`): `debounceAsync`,
32
+ `throttleAsync`, with an injectable `clock`.
33
+ - **`transducePush`**: drive transducers one value at a time, with no iterator.
34
+ - **Options:** `teeSync`/`teeAsync` take `{ limit, overflow }`, and
35
+ `AsyncChannel` takes `errors: "value" | "throw"`.
36
+ - **Fixes:** `zipSync`/`zipAsync`/`zipLongestSync`/`zipLongestAsync` now close
37
+ every input when the zip ends, errors or is exited early, and concurrent
38
+ `AsyncChannel#take()` calls are now served FIFO (a second waiting `take()`
39
+ used to replace the first, which then never resolved).
40
+
24
41
  ## What's new in 2.0
25
42
 
26
43
  - **Transducer protocol rewritten (memory-leak fix).** In the 1.x line the
@@ -161,6 +178,13 @@ for await (const pair of zipAsync([1, 2, 3], ["a", "b"])) {
161
178
  } // logs [1,'a'], [2,'b'] -- stops at the shorter input
162
179
  ```
163
180
 
181
+ When the zip ends (the shortest input runs out), when an input throws, or
182
+ when the consumer exits early, every input that has not ended is closed
183
+ with `return()`, so generator `finally` blocks and the resources they guard
184
+ are released. The same holds for `zipLongestSync`/`zipLongestAsync`. An input
185
+ that ended or threw is not closed again. `zipAsync` does not take a
186
+ `{ signal }`; wrap it in `abortable` for cancellation.
187
+
164
188
  ## Transformation: transducers
165
189
 
166
190
  This library employs transducers,
@@ -245,7 +269,10 @@ for (const x of transduceSync(sum)([1, 2, 3, 4])) {
245
269
 
246
270
  Place items into groups of size N. A trailing, under-sized group is flushed
247
271
  once the source is exhausted (via the transducer completion protocol — see
248
- `transduceSync`/`transduceAsync`).
272
+ `transduceSync`/`transduceAsync`), wherever `group` sits in the pipeline:
273
+ every built-in transducer forwards the flush to the transducers after it.
274
+ (Before 0.0.1, the stateless built-ins did not, so `map(f)` followed by
275
+ `group(2)` silently dropped the trailing group.)
249
276
 
250
277
  ```javascript
251
278
  import { group } from "@johnhenry/iteration/transducers";
@@ -376,6 +403,34 @@ for (const x of transformation([
376
403
  // logs 3, 5, 7, 9
377
404
  ```
378
405
 
406
+ ### `transducePush`: push values in, one at a time
407
+
408
+ `transducePush(...transducers)` runs the same transducers without an
409
+ iterator, for code that receives values by callback (an event handler, a
410
+ wire in a dataflow graph). It returns `{ step(value), complete(), halted }`:
411
+ `step` returns the items that value emitted, `complete()` flushes stateful
412
+ transducers (such as a trailing partial `group`), and `halted` turns true
413
+ once a transducer returns `HALT`.
414
+
415
+ ```javascript
416
+ import { transducePush, transducers } from "@johnhenry/iteration";
417
+ const { group, map } = transducers;
418
+ const pipe = transducePush(group(2), map((pair) => pair.join("+")));
419
+ socket.onmessage = (event) => pipe.step(event.data).forEach(send); // "a+b", "c+d", ...
420
+ socket.onclose = () => pipe.complete().forEach(send); // a trailing "e", if any
421
+ ```
422
+
423
+ It uses the same composition order, `.complete` flush and `HALT` as
424
+ `transduceSync`: pushing every item of `xs` and then calling `complete()`
425
+ returns exactly what `transduceSync(...)(xs)` yields. What it does **not**
426
+ do:
427
+
428
+ - It does not buffer or schedule anything: each `step` runs synchronously
429
+ and returns its output. There is no async variant; a transducer is pure.
430
+ - After `HALT`, or after `complete()`, `step` ignores its value and returns
431
+ `[]`; it does not throw. `complete()` is idempotent.
432
+ - Like `transduceSync`, it drops what the halting step emitted.
433
+
379
434
  ### Early termination: `HALT`
380
435
 
381
436
  A step may return the `HALT` sentinel to stop consumption of the source —
@@ -405,6 +460,11 @@ iterator completes by attaching a `.complete(init)` method to the step
405
460
  function they return (see `group`, above, for a worked example, and note
406
461
  that `.complete` must cascade to the inner step's own `.complete`) — see
407
462
  `reduceSync`/`reduceAsync` in `src/iterator-tools.ts` for the protocol.
463
+ A custom *stateless* transducer has nothing to flush, but must still pass
464
+ the flush on, or a stateful transducer after it never sees it: copy the
465
+ inner step's `.complete` onto the step you return
466
+ (`if (next.complete) step.complete = next.complete;`), as every built-in
467
+ does.
408
468
 
409
469
  > **Migrating from 2.0:** the old protocol threaded an *iterable*
410
470
  > accumulator that each step wrapped in a new generator via
@@ -609,6 +669,185 @@ for await (const record of prefetchAsync(8, slowDatabaseCursor())) {
609
669
  }
610
670
  ```
611
671
 
672
+ ## Live values and events (`@johnhenry/iteration/combine`, new in 0.0.1)
673
+
674
+ Combinators for sources that produce values over time: UI events, sockets,
675
+ channels, sensors. Shared rules:
676
+
677
+ - Inputs may be async or sync iterables.
678
+ - Each takes an optional `{ signal }`. On abort it rejects with the signal's
679
+ reason (an `"AbortError"` `DOMException` by default).
680
+ - Each closes (`return()`) every input that has not ended when it ends, when
681
+ an input errors (the error is rethrown), when the signal aborts, and when
682
+ the consumer exits early. An input whose `next()` is still pending is
683
+ closed without waiting for that `next()` to settle, so a stalled source
684
+ cannot block cleanup.
685
+ - Like every generator here, nothing starts until the first `next()`.
686
+
687
+ The "latest value" combinators (`combineLatestAsync`, `withLatestFromAsync`,
688
+ `latestAsync`, `sampleAsync`) drain some inputs **eagerly**: they keep
689
+ pulling whether or not the consumer is reading, and keep only the newest
690
+ value. That is the point for live values, but it means they do **not**
691
+ apply backpressure to those inputs, and a synchronous infinite iterable
692
+ given as one of those inputs would be drained forever. Give them event
693
+ sources, not generators of unbounded data.
694
+
695
+ ### `mergeAsync`
696
+
697
+ `mergeAsync(...iterables, { signal })` yields each value as it arrives from
698
+ any input, and ends when every input has ended.
699
+
700
+ ```javascript
701
+ import { mergeAsync } from "@johnhenry/iteration";
702
+ // or: import { mergeAsync } from "@johnhenry/iteration/combine";
703
+ for await (const event of mergeAsync(clicks, keypresses, { signal })) {
704
+ handle(event);
705
+ }
706
+ ```
707
+
708
+ It does **not** read ahead: each input has at most one `next()` pending, and
709
+ an input is asked for its next value only after its previous one has been
710
+ consumed. Inputs that are ready at the same moment are served in rotation,
711
+ so one fast (or synchronous) input cannot starve the rest. It does not tag
712
+ values with their input; map each input first if you need that.
713
+
714
+ ### `combineLatestAsync`
715
+
716
+ `combineLatestAsync(inputs, { signal, initial, endOn })`: `inputs` is an
717
+ array or a record of iterables, and each output is a snapshot of the same
718
+ shape holding every input's latest value. A snapshot is emitted whenever an
719
+ input yields, once every input has a value.
720
+
721
+ ```javascript
722
+ import { combineLatestAsync } from "@johnhenry/iteration";
723
+ for await (const { width, theme } of combineLatestAsync(
724
+ { width: widths, theme: themes },
725
+ { initial: { theme: "light" } },
726
+ )) {
727
+ render(width, theme);
728
+ }
729
+ ```
730
+
731
+ - **Latest wins:** while the consumer is busy, the snapshots it would have
732
+ received coalesce into one, holding the newest values.
733
+ - `initial` supplies starting values for some or all inputs. It does **not**
734
+ emit on its own: the first snapshot still waits for an input to yield.
735
+ - `endOn: "all"` (default) ends once every input has ended; `"any"` ends as
736
+ soon as one does. A snapshot still pending is delivered first. Either way,
737
+ an input that ends without ever having a value (and no `initial`) ends the
738
+ output at once, since no complete snapshot could follow.
739
+ - It does **not** emit every intermediate combination, and it does not
740
+ compare values: an input that yields the same value again still produces
741
+ a snapshot. Each snapshot is a fresh object. An input error drops a
742
+ snapshot that was still pending.
743
+
744
+ ### `withLatestFromAsync`
745
+
746
+ `withLatestFromAsync(source, others, { signal })` pairs each value of
747
+ `source` with the latest value of each of `others` (an array or a record),
748
+ yielding `[value, snapshot]`. This is the "an event samples values" pattern.
749
+
750
+ ```javascript
751
+ import { withLatestFromAsync } from "@johnhenry/iteration";
752
+ for await (const [click, { price, qty }] of withLatestFromAsync(buyClicks, { price: prices, qty: quantities })) {
753
+ placeOrder(price, qty);
754
+ }
755
+ ```
756
+
757
+ `source` is pulled as the consumer reads; `others` are drained eagerly.
758
+ Values of `others` do **not** produce output by themselves. Source values that
759
+ arrive before every one of `others` has a value are **dropped**, not
760
+ buffered. An input in `others` that ends keeps its last value. The output
761
+ ends when `source` ends.
762
+
763
+ ### `latestAsync`
764
+
765
+ `latestAsync(iterable, { signal })` conflates a source: it drains the source
766
+ eagerly and keeps only the newest value not yet read. `next()` returns that
767
+ value, or waits for the next one.
768
+
769
+ ```javascript
770
+ import { latestAsync } from "@johnhenry/iteration";
771
+ for await (const position of latestAsync(pointerMoves)) {
772
+ await expensiveRender(position); // never falls behind: stale positions are skipped
773
+ }
774
+ ```
775
+
776
+ It does **not** repeat a value: each value is returned at most once, so a
777
+ consumer faster than the source waits. It ends after the source ends and
778
+ its last value has been read. A source error is rethrown on the next read,
779
+ and an unread value is dropped. Closing it closes the source.
780
+
781
+ ### `sampleAsync`
782
+
783
+ `sampleAsync(source, trigger, { signal })`: on each `trigger` value, yields
784
+ the latest `source` value, if the source has produced one yet.
785
+
786
+ ```javascript
787
+ import { sampleAsync } from "@johnhenry/iteration";
788
+ for await (const position of sampleAsync(pointerMoves, animationFrames)) {
789
+ draw(position);
790
+ }
791
+ ```
792
+
793
+ Unlike RxJS `sample`, it does **not** skip a trigger when the source has not
794
+ changed since the last one: the same value is yielded again. Triggers that
795
+ fire before the source has a value yield nothing. The source is drained
796
+ eagerly and the trigger is pulled as the consumer reads. It ends when the
797
+ trigger ends; a source that ends keeps its last value.
798
+
799
+ ## Time operators (`@johnhenry/iteration/time`, new in 0.0.1)
800
+
801
+ `debounceAsync` and `throttleAsync` drain their source eagerly (timing
802
+ depends on when values arrive, not on when you read them) and queue what
803
+ they emit until it is read. Both take `{ clock, signal }`. On abort, a source
804
+ error or consumer exit they clear their timer and close the source.
805
+
806
+ `clock` is `{ setTimeout(callback, ms), clearTimeout(handle), now() }`, and
807
+ defaults to `systemClock` (the host's timers and `performance.now()`). Pass
808
+ your own to drive them from a test's fake clock or from a host scheduler;
809
+ `now()` must share the time base of the delays given to `setTimeout`.
810
+
811
+ ### `debounceAsync`
812
+
813
+ `debounceAsync(ms, iterable, { clock, signal })` emits a value only after
814
+ `ms` has passed with no newer value: a burst becomes its last value, `ms`
815
+ after the burst ends. When the source ends, a value still waiting is
816
+ emitted at once.
817
+
818
+ ```javascript
819
+ import { debounceAsync } from "@johnhenry/iteration";
820
+ // or: import { debounceAsync } from "@johnhenry/iteration/time";
821
+ for await (const query of debounceAsync(250, searchInputs)) {
822
+ search(query);
823
+ }
824
+ ```
825
+
826
+ It has no `leading` or `maxWait` option: nothing is emitted during a burst
827
+ that never pauses. A source error is rethrown after the values already
828
+ emitted, and a value still waiting for its quiet period is dropped.
829
+
830
+ ### `throttleAsync`
831
+
832
+ `throttleAsync(ms, iterable, { leading, trailing, clock, signal })` emits at
833
+ most one value per `ms` window. A value arriving while no window is open
834
+ opens one, and with `leading` (default `true`) is emitted at once. With
835
+ `trailing` (default `true`), the newest value that arrived during the
836
+ window is emitted when it closes, which opens the next window. When the
837
+ source ends, a trailing value still waiting is emitted at once.
838
+
839
+ ```javascript
840
+ import { throttleAsync } from "@johnhenry/iteration";
841
+ for await (const position of throttleAsync(16, pointerMoves)) {
842
+ draw(position); // at most one per 16 ms, and always the final position
843
+ }
844
+ ```
845
+
846
+ `leading` and `trailing` cannot both be `false` (a `RangeError`). The
847
+ defaults match lodash's `throttle`, not RxJS `throttleTime` (whose
848
+ `trailing` defaults to `false`). It does **not** align windows to wall-clock
849
+ boundaries, and a source error drops a trailing value still waiting.
850
+
612
851
  ## Utilities
613
852
 
614
853
  This library provides a number of iterator related utilities.
@@ -682,6 +921,28 @@ for (const x of transduce(filter((x) => x > 2))(i2)) {
682
921
  } // logs 3
683
922
  ```
684
923
 
924
+ By default a branch that falls behind buffers without limit. Pass
925
+ `{ limit, overflow }` as a second argument to bound each branch's unread
926
+ items (new in 0.0.1):
927
+
928
+ ```javascript
929
+ const [ui, log] = teeAsync(2, { limit: 100, overflow: "wait" })(events);
930
+ ```
931
+
932
+ - `"wait"` (the `teeAsync` default when a `limit` is given): a branch that
933
+ wants a new item waits until the slowest branch has room, which is true
934
+ backpressure. Read the branches concurrently: draining one to the end
935
+ while another sits unread stalls once that one holds `limit` items.
936
+ - `"drop-oldest"`: the item is delivered, and a full branch discards its
937
+ oldest unread item, so each branch keeps its newest `limit`.
938
+ - `"error"` (the `teeSync` default when a `limit` is given): the pull throws
939
+ a `RangeError` instead of growing a buffer past `limit`.
940
+
941
+ `teeSync` cannot block, so it does **not** support `"wait"` (a `TypeError`).
942
+ `limit` must be an integer `>= 1`. A branch you are not going to read should
943
+ be closed with `return()`: a closed branch stops receiving items and no
944
+ longer counts toward the limit.
945
+
685
946
  ### `AsyncChannel`
686
947
 
687
948
  AsyncChannel is an experimental primative object.
@@ -699,6 +960,30 @@ const bounded = new AsyncChannel({ limit: 100 });
699
960
  await bounded.put(item); // suspends the producer while the channel is full
700
961
  ```
701
962
 
963
+ **Concurrent takers (fixed in 0.0.1):** several `take()` calls waiting at
964
+ once are served in call order, one item each. Before 0.0.1 the channel kept
965
+ a single waiting-taker slot, so a second `take()` replaced the first, which
966
+ then never resolved. `break()` and `throw()` likewise reach only the
967
+ longest-waiting taker; the others keep waiting.
968
+
969
+ **Errors (`errors` option, new in 0.0.1):** `throw(message)` queues an error.
970
+ A `take()` already waiting when it is called always rejects. For a later
971
+ `take()`, the `errors` option decides:
972
+
973
+ ```javascript
974
+ const lenient = new AsyncChannel(); // errors: "value" (default, unchanged)
975
+ await lenient.throw("bad");
976
+ await lenient.take(); // resolves with an Error object; iteration yields it
977
+
978
+ const strict = new AsyncChannel({ errors: "throw" });
979
+ await strict.throw("bad");
980
+ await strict.take(); // rejects with the Error; `for await` throws
981
+ ```
982
+
983
+ With `"throw"`, only errors queued by `throw()` reject. An `Error` passed to
984
+ `put()` is still delivered as a value. The error does **not** close the
985
+ channel: items queued after it are still delivered.
986
+
702
987
  ```javascript
703
988
  // file://declare.mjs
704
989
  import { AsyncChannel } from "@johnhenry/iteration";
@@ -739,19 +1024,24 @@ withEmitter(c, source);
739
1024
 
740
1025
  ## Releasing
741
1026
 
742
- Publishing is release-gated in GitHub Actions, matching the deployment setup in
743
- [`johnhenry/math`](https://github.com/johnhenry/math):
744
-
745
- 1. Bump `version` in `package.json` (and run `npm install --package-lock-only`
746
- so the lockfile matches — `npm ci` fails if they diverge).
747
- 2. Merge to `main`; CI runs typecheck, tests, build and the dist/`require(esm)`
748
- smoke tests across Node 22.x and 24.x.
749
- 3. Cut a **GitHub Release**. `.github/workflows/publish.yml` re-runs the full
750
- verification, then publishes with npm provenance attestation.
751
-
752
- The publish step is idempotent (`scripts/npm-publish-if-new.mjs` exits 0 when
753
- the current version is already on the registry), so re-running a release or a
754
- manual `workflow_dispatch` is safe.
1027
+ `@johnhenry/iteration` publishes as part of the `johnhenry/math` monorepo, via
1028
+ that repo's root [Changesets](https://github.com/changesets/changesets) flow
1029
+ (`.github/workflows/release.yml`), not a standalone per-package workflow:
1030
+
1031
+ 1. Bump `version` in `package.json` and describe the change with
1032
+ `npm run changeset` (run from the `math` repo root), or bump it directly
1033
+ in a PR that includes a changeset.
1034
+ 2. Merge to `main`; the root release workflow opens or updates a
1035
+ "chore: version packages" PR.
1036
+ 3. Merge that PR. The workflow verifies (typecheck, check, test, build,
1037
+ test:build) and publishes every workspace package whose version isn't yet
1038
+ on npm — idempotent, so re-running (including manual `workflow_dispatch`)
1039
+ is safe.
1040
+
1041
+ See the root [README](../../README.md#releasing) for the full flow. The
1042
+ `scripts/npm-publish-if-new.mjs` script in this package's own directory
1043
+ predates the monorepo merge and is not what the current release workflow
1044
+ uses.
755
1045
 
756
1046
  Requires the **`NPM_TOKEN`** repository secret — a granular automation token
757
1047
  with write access to this package:
@@ -11,32 +11,25 @@
11
11
  */
12
12
  export const CHANNEL_END: unique symbol = Symbol("CHANNEL_END");
13
13
 
14
- interface InvertedPromiseParts<T> {
15
- promise: Promise<T>;
16
- resolve: (value: T) => void;
17
- reject: (reason?: unknown) => void;
18
- }
19
-
20
- /**
21
- * Creates a promise that can be resolved/rejected outside of initial closure
22
- * @kind function
23
- * @name InvertedPromise
24
- * @ignore
25
- */
26
- const InvertedPromise = <T>(): InvertedPromiseParts<T> => {
27
- const out = {} as InvertedPromiseParts<T>;
28
- out.promise = new Promise<T>((resolve, reject) => {
29
- out.resolve = resolve;
30
- out.reject = reject;
31
- });
32
- return out;
33
- };
34
-
35
14
  export interface AsyncChannelOptions<T> {
36
15
  cache?: Array<T | typeof CHANNEL_END | Error>;
37
16
  limit?: number;
38
17
  transform?: (item: T) => T | Promise<T>;
39
18
  debug?: (...args: unknown[]) => unknown;
19
+ /**
20
+ * How an error queued by `throw()` reaches a `take()` that arrives after
21
+ * it (a `take()` already waiting when `throw()` is called always rejects).
22
+ * - `"value"` (default, the pre-0.0.1 behaviour): `take()` resolves with
23
+ * the `Error` object, and async iteration yields it as an item.
24
+ * - `"throw"`: `take()` rejects with it, so async iteration throws.
25
+ */
26
+ errors?: "value" | "throw";
27
+ }
28
+
29
+ /** A pending take(), waiting for an item. */
30
+ interface Taker<T> {
31
+ resolve: (value: T | typeof CHANNEL_END) => void;
32
+ reject: (reason?: unknown) => void;
40
33
  }
41
34
 
42
35
  /**
@@ -49,9 +42,15 @@ export class AsyncChannel<T = unknown> {
49
42
  cache: Array<T | typeof CHANNEL_END | Error>;
50
43
  transform: (item: T) => T | Promise<T>;
51
44
  debug?: (...args: unknown[]) => unknown;
52
- private promise?: Promise<T | typeof CHANNEL_END>;
53
- private resolve?: (value: T | typeof CHANNEL_END) => void;
54
- private reject?: (reason?: unknown) => void;
45
+ errors: "value" | "throw";
46
+ /**
47
+ * Consumers waiting in take(), served FIFO. Before 0.0.1 this was a
48
+ * single slot, so a second concurrent take() replaced the first, which
49
+ * then never resolved.
50
+ */
51
+ private takers: Array<Taker<T>> = [];
52
+ /** Errors queued by throw() (as opposed to Error objects put() as values). */
53
+ private thrown = new WeakSet<Error>();
55
54
  /** Producers awaiting capacity, FIFO (backpressure -- see put()). */
56
55
  private putters: Array<{
57
56
  value: T | typeof CHANNEL_END | Error;
@@ -71,11 +70,29 @@ export class AsyncChannel<T = unknown> {
71
70
  * @kind function
72
71
  * @name constructor
73
72
  */
74
- constructor({ cache = [], limit = Infinity, transform = ($: T) => $, debug }: AsyncChannelOptions<T> = {}) {
73
+ constructor({
74
+ cache = [],
75
+ limit = Infinity,
76
+ transform = ($: T) => $,
77
+ debug,
78
+ errors = "value",
79
+ }: AsyncChannelOptions<T> = {}) {
75
80
  this.limit = limit;
76
81
  this.cache = cache.slice(0, limit);
77
82
  this.transform = transform;
78
83
  this.debug = debug;
84
+ this.errors = errors;
85
+ }
86
+ /**
87
+ * Hand an item that came off the cache or a producer to a take() caller:
88
+ * with `errors: "throw"`, an error queued by throw() rejects.
89
+ * @ignore
90
+ */
91
+ private deliver(value: T | typeof CHANNEL_END | Error): T | typeof CHANNEL_END {
92
+ if (this.errors === "throw" && value instanceof Error && this.thrown.has(value)) {
93
+ throw value;
94
+ }
95
+ return value as T | typeof CHANNEL_END;
79
96
  }
80
97
  /**
81
98
  * Move waiting producers' items into the cache while there is capacity,
@@ -114,8 +131,9 @@ export class AsyncChannel<T = unknown> {
114
131
  try {
115
132
  const value = await this.transform(item);
116
133
  await myTurn;
117
- if (this.promise) {
118
- this.resolve?.(value);
134
+ const taker = this.takers.shift();
135
+ if (taker) {
136
+ taker.resolve(value);
119
137
  return;
120
138
  }
121
139
  if (this.cache.length < this.limit) {
@@ -137,29 +155,23 @@ export class AsyncChannel<T = unknown> {
137
155
  async take(...debug: unknown[]): Promise<T | typeof CHANNEL_END> {
138
156
  this.debug?.("take", ...debug);
139
157
  if (this.cache.length) {
140
- const value = this.cache.shift() as T | typeof CHANNEL_END;
158
+ const value = this.cache.shift() as T | typeof CHANNEL_END | Error;
141
159
  this.drainPutters();
142
- return value;
160
+ return this.deliver(value);
143
161
  }
144
162
  if (this.putters.length) {
145
163
  // limit of 0 (rendezvous) or drained cache with waiting producers:
146
164
  // hand off directly.
147
165
  const { value, release } = this.putters.shift() as {
148
- value: T | typeof CHANNEL_END;
166
+ value: T | typeof CHANNEL_END | Error;
149
167
  release: () => void;
150
168
  };
151
169
  release();
152
- return value;
170
+ return this.deliver(value);
153
171
  }
154
- const { promise, resolve, reject } = InvertedPromise<T | typeof CHANNEL_END>();
155
- this.promise = promise;
156
- this.resolve = resolve;
157
- this.reject = reject;
158
- const value = await this.promise;
159
- delete this.promise;
160
- delete this.resolve;
161
- delete this.reject;
162
- return value;
172
+ return new Promise<T | typeof CHANNEL_END>((resolve, reject) => {
173
+ this.takers.push({ resolve, reject });
174
+ });
163
175
  }
164
176
  /**
165
177
  * Pause Asynchronous Channel
@@ -168,8 +180,9 @@ export class AsyncChannel<T = unknown> {
168
180
  */
169
181
  async break(...debug: unknown[]): Promise<void> {
170
182
  this.debug?.("break", ...debug);
171
- if (this.promise) {
172
- await this.resolve?.(CHANNEL_END);
183
+ const taker = this.takers.shift();
184
+ if (taker) {
185
+ taker.resolve(CHANNEL_END);
173
186
  } else if (this.putters.length > 0) {
174
187
  // Keep FIFO order: the end marker must not overtake producers
175
188
  // already waiting for capacity.
@@ -185,13 +198,16 @@ export class AsyncChannel<T = unknown> {
185
198
  */
186
199
  async throw(message?: string, ...debug: unknown[]): Promise<void> {
187
200
  this.debug?.("throw", ...debug);
188
- if (this.promise) {
189
- await this.reject?.(new Error(message));
201
+ const error = new Error(message);
202
+ this.thrown.add(error);
203
+ const taker = this.takers.shift();
204
+ if (taker) {
205
+ taker.reject(error);
190
206
  } else if (this.putters.length > 0) {
191
207
  // Keep FIFO order behind producers already waiting for capacity.
192
- this.putters.push({ value: new Error(message), release: () => {} });
208
+ this.putters.push({ value: error, release: () => {} });
193
209
  } else {
194
- this.cache.push(new Error(message));
210
+ this.cache.push(error);
195
211
  }
196
212
  }
197
213
  /**
@@ -201,7 +217,7 @@ export class AsyncChannel<T = unknown> {
201
217
  * Note: should this be a getter?
202
218
  */
203
219
  pending(): boolean {
204
- return !!this.promise;
220
+ return this.takers.length > 0;
205
221
  }
206
222
  /**
207
223
  * Return string representation of Asynchronous Channel