@johnhenry/iteration 0.0.0 → 0.1.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/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 +13 -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 +65 -0
- package/dist/transduce.d.ts.map +1 -1
- package/dist/transduce.js +33 -1
- package/dist/transduce.js.map +1 -1
- package/dist/transducers.d.ts.map +1 -1
- package/dist/transducers.js +86 -16
- package/dist/transducers.js.map +1 -1
- package/package.json +10 -2
- package/readme.md +338 -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 +48 -20
- package/src/itertools.ts +36 -23
- package/src/tee.ts +202 -2
- package/src/time.ts +262 -0
- package/src/transduce.ts +113 -1
- package/src/transducers.ts +121 -30
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
|
|
@@ -28,6 +28,19 @@ export { HALT as HAULT };
|
|
|
28
28
|
export interface ReducerStep<in In> {
|
|
29
29
|
(buffer: unknown[], item: In, iterator?: Iterable<In> | AsyncIterable<In>): unknown[] | Halt;
|
|
30
30
|
complete?: (buffer: unknown[]) => unknown[];
|
|
31
|
+
/**
|
|
32
|
+
* What this step (and the steps after it) keep between calls: a half-full
|
|
33
|
+
* `group`, `partitionBy`'s open run, `accumulate`'s running value,
|
|
34
|
+
* `dedupe`'s last key. Steps that keep nothing omit it. Cascades like
|
|
35
|
+
* `complete`.
|
|
36
|
+
*/
|
|
37
|
+
held?: () => unknown[];
|
|
38
|
+
/**
|
|
39
|
+
* Let go of the held items for which `predicate` is true, and return how
|
|
40
|
+
* many were let go. It never emits, and never changes what is later
|
|
41
|
+
* emitted beyond leaving the dropped items out. Cascades like `complete`.
|
|
42
|
+
*/
|
|
43
|
+
drop?: (predicate: (held: unknown) => boolean) => number;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -221,17 +234,30 @@ export const conjoinAsync = async function* <T>(
|
|
|
221
234
|
* @returns an iterator who's members are the members of the given iterators zipped sequencially
|
|
222
235
|
*/
|
|
223
236
|
export const zipSync = function* <T>(...iteratorList: Array<Iterable<T>>): Generator<T[]> {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
237
|
+
const entries = iteratorList.map((iterator) => ({ it: iterator[Symbol.iterator](), done: false }));
|
|
238
|
+
try {
|
|
239
|
+
outer: while (true) {
|
|
240
|
+
const result: T[] = [];
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
let step: IteratorResult<T>;
|
|
243
|
+
try {
|
|
244
|
+
step = entry.it.next();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
entry.done = true; // an iterator that threw is not closed again
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
if (step.done) {
|
|
250
|
+
entry.done = true;
|
|
251
|
+
break outer;
|
|
252
|
+
}
|
|
253
|
+
result.push(step.value);
|
|
231
254
|
}
|
|
232
|
-
result
|
|
255
|
+
yield result;
|
|
233
256
|
}
|
|
234
|
-
|
|
257
|
+
} finally {
|
|
258
|
+
// Close every input that has not ended -- when the shortest input ends,
|
|
259
|
+
// when an input throws, and when the consumer exits early.
|
|
260
|
+
closeAllSync(entries);
|
|
235
261
|
}
|
|
236
262
|
};
|
|
237
263
|
|
|
@@ -248,17 +274,19 @@ export const zipSync = function* <T>(...iteratorList: Array<Iterable<T>>): Gener
|
|
|
248
274
|
export const zipAsync = async function* <T>(
|
|
249
275
|
...iteratorList: Array<AsyncIterable<T> | Iterable<T>>
|
|
250
276
|
): AsyncGenerator<T[]> {
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
break;
|
|
277
|
+
const entries = iteratorList.map((iterator) => entryOf(iterator));
|
|
278
|
+
try {
|
|
279
|
+
while (true) {
|
|
280
|
+
const results = await Promise.all(entries.map((entry) => pull(entry)));
|
|
281
|
+
if (results.some(({ done }) => done)) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
yield results.map(({ value }) => value as T);
|
|
260
285
|
}
|
|
261
|
-
|
|
286
|
+
} finally {
|
|
287
|
+
// Close every input that has not ended -- when the shortest input ends,
|
|
288
|
+
// when an input rejects, and when the consumer exits early.
|
|
289
|
+
await closeAll(entries);
|
|
262
290
|
}
|
|
263
291
|
};
|
|
264
292
|
|
package/src/itertools.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Python's itertools module and the documented design divergences.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import { closeAll, closeAllSync, entryOf, pull } from "./internal.ts";
|
|
23
23
|
import { concatAsync, concatSync, zipAsync, zipSync } from "./iterator-tools.ts";
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -428,21 +428,33 @@ export async function* starmapAsync<Args extends unknown[], R>(
|
|
|
428
428
|
* @param iteratorList iterables to zip
|
|
429
429
|
*/
|
|
430
430
|
export function* zipLongestSync<T, F>(fillValue: F, ...iteratorList: Array<Iterable<T>>): Generator<Array<T | F>> {
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
431
|
+
const entries = iteratorList.map((iterator) => ({ it: iterator[Symbol.iterator](), done: false }));
|
|
432
|
+
try {
|
|
433
|
+
while (true) {
|
|
434
|
+
const result: Array<T | F> = [];
|
|
435
|
+
let anyNotDone = false;
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
let step: IteratorResult<T>;
|
|
438
|
+
try {
|
|
439
|
+
step = entry.it.next();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
entry.done = true; // an iterator that threw is not closed again
|
|
442
|
+
throw error;
|
|
443
|
+
}
|
|
444
|
+
if (step.done) {
|
|
445
|
+
entry.done = true;
|
|
446
|
+
result.push(fillValue);
|
|
447
|
+
} else {
|
|
448
|
+
anyNotDone = true;
|
|
449
|
+
result.push(step.value);
|
|
450
|
+
}
|
|
442
451
|
}
|
|
452
|
+
if (!anyNotDone) return;
|
|
453
|
+
yield result;
|
|
443
454
|
}
|
|
444
|
-
|
|
445
|
-
|
|
455
|
+
} finally {
|
|
456
|
+
// Close every input that has not ended, on an error or an early exit.
|
|
457
|
+
closeAllSync(entries);
|
|
446
458
|
}
|
|
447
459
|
}
|
|
448
460
|
|
|
@@ -456,15 +468,16 @@ export async function* zipLongestAsync<T, F>(
|
|
|
456
468
|
fillValue: F,
|
|
457
469
|
...iteratorList: Array<AsyncIterable<T> | Iterable<T>>
|
|
458
470
|
): AsyncGenerator<Array<T | F>> {
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
471
|
+
const entries = iteratorList.map((iterator) => entryOf(iterator));
|
|
472
|
+
try {
|
|
473
|
+
while (true) {
|
|
474
|
+
const results = await Promise.all(entries.map((entry) => pull(entry)));
|
|
475
|
+
if (results.every(({ done }) => done)) return;
|
|
476
|
+
yield results.map(({ value, done }) => (done ? fillValue : (value as T)));
|
|
477
|
+
}
|
|
478
|
+
} finally {
|
|
479
|
+
// Close every input that has not ended, on an error or an early exit.
|
|
480
|
+
await closeAll(entries);
|
|
468
481
|
}
|
|
469
482
|
}
|
|
470
483
|
|
package/src/tee.ts
CHANGED
|
@@ -16,9 +16,66 @@
|
|
|
16
16
|
|
|
17
17
|
const DONE = Symbol("DONE");
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Optional bound on how far branches may drift apart. Without it (the
|
|
21
|
+
* default), a branch that falls behind buffers without limit.
|
|
22
|
+
*/
|
|
23
|
+
export interface TeeOptions {
|
|
24
|
+
/** Most items a branch may hold unread (an integer >= 1). */
|
|
25
|
+
limit: number;
|
|
26
|
+
/**
|
|
27
|
+
* What happens when a branch pulls a new item while another branch
|
|
28
|
+
* already holds `limit` unread items:
|
|
29
|
+
* - `"wait"` (teeAsync only): the pulling branch waits until the slowest
|
|
30
|
+
* branch reads (true backpressure);
|
|
31
|
+
* - `"drop-oldest"`: the item is delivered and that branch's oldest
|
|
32
|
+
* unread item is discarded, so each branch keeps its newest `limit`;
|
|
33
|
+
* - `"error"`: the pull throws a RangeError instead.
|
|
34
|
+
*/
|
|
35
|
+
overflow?: "wait" | "drop-oldest" | "error";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** teeSync cannot block, so it supports every overflow mode except "wait". */
|
|
39
|
+
export interface TeeSyncOptions extends TeeOptions {
|
|
40
|
+
overflow?: "drop-oldest" | "error";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const validate = (name: string, options: TeeOptions, allowWait: boolean): Required<TeeOptions> => {
|
|
44
|
+
const { limit, overflow = allowWait ? "wait" : "error" } = options;
|
|
45
|
+
if (!(limit === Infinity || (Number.isInteger(limit) && limit >= 1))) {
|
|
46
|
+
throw new RangeError(`${name}: limit must be an integer >= 1`);
|
|
47
|
+
}
|
|
48
|
+
if (overflow === "wait" && !allowWait) {
|
|
49
|
+
throw new TypeError(`${name}: overflow "wait" needs to block, which only teeAsync can do`);
|
|
50
|
+
}
|
|
51
|
+
if (overflow !== "wait" && overflow !== "drop-oldest" && overflow !== "error") {
|
|
52
|
+
throw new TypeError(`${name}: unknown overflow mode ${String(overflow)}`);
|
|
53
|
+
}
|
|
54
|
+
return { limit, overflow };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const overflowError = (name: string, limit: number): RangeError =>
|
|
58
|
+
new RangeError(`${name}: a branch already holds ${limit} unread items (overflow: "error")`);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Track branches that have finished or been closed, so they stop
|
|
62
|
+
* receiving items (and stop counting toward the limit). Overriding
|
|
63
|
+
* `return` on the instance also catches a branch closed before its first
|
|
64
|
+
* next(), whose generator body (and finally block) never runs.
|
|
65
|
+
*/
|
|
66
|
+
const trackClose = <G extends Generator<unknown> | AsyncGenerator<unknown>>(gen: G, onClose: () => void): G => {
|
|
67
|
+
const original = gen.return.bind(gen) as (value?: unknown) => unknown;
|
|
68
|
+
(gen as { return: (value?: unknown) => unknown }).return = (value?: unknown) => {
|
|
69
|
+
onClose();
|
|
70
|
+
return original(value);
|
|
71
|
+
};
|
|
72
|
+
return gen;
|
|
73
|
+
};
|
|
74
|
+
|
|
19
75
|
export const teeSync =
|
|
20
|
-
(num = 0) =>
|
|
76
|
+
(num = 0, options?: TeeSyncOptions) =>
|
|
21
77
|
<T>(iterable: Iterable<T>): Array<Generator<T>> => {
|
|
78
|
+
if (options) return boundedTeeSync(num, validate("teeSync", options, false), iterable);
|
|
22
79
|
const source = iterable[Symbol.iterator]();
|
|
23
80
|
const buffers: T[][] = new Array(num).fill(null).map(() => []);
|
|
24
81
|
|
|
@@ -62,8 +119,9 @@ export const teeSync =
|
|
|
62
119
|
};
|
|
63
120
|
|
|
64
121
|
export const teeAsync =
|
|
65
|
-
(num = 0) =>
|
|
122
|
+
(num = 0, options?: TeeOptions) =>
|
|
66
123
|
<T>(iterable: AsyncIterable<T>): Array<AsyncGenerator<T>> => {
|
|
124
|
+
if (options) return boundedTeeAsync(num, validate("teeAsync", options, true), iterable);
|
|
67
125
|
const source = iterable[Symbol.asyncIterator]();
|
|
68
126
|
const buffers: T[][] = new Array(num).fill(null).map(() => []);
|
|
69
127
|
|
|
@@ -134,3 +192,145 @@ export const teeAsync =
|
|
|
134
192
|
}
|
|
135
193
|
});
|
|
136
194
|
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* teeSync with a per-branch limit. A separate code path so the default,
|
|
198
|
+
* unbounded teeSync is untouched.
|
|
199
|
+
* @ignore
|
|
200
|
+
*/
|
|
201
|
+
const boundedTeeSync = <T>(num: number, { limit, overflow }: Required<TeeOptions>, iterable: Iterable<T>) => {
|
|
202
|
+
const source = iterable[Symbol.iterator]();
|
|
203
|
+
const buffers: T[][] = new Array(num).fill(null).map(() => []);
|
|
204
|
+
const open: boolean[] = new Array(num).fill(true);
|
|
205
|
+
|
|
206
|
+
const next = (i: number): T | typeof DONE => {
|
|
207
|
+
const buffer = buffers[i] as T[];
|
|
208
|
+
if (buffer.length !== 0) {
|
|
209
|
+
return buffer.shift() as T;
|
|
210
|
+
}
|
|
211
|
+
if (overflow === "error" && buffers.some((b, j) => j !== i && open[j] && b.length >= limit)) {
|
|
212
|
+
throw overflowError("teeSync", limit);
|
|
213
|
+
}
|
|
214
|
+
const x = source.next();
|
|
215
|
+
if (x.done) {
|
|
216
|
+
return DONE;
|
|
217
|
+
}
|
|
218
|
+
for (let j = 0; j < buffers.length; j++) {
|
|
219
|
+
if (j === i || !open[j]) continue;
|
|
220
|
+
const other = buffers[j] as T[];
|
|
221
|
+
other.push(x.value);
|
|
222
|
+
if (other.length > limit) other.shift(); // drop-oldest
|
|
223
|
+
}
|
|
224
|
+
return x.value;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return buffers.map((_, i) => {
|
|
228
|
+
const close = () => {
|
|
229
|
+
open[i] = false;
|
|
230
|
+
(buffers[i] as T[]).length = 0;
|
|
231
|
+
};
|
|
232
|
+
return trackClose(
|
|
233
|
+
(function* () {
|
|
234
|
+
try {
|
|
235
|
+
for (;;) {
|
|
236
|
+
const x = next(i);
|
|
237
|
+
if (x === DONE) break;
|
|
238
|
+
yield x;
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
close();
|
|
242
|
+
source.return?.();
|
|
243
|
+
}
|
|
244
|
+
})(),
|
|
245
|
+
close,
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* teeAsync with a per-branch limit. A separate code path so the default,
|
|
252
|
+
* unbounded teeAsync is untouched.
|
|
253
|
+
* @ignore
|
|
254
|
+
*/
|
|
255
|
+
const boundedTeeAsync = <T>(num: number, { limit, overflow }: Required<TeeOptions>, iterable: AsyncIterable<T>) => {
|
|
256
|
+
const source = iterable[Symbol.asyncIterator]();
|
|
257
|
+
const buffers: T[][] = new Array(num).fill(null).map(() => []);
|
|
258
|
+
const open: boolean[] = new Array(num).fill(true);
|
|
259
|
+
let inFlight: Promise<unknown> | null = null;
|
|
260
|
+
let sourceDone = false;
|
|
261
|
+
// Branches blocked by overflow "wait", woken whenever a buffer shrinks
|
|
262
|
+
// or a branch closes.
|
|
263
|
+
let waiters: Array<() => void> = [];
|
|
264
|
+
const wake = () => {
|
|
265
|
+
const ready = waiters;
|
|
266
|
+
waiters = [];
|
|
267
|
+
for (const resolve of ready) resolve();
|
|
268
|
+
};
|
|
269
|
+
const full = (i: number) => buffers.some((b, j) => j !== i && open[j] && b.length >= limit);
|
|
270
|
+
|
|
271
|
+
const next = async (i: number): Promise<T | typeof DONE> => {
|
|
272
|
+
const buffer = buffers[i] as T[];
|
|
273
|
+
if (buffer.length !== 0) {
|
|
274
|
+
const value = buffer.shift() as T;
|
|
275
|
+
wake();
|
|
276
|
+
return value;
|
|
277
|
+
}
|
|
278
|
+
if (sourceDone) {
|
|
279
|
+
return DONE;
|
|
280
|
+
}
|
|
281
|
+
if (inFlight) {
|
|
282
|
+
await inFlight;
|
|
283
|
+
return next(i);
|
|
284
|
+
}
|
|
285
|
+
if (full(i)) {
|
|
286
|
+
if (overflow === "error") throw overflowError("teeAsync", limit);
|
|
287
|
+
if (overflow === "wait") {
|
|
288
|
+
await new Promise<void>((resolve) => waiters.push(resolve));
|
|
289
|
+
return next(i);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const pull = (async (): Promise<T | typeof DONE> => {
|
|
293
|
+
const x = await source.next();
|
|
294
|
+
if (x.done) {
|
|
295
|
+
sourceDone = true;
|
|
296
|
+
return DONE;
|
|
297
|
+
}
|
|
298
|
+
for (let j = 0; j < buffers.length; j++) {
|
|
299
|
+
if (j === i || !open[j]) continue;
|
|
300
|
+
const other = buffers[j] as T[];
|
|
301
|
+
other.push(x.value);
|
|
302
|
+
if (other.length > limit) other.shift(); // drop-oldest
|
|
303
|
+
}
|
|
304
|
+
return x.value;
|
|
305
|
+
})();
|
|
306
|
+
inFlight = pull;
|
|
307
|
+
try {
|
|
308
|
+
return await pull;
|
|
309
|
+
} finally {
|
|
310
|
+
inFlight = null;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
return buffers.map((_, i) => {
|
|
315
|
+
const close = () => {
|
|
316
|
+
open[i] = false;
|
|
317
|
+
(buffers[i] as T[]).length = 0;
|
|
318
|
+
wake();
|
|
319
|
+
};
|
|
320
|
+
return trackClose(
|
|
321
|
+
(async function* () {
|
|
322
|
+
try {
|
|
323
|
+
for (;;) {
|
|
324
|
+
const x = await next(i);
|
|
325
|
+
if (x === DONE) break;
|
|
326
|
+
yield x;
|
|
327
|
+
}
|
|
328
|
+
} finally {
|
|
329
|
+
close();
|
|
330
|
+
await source.return?.();
|
|
331
|
+
}
|
|
332
|
+
})(),
|
|
333
|
+
close,
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
};
|
package/src/time.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time-based operators: debounceAsync and throttleAsync.
|
|
3
|
+
*
|
|
4
|
+
* Both drain their source eagerly (timing depends on when values arrive,
|
|
5
|
+
* not on when the consumer reads) and queue what they emit until the
|
|
6
|
+
* consumer reads it. Both take an injectable `clock`, so they can be driven
|
|
7
|
+
* by a fake clock in tests or by a host's own scheduler, and an optional
|
|
8
|
+
* `signal`. On abort, on a source error, and on consumer exit they clear
|
|
9
|
+
* their timer and close (`return()`) the source.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { type SignalOptions, throwIfAborted } from "./abort.ts";
|
|
13
|
+
import { type AnyIterable, closeAll, createFailure, entryOf, listenAbort, pull, Wakeup } from "./internal.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The timer functions the time operators use. Defaults to the host's
|
|
17
|
+
* global `setTimeout`/`clearTimeout` and `performance.now()` (or
|
|
18
|
+
* `Date.now()` where `performance` is missing). `now()` must use the same
|
|
19
|
+
* time base as the delays given to `setTimeout`.
|
|
20
|
+
*/
|
|
21
|
+
export interface Clock {
|
|
22
|
+
setTimeout(callback: () => void, ms: number): unknown;
|
|
23
|
+
clearTimeout(handle: unknown): void;
|
|
24
|
+
now(): number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TimeOptions extends SignalOptions {
|
|
28
|
+
clock?: Clock;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ThrottleOptions extends TimeOptions {
|
|
32
|
+
/** Emit the value that opens a window immediately. Default `true`. */
|
|
33
|
+
leading?: boolean;
|
|
34
|
+
/** At the end of a window, emit the newest value that arrived during it. Default `true`. */
|
|
35
|
+
trailing?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The host's timers. */
|
|
39
|
+
export const systemClock: Clock = {
|
|
40
|
+
setTimeout: (callback, ms) => globalThis.setTimeout(callback, ms),
|
|
41
|
+
clearTimeout: (handle) => globalThis.clearTimeout(handle as Parameters<typeof globalThis.clearTimeout>[0]),
|
|
42
|
+
now: () => (typeof globalThis.performance?.now === "function" ? globalThis.performance.now() : Date.now()),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const checkMs = (name: string, ms: number): void => {
|
|
46
|
+
if (!(typeof ms === "number" && ms >= 0 && Number.isFinite(ms))) {
|
|
47
|
+
throw new RangeError(`${name}: ms must be a finite number >= 0`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the shared machinery of a time operator: pump the source, hand each
|
|
53
|
+
* value and the end to `hooks`, and yield whatever they emit, in order.
|
|
54
|
+
*/
|
|
55
|
+
const timed = async function* <T>(
|
|
56
|
+
iterable: AnyIterable<T>,
|
|
57
|
+
{ signal }: SignalOptions,
|
|
58
|
+
setup: (emit: (value: T) => void) => { onValue(value: T): void; onEnd(): void; dispose(): void },
|
|
59
|
+
): AsyncGenerator<T> {
|
|
60
|
+
throwIfAborted(signal);
|
|
61
|
+
const entry = entryOf(iterable);
|
|
62
|
+
const aborted = createFailure();
|
|
63
|
+
const wakeup = new Wakeup();
|
|
64
|
+
const queue: T[] = [];
|
|
65
|
+
let ended = false;
|
|
66
|
+
let stopped = false;
|
|
67
|
+
let sourceError: { error: unknown } | undefined;
|
|
68
|
+
const hooks = setup((value) => {
|
|
69
|
+
queue.push(value);
|
|
70
|
+
wakeup.wake();
|
|
71
|
+
});
|
|
72
|
+
const unlisten = listenAbort(signal, (reason) => {
|
|
73
|
+
aborted.fail(reason);
|
|
74
|
+
wakeup.wake();
|
|
75
|
+
});
|
|
76
|
+
const pump = async () => {
|
|
77
|
+
try {
|
|
78
|
+
while (!stopped) {
|
|
79
|
+
const result = await pull(entry);
|
|
80
|
+
if (stopped) return;
|
|
81
|
+
if (result.done) {
|
|
82
|
+
hooks.onEnd();
|
|
83
|
+
ended = true;
|
|
84
|
+
wakeup.wake();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
hooks.onValue(result.value);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
hooks.dispose(); // drop a waiting value; stop the timer
|
|
91
|
+
sourceError = { error };
|
|
92
|
+
wakeup.wake();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
void pump();
|
|
97
|
+
while (true) {
|
|
98
|
+
// Abort wins at once; a source error comes after the values that
|
|
99
|
+
// were already emitted.
|
|
100
|
+
if (aborted.failed) throw aborted.error;
|
|
101
|
+
if (queue.length > 0) {
|
|
102
|
+
yield queue.shift() as T;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (sourceError) throw sourceError.error;
|
|
106
|
+
if (ended) return;
|
|
107
|
+
await Promise.race([wakeup.wait(), aborted.promise]);
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
stopped = true;
|
|
111
|
+
hooks.dispose();
|
|
112
|
+
unlisten();
|
|
113
|
+
await closeAll([entry]);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Emit a value only after `ms` have passed with no newer value: a burst of
|
|
119
|
+
* values becomes its last value, emitted `ms` after the burst ends. When
|
|
120
|
+
* the source ends, a value still waiting is emitted at once.
|
|
121
|
+
*
|
|
122
|
+
* Emitted values queue until the consumer reads them. A source error is
|
|
123
|
+
* rethrown after the queued values; a value still waiting for its quiet
|
|
124
|
+
* period is dropped.
|
|
125
|
+
* @kind function
|
|
126
|
+
* @name debounceAsync
|
|
127
|
+
* @example
|
|
128
|
+
* ```javascript
|
|
129
|
+
* for await (const query of debounceAsync(250, keystrokes)) search(query);
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export const debounceAsync = <T>(
|
|
133
|
+
ms: number,
|
|
134
|
+
iterable: AnyIterable<T>,
|
|
135
|
+
{ clock = systemClock, signal }: TimeOptions = {},
|
|
136
|
+
): AsyncGenerator<T> => {
|
|
137
|
+
checkMs("debounceAsync", ms);
|
|
138
|
+
return timed<T>(iterable, { signal }, (emit) => {
|
|
139
|
+
let timer: unknown;
|
|
140
|
+
let armed = false;
|
|
141
|
+
let waiting = false;
|
|
142
|
+
let value: T | undefined;
|
|
143
|
+
let lastAt = 0;
|
|
144
|
+
const fire = () => {
|
|
145
|
+
armed = false;
|
|
146
|
+
if (!waiting) return;
|
|
147
|
+
// Re-arm rather than reset the timer on every value: one timer per
|
|
148
|
+
// quiet period instead of one per value.
|
|
149
|
+
const elapsed = clock.now() - lastAt;
|
|
150
|
+
if (elapsed < ms) {
|
|
151
|
+
armed = true;
|
|
152
|
+
timer = clock.setTimeout(fire, ms - elapsed);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const out = value as T;
|
|
156
|
+
waiting = false;
|
|
157
|
+
value = undefined;
|
|
158
|
+
emit(out);
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
onValue(next) {
|
|
162
|
+
value = next;
|
|
163
|
+
waiting = true;
|
|
164
|
+
lastAt = clock.now();
|
|
165
|
+
if (!armed) {
|
|
166
|
+
armed = true;
|
|
167
|
+
timer = clock.setTimeout(fire, ms);
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
onEnd() {
|
|
171
|
+
if (armed) clock.clearTimeout(timer);
|
|
172
|
+
armed = false;
|
|
173
|
+
if (waiting) {
|
|
174
|
+
waiting = false;
|
|
175
|
+
emit(value as T);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
dispose() {
|
|
179
|
+
if (armed) clock.clearTimeout(timer);
|
|
180
|
+
armed = false;
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Emit at most one value per `ms` window. A value arriving while no window
|
|
188
|
+
* is open opens one, and is emitted at once if `leading` (default `true`).
|
|
189
|
+
* With `trailing` (default `true`), the newest value that arrived during a
|
|
190
|
+
* window is emitted when the window closes, which opens the next window.
|
|
191
|
+
* When the source ends, a trailing value still waiting is emitted at once.
|
|
192
|
+
*
|
|
193
|
+
* `leading` and `trailing` cannot both be false. Emitted values queue
|
|
194
|
+
* until the consumer reads them. A source error is rethrown after the
|
|
195
|
+
* queued values; a trailing value still waiting is dropped.
|
|
196
|
+
* @kind function
|
|
197
|
+
* @name throttleAsync
|
|
198
|
+
* @example
|
|
199
|
+
* ```javascript
|
|
200
|
+
* for await (const position of throttleAsync(16, pointerMoves)) draw(position);
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export const throttleAsync = <T>(
|
|
204
|
+
ms: number,
|
|
205
|
+
iterable: AnyIterable<T>,
|
|
206
|
+
{ leading = true, trailing = true, clock = systemClock, signal }: ThrottleOptions = {},
|
|
207
|
+
): AsyncGenerator<T> => {
|
|
208
|
+
checkMs("throttleAsync", ms);
|
|
209
|
+
if (!leading && !trailing) {
|
|
210
|
+
throw new RangeError("throttleAsync: leading and trailing cannot both be false");
|
|
211
|
+
}
|
|
212
|
+
return timed<T>(iterable, { signal }, (emit) => {
|
|
213
|
+
let timer: unknown;
|
|
214
|
+
let open = false;
|
|
215
|
+
let waiting = false;
|
|
216
|
+
let value: T | undefined;
|
|
217
|
+
const close = () => {
|
|
218
|
+
open = false;
|
|
219
|
+
if (trailing && waiting) {
|
|
220
|
+
const out = value as T;
|
|
221
|
+
waiting = false;
|
|
222
|
+
value = undefined;
|
|
223
|
+
emit(out);
|
|
224
|
+
openWindow();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const openWindow = () => {
|
|
228
|
+
open = true;
|
|
229
|
+
timer = clock.setTimeout(close, ms);
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
onValue(next) {
|
|
233
|
+
if (open) {
|
|
234
|
+
if (trailing) {
|
|
235
|
+
value = next;
|
|
236
|
+
waiting = true;
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (leading) {
|
|
241
|
+
emit(next);
|
|
242
|
+
} else {
|
|
243
|
+
value = next;
|
|
244
|
+
waiting = true;
|
|
245
|
+
}
|
|
246
|
+
openWindow();
|
|
247
|
+
},
|
|
248
|
+
onEnd() {
|
|
249
|
+
if (open) clock.clearTimeout(timer);
|
|
250
|
+
open = false;
|
|
251
|
+
if (trailing && waiting) {
|
|
252
|
+
waiting = false;
|
|
253
|
+
emit(value as T);
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
dispose() {
|
|
257
|
+
if (open) clock.clearTimeout(timer);
|
|
258
|
+
open = false;
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
};
|