@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/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
|
+
};
|
package/src/transduce.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { abortable, type SignalOptions } from "./abort.ts";
|
|
2
|
-
import { type ReducerStep, reduceAsync, reduceSync, type Transducer } from "./iterator-tools.ts";
|
|
2
|
+
import { HALT, type ReducerStep, reduceAsync, reduceSync, type Transducer } from "./iterator-tools.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The innermost reducer step ("emit"): pushes an emitted item onto the
|
|
@@ -159,3 +159,93 @@ export function transduceSync(
|
|
|
159
159
|
export function transduceSync(...functions: Array<Transducer<any, any>>) {
|
|
160
160
|
return (itemCollection: Iterable<unknown>) => reduceSync(itemCollection, composeFunctions(...functions)(emit), []);
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A push-driven transducer pipeline: feed values in one at a time with
|
|
165
|
+
* `step`, and flush with `complete` when the input ends.
|
|
166
|
+
*/
|
|
167
|
+
export interface PushTransducer<In, Out> {
|
|
168
|
+
/**
|
|
169
|
+
* Run one value through the pipeline and return what it emitted (often
|
|
170
|
+
* zero or one item; `group` and friends may emit several). Returns `[]`
|
|
171
|
+
* once the pipeline has halted or completed.
|
|
172
|
+
*/
|
|
173
|
+
step(value: In): Out[];
|
|
174
|
+
/**
|
|
175
|
+
* Flush stateful transducers (`.complete`, e.g. a trailing partial
|
|
176
|
+
* `group`) and return what they emitted. Call it once when the input
|
|
177
|
+
* ends, or after `halted` becomes true. Later calls return `[]`.
|
|
178
|
+
*/
|
|
179
|
+
complete(): Out[];
|
|
180
|
+
/** True once a transducer returned HALT (e.g. `take(n)` saw item n + 1). */
|
|
181
|
+
readonly halted: boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Drive transducers by pushing values in, with no iterator: for code that
|
|
186
|
+
* receives values by callback (an event handler, a wire in a dataflow
|
|
187
|
+
* graph). Uses the same left-to-right composition order, `.complete` flush
|
|
188
|
+
* protocol and HALT semantics as transduceSync:
|
|
189
|
+
* `transduceSync(...t)(xs)` yields exactly what pushing each `x` through
|
|
190
|
+
* `step` and then calling `complete()` returns.
|
|
191
|
+
*
|
|
192
|
+
* Like transduceSync, the items emitted by the step that returns HALT are
|
|
193
|
+
* discarded.
|
|
194
|
+
* @kind function
|
|
195
|
+
* @name transducePush
|
|
196
|
+
* @example
|
|
197
|
+
* ```javascript
|
|
198
|
+
* import { transducePush, transducers } from '@johnhenry/iteration';
|
|
199
|
+
* const { map, group } = transducers;
|
|
200
|
+
* const pipe = transducePush(map((x) => x * 10), group(2));
|
|
201
|
+
* pipe.step(1); // []
|
|
202
|
+
* pipe.step(2); // [[10, 20]]
|
|
203
|
+
* pipe.step(3); // []
|
|
204
|
+
* pipe.complete(); // [[30]]
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export function transducePush<A, B>(t1: Transducer<A, B>): PushTransducer<A, B>;
|
|
208
|
+
export function transducePush<A, B, C>(t1: Transducer<A, B>, t2: Transducer<B, C>): PushTransducer<A, C>;
|
|
209
|
+
export function transducePush<A, B, C, D>(
|
|
210
|
+
t1: Transducer<A, B>,
|
|
211
|
+
t2: Transducer<B, C>,
|
|
212
|
+
t3: Transducer<C, D>,
|
|
213
|
+
): PushTransducer<A, D>;
|
|
214
|
+
export function transducePush<A, B, C, D, E>(
|
|
215
|
+
t1: Transducer<A, B>,
|
|
216
|
+
t2: Transducer<B, C>,
|
|
217
|
+
t3: Transducer<C, D>,
|
|
218
|
+
t4: Transducer<D, E>,
|
|
219
|
+
): PushTransducer<A, E>;
|
|
220
|
+
export function transducePush<A, B, C, D, E, F>(
|
|
221
|
+
t1: Transducer<A, B>,
|
|
222
|
+
t2: Transducer<B, C>,
|
|
223
|
+
t3: Transducer<C, D>,
|
|
224
|
+
t4: Transducer<D, E>,
|
|
225
|
+
t5: Transducer<E, F>,
|
|
226
|
+
): PushTransducer<A, F>;
|
|
227
|
+
export function transducePush(...functions: Array<Transducer<any, any>>): PushTransducer<unknown, unknown>;
|
|
228
|
+
export function transducePush(...functions: Array<Transducer<any, any>>): PushTransducer<unknown, unknown> {
|
|
229
|
+
const step = composeFunctions(...functions)(emit);
|
|
230
|
+
let halted = false;
|
|
231
|
+
let completed = false;
|
|
232
|
+
return {
|
|
233
|
+
step(value) {
|
|
234
|
+
if (halted || completed) return [];
|
|
235
|
+
const out = step([], value);
|
|
236
|
+
if (out === HALT) {
|
|
237
|
+
halted = true;
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
},
|
|
242
|
+
complete() {
|
|
243
|
+
if (completed) return [];
|
|
244
|
+
completed = true;
|
|
245
|
+
return step.complete ? step.complete([]) : [];
|
|
246
|
+
},
|
|
247
|
+
get halted() {
|
|
248
|
+
return halted;
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|