@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.
- package/dist/async-channel.d.ts +24 -4
- package/dist/async-channel.d.ts.map +1 -1
- package/dist/async-channel.js +41 -38
- package/dist/async-channel.js.map +1 -1
- package/dist/combine.d.ts +146 -0
- package/dist/combine.d.ts.map +1 -0
- package/dist/combine.js +353 -0
- package/dist/combine.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +66 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +134 -0
- package/dist/internal.js.map +1 -0
- package/dist/iterator-tools.d.ts.map +1 -1
- package/dist/iterator-tools.js +38 -18
- package/dist/iterator-tools.js.map +1 -1
- package/dist/itertools.d.ts.map +1 -1
- package/dist/itertools.js +42 -24
- package/dist/itertools.js.map +1 -1
- package/dist/tee.d.ts +24 -2
- package/dist/tee.d.ts.map +1 -1
- package/dist/tee.js +175 -2
- package/dist/tee.js.map +1 -1
- package/dist/time.d.ts +69 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +234 -0
- package/dist/time.js.map +1 -0
- package/dist/transduce.d.ts +49 -0
- package/dist/transduce.d.ts.map +1 -1
- package/dist/transduce.js +27 -1
- package/dist/transduce.js.map +1 -1
- package/dist/transducers.d.ts.map +1 -1
- package/dist/transducers.js +32 -14
- package/dist/transducers.js.map +1 -1
- package/package.json +10 -2
- package/readme.md +304 -14
- package/src/async-channel.ts +63 -47
- package/src/combine.ts +441 -0
- package/src/index.ts +2 -0
- package/src/internal.ts +164 -0
- package/src/iterator-tools.ts +35 -20
- package/src/itertools.ts +36 -23
- package/src/tee.ts +202 -2
- package/src/time.ts +262 -0
- package/src/transduce.ts +91 -1
- package/src/transducers.ts +34 -19
package/src/combine.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combinators for live values and events: merge several async sources,
|
|
3
|
+
* combine their latest values, sample one by another, and conflate a fast
|
|
4
|
+
* source down to its newest value.
|
|
5
|
+
*
|
|
6
|
+
* Every combinator here:
|
|
7
|
+
* - accepts async or sync iterables as inputs;
|
|
8
|
+
* - takes an optional `{ signal }`; on abort it rejects with the signal's
|
|
9
|
+
* reason (an "AbortError" DOMException by default);
|
|
10
|
+
* - closes (`return()`) every input that has not ended when it finishes,
|
|
11
|
+
* when an input errors, when the signal aborts, and when the consumer
|
|
12
|
+
* exits early (`break`, or calling `return()` on it). An input with a
|
|
13
|
+
* `next()` still pending is closed without waiting for that `next()`.
|
|
14
|
+
*
|
|
15
|
+
* The "latest value" combinators (combineLatestAsync, withLatestFromAsync,
|
|
16
|
+
* latestAsync, sampleAsync) drain some inputs eagerly: they keep pulling
|
|
17
|
+
* from them whether or not the consumer is reading, and keep only the
|
|
18
|
+
* newest value. Like every generator in this library they start when the
|
|
19
|
+
* consumer first calls `next()`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { type SignalOptions, throwIfAborted } from "./abort.ts";
|
|
23
|
+
import {
|
|
24
|
+
type AnyIterable,
|
|
25
|
+
closeAll,
|
|
26
|
+
createFailure,
|
|
27
|
+
type Entry,
|
|
28
|
+
entryOf,
|
|
29
|
+
type Failure,
|
|
30
|
+
listenAbort,
|
|
31
|
+
pull,
|
|
32
|
+
Wakeup,
|
|
33
|
+
} from "./internal.ts";
|
|
34
|
+
|
|
35
|
+
/** The value type of an async or sync iterable. */
|
|
36
|
+
export type IteratedValue<I> = I extends AsyncIterable<infer V> ? V : I extends Iterable<infer V> ? V : never;
|
|
37
|
+
|
|
38
|
+
/** Inputs for combineLatestAsync / withLatestFromAsync: an array or a record of iterables. */
|
|
39
|
+
export type IterableShape = readonly AnyIterable<unknown>[] | { readonly [key: string]: AnyIterable<unknown> };
|
|
40
|
+
|
|
41
|
+
/** A snapshot with the same shape as its inputs: each iterable replaced by its latest value. */
|
|
42
|
+
export type LatestOf<I> = { -readonly [K in keyof I]: IteratedValue<I[K]> };
|
|
43
|
+
|
|
44
|
+
const isOptions = (value: unknown): boolean =>
|
|
45
|
+
typeof value === "object" &&
|
|
46
|
+
value !== null &&
|
|
47
|
+
!(Symbol.iterator in value) &&
|
|
48
|
+
!(Symbol.asyncIterator in (value as object));
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Pull from an entry eagerly until it ends, errors, or `isStopped()`:
|
|
52
|
+
* `onValue` gets each value, `onEnd` runs once it ends, and an error fails
|
|
53
|
+
* the whole combinator.
|
|
54
|
+
*/
|
|
55
|
+
const pump = async <T>(
|
|
56
|
+
entry: Entry<T>,
|
|
57
|
+
isStopped: () => boolean,
|
|
58
|
+
onValue: (value: T) => void,
|
|
59
|
+
onEnd: () => void,
|
|
60
|
+
failure: Failure,
|
|
61
|
+
): Promise<void> => {
|
|
62
|
+
try {
|
|
63
|
+
while (!isStopped()) {
|
|
64
|
+
const result = await pull(entry);
|
|
65
|
+
if (isStopped()) return;
|
|
66
|
+
if (result.done) {
|
|
67
|
+
onEnd();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
onValue(result.value);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
failure.fail(error);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Split an array-or-record of inputs into keys and iterables. */
|
|
78
|
+
const shapeOf = (inputs: IterableShape): { isArray: boolean; keys: string[]; iterables: AnyIterable<unknown>[] } => {
|
|
79
|
+
if (Array.isArray(inputs)) {
|
|
80
|
+
return { isArray: true, keys: inputs.map((_, i) => String(i)), iterables: [...inputs] };
|
|
81
|
+
}
|
|
82
|
+
const keys = Object.keys(inputs);
|
|
83
|
+
return {
|
|
84
|
+
isArray: false,
|
|
85
|
+
keys,
|
|
86
|
+
iterables: keys.map((key) => (inputs as Record<string, AnyIterable<unknown>>)[key] as AnyIterable<unknown>),
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const snapshotOf = (isArray: boolean, keys: string[], values: unknown[]): any => {
|
|
91
|
+
if (isArray) return values.slice();
|
|
92
|
+
const out: Record<string, unknown> = {};
|
|
93
|
+
keys.forEach((key, i) => {
|
|
94
|
+
out[key] = values[i];
|
|
95
|
+
});
|
|
96
|
+
return out;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Merge several (async or sync) iterables into one, yielding each value as
|
|
101
|
+
* it arrives from any input. Ends when every input has ended.
|
|
102
|
+
*
|
|
103
|
+
* - Lazy: each input has at most one `next()` outstanding, and the next
|
|
104
|
+
* value is requested from an input only after its previous value has
|
|
105
|
+
* been consumed. Inputs that are ready at the same time are served in
|
|
106
|
+
* rotation, so a fast or synchronous input cannot starve the others.
|
|
107
|
+
* - An error from any input closes the others and is rethrown.
|
|
108
|
+
* - Consumer exit or abort closes every input.
|
|
109
|
+
*
|
|
110
|
+
* Pass `{ signal }` as the last argument.
|
|
111
|
+
* @kind function
|
|
112
|
+
* @name mergeAsync
|
|
113
|
+
* @example
|
|
114
|
+
* ```javascript
|
|
115
|
+
* for await (const event of mergeAsync(clicks, keys, { signal })) { ... }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export function mergeAsync<T>(...inputs: Array<AnyIterable<T>>): AsyncGenerator<T>;
|
|
119
|
+
export function mergeAsync<T>(...args: [...inputs: Array<AnyIterable<T>>, options: SignalOptions]): AsyncGenerator<T>;
|
|
120
|
+
export function mergeAsync<T>(...args: Array<AnyIterable<T> | SignalOptions>): AsyncGenerator<T> {
|
|
121
|
+
const last = args[args.length - 1];
|
|
122
|
+
const options = isOptions(last) ? (args.pop() as SignalOptions) : {};
|
|
123
|
+
return mergeImpl(args as Array<AnyIterable<T>>, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const mergeImpl = async function* <T>(inputs: Array<AnyIterable<T>>, { signal }: SignalOptions): AsyncGenerator<T> {
|
|
127
|
+
throwIfAborted(signal);
|
|
128
|
+
const entries = inputs.map((input) => entryOf(input));
|
|
129
|
+
const failure = createFailure();
|
|
130
|
+
const unlisten = listenAbort(signal, (reason) => failure.fail(reason));
|
|
131
|
+
// The outstanding next() of each input, tagged with its index.
|
|
132
|
+
const pending = new Map<number, Promise<[number, IteratorResult<T>]>>();
|
|
133
|
+
const request = (i: number) => {
|
|
134
|
+
const tagged = pull(entries[i] as Entry<T>).then((result): [number, IteratorResult<T>] => [i, result]);
|
|
135
|
+
tagged.catch(() => {}); // rejections are observed through the race below
|
|
136
|
+
pending.set(i, tagged);
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
entries.forEach((_, i) => {
|
|
140
|
+
request(i);
|
|
141
|
+
});
|
|
142
|
+
let lastWinner = -1;
|
|
143
|
+
while (pending.size > 0) {
|
|
144
|
+
if (failure.failed) throw failure.error;
|
|
145
|
+
// Rotate the race so the input after the last winner goes first:
|
|
146
|
+
// Promise.race favours the earliest already-settled promise.
|
|
147
|
+
const rank = (k: number) => (k - lastWinner - 1 + entries.length) % entries.length;
|
|
148
|
+
const order = [...pending.keys()].sort((a, b) => rank(a) - rank(b));
|
|
149
|
+
const [i, result] = await Promise.race([...order.map((k) => pending.get(k)!), failure.promise]);
|
|
150
|
+
pending.delete(i);
|
|
151
|
+
lastWinner = i;
|
|
152
|
+
if (result.done) continue;
|
|
153
|
+
yield result.value;
|
|
154
|
+
request(i);
|
|
155
|
+
}
|
|
156
|
+
if (failure.failed) throw failure.error;
|
|
157
|
+
} finally {
|
|
158
|
+
unlisten();
|
|
159
|
+
await closeAll(entries);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export interface CombineLatestOptions<I> extends SignalOptions {
|
|
164
|
+
/**
|
|
165
|
+
* Starting values for some or all inputs (same shape as the inputs), so a
|
|
166
|
+
* snapshot can be emitted before those inputs have yielded. They are not
|
|
167
|
+
* emitted on their own: the first snapshot still waits for an input to
|
|
168
|
+
* yield.
|
|
169
|
+
*/
|
|
170
|
+
initial?: Partial<LatestOf<I>>;
|
|
171
|
+
/**
|
|
172
|
+
* `"all"` (default): end once every input has ended. `"any"`: end as
|
|
173
|
+
* soon as any input ends. Either way, an input that ends without ever
|
|
174
|
+
* having a value (and no `initial` for it) ends the output at once,
|
|
175
|
+
* since no complete snapshot can ever be made.
|
|
176
|
+
*/
|
|
177
|
+
endOn?: "all" | "any";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Combine the latest values of several inputs. `inputs` is an array or a
|
|
182
|
+
* record of (async or sync) iterables; each output is a snapshot of the
|
|
183
|
+
* same shape holding every input's latest value. A snapshot is emitted
|
|
184
|
+
* whenever an input yields, once every input has a value (from the input
|
|
185
|
+
* itself or from `initial`).
|
|
186
|
+
*
|
|
187
|
+
* Latest-wins: inputs are drained eagerly, and while the consumer is busy
|
|
188
|
+
* the snapshots that would have been emitted coalesce into one, holding
|
|
189
|
+
* the newest values. Every snapshot is a fresh object.
|
|
190
|
+
*
|
|
191
|
+
* An input error closes the others and is rethrown (a pending snapshot is
|
|
192
|
+
* dropped). Consumer exit or abort closes every input.
|
|
193
|
+
* @kind function
|
|
194
|
+
* @name combineLatestAsync
|
|
195
|
+
* @example
|
|
196
|
+
* ```javascript
|
|
197
|
+
* for await (const { x, y } of combineLatestAsync({ x: xs, y: ys })) { ... }
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export const combineLatestAsync = async function* <const I extends IterableShape>(
|
|
201
|
+
inputs: I,
|
|
202
|
+
{ signal, initial, endOn = "all" }: CombineLatestOptions<I> = {},
|
|
203
|
+
): AsyncGenerator<LatestOf<I>> {
|
|
204
|
+
throwIfAborted(signal);
|
|
205
|
+
const { isArray, keys, iterables } = shapeOf(inputs);
|
|
206
|
+
const n = keys.length;
|
|
207
|
+
const values: unknown[] = new Array(n);
|
|
208
|
+
const has: boolean[] = new Array(n).fill(false);
|
|
209
|
+
if (initial) {
|
|
210
|
+
keys.forEach((key, i) => {
|
|
211
|
+
if (Object.hasOwn(initial, key)) {
|
|
212
|
+
values[i] = (initial as Record<string, unknown>)[key];
|
|
213
|
+
has[i] = true;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const entries = iterables.map((iterable) => entryOf(iterable));
|
|
218
|
+
const failure = createFailure();
|
|
219
|
+
const wakeup = new Wakeup();
|
|
220
|
+
const unlisten = listenAbort(signal, (reason) => {
|
|
221
|
+
failure.fail(reason);
|
|
222
|
+
wakeup.wake();
|
|
223
|
+
});
|
|
224
|
+
let stopped = false;
|
|
225
|
+
let dirty = false;
|
|
226
|
+
let ended = n === 0;
|
|
227
|
+
try {
|
|
228
|
+
entries.forEach((entry, i) => {
|
|
229
|
+
void pump(
|
|
230
|
+
entry,
|
|
231
|
+
() => stopped,
|
|
232
|
+
(value) => {
|
|
233
|
+
values[i] = value;
|
|
234
|
+
has[i] = true;
|
|
235
|
+
dirty = true;
|
|
236
|
+
wakeup.wake();
|
|
237
|
+
},
|
|
238
|
+
() => {
|
|
239
|
+
if (endOn === "any" || !has[i] || entries.every((e) => e.done)) ended = true;
|
|
240
|
+
wakeup.wake();
|
|
241
|
+
},
|
|
242
|
+
failure,
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
while (true) {
|
|
246
|
+
if (failure.failed) throw failure.error;
|
|
247
|
+
if (dirty && has.every(Boolean)) {
|
|
248
|
+
dirty = false;
|
|
249
|
+
yield snapshotOf(isArray, keys, values);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ended) return;
|
|
253
|
+
await Promise.race([wakeup.wait(), failure.promise]);
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
stopped = true;
|
|
257
|
+
unlisten();
|
|
258
|
+
await closeAll(entries);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Pair each value of `source` with the latest value of each of `others`
|
|
264
|
+
* (an array or a record of iterables): yields `[value, snapshot]`, where
|
|
265
|
+
* `snapshot` has the same shape as `others`. This is the "an event samples
|
|
266
|
+
* values" pattern.
|
|
267
|
+
*
|
|
268
|
+
* `source` is pulled lazily, as the consumer reads; `others` are drained
|
|
269
|
+
* eagerly. Source values that arrive before every one of `others` has a
|
|
270
|
+
* value are dropped. An input in `others` that ends keeps its last value.
|
|
271
|
+
* Ends when `source` ends. An error from any input closes the rest and is
|
|
272
|
+
* rethrown; consumer exit or abort closes every input.
|
|
273
|
+
* @kind function
|
|
274
|
+
* @name withLatestFromAsync
|
|
275
|
+
* @example
|
|
276
|
+
* ```javascript
|
|
277
|
+
* for await (const [click, { price }] of withLatestFromAsync(clicks, { price: prices })) { ... }
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
export const withLatestFromAsync = async function* <T, const I extends IterableShape>(
|
|
281
|
+
source: AnyIterable<T>,
|
|
282
|
+
others: I,
|
|
283
|
+
{ signal }: SignalOptions = {},
|
|
284
|
+
): AsyncGenerator<[T, LatestOf<I>]> {
|
|
285
|
+
throwIfAborted(signal);
|
|
286
|
+
const { isArray, keys, iterables } = shapeOf(others);
|
|
287
|
+
const values: unknown[] = new Array(keys.length);
|
|
288
|
+
const has: boolean[] = new Array(keys.length).fill(false);
|
|
289
|
+
const src = entryOf(source);
|
|
290
|
+
const entries = iterables.map((iterable) => entryOf(iterable));
|
|
291
|
+
const failure = createFailure();
|
|
292
|
+
const unlisten = listenAbort(signal, (reason) => failure.fail(reason));
|
|
293
|
+
let stopped = false;
|
|
294
|
+
try {
|
|
295
|
+
entries.forEach((entry, i) => {
|
|
296
|
+
void pump(
|
|
297
|
+
entry,
|
|
298
|
+
() => stopped,
|
|
299
|
+
(value) => {
|
|
300
|
+
values[i] = value;
|
|
301
|
+
has[i] = true;
|
|
302
|
+
},
|
|
303
|
+
() => {},
|
|
304
|
+
failure,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
while (true) {
|
|
308
|
+
if (failure.failed) throw failure.error;
|
|
309
|
+
const result = await Promise.race([pull(src), failure.promise]);
|
|
310
|
+
if (result.done) return;
|
|
311
|
+
if (failure.failed) throw failure.error;
|
|
312
|
+
if (has.every(Boolean)) yield [result.value, snapshotOf(isArray, keys, values)];
|
|
313
|
+
}
|
|
314
|
+
} finally {
|
|
315
|
+
stopped = true;
|
|
316
|
+
unlisten();
|
|
317
|
+
await closeAll([src, ...entries]);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Conflate a source down to its newest value. The source is drained
|
|
323
|
+
* eagerly; only the newest value not yet read is kept. Each `next()`
|
|
324
|
+
* returns that value, or waits for the next one. A value is returned at
|
|
325
|
+
* most once.
|
|
326
|
+
*
|
|
327
|
+
* Ends once the source has ended and its last value has been read. A source
|
|
328
|
+
* error is rethrown on the next read (an unread value is dropped). Closing
|
|
329
|
+
* the result, or aborting, closes the source.
|
|
330
|
+
* @kind function
|
|
331
|
+
* @name latestAsync
|
|
332
|
+
* @example
|
|
333
|
+
* ```javascript
|
|
334
|
+
* for await (const position of latestAsync(pointerMoves)) {
|
|
335
|
+
* await render(position); // never falls behind: stale positions are skipped
|
|
336
|
+
* }
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
export const latestAsync = async function* <T>(
|
|
340
|
+
iterable: AnyIterable<T>,
|
|
341
|
+
{ signal }: SignalOptions = {},
|
|
342
|
+
): AsyncGenerator<T> {
|
|
343
|
+
throwIfAborted(signal);
|
|
344
|
+
const entry = entryOf(iterable);
|
|
345
|
+
const failure = createFailure();
|
|
346
|
+
const wakeup = new Wakeup();
|
|
347
|
+
const unlisten = listenAbort(signal, (reason) => {
|
|
348
|
+
failure.fail(reason);
|
|
349
|
+
wakeup.wake();
|
|
350
|
+
});
|
|
351
|
+
let stopped = false;
|
|
352
|
+
let fresh = false;
|
|
353
|
+
let ended = false;
|
|
354
|
+
let latest: T | undefined;
|
|
355
|
+
try {
|
|
356
|
+
void pump(
|
|
357
|
+
entry,
|
|
358
|
+
() => stopped,
|
|
359
|
+
(value) => {
|
|
360
|
+
latest = value;
|
|
361
|
+
fresh = true;
|
|
362
|
+
wakeup.wake();
|
|
363
|
+
},
|
|
364
|
+
() => {
|
|
365
|
+
ended = true;
|
|
366
|
+
wakeup.wake();
|
|
367
|
+
},
|
|
368
|
+
failure,
|
|
369
|
+
);
|
|
370
|
+
while (true) {
|
|
371
|
+
if (failure.failed) throw failure.error;
|
|
372
|
+
if (fresh) {
|
|
373
|
+
const value = latest as T;
|
|
374
|
+
fresh = false;
|
|
375
|
+
latest = undefined; // don't retain a value that has been handed out
|
|
376
|
+
yield value;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (ended) return;
|
|
380
|
+
await Promise.race([wakeup.wait(), failure.promise]);
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
stopped = true;
|
|
384
|
+
unlisten();
|
|
385
|
+
await closeAll([entry]);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* On each value from `trigger`, yield the latest value of `source`, if it
|
|
391
|
+
* has produced one yet. The same source value is yielded again on every
|
|
392
|
+
* trigger until the source produces a new one.
|
|
393
|
+
*
|
|
394
|
+
* `source` is drained eagerly; `trigger` is pulled lazily, as the consumer
|
|
395
|
+
* reads. Ends when `trigger` ends (a source that ends keeps its last
|
|
396
|
+
* value). An error from either closes the other and is rethrown; consumer
|
|
397
|
+
* exit or abort closes both.
|
|
398
|
+
* @kind function
|
|
399
|
+
* @name sampleAsync
|
|
400
|
+
* @example
|
|
401
|
+
* ```javascript
|
|
402
|
+
* for await (const position of sampleAsync(pointerMoves, animationFrames)) { ... }
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
export const sampleAsync = async function* <T>(
|
|
406
|
+
source: AnyIterable<T>,
|
|
407
|
+
trigger: AnyIterable<unknown>,
|
|
408
|
+
{ signal }: SignalOptions = {},
|
|
409
|
+
): AsyncGenerator<T> {
|
|
410
|
+
throwIfAborted(signal);
|
|
411
|
+
const src = entryOf(source);
|
|
412
|
+
const trig = entryOf(trigger);
|
|
413
|
+
const failure = createFailure();
|
|
414
|
+
const unlisten = listenAbort(signal, (reason) => failure.fail(reason));
|
|
415
|
+
let stopped = false;
|
|
416
|
+
let has = false;
|
|
417
|
+
let latest: T | undefined;
|
|
418
|
+
try {
|
|
419
|
+
void pump(
|
|
420
|
+
src,
|
|
421
|
+
() => stopped,
|
|
422
|
+
(value) => {
|
|
423
|
+
latest = value;
|
|
424
|
+
has = true;
|
|
425
|
+
},
|
|
426
|
+
() => {},
|
|
427
|
+
failure,
|
|
428
|
+
);
|
|
429
|
+
while (true) {
|
|
430
|
+
if (failure.failed) throw failure.error;
|
|
431
|
+
const result = await Promise.race([pull(trig), failure.promise]);
|
|
432
|
+
if (result.done) return;
|
|
433
|
+
if (failure.failed) throw failure.error;
|
|
434
|
+
if (has) yield latest as T;
|
|
435
|
+
}
|
|
436
|
+
} finally {
|
|
437
|
+
stopped = true;
|
|
438
|
+
unlisten();
|
|
439
|
+
await closeAll([src, trig]);
|
|
440
|
+
}
|
|
441
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from "./abort.ts";
|
|
2
2
|
export * from "./async-channel.ts";
|
|
3
|
+
export * from "./combine.ts";
|
|
3
4
|
export * from "./concurrency.ts";
|
|
4
5
|
export * from "./consumers.ts";
|
|
5
6
|
export * from "./count.ts";
|
|
@@ -9,6 +10,7 @@ export * from "./is-iterator.ts";
|
|
|
9
10
|
export * from "./iterator-tools.ts";
|
|
10
11
|
export * from "./itertools.ts";
|
|
11
12
|
export * from "./tee.ts";
|
|
13
|
+
export * from "./time.ts";
|
|
12
14
|
export * from "./transduce.ts";
|
|
13
15
|
|
|
14
16
|
import * as channelDecorators from "./channel-decorators.ts";
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal plumbing shared by combine.ts, time.ts and the zip fixes.
|
|
3
|
+
* Not re-exported from index.ts and not in the package `exports` map.
|
|
4
|
+
* @ignore
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type AnyIterable<T> = AsyncIterable<T> | Iterable<T>;
|
|
8
|
+
export type AnyIterator<T> = AsyncIterator<T> | Iterator<T>;
|
|
9
|
+
|
|
10
|
+
const noop = (): void => {};
|
|
11
|
+
|
|
12
|
+
/** Get an iterator from an async or sync iterable (async preferred). */
|
|
13
|
+
export const getIterator = <T>(iterable: AnyIterable<T>): AnyIterator<T> =>
|
|
14
|
+
Symbol.asyncIterator in Object(iterable)
|
|
15
|
+
? (iterable as AsyncIterable<T>)[Symbol.asyncIterator]()
|
|
16
|
+
: (iterable as Iterable<T>)[Symbol.iterator]();
|
|
17
|
+
|
|
18
|
+
/** The reason an aborted signal carries ("AbortError" DOMException by default). */
|
|
19
|
+
export const abortReason = (signal: AbortSignal): unknown =>
|
|
20
|
+
signal.reason ?? new DOMException("This operation was aborted", "AbortError");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Call `onAbort(reason)` once when `signal` aborts (immediately if it
|
|
24
|
+
* already has). Returns a function that removes the listener.
|
|
25
|
+
*/
|
|
26
|
+
export const listenAbort = (signal: AbortSignal | undefined, onAbort: (reason: unknown) => void): (() => void) => {
|
|
27
|
+
if (!signal) return noop;
|
|
28
|
+
if (signal.aborted) {
|
|
29
|
+
onAbort(abortReason(signal));
|
|
30
|
+
return noop;
|
|
31
|
+
}
|
|
32
|
+
const listener = () => onAbort(abortReason(signal));
|
|
33
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
34
|
+
return () => signal.removeEventListener("abort", listener);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A one-shot failure latch: `fail(error)` records the first error and
|
|
39
|
+
* rejects `promise` with it. `promise` is pre-handled, so it never surfaces
|
|
40
|
+
* as an unhandled rejection; race it against a pending read to wake up on
|
|
41
|
+
* failure.
|
|
42
|
+
*/
|
|
43
|
+
export interface Failure {
|
|
44
|
+
failed: boolean;
|
|
45
|
+
error: unknown;
|
|
46
|
+
promise: Promise<never>;
|
|
47
|
+
fail(error: unknown): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const createFailure = (): Failure => {
|
|
51
|
+
let reject!: (error: unknown) => void;
|
|
52
|
+
const promise = new Promise<never>((_, r) => {
|
|
53
|
+
reject = r;
|
|
54
|
+
});
|
|
55
|
+
promise.catch(noop);
|
|
56
|
+
const failure: Failure = {
|
|
57
|
+
failed: false,
|
|
58
|
+
error: undefined,
|
|
59
|
+
promise,
|
|
60
|
+
fail(error) {
|
|
61
|
+
if (failure.failed) return;
|
|
62
|
+
failure.failed = true;
|
|
63
|
+
failure.error = error;
|
|
64
|
+
reject(error);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return failure;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** A reusable wake-up: `wait()` resolves on the next `wake()`. */
|
|
71
|
+
export class Wakeup {
|
|
72
|
+
#promise: Promise<void> | undefined;
|
|
73
|
+
#resolve: (() => void) | undefined;
|
|
74
|
+
wait(): Promise<void> {
|
|
75
|
+
if (!this.#promise) {
|
|
76
|
+
this.#promise = new Promise<void>((resolve) => {
|
|
77
|
+
this.#resolve = resolve;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return this.#promise;
|
|
81
|
+
}
|
|
82
|
+
wake(): void {
|
|
83
|
+
const resolve = this.#resolve;
|
|
84
|
+
this.#promise = undefined;
|
|
85
|
+
this.#resolve = undefined;
|
|
86
|
+
resolve?.();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** An input iterator plus the state needed to close it correctly. */
|
|
91
|
+
export interface Entry<T> {
|
|
92
|
+
it: AnyIterator<T>;
|
|
93
|
+
/** Ended, errored, or already closed: never call return() on it. */
|
|
94
|
+
done: boolean;
|
|
95
|
+
/** A next() call is outstanding. */
|
|
96
|
+
inFlight: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const entryOf = <T>(iterable: AnyIterable<T>): Entry<T> => ({
|
|
100
|
+
it: getIterator(iterable),
|
|
101
|
+
done: false,
|
|
102
|
+
inFlight: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Call `next()` on an entry, tracking `inFlight`/`done`. A synchronous
|
|
107
|
+
* throw from `next()` becomes a rejection. An input whose `next()` rejects
|
|
108
|
+
* or throws is marked done (the iterator protocol does not call return()
|
|
109
|
+
* on an iterator that threw).
|
|
110
|
+
*/
|
|
111
|
+
export const pull = <T>(entry: Entry<T>): Promise<IteratorResult<T>> => {
|
|
112
|
+
entry.inFlight = true;
|
|
113
|
+
return new Promise<IteratorResult<T>>((resolve) => resolve(entry.it.next())).then(
|
|
114
|
+
(result) => {
|
|
115
|
+
entry.inFlight = false;
|
|
116
|
+
if (result.done) entry.done = true;
|
|
117
|
+
return result;
|
|
118
|
+
},
|
|
119
|
+
(error: unknown) => {
|
|
120
|
+
entry.inFlight = false;
|
|
121
|
+
entry.done = true;
|
|
122
|
+
throw error;
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Close (`return()`) every entry that has not ended. Entries with no
|
|
129
|
+
* outstanding `next()` are awaited, so their cleanup has finished when
|
|
130
|
+
* this resolves. Entries with a `next()` still in flight are closed
|
|
131
|
+
* fire-and-forget: an async generator queues `return()` behind its pending
|
|
132
|
+
* `next()`, so awaiting it would hang on a stalled source (the case that
|
|
133
|
+
* aborting is for). Errors from `return()` are swallowed.
|
|
134
|
+
*/
|
|
135
|
+
export const closeAll = async (entries: Iterable<Entry<unknown>>): Promise<void> => {
|
|
136
|
+
const waits: Array<Promise<unknown>> = [];
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.done) continue;
|
|
139
|
+
entry.done = true;
|
|
140
|
+
if (typeof entry.it.return !== "function") continue;
|
|
141
|
+
let result: unknown;
|
|
142
|
+
try {
|
|
143
|
+
result = entry.it.return();
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const settled = Promise.resolve(result).then(noop, noop);
|
|
148
|
+
if (!entry.inFlight) waits.push(settled);
|
|
149
|
+
}
|
|
150
|
+
await Promise.all(waits);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Synchronous counterpart of closeAll, for sync iterators. */
|
|
154
|
+
export const closeAllSync = (entries: Iterable<{ it: Iterator<unknown>; done: boolean }>): void => {
|
|
155
|
+
for (const entry of entries) {
|
|
156
|
+
if (entry.done) continue;
|
|
157
|
+
entry.done = true;
|
|
158
|
+
try {
|
|
159
|
+
entry.it.return?.();
|
|
160
|
+
} catch {
|
|
161
|
+
// Cleanup is best-effort; the original completion (or error) wins.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
package/src/iterator-tools.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { closeAll, closeAllSync, entryOf, pull } from "./internal.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Sentinel returned by a reducer step to stop consumption of the source
|
|
@@ -221,17 +221,30 @@ export const conjoinAsync = async function* <T>(
|
|
|
221
221
|
* @returns an iterator who's members are the members of the given iterators zipped sequencially
|
|
222
222
|
*/
|
|
223
223
|
export const zipSync = function* <T>(...iteratorList: Array<Iterable<T>>): Generator<T[]> {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
const entries = iteratorList.map((iterator) => ({ it: iterator[Symbol.iterator](), done: false }));
|
|
225
|
+
try {
|
|
226
|
+
outer: while (true) {
|
|
227
|
+
const result: T[] = [];
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
let step: IteratorResult<T>;
|
|
230
|
+
try {
|
|
231
|
+
step = entry.it.next();
|
|
232
|
+
} catch (error) {
|
|
233
|
+
entry.done = true; // an iterator that threw is not closed again
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
if (step.done) {
|
|
237
|
+
entry.done = true;
|
|
238
|
+
break outer;
|
|
239
|
+
}
|
|
240
|
+
result.push(step.value);
|
|
231
241
|
}
|
|
232
|
-
result
|
|
242
|
+
yield result;
|
|
233
243
|
}
|
|
234
|
-
|
|
244
|
+
} finally {
|
|
245
|
+
// Close every input that has not ended -- when the shortest input ends,
|
|
246
|
+
// when an input throws, and when the consumer exits early.
|
|
247
|
+
closeAllSync(entries);
|
|
235
248
|
}
|
|
236
249
|
};
|
|
237
250
|
|
|
@@ -248,17 +261,19 @@ export const zipSync = function* <T>(...iteratorList: Array<Iterable<T>>): Gener
|
|
|
248
261
|
export const zipAsync = async function* <T>(
|
|
249
262
|
...iteratorList: Array<AsyncIterable<T> | Iterable<T>>
|
|
250
263
|
): AsyncGenerator<T[]> {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
break;
|
|
264
|
+
const entries = iteratorList.map((iterator) => entryOf(iterator));
|
|
265
|
+
try {
|
|
266
|
+
while (true) {
|
|
267
|
+
const results = await Promise.all(entries.map((entry) => pull(entry)));
|
|
268
|
+
if (results.some(({ done }) => done)) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
yield results.map(({ value }) => value as T);
|
|
260
272
|
}
|
|
261
|
-
|
|
273
|
+
} finally {
|
|
274
|
+
// Close every input that has not ended -- when the shortest input ends,
|
|
275
|
+
// when an input rejects, and when the consumer exits early.
|
|
276
|
+
await closeAll(entries);
|
|
262
277
|
}
|
|
263
278
|
};
|
|
264
279
|
|