@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.
Files changed (50) 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 +13 -0
  18. package/dist/iterator-tools.d.ts.map +1 -1
  19. package/dist/iterator-tools.js +38 -18
  20. package/dist/iterator-tools.js.map +1 -1
  21. package/dist/itertools.d.ts.map +1 -1
  22. package/dist/itertools.js +42 -24
  23. package/dist/itertools.js.map +1 -1
  24. package/dist/tee.d.ts +24 -2
  25. package/dist/tee.d.ts.map +1 -1
  26. package/dist/tee.js +175 -2
  27. package/dist/tee.js.map +1 -1
  28. package/dist/time.d.ts +69 -0
  29. package/dist/time.d.ts.map +1 -0
  30. package/dist/time.js +234 -0
  31. package/dist/time.js.map +1 -0
  32. package/dist/transduce.d.ts +65 -0
  33. package/dist/transduce.d.ts.map +1 -1
  34. package/dist/transduce.js +33 -1
  35. package/dist/transduce.js.map +1 -1
  36. package/dist/transducers.d.ts.map +1 -1
  37. package/dist/transducers.js +86 -16
  38. package/dist/transducers.js.map +1 -1
  39. package/package.json +10 -2
  40. package/readme.md +338 -14
  41. package/src/async-channel.ts +63 -47
  42. package/src/combine.ts +441 -0
  43. package/src/index.ts +2 -0
  44. package/src/internal.ts +164 -0
  45. package/src/iterator-tools.ts +48 -20
  46. package/src/itertools.ts +36 -23
  47. package/src/tee.ts +202 -2
  48. package/src/time.ts +262 -0
  49. package/src/transduce.ts +113 -1
  50. package/src/transducers.ts +121 -30
@@ -1,4 +1,4 @@
1
- import { isAsyncIterator } from "./is-iterator.ts";
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 generators = iteratorList.map((iterator) => iterator[Symbol.iterator]());
225
- outer: while (true) {
226
- const result: T[] = [];
227
- for (const generator of generators) {
228
- const { value, done } = generator.next();
229
- if (done) {
230
- break outer;
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.push(value);
255
+ yield result;
233
256
  }
234
- yield result;
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 generators = iteratorList.map((iterator) =>
252
- isAsyncIterator(iterator)
253
- ? (iterator as AsyncIterable<T>)[Symbol.asyncIterator]()
254
- : (iterator as Iterable<T>)[Symbol.iterator](),
255
- );
256
- while (true) {
257
- const results = await Promise.all(generators.map((g) => g.next()));
258
- if (results.some(({ done }) => done)) {
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
- yield results.map(({ value }) => value as T);
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 { isAsyncIterator } from "./is-iterator.ts";
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 generators = iteratorList.map((iterator) => iterator[Symbol.iterator]());
432
- while (true) {
433
- const result: Array<T | F> = [];
434
- let anyNotDone = false;
435
- for (const generator of generators) {
436
- const { value, done } = generator.next();
437
- if (done) {
438
- result.push(fillValue);
439
- } else {
440
- anyNotDone = true;
441
- result.push(value);
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
- if (!anyNotDone) return;
445
- yield result;
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 generators = iteratorList.map((iterator) =>
460
- isAsyncIterator(iterator)
461
- ? (iterator as AsyncIterable<T>)[Symbol.asyncIterator]()
462
- : (iterator as Iterable<T>)[Symbol.iterator](),
463
- );
464
- while (true) {
465
- const results = await Promise.all(generators.map((g) => g.next()));
466
- if (results.every(({ done }) => done)) return;
467
- yield results.map(({ value, done }) => (done ? fillValue : (value as T)));
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
+ };