@johnhenry/iteration 0.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.
Files changed (88) hide show
  1. package/MIT-LICENSE.txt +20 -0
  2. package/dist/abort.d.ts +26 -0
  3. package/dist/abort.d.ts.map +1 -0
  4. package/dist/abort.js +69 -0
  5. package/dist/abort.js.map +1 -0
  6. package/dist/assertions/eventualequal.d.ts +9 -0
  7. package/dist/assertions/eventualequal.d.ts.map +1 -0
  8. package/dist/assertions/eventualequal.js +30 -0
  9. package/dist/assertions/eventualequal.js.map +1 -0
  10. package/dist/async-channel.d.ts +101 -0
  11. package/dist/async-channel.d.ts.map +1 -0
  12. package/dist/async-channel.js +208 -0
  13. package/dist/async-channel.js.map +1 -0
  14. package/dist/channel-decorators.d.ts +23 -0
  15. package/dist/channel-decorators.d.ts.map +1 -0
  16. package/dist/channel-decorators.js +31 -0
  17. package/dist/channel-decorators.js.map +1 -0
  18. package/dist/concurrency.d.ts +43 -0
  19. package/dist/concurrency.d.ts.map +1 -0
  20. package/dist/concurrency.js +145 -0
  21. package/dist/concurrency.js.map +1 -0
  22. package/dist/consumers.d.ts +199 -0
  23. package/dist/consumers.d.ts.map +1 -0
  24. package/dist/consumers.js +349 -0
  25. package/dist/consumers.js.map +1 -0
  26. package/dist/count.d.ts +37 -0
  27. package/dist/count.d.ts.map +1 -0
  28. package/dist/count.js +70 -0
  29. package/dist/count.js.map +1 -0
  30. package/dist/empty-iterator.d.ts +15 -0
  31. package/dist/empty-iterator.d.ts.map +1 -0
  32. package/dist/empty-iterator.js +16 -0
  33. package/dist/empty-iterator.js.map +1 -0
  34. package/dist/eventualequal.d.ts +2 -0
  35. package/dist/eventualequal.d.ts.map +1 -0
  36. package/dist/eventualequal.js +54 -0
  37. package/dist/eventualequal.js.map +1 -0
  38. package/dist/exhaust.d.ts +7 -0
  39. package/dist/exhaust.d.ts.map +1 -0
  40. package/dist/exhaust.js +22 -0
  41. package/dist/exhaust.js.map +1 -0
  42. package/dist/index.d.ts +16 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +16 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/is-iterator.d.ts +5 -0
  47. package/dist/is-iterator.d.ts.map +1 -0
  48. package/dist/is-iterator.js +5 -0
  49. package/dist/is-iterator.js.map +1 -0
  50. package/dist/iterator-tools.d.ts +164 -0
  51. package/dist/iterator-tools.d.ts.map +1 -0
  52. package/dist/iterator-tools.js +231 -0
  53. package/dist/iterator-tools.js.map +1 -0
  54. package/dist/itertools.d.ts +237 -0
  55. package/dist/itertools.d.ts.map +1 -0
  56. package/dist/itertools.js +471 -0
  57. package/dist/itertools.js.map +1 -0
  58. package/dist/tee.d.ts +18 -0
  59. package/dist/tee.d.ts.map +1 -0
  60. package/dist/tee.js +118 -0
  61. package/dist/tee.js.map +1 -0
  62. package/dist/transduce.d.ts +79 -0
  63. package/dist/transduce.d.ts.map +1 -0
  64. package/dist/transduce.js +37 -0
  65. package/dist/transduce.js.map +1 -0
  66. package/dist/transducers.d.ts +123 -0
  67. package/dist/transducers.d.ts.map +1 -0
  68. package/dist/transducers.js +212 -0
  69. package/dist/transducers.js.map +1 -0
  70. package/package.json +122 -0
  71. package/readme.md +761 -0
  72. package/src/abort.ts +80 -0
  73. package/src/assertions/eventualequal.ts +36 -0
  74. package/src/async-channel.ts +228 -0
  75. package/src/channel-decorators.ts +49 -0
  76. package/src/concurrency.ts +168 -0
  77. package/src/consumers.ts +425 -0
  78. package/src/count.ts +72 -0
  79. package/src/empty-iterator.ts +17 -0
  80. package/src/eventualequal.ts +54 -0
  81. package/src/exhaust.ts +26 -0
  82. package/src/index.ts +17 -0
  83. package/src/is-iterator.ts +9 -0
  84. package/src/iterator-tools.ts +290 -0
  85. package/src/itertools.ts +518 -0
  86. package/src/tee.ts +136 -0
  87. package/src/transduce.ts +161 -0
  88. package/src/transducers.ts +256 -0
package/src/abort.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * AbortSignal plumbing shared by the async consumers (consumers.ts),
3
+ * transduceAsync (transduce.ts), and the concurrency helpers
4
+ * (concurrency.ts).
5
+ */
6
+
7
+ /** Options bag accepted by cancellable async operations. */
8
+ export interface SignalOptions {
9
+ signal?: AbortSignal;
10
+ }
11
+
12
+ /**
13
+ * The error an aborted operation rejects with: the signal's own `reason`
14
+ * when set (AbortController.abort() defaults it to an "AbortError"
15
+ * DOMException), otherwise a fresh "AbortError" DOMException.
16
+ * @ignore
17
+ */
18
+ const abortError = (signal: AbortSignal): unknown =>
19
+ signal.reason ?? new DOMException("This operation was aborted", "AbortError");
20
+
21
+ /** Throw the signal's abort reason if it has already aborted. */
22
+ export const throwIfAborted = (signal?: AbortSignal): void => {
23
+ if (signal?.aborted) {
24
+ throw abortError(signal);
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Wrap an (a)sync iterable so that iteration rejects promptly with an
30
+ * AbortError when `signal` aborts -- even while awaiting a slow source --
31
+ * and `iterator.return()` is propagated to the source on abort, error, or
32
+ * early consumer exit. With no signal, delegates straight through.
33
+ *
34
+ * Note: on abort the source's `return()` is invoked but deliberately not
35
+ * awaited -- an async generator queues `return()` behind its in-flight
36
+ * `next()`, so awaiting it could postpone the rejection indefinitely on a
37
+ * hung source (the very case aborting is for).
38
+ * @kind function
39
+ * @name abortable
40
+ */
41
+ export const abortable = async function* <T>(
42
+ iterable: AsyncIterable<T> | Iterable<T>,
43
+ signal?: AbortSignal,
44
+ ): AsyncGenerator<T> {
45
+ if (!signal) {
46
+ yield* iterable;
47
+ return;
48
+ }
49
+ throwIfAborted(signal);
50
+ const it =
51
+ Symbol.asyncIterator in Object(iterable)
52
+ ? (iterable as AsyncIterable<T>)[Symbol.asyncIterator]()
53
+ : (iterable as Iterable<T>)[Symbol.iterator]();
54
+ let onAbort: () => void = () => {};
55
+ const aborted = new Promise<never>((_, reject) => {
56
+ onAbort = () => reject(abortError(signal));
57
+ signal.addEventListener("abort", onAbort, { once: true });
58
+ });
59
+ // Pre-handle so an abort that never races (e.g. after completion, before
60
+ // listener removal) can't surface as an unhandled rejection.
61
+ aborted.catch(() => {});
62
+ let done = false;
63
+ try {
64
+ while (true) {
65
+ const result = await Promise.race([Promise.resolve(it.next()), aborted]);
66
+ if (result.done) {
67
+ done = true;
68
+ return;
69
+ }
70
+ yield result.value;
71
+ }
72
+ } finally {
73
+ signal.removeEventListener("abort", onAbort);
74
+ if (!done && it.return) {
75
+ // Close the source on abort, error, or early consumer exit --
76
+ // fire-and-forget; see note above.
77
+ Promise.resolve(it.return()).catch(() => {});
78
+ }
79
+ }
80
+ };
@@ -0,0 +1,36 @@
1
+ // Historically this module was a pop-quiz assertion and imported
2
+ // pop-quiz/testerror. pop-quiz is no longer a dependency (the test suite
3
+ // moved to node:test), so a structurally-compatible TestError is inlined
4
+ // here to keep the "@johnhenry/iteration/pop-quiz/asserteventualequal" subpath
5
+ // export working for existing consumers: it extends Error, exposes the
6
+ // details on `.val`, and is iterable over the [key, value] entries --
7
+ // exactly the shape pop-quiz@1's TestError had.
8
+ import { eventualequal } from "../eventualequal.ts";
9
+
10
+ export class TestError extends Error {
11
+ val: Record<string, unknown>;
12
+ constructor(message: string, val: Record<string, unknown> = {}) {
13
+ super(message);
14
+ this.val = val;
15
+ }
16
+ [Symbol.iterator](): IterableIterator<[string, unknown]> {
17
+ return Object.entries(this.val).values();
18
+ }
19
+ }
20
+
21
+ export const DefaultMessage = "should eventually be deep equal";
22
+ export default async (
23
+ actual: unknown,
24
+ expected: unknown,
25
+ message: string = DefaultMessage,
26
+ operator = "eventualequal",
27
+ ): Promise<string | TestError> => {
28
+ if (await eventualequal(actual, expected)) {
29
+ return message;
30
+ }
31
+ return new TestError(message, {
32
+ actual: actual,
33
+ expected: expected,
34
+ operator,
35
+ });
36
+ };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Asynchronous Channel
3
+ * @kind namespace
4
+ * @name AsyncChannel
5
+ */
6
+
7
+ /**
8
+ * Constant signaling channel's end
9
+ * @kind constant
10
+ * @name CHANNEL_END
11
+ */
12
+ export const CHANNEL_END: unique symbol = Symbol("CHANNEL_END");
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
+ export interface AsyncChannelOptions<T> {
36
+ cache?: Array<T | typeof CHANNEL_END | Error>;
37
+ limit?: number;
38
+ transform?: (item: T) => T | Promise<T>;
39
+ debug?: (...args: unknown[]) => unknown;
40
+ }
41
+
42
+ /**
43
+ * Asynchronous Channel class
44
+ * @kind class
45
+ * @name AsyncChannel
46
+ */
47
+ export class AsyncChannel<T = unknown> {
48
+ limit: number;
49
+ cache: Array<T | typeof CHANNEL_END | Error>;
50
+ transform: (item: T) => T | Promise<T>;
51
+ 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;
55
+ /** Producers awaiting capacity, FIFO (backpressure -- see put()). */
56
+ private putters: Array<{
57
+ value: T | typeof CHANNEL_END | Error;
58
+ release: () => void;
59
+ }> = [];
60
+ /**
61
+ * Serializes delivery of concurrent put() calls into call order. Each
62
+ * put() grabs the current tail synchronously (before any `await`, so
63
+ * position is fixed by call order rather than by how long e.g.
64
+ * `transform()` happens to take) and only commits its value once its
65
+ * predecessor has committed.
66
+ */
67
+ private putOrder: Promise<void> = Promise.resolve();
68
+
69
+ /**
70
+ * Asynchronous Channel constructor
71
+ * @kind function
72
+ * @name constructor
73
+ */
74
+ constructor({ cache = [], limit = Infinity, transform = ($: T) => $, debug }: AsyncChannelOptions<T> = {}) {
75
+ this.limit = limit;
76
+ this.cache = cache.slice(0, limit);
77
+ this.transform = transform;
78
+ this.debug = debug;
79
+ }
80
+ /**
81
+ * Move waiting producers' items into the cache while there is capacity,
82
+ * releasing their pending put() promises (FIFO).
83
+ * @ignore
84
+ */
85
+ private drainPutters(): void {
86
+ while (this.putters.length > 0 && this.cache.length < this.limit) {
87
+ const { value, release } = this.putters.shift() as {
88
+ value: T | typeof CHANNEL_END | Error;
89
+ release: () => void;
90
+ };
91
+ this.cache.push(value);
92
+ release();
93
+ }
94
+ }
95
+ /**
96
+ * Put item onto Asynchronous Channel. Returns a Promise<void> that
97
+ * resolves once the channel has accepted the item: immediately when a
98
+ * taker is waiting or the cache has capacity, otherwise when a later
99
+ * take() frees a slot (backpressure -- bounded by `limit`). Replaces the
100
+ * pre-2.1 behavior of throwing "cache full" at capacity.
101
+ * @kind function
102
+ * @name put
103
+ */
104
+ async put(item: T, ...debug: unknown[]): Promise<void> {
105
+ this.debug?.("put", item, ...debug);
106
+ // Reserve this call's delivery slot synchronously, in call order,
107
+ // before doing any async work (transform() may resolve at a different
108
+ // speed for different items -- see class doc on putOrder).
109
+ const myTurn = this.putOrder;
110
+ let advance!: () => void;
111
+ this.putOrder = new Promise<void>((resolve) => {
112
+ advance = resolve;
113
+ });
114
+ try {
115
+ const value = await this.transform(item);
116
+ await myTurn;
117
+ if (this.promise) {
118
+ this.resolve?.(value);
119
+ return;
120
+ }
121
+ if (this.cache.length < this.limit) {
122
+ this.cache.push(value);
123
+ return;
124
+ }
125
+ await new Promise<void>((release) => {
126
+ this.putters.push({ value, release });
127
+ });
128
+ } finally {
129
+ advance();
130
+ }
131
+ }
132
+ /**
133
+ * Take item off of Asynchronous Channel
134
+ * @kind function
135
+ * @name take
136
+ */
137
+ async take(...debug: unknown[]): Promise<T | typeof CHANNEL_END> {
138
+ this.debug?.("take", ...debug);
139
+ if (this.cache.length) {
140
+ const value = this.cache.shift() as T | typeof CHANNEL_END;
141
+ this.drainPutters();
142
+ return value;
143
+ }
144
+ if (this.putters.length) {
145
+ // limit of 0 (rendezvous) or drained cache with waiting producers:
146
+ // hand off directly.
147
+ const { value, release } = this.putters.shift() as {
148
+ value: T | typeof CHANNEL_END;
149
+ release: () => void;
150
+ };
151
+ release();
152
+ return value;
153
+ }
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;
163
+ }
164
+ /**
165
+ * Pause Asynchronous Channel
166
+ * @kind function
167
+ * @name break
168
+ */
169
+ async break(...debug: unknown[]): Promise<void> {
170
+ this.debug?.("break", ...debug);
171
+ if (this.promise) {
172
+ await this.resolve?.(CHANNEL_END);
173
+ } else if (this.putters.length > 0) {
174
+ // Keep FIFO order: the end marker must not overtake producers
175
+ // already waiting for capacity.
176
+ this.putters.push({ value: CHANNEL_END, release: () => {} });
177
+ } else {
178
+ this.cache.push(CHANNEL_END);
179
+ }
180
+ }
181
+ /**
182
+ * Stop Asynchronous Channel
183
+ * @kind function
184
+ * @name throw
185
+ */
186
+ async throw(message?: string, ...debug: unknown[]): Promise<void> {
187
+ this.debug?.("throw", ...debug);
188
+ if (this.promise) {
189
+ await this.reject?.(new Error(message));
190
+ } else if (this.putters.length > 0) {
191
+ // Keep FIFO order behind producers already waiting for capacity.
192
+ this.putters.push({ value: new Error(message), release: () => {} });
193
+ } else {
194
+ this.cache.push(new Error(message));
195
+ }
196
+ }
197
+ /**
198
+ * Return pending status of Asynchronous Channel
199
+ * @kind function
200
+ * @name pending
201
+ * Note: should this be a getter?
202
+ */
203
+ pending(): boolean {
204
+ return !!this.promise;
205
+ }
206
+ /**
207
+ * Return string representation of Asynchronous Channel
208
+ * @kind function
209
+ * @name toString
210
+ */
211
+ toString(): string {
212
+ return `AsyncChannel {${this.pending() ? "pending" : ""}} [${this.cache.length}/${this.limit}]`;
213
+ }
214
+ /**
215
+ * Return Asynchronous Channel's iterator
216
+ * @kind function
217
+ * @name [Symbol.asyncIterator]
218
+ */
219
+ async *[Symbol.asyncIterator](...debug: unknown[]): AsyncGenerator<T> {
220
+ while (true) {
221
+ const answer = await this.take(...debug);
222
+ if (answer === CHANNEL_END) {
223
+ return;
224
+ }
225
+ yield answer;
226
+ }
227
+ }
228
+ }
@@ -0,0 +1,49 @@
1
+ import type { AsyncChannel } from "./async-channel.ts";
2
+
3
+ interface EmitterLike {
4
+ addListener(event: string, listener: (...args: never[]) => unknown): unknown;
5
+ }
6
+
7
+ interface WebSocketLike {
8
+ onmessage: ((event: unknown) => unknown) | null;
9
+ onclose: ((event?: unknown) => unknown) | null;
10
+ onerror: ((event?: unknown) => unknown) | null;
11
+ }
12
+
13
+ /**
14
+ * Decorate Asynchronous Channel with generic emitter
15
+ * @kind function
16
+ * @name withEmitter
17
+ */
18
+ export const withEmitter = <T, C extends AsyncChannel<T>>(
19
+ channel: C,
20
+ emitter: EmitterLike,
21
+ data = "data",
22
+ end = "end",
23
+ error = "error",
24
+ ): C => {
25
+ emitter.addListener(data, (...args: never[]) => {
26
+ void channel.put(...(args as unknown as Parameters<typeof channel.put>)).catch((reason: unknown) => {
27
+ void channel.throw(reason instanceof Error ? reason.message : String(reason));
28
+ });
29
+ });
30
+ emitter.addListener(end, channel.break.bind(channel));
31
+ emitter.addListener(error, channel.throw.bind(channel));
32
+ return channel;
33
+ };
34
+
35
+ /**
36
+ * Decorate Asynchronous Channel with websocket
37
+ * @kind function
38
+ * @name withWebSocket
39
+ */
40
+ export const withWebSocket = <T, C extends AsyncChannel<T>>(channel: C, websocket: WebSocketLike): C => {
41
+ websocket.onmessage = (event: unknown) => {
42
+ void channel.put(event as T).catch((reason: unknown) => {
43
+ void channel.throw(reason instanceof Error ? reason.message : String(reason));
44
+ });
45
+ };
46
+ websocket.onclose = channel.break.bind(channel);
47
+ websocket.onerror = channel.throw.bind(channel) as (event?: unknown) => unknown;
48
+ return channel;
49
+ };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Bounded-concurrency helpers: mapConcurrentAsync (parallel map over an
3
+ * (a)sync iterable with a concurrency limit) and prefetchAsync (eager
4
+ * read-ahead into a bounded buffer). Both propagate cancellation/early
5
+ * consumer exit upstream via iterator.return().
6
+ */
7
+
8
+ import { type SignalOptions, throwIfAborted } from "./abort.ts";
9
+
10
+ export interface MapConcurrentOptions extends SignalOptions {
11
+ /** Maximum number of `fn` invocations in flight at once (required, >= 1). */
12
+ concurrency: number;
13
+ /**
14
+ * true (default): yield results in input order.
15
+ * false: yield results in completion order.
16
+ */
17
+ ordered?: boolean;
18
+ }
19
+
20
+ const abortErrorOf = (signal: AbortSignal): unknown =>
21
+ signal.reason ?? new DOMException("This operation was aborted", "AbortError");
22
+
23
+ const getIterator = <T>(iterable: AsyncIterable<T> | Iterable<T>): AsyncIterator<T> | Iterator<T> =>
24
+ Symbol.asyncIterator in Object(iterable)
25
+ ? (iterable as AsyncIterable<T>)[Symbol.asyncIterator]()
26
+ : (iterable as Iterable<T>)[Symbol.iterator]();
27
+
28
+ /**
29
+ * Map `fn` over `iterable` with up to `concurrency` invocations in flight
30
+ * at once. With `ordered: true` (the default) results are yielded in input
31
+ * order; with `ordered: false` they are yielded in completion order.
32
+ *
33
+ * - Backpressure: at most `concurrency` results are held; the source is
34
+ * only pulled while there is spare in-flight capacity.
35
+ * - Cancellation: an optional `signal` aborts promptly (rejecting with the
36
+ * signal's reason, an "AbortError" DOMException by default) even while
37
+ * awaiting a slow `fn` or source; the source iterator is closed via
38
+ * `return()`.
39
+ * - Early exit / error: breaking out of the loop, or an `fn` rejection,
40
+ * also closes the source via `return()`.
41
+ * @kind function
42
+ * @name mapConcurrentAsync
43
+ */
44
+ export const mapConcurrentAsync = async function* <In, Out>(
45
+ fn: (item: In) => Out | Promise<Out>,
46
+ iterable: AsyncIterable<In> | Iterable<In>,
47
+ { concurrency, ordered = true, signal }: MapConcurrentOptions,
48
+ ): AsyncGenerator<Out> {
49
+ if (!(typeof concurrency === "number" && concurrency >= 1)) {
50
+ throw new RangeError("mapConcurrentAsync: concurrency must be >= 1");
51
+ }
52
+ throwIfAborted(signal);
53
+ const it = getIterator(iterable);
54
+
55
+ let onAbort: (() => void) | undefined;
56
+ const aborted = signal
57
+ ? new Promise<never>((_, reject) => {
58
+ onAbort = () => reject(abortErrorOf(signal));
59
+ signal.addEventListener("abort", onAbort, { once: true });
60
+ })
61
+ : undefined;
62
+ aborted?.catch(() => {});
63
+ const race = <T>(promise: Promise<T>): Promise<T> => (aborted ? Promise.race([promise, aborted]) : promise);
64
+
65
+ let sourceDone = false;
66
+ try {
67
+ if (ordered) {
68
+ const queue: Array<Promise<Out>> = [];
69
+ while (true) {
70
+ while (!sourceDone && queue.length < concurrency) {
71
+ const r = await race(Promise.resolve(it.next()));
72
+ if (r.done) {
73
+ sourceDone = true;
74
+ break;
75
+ }
76
+ const task = (async () => fn(r.value))();
77
+ task.catch(() => {}); // handled when shifted; avoid unhandledRejection
78
+ queue.push(task);
79
+ }
80
+ if (queue.length === 0) {
81
+ return;
82
+ }
83
+ yield await race(queue.shift() as Promise<Out>);
84
+ }
85
+ } else {
86
+ type Tagged = [Promise<unknown>, Out];
87
+ const pending = new Set<Promise<Tagged>>();
88
+ const add = (value: In) => {
89
+ let self!: Promise<Tagged>;
90
+ self = (async () => {
91
+ const out = await fn(value);
92
+ return [self, out] as Tagged;
93
+ })();
94
+ self.catch(() => {}); // handled via the race below
95
+ pending.add(self);
96
+ };
97
+ while (true) {
98
+ while (!sourceDone && pending.size < concurrency) {
99
+ const r = await race(Promise.resolve(it.next()));
100
+ if (r.done) {
101
+ sourceDone = true;
102
+ break;
103
+ }
104
+ add(r.value);
105
+ }
106
+ if (pending.size === 0) {
107
+ return;
108
+ }
109
+ const [task, out] = await race(Promise.race(pending));
110
+ pending.delete(task as Promise<Tagged>);
111
+ yield out;
112
+ }
113
+ }
114
+ } finally {
115
+ if (signal && onAbort) {
116
+ signal.removeEventListener("abort", onAbort);
117
+ }
118
+ if (!sourceDone && it.return) {
119
+ // Close the source on abort, error, or early consumer exit --
120
+ // fire-and-forget so a hung source can't block the rejection.
121
+ Promise.resolve(it.return()).catch(() => {});
122
+ }
123
+ }
124
+ };
125
+
126
+ /**
127
+ * Eagerly pull up to `n` items ahead of the consumer into a bounded
128
+ * buffer, so a slow consumer overlaps with a slow producer. `n <= 0`
129
+ * degenerates to plain iteration. Early consumer exit (or an error)
130
+ * closes the source via `return()`.
131
+ * @kind function
132
+ * @name prefetchAsync
133
+ */
134
+ export const prefetchAsync = async function* <T>(
135
+ n: number,
136
+ iterable: AsyncIterable<T> | Iterable<T>,
137
+ ): AsyncGenerator<T> {
138
+ if (n <= 0) {
139
+ yield* iterable;
140
+ return;
141
+ }
142
+ const it = getIterator(iterable);
143
+ const buffer: Array<Promise<IteratorResult<T>> | IteratorResult<T>> = [];
144
+ let done = false;
145
+ const pull = () => {
146
+ const p = Promise.resolve(it.next());
147
+ p.catch(() => {}); // handled when shifted
148
+ buffer.push(p);
149
+ };
150
+ try {
151
+ for (let i = 0; i < n; i++) {
152
+ pull();
153
+ }
154
+ while (true) {
155
+ const r = await (buffer.shift() as Promise<IteratorResult<T>>);
156
+ if (r.done) {
157
+ done = true;
158
+ return;
159
+ }
160
+ pull(); // keep n reads in flight while the consumer works
161
+ yield r.value;
162
+ }
163
+ } finally {
164
+ if (!done && it.return) {
165
+ Promise.resolve(it.return()).catch(() => {});
166
+ }
167
+ }
168
+ };