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