@ix-xs/node-comfort 1.2.0 → 2.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/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Async.js
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Promises and concurrency: map with a limit, queues with priorities,
|
|
5
|
+
* mutexes, polling, and friendlier `Promise.allSettled`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const pages = await async.map(urls, (url) => fetchPage(url), { concurrency: 5 });
|
|
9
|
+
*
|
|
10
|
+
* const limit = async.limit(2);
|
|
11
|
+
* await Promise.all(files.map((file) => limit(() => upload(file))));
|
|
12
|
+
*
|
|
13
|
+
* await async.poll(() => isServerReady(), { interval: 250, timeout: "10s" });
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { AbortError, TimeoutError } = require("./errors.js");
|
|
17
|
+
const { toMs } = require("./internal/shared.js");
|
|
18
|
+
const { retry, timeout } = require("./Func.js");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for `map()`, `forEach()` and `filter()`.
|
|
22
|
+
* @typedef {object} MapOptions
|
|
23
|
+
* @property {number} [concurrency] How many run at the same time. No limit by default.
|
|
24
|
+
* @property {boolean} [stopOnError] Stop at the first error. With `false`, everything runs and you get an `AggregateError` of all failures. Defaults to `true`.
|
|
25
|
+
* @property {AbortSignal} [signal] Stops starting new work and rejects with an `AbortError`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A rate limiter from `rateLimit()`. Call it with a function to run it as
|
|
30
|
+
* soon as the rate allows.
|
|
31
|
+
* @typedef {(<T>(fn: () => T | PromiseLike<T>) => Promise<Awaited<T>>) & { readonly pendingCount: number }} RateLimiter
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A limiter from `limit()`. Call it with a function to run it when there's room.
|
|
36
|
+
* @typedef {(<T>(fn: () => T | PromiseLike<T>) => Promise<Awaited<T>>) & { readonly activeCount: number, readonly pendingCount: number, concurrency: number, clearQueue(): void, onIdle(): Promise<void> }} Limiter
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options for `queue()`.
|
|
41
|
+
* @typedef {object} QueueOptions
|
|
42
|
+
* @property {number} [concurrency] How many tasks run at the same time. Defaults to `1`.
|
|
43
|
+
* @property {boolean} [autoStart] Start right away. Defaults to `true`.
|
|
44
|
+
* @property {number | string} [timeout] Time limit per task, like `"30s"`. A slower task rejects with a `TimeoutError`.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for `queue.add()`.
|
|
49
|
+
* @typedef {object} QueueAddOptions
|
|
50
|
+
* @property {number} [priority] Higher goes first; equal priorities keep their order. Defaults to `0`.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A queue from `queue()`.
|
|
55
|
+
* @typedef {object} Queue
|
|
56
|
+
* @property {<T>(task: () => T | PromiseLike<T>, options?: QueueAddOptions) => Promise<Awaited<T>>} add Adds a task; resolves with its result.
|
|
57
|
+
* @property {<T>(tasks: Array<() => T | PromiseLike<T>>, options?: QueueAddOptions) => Promise<Array<Awaited<T>>>} addAll Adds several tasks; resolves with all results.
|
|
58
|
+
* @property {() => Queue} pause Stops starting tasks. Running ones finish.
|
|
59
|
+
* @property {() => Queue} start Starts again.
|
|
60
|
+
* @property {() => void} clear Drops the tasks that haven't started. Their promises never settle.
|
|
61
|
+
* @property {() => Promise<void>} onIdle Resolves when nothing is waiting or running.
|
|
62
|
+
* @property {() => Promise<void>} onEmpty Resolves when nothing is waiting.
|
|
63
|
+
* @property {number} size Tasks waiting.
|
|
64
|
+
* @property {number} pending Tasks running.
|
|
65
|
+
* @property {boolean} isPaused
|
|
66
|
+
* @property {number} concurrency Can be changed on the fly.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A promise with its `resolve` and `reject`.
|
|
71
|
+
* @template T
|
|
72
|
+
* @typedef {object} Deferred
|
|
73
|
+
* @property {Promise<T>} promise
|
|
74
|
+
* @property {(value: T | PromiseLike<T>) => void} resolve
|
|
75
|
+
* @property {(reason?: unknown) => void} reject
|
|
76
|
+
* @property {boolean} settled Whether it's already resolved or rejected.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What `settle()` returns.
|
|
81
|
+
* @template T
|
|
82
|
+
* @typedef {object} Settled
|
|
83
|
+
* @property {T[]} fulfilled Successful values, in order.
|
|
84
|
+
* @property {unknown[]} rejected Errors, in order.
|
|
85
|
+
* @property {Array<PromiseSettledResult<T>>} results Everything, like `Promise.allSettled`.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Options for `poll()`.
|
|
90
|
+
* @typedef {object} PollOptions
|
|
91
|
+
* @property {number | string} [interval] Time between tries. Defaults to 100 ms.
|
|
92
|
+
* @property {number | string} [timeout] Give up with a `TimeoutError` after this long.
|
|
93
|
+
* @property {AbortSignal} [signal] Stops with an `AbortError`.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A mutex from `mutex()`.
|
|
98
|
+
* @typedef {object} Mutex
|
|
99
|
+
* @property {<T>(fn: () => T | PromiseLike<T>) => Promise<Awaited<T>>} run Runs `fn` once no other `run` is in progress.
|
|
100
|
+
* @property {() => Promise<() => void>} lock Waits for the lock and gives you the function that releases it.
|
|
101
|
+
* @property {boolean} isLocked
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
/** @param {number | string | undefined} value */
|
|
105
|
+
const _ms = (value) => (value === undefined ? undefined : toMs(value));
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A first-in first-out list where taking the first item costs nothing,
|
|
109
|
+
* unlike `Array.prototype.shift` on a long array.
|
|
110
|
+
* @template T
|
|
111
|
+
*/
|
|
112
|
+
class _Fifo {
|
|
113
|
+
/** @type {T[]} */
|
|
114
|
+
#items = [];
|
|
115
|
+
#head = 0;
|
|
116
|
+
|
|
117
|
+
/** @param {T} item */
|
|
118
|
+
push(item) {
|
|
119
|
+
this.#items.push(item);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @returns {T | undefined} */
|
|
123
|
+
shift() {
|
|
124
|
+
if (this.#head >= this.#items.length) return undefined;
|
|
125
|
+
const item = this.#items[this.#head];
|
|
126
|
+
this.#items[this.#head++] = /** @type {any} */ (undefined);
|
|
127
|
+
if (this.#head > 1024 && this.#head * 2 > this.#items.length) {
|
|
128
|
+
this.#items = this.#items.slice(this.#head);
|
|
129
|
+
this.#head = 0;
|
|
130
|
+
}
|
|
131
|
+
return item;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
clear() {
|
|
135
|
+
this.#items = [];
|
|
136
|
+
this.#head = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
get size() {
|
|
140
|
+
return this.#items.length - this.#head;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A binary heap: the entry with the highest priority comes out first, and
|
|
146
|
+
* entries with the same priority come out in the order they went in.
|
|
147
|
+
* @template {{ priority: number, order: number }} T
|
|
148
|
+
*/
|
|
149
|
+
class _Heap {
|
|
150
|
+
/** @type {T[]} */
|
|
151
|
+
#items = [];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {T} a
|
|
155
|
+
* @param {T} b
|
|
156
|
+
*/
|
|
157
|
+
#before(a, b) {
|
|
158
|
+
return a.priority > b.priority || (a.priority === b.priority && a.order < b.order);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** @param {T} item */
|
|
162
|
+
push(item) {
|
|
163
|
+
const items = this.#items;
|
|
164
|
+
let i = items.length;
|
|
165
|
+
items.push(item);
|
|
166
|
+
while (i > 0) {
|
|
167
|
+
const parent = (i - 1) >> 1;
|
|
168
|
+
if (!this.#before(item, items[parent])) break;
|
|
169
|
+
items[i] = items[parent];
|
|
170
|
+
i = parent;
|
|
171
|
+
}
|
|
172
|
+
items[i] = item;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @returns {T | undefined} */
|
|
176
|
+
pop() {
|
|
177
|
+
const items = this.#items;
|
|
178
|
+
const top = items[0];
|
|
179
|
+
const last = items.pop();
|
|
180
|
+
if (items.length && last !== undefined) {
|
|
181
|
+
let i = 0;
|
|
182
|
+
const half = items.length >> 1;
|
|
183
|
+
while (i < half) {
|
|
184
|
+
let child = i * 2 + 1;
|
|
185
|
+
if (child + 1 < items.length && this.#before(items[child + 1], items[child])) child++;
|
|
186
|
+
if (!this.#before(items[child], last)) break;
|
|
187
|
+
items[i] = items[child];
|
|
188
|
+
i = child;
|
|
189
|
+
}
|
|
190
|
+
items[i] = last;
|
|
191
|
+
}
|
|
192
|
+
return top;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
clear() {
|
|
196
|
+
this.#items = [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
get size() {
|
|
200
|
+
return this.#items.length;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Waits for a while. Same as `nc.wait()`.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* await async.sleep("250ms");
|
|
209
|
+
*
|
|
210
|
+
* @param {number | string} duration
|
|
211
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
212
|
+
* @returns {Promise<void>}
|
|
213
|
+
*/
|
|
214
|
+
function sleep(duration, options = {}) {
|
|
215
|
+
return require("./Utils.js").wait(duration, options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Maps items through an async function, with at most `concurrency` calls
|
|
220
|
+
* at once. Results stay in order. Items are taken from the source as slots
|
|
221
|
+
* free up, so a stream is never read faster than it's processed.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* const users = await async.map(ids, (id) => api.getUser(id), { concurrency: 10 });
|
|
225
|
+
* await async.forEach(readline.createInterface({ input }), importLine, { concurrency: 20 });
|
|
226
|
+
*
|
|
227
|
+
* @template T
|
|
228
|
+
* @template R
|
|
229
|
+
* @param {Iterable<T> | AsyncIterable<T>} items
|
|
230
|
+
* @param {(item: T, index: number) => R | PromiseLike<R>} fn
|
|
231
|
+
* @param {MapOptions} [options]
|
|
232
|
+
* @returns {Promise<Array<Awaited<R>>>}
|
|
233
|
+
* @throws The first error, an `AggregateError` with `stopOnError: false`, or an `AbortError`.
|
|
234
|
+
*/
|
|
235
|
+
function map(items, fn, options = {}) {
|
|
236
|
+
const concurrency = Math.max(1, options.concurrency ?? Infinity);
|
|
237
|
+
const stopOnError = options.stopOnError ?? true;
|
|
238
|
+
const signal = options.signal;
|
|
239
|
+
/** @type {Array<Awaited<R>>} */
|
|
240
|
+
const results = [];
|
|
241
|
+
/** @type {unknown[]} */
|
|
242
|
+
const errors = [];
|
|
243
|
+
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
const source = /** @type {any} */ (items);
|
|
246
|
+
const isAsync = source !== null && source !== undefined && typeof source[Symbol.asyncIterator] === "function";
|
|
247
|
+
/** @type {Iterator<T> | AsyncIterator<T>} */
|
|
248
|
+
let iterator;
|
|
249
|
+
try {
|
|
250
|
+
iterator = isAsync ? source[Symbol.asyncIterator]() : source[Symbol.iterator]();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return reject(error);
|
|
253
|
+
}
|
|
254
|
+
let started = 0;
|
|
255
|
+
let active = 0;
|
|
256
|
+
let exhausted = false;
|
|
257
|
+
let pulling = false;
|
|
258
|
+
let settled = false;
|
|
259
|
+
|
|
260
|
+
/** @param {unknown} [error] */
|
|
261
|
+
const end = (error) => {
|
|
262
|
+
if (settled) return;
|
|
263
|
+
settled = true;
|
|
264
|
+
signal?.removeEventListener("abort", onAbort);
|
|
265
|
+
// Stopped early: let the source release what it holds (an open file, a cursor).
|
|
266
|
+
if (!exhausted) void Promise.resolve().then(() => iterator.return?.()).catch(() => {});
|
|
267
|
+
if (error !== undefined) return reject(error);
|
|
268
|
+
results.length = started;
|
|
269
|
+
const failures = errors.filter((e) => e !== undefined);
|
|
270
|
+
if (failures.length) reject(new AggregateError(failures, `${failures.length} of ${started} tasks failed`));
|
|
271
|
+
else resolve(results);
|
|
272
|
+
};
|
|
273
|
+
const onAbort = () => end(new AbortError(undefined, { cause: signal?.reason }));
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* @param {T} item
|
|
277
|
+
* @param {number} index
|
|
278
|
+
*/
|
|
279
|
+
const run = (item, index) => {
|
|
280
|
+
active++;
|
|
281
|
+
/** @type {Promise<any>} */
|
|
282
|
+
let work;
|
|
283
|
+
try {
|
|
284
|
+
work = Promise.resolve(fn(item, index));
|
|
285
|
+
} catch (error) {
|
|
286
|
+
work = Promise.reject(error);
|
|
287
|
+
}
|
|
288
|
+
work.then(
|
|
289
|
+
(value) => {
|
|
290
|
+
results[index] = value;
|
|
291
|
+
},
|
|
292
|
+
(error) => {
|
|
293
|
+
if (stopOnError) end(error ?? new Error("Task failed"));
|
|
294
|
+
else errors[index] = error ?? new Error("Task failed");
|
|
295
|
+
},
|
|
296
|
+
).then(() => {
|
|
297
|
+
active--;
|
|
298
|
+
pump();
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const pump = () => {
|
|
303
|
+
if (settled || pulling) return;
|
|
304
|
+
while (!exhausted && active < concurrency) {
|
|
305
|
+
/** @type {any} */
|
|
306
|
+
let step;
|
|
307
|
+
try {
|
|
308
|
+
step = iterator.next();
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return end(error ?? new Error("Iterator failed"));
|
|
311
|
+
}
|
|
312
|
+
if (isAsync) {
|
|
313
|
+
pulling = true;
|
|
314
|
+
/** @type {Promise<IteratorResult<T>>} */ (step).then(
|
|
315
|
+
(result) => {
|
|
316
|
+
pulling = false;
|
|
317
|
+
if (settled) return;
|
|
318
|
+
if (result.done) exhausted = true;
|
|
319
|
+
else run(result.value, started++);
|
|
320
|
+
pump();
|
|
321
|
+
},
|
|
322
|
+
(error) => end(error ?? new Error("Iterator failed")),
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (step.done) exhausted = true;
|
|
327
|
+
else run(step.value, started++);
|
|
328
|
+
if (settled) return;
|
|
329
|
+
}
|
|
330
|
+
if (exhausted && active === 0) end();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
if (signal?.aborted) return onAbort();
|
|
334
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
335
|
+
pump();
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Runs an async function for every item, with a concurrency limit.
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* await async.forEach(images, (image) => optimize(image), { concurrency: 4 });
|
|
344
|
+
*
|
|
345
|
+
* @template T
|
|
346
|
+
* @param {Iterable<T> | AsyncIterable<T>} items
|
|
347
|
+
* @param {(item: T, index: number) => unknown} fn
|
|
348
|
+
* @param {MapOptions} [options]
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
*/
|
|
351
|
+
async function forEach(items, fn, options = {}) {
|
|
352
|
+
await map(items, fn, options);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Keeps the items that pass an async test, with a concurrency limit.
|
|
357
|
+
* Order is preserved.
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* const alive = await async.filter(hosts, (host) => ping(host), { concurrency: 20 });
|
|
361
|
+
*
|
|
362
|
+
* @template T
|
|
363
|
+
* @param {Iterable<T> | AsyncIterable<T>} items
|
|
364
|
+
* @param {(item: T, index: number) => unknown} predicate
|
|
365
|
+
* @param {MapOptions} [options]
|
|
366
|
+
* @returns {Promise<T[]>}
|
|
367
|
+
*/
|
|
368
|
+
async function filter(items, predicate, options = {}) {
|
|
369
|
+
/** @type {T[]} */
|
|
370
|
+
const list = [];
|
|
371
|
+
const keep = await map(items, (item, index) => {
|
|
372
|
+
list[index] = item;
|
|
373
|
+
return predicate(item, index);
|
|
374
|
+
}, options);
|
|
375
|
+
return list.filter((_, i) => keep[i]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Runs async functions one after the other and returns their results.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* await async.series([() => migrateUsers(), () => migrateOrders()]);
|
|
383
|
+
*
|
|
384
|
+
* @template T
|
|
385
|
+
* @param {Iterable<() => T | PromiseLike<T>>} tasks
|
|
386
|
+
* @returns {Promise<Array<Awaited<T>>>}
|
|
387
|
+
*/
|
|
388
|
+
async function series(tasks) {
|
|
389
|
+
/** @type {Array<Awaited<T>>} */
|
|
390
|
+
const out = [];
|
|
391
|
+
for (const task of tasks) out.push(await task());
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Runs async functions at the same time, or a few at once with
|
|
397
|
+
* `concurrency`, and returns their results in order.
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* const [users, posts] = await async.parallel([() => loadUsers(), () => loadPosts()]);
|
|
401
|
+
* await async.parallel(jobs, { concurrency: 4 });
|
|
402
|
+
*
|
|
403
|
+
* @template T
|
|
404
|
+
* @param {Iterable<() => T | PromiseLike<T>>} tasks
|
|
405
|
+
* @param {MapOptions} [options]
|
|
406
|
+
* @returns {Promise<Array<Awaited<T>>>}
|
|
407
|
+
*/
|
|
408
|
+
function parallel(tasks, options = {}) {
|
|
409
|
+
return map(tasks, (task) => task(), options);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* A limiter: wrap calls with it, and at most `concurrency` run at once
|
|
414
|
+
* while the rest wait their turn. Like `p-limit`.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* const limit = async.limit(3);
|
|
418
|
+
* const results = await Promise.all(urls.map((url) => limit(() => fetch(url))));
|
|
419
|
+
*
|
|
420
|
+
* @param {number} concurrency
|
|
421
|
+
* @returns {Limiter}
|
|
422
|
+
*/
|
|
423
|
+
function limit(concurrency) {
|
|
424
|
+
let max = Math.max(1, concurrency);
|
|
425
|
+
let active = 0;
|
|
426
|
+
/** @type {_Fifo<() => void>} */
|
|
427
|
+
const waiting = new _Fifo();
|
|
428
|
+
/** @type {Array<() => void>} */
|
|
429
|
+
let idleWaiters = [];
|
|
430
|
+
const next = () => {
|
|
431
|
+
while (active < max && waiting.size) {
|
|
432
|
+
active++;
|
|
433
|
+
/** @type {() => void} */ (waiting.shift())();
|
|
434
|
+
}
|
|
435
|
+
if (active === 0 && !waiting.size && idleWaiters.length) {
|
|
436
|
+
const resolvers = idleWaiters;
|
|
437
|
+
idleWaiters = [];
|
|
438
|
+
for (const resolve of resolvers) resolve();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
/** @type {any} */
|
|
442
|
+
const limiter = (/** @type {() => unknown} */ fn) =>
|
|
443
|
+
new Promise((resolve, reject) => {
|
|
444
|
+
waiting.push(() => {
|
|
445
|
+
Promise.resolve()
|
|
446
|
+
.then(fn)
|
|
447
|
+
.then(resolve, reject)
|
|
448
|
+
.finally(() => {
|
|
449
|
+
active--;
|
|
450
|
+
next();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
next();
|
|
454
|
+
});
|
|
455
|
+
Object.defineProperties(limiter, {
|
|
456
|
+
activeCount: { get: () => active },
|
|
457
|
+
pendingCount: { get: () => waiting.size },
|
|
458
|
+
concurrency: {
|
|
459
|
+
get: () => max,
|
|
460
|
+
set: (/** @type {number} */ value) => {
|
|
461
|
+
max = Math.max(1, value);
|
|
462
|
+
next();
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
clearQueue: { value: () => waiting.clear() },
|
|
466
|
+
onIdle: { value: () => (active === 0 && !waiting.size ? Promise.resolve() : new Promise((resolve) => idleWaiters.push(() => resolve(undefined)))) },
|
|
467
|
+
});
|
|
468
|
+
return limiter;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* A rate limiter: at most `max` calls start within any `interval`. Made
|
|
473
|
+
* for APIs that allow, say, 10 requests per second.
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* const throttled = async.rateLimit(10, "1s");
|
|
477
|
+
* const pages = await Promise.all(ids.map((id) => throttled(() => api.get(`items/${id}`))));
|
|
478
|
+
*
|
|
479
|
+
* @param {number} max How many calls may start per interval.
|
|
480
|
+
* @param {number | string} interval The window, in ms or like `"1s"`.
|
|
481
|
+
* @returns {RateLimiter}
|
|
482
|
+
*/
|
|
483
|
+
function rateLimit(max, interval) {
|
|
484
|
+
const perWindow = Math.max(1, Math.floor(max));
|
|
485
|
+
const windowMs = toMs(interval, "interval");
|
|
486
|
+
/** @type {number[]} */
|
|
487
|
+
const starts = [];
|
|
488
|
+
/** @type {_Fifo<() => void>} */
|
|
489
|
+
const waiting = new _Fifo();
|
|
490
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
491
|
+
let timer;
|
|
492
|
+
|
|
493
|
+
const pump = () => {
|
|
494
|
+
timer = undefined;
|
|
495
|
+
while (waiting.size) {
|
|
496
|
+
const now = Date.now();
|
|
497
|
+
while (starts.length && now - starts[0] >= windowMs) starts.shift();
|
|
498
|
+
if (starts.length >= perWindow) {
|
|
499
|
+
timer = setTimeout(pump, Math.max(1, starts[0] + windowMs - now));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
starts.push(now);
|
|
503
|
+
/** @type {() => void} */ (waiting.shift())();
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
/** @type {any} */
|
|
507
|
+
const limiter = (/** @type {() => unknown} */ fn) =>
|
|
508
|
+
new Promise((resolve, reject) => {
|
|
509
|
+
waiting.push(() => void Promise.resolve().then(fn).then(resolve, reject));
|
|
510
|
+
if (timer === undefined) pump();
|
|
511
|
+
});
|
|
512
|
+
Object.defineProperty(limiter, "pendingCount", { get: () => waiting.size });
|
|
513
|
+
return limiter;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* A task queue with a concurrency limit, priorities, pause and per-task
|
|
518
|
+
* timeouts.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* const jobs = async.queue({ concurrency: 2, timeout: "30s" });
|
|
522
|
+
* jobs.add(() => sendEmail(a));
|
|
523
|
+
* jobs.add(() => sendEmail(vip), { priority: 10 }); // jumps the line
|
|
524
|
+
* await jobs.onIdle();
|
|
525
|
+
*
|
|
526
|
+
* @param {QueueOptions} [options]
|
|
527
|
+
* @returns {Queue}
|
|
528
|
+
*/
|
|
529
|
+
function queue(options = {}) {
|
|
530
|
+
let concurrency = Math.max(1, options.concurrency ?? 1);
|
|
531
|
+
let paused = options.autoStart === false;
|
|
532
|
+
let running = 0;
|
|
533
|
+
let order = 0;
|
|
534
|
+
const taskTimeout = _ms(options.timeout);
|
|
535
|
+
/** @type {_Heap<{ run: () => void, priority: number, order: number }>} */
|
|
536
|
+
const waiting = new _Heap();
|
|
537
|
+
/** @type {Array<() => void>} */
|
|
538
|
+
let idle = [];
|
|
539
|
+
/** @type {Array<() => void>} */
|
|
540
|
+
let empty = [];
|
|
541
|
+
|
|
542
|
+
const check = () => {
|
|
543
|
+
if (!waiting.size) {
|
|
544
|
+
for (const resolve of empty) resolve();
|
|
545
|
+
empty = [];
|
|
546
|
+
if (running === 0) {
|
|
547
|
+
for (const resolve of idle) resolve();
|
|
548
|
+
idle = [];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const pump = () => {
|
|
553
|
+
while (!paused && running < concurrency && waiting.size) {
|
|
554
|
+
const task = /** @type {{ run: () => void }} */ (waiting.pop());
|
|
555
|
+
running++;
|
|
556
|
+
task.run();
|
|
557
|
+
}
|
|
558
|
+
check();
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/** @type {Queue} */
|
|
562
|
+
const self = {
|
|
563
|
+
add(task, addOptions = {}) {
|
|
564
|
+
return new Promise((resolve, reject) => {
|
|
565
|
+
const entry = {
|
|
566
|
+
priority: addOptions.priority ?? 0,
|
|
567
|
+
order: order++,
|
|
568
|
+
run: () => {
|
|
569
|
+
const work = Promise.resolve().then(task);
|
|
570
|
+
(taskTimeout === undefined ? work : timeout(work, taskTimeout, `Queue task timed out after ${taskTimeout}ms`))
|
|
571
|
+
.then(/** @type {any} */ (resolve), reject)
|
|
572
|
+
.finally(() => {
|
|
573
|
+
running--;
|
|
574
|
+
pump();
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
waiting.push(entry);
|
|
579
|
+
pump();
|
|
580
|
+
});
|
|
581
|
+
},
|
|
582
|
+
addAll(tasks, addOptions) {
|
|
583
|
+
return Promise.all(tasks.map((task) => self.add(task, addOptions)));
|
|
584
|
+
},
|
|
585
|
+
pause() {
|
|
586
|
+
paused = true;
|
|
587
|
+
return self;
|
|
588
|
+
},
|
|
589
|
+
start() {
|
|
590
|
+
paused = false;
|
|
591
|
+
pump();
|
|
592
|
+
return self;
|
|
593
|
+
},
|
|
594
|
+
clear() {
|
|
595
|
+
waiting.clear();
|
|
596
|
+
check();
|
|
597
|
+
},
|
|
598
|
+
onIdle() {
|
|
599
|
+
return !waiting.size && running === 0 ? Promise.resolve() : new Promise((resolve) => idle.push(() => resolve(undefined)));
|
|
600
|
+
},
|
|
601
|
+
onEmpty() {
|
|
602
|
+
return !waiting.size ? Promise.resolve() : new Promise((resolve) => empty.push(() => resolve(undefined)));
|
|
603
|
+
},
|
|
604
|
+
get size() {
|
|
605
|
+
return waiting.size;
|
|
606
|
+
},
|
|
607
|
+
get pending() {
|
|
608
|
+
return running;
|
|
609
|
+
},
|
|
610
|
+
get isPaused() {
|
|
611
|
+
return paused;
|
|
612
|
+
},
|
|
613
|
+
get concurrency() {
|
|
614
|
+
return concurrency;
|
|
615
|
+
},
|
|
616
|
+
set concurrency(value) {
|
|
617
|
+
concurrency = Math.max(1, value);
|
|
618
|
+
pump();
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
return self;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* A mutex: tasks given to `run()` never overlap. Put it around
|
|
626
|
+
* read-modify-write steps, like updating a file or refreshing a token.
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* const lock = async.mutex();
|
|
630
|
+
* await lock.run(async () => {
|
|
631
|
+
* const data = nc.readJSON("./counter.json", { n: 0 });
|
|
632
|
+
* nc.writeJSON("./counter.json", { n: data.n + 1 });
|
|
633
|
+
* });
|
|
634
|
+
*
|
|
635
|
+
* @returns {Mutex}
|
|
636
|
+
*/
|
|
637
|
+
function mutex() {
|
|
638
|
+
const limiter = limit(1);
|
|
639
|
+
let locked = false;
|
|
640
|
+
return {
|
|
641
|
+
run: (fn) => limiter(async () => {
|
|
642
|
+
locked = true;
|
|
643
|
+
try {
|
|
644
|
+
return await fn();
|
|
645
|
+
} finally {
|
|
646
|
+
locked = false;
|
|
647
|
+
}
|
|
648
|
+
}),
|
|
649
|
+
lock: () =>
|
|
650
|
+
new Promise((resolveLock) => {
|
|
651
|
+
void limiter(() => new Promise((release) => {
|
|
652
|
+
locked = true;
|
|
653
|
+
resolveLock(() => {
|
|
654
|
+
locked = false;
|
|
655
|
+
release(undefined);
|
|
656
|
+
});
|
|
657
|
+
}));
|
|
658
|
+
}),
|
|
659
|
+
get isLocked() {
|
|
660
|
+
return locked;
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* A promise along with its `resolve` and `reject`. Handy to connect
|
|
667
|
+
* callbacks or events to `await`.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* const ready = async.deferred();
|
|
671
|
+
* socket.once("open", () => ready.resolve());
|
|
672
|
+
* await ready.promise;
|
|
673
|
+
*
|
|
674
|
+
* @template [T=void]
|
|
675
|
+
* @returns {Deferred<T>}
|
|
676
|
+
*/
|
|
677
|
+
function deferred() {
|
|
678
|
+
/** @type {(value: T | PromiseLike<T>) => void} */
|
|
679
|
+
let resolve = () => {};
|
|
680
|
+
/** @type {(reason?: unknown) => void} */
|
|
681
|
+
let reject = () => {};
|
|
682
|
+
/** @type {Deferred<T>} */
|
|
683
|
+
const out = {
|
|
684
|
+
promise: new Promise((res, rej) => {
|
|
685
|
+
resolve = res;
|
|
686
|
+
reject = rej;
|
|
687
|
+
}),
|
|
688
|
+
resolve: (value) => {
|
|
689
|
+
out.settled = true;
|
|
690
|
+
resolve(value);
|
|
691
|
+
},
|
|
692
|
+
reject: (reason) => {
|
|
693
|
+
out.settled = true;
|
|
694
|
+
reject(reason);
|
|
695
|
+
},
|
|
696
|
+
settled: false,
|
|
697
|
+
};
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Waits for every promise and sorts successes from failures.
|
|
703
|
+
*
|
|
704
|
+
* @example
|
|
705
|
+
* const { fulfilled, rejected } = await async.settle(emails.map(send));
|
|
706
|
+
* nc.info(`${fulfilled.length} sent, ${rejected.length} failed`);
|
|
707
|
+
*
|
|
708
|
+
* @template T
|
|
709
|
+
* @param {Iterable<T | PromiseLike<T>>} promises
|
|
710
|
+
* @returns {Promise<Settled<Awaited<T>>>}
|
|
711
|
+
*/
|
|
712
|
+
async function settle(promises) {
|
|
713
|
+
const results = /** @type {Array<PromiseSettledResult<Awaited<T>>>} */ (await Promise.allSettled(promises));
|
|
714
|
+
return {
|
|
715
|
+
fulfilled: results.filter((r) => r.status === "fulfilled").map((r) => /** @type {PromiseFulfilledResult<Awaited<T>>} */ (r).value),
|
|
716
|
+
rejected: results.filter((r) => r.status === "rejected").map((r) => /** @type {PromiseRejectedResult} */ (r).reason),
|
|
717
|
+
results,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Like `Promise.all`, for an object of promises.
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* const { user, orders } = await async.props({ user: getUser(id), orders: getOrders(id) });
|
|
726
|
+
*
|
|
727
|
+
* @template {Record<string, unknown>} T
|
|
728
|
+
* @param {T} object
|
|
729
|
+
* @returns {Promise<{ [K in keyof T]: Awaited<T[K]> }>}
|
|
730
|
+
*/
|
|
731
|
+
async function props(object) {
|
|
732
|
+
const keys = Object.keys(object);
|
|
733
|
+
const values = await Promise.all(keys.map((key) => object[key]));
|
|
734
|
+
return /** @type {any} */ (Object.fromEntries(keys.map((key, i) => [key, values[i]])));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Calls `fn` until it returns something truthy, and resolves with it.
|
|
739
|
+
* Errors count as "not yet".
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* await async.poll(() => fetch(healthUrl).then((res) => res.ok), { interval: "500ms", timeout: "30s" });
|
|
743
|
+
*
|
|
744
|
+
* @template T
|
|
745
|
+
* @param {() => T | PromiseLike<T>} fn
|
|
746
|
+
* @param {PollOptions} [options]
|
|
747
|
+
* @returns {Promise<NonNullable<Awaited<T>>>}
|
|
748
|
+
* @throws {TimeoutError}
|
|
749
|
+
* @throws {AbortError}
|
|
750
|
+
*/
|
|
751
|
+
async function poll(fn, options = {}) {
|
|
752
|
+
const interval = _ms(options.interval) ?? 100;
|
|
753
|
+
const limitMs = _ms(options.timeout);
|
|
754
|
+
const deadline = limitMs === undefined ? Infinity : Date.now() + limitMs;
|
|
755
|
+
/** @type {unknown} */
|
|
756
|
+
let lastError;
|
|
757
|
+
for (;;) {
|
|
758
|
+
if (options.signal?.aborted) throw new AbortError(undefined, { cause: options.signal.reason });
|
|
759
|
+
try {
|
|
760
|
+
const value = await fn();
|
|
761
|
+
if (value) return /** @type {NonNullable<Awaited<T>>} */ (value);
|
|
762
|
+
lastError = undefined;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
// Not ready yet. Kept, so a timeout can say why.
|
|
765
|
+
lastError = error;
|
|
766
|
+
}
|
|
767
|
+
const remaining = deadline - Date.now();
|
|
768
|
+
if (remaining <= 0) throw new TimeoutError(`poll() timed out after ${limitMs}ms`, { timeout: limitMs, cause: lastError });
|
|
769
|
+
await sleep(Math.min(interval, remaining), { signal: options.signal });
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Makes any promise give up when a signal fires. The work behind it keeps
|
|
775
|
+
* running unless it listens to the signal too.
|
|
776
|
+
*
|
|
777
|
+
* @example
|
|
778
|
+
* const controller = new AbortController();
|
|
779
|
+
* const rows = await async.abortable(slowQuery(), controller.signal);
|
|
780
|
+
*
|
|
781
|
+
* @template T
|
|
782
|
+
* @param {T | PromiseLike<T>} promise
|
|
783
|
+
* @param {AbortSignal} [signal]
|
|
784
|
+
* @returns {Promise<Awaited<T>>}
|
|
785
|
+
* @throws {AbortError} When the signal fires first.
|
|
786
|
+
*/
|
|
787
|
+
function abortable(promise, signal) {
|
|
788
|
+
if (!signal) return /** @type {Promise<Awaited<T>>} */ (Promise.resolve(promise));
|
|
789
|
+
if (signal.aborted) return Promise.reject(new AbortError(undefined, { cause: signal.reason }));
|
|
790
|
+
return new Promise((resolve, reject) => {
|
|
791
|
+
const onAbort = () => reject(new AbortError(undefined, { cause: signal.reason }));
|
|
792
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
793
|
+
Promise.resolve(promise)
|
|
794
|
+
.then(/** @type {any} */ (resolve), reject)
|
|
795
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
module.exports = {
|
|
800
|
+
sleep,
|
|
801
|
+
map,
|
|
802
|
+
forEach,
|
|
803
|
+
filter,
|
|
804
|
+
series,
|
|
805
|
+
parallel,
|
|
806
|
+
limit,
|
|
807
|
+
rateLimit,
|
|
808
|
+
queue,
|
|
809
|
+
mutex,
|
|
810
|
+
deferred,
|
|
811
|
+
settle,
|
|
812
|
+
props,
|
|
813
|
+
poll,
|
|
814
|
+
abortable,
|
|
815
|
+
retry,
|
|
816
|
+
timeout,
|
|
817
|
+
};
|