@ix-xs/node-comfort 1.2.0 → 2.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/CHANGELOG.md +190 -0
- package/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +501 -235
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Async.js
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
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 { retry, timeout } = require("./Func.js");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for `map()`, `forEach()` and `filter()`.
|
|
21
|
+
* @typedef {object} MapOptions
|
|
22
|
+
* @property {number} [concurrency] How many run at the same time. No limit by default.
|
|
23
|
+
* @property {boolean} [stopOnError] Stop at the first error. With `false`, everything runs and you get an `AggregateError` of all failures. Defaults to `true`.
|
|
24
|
+
* @property {AbortSignal} [signal] Stops starting new work and rejects with an `AbortError`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A limiter from `limit()`. Call it with a function to run it when there's room.
|
|
29
|
+
* @typedef {(<T>(fn: () => T | PromiseLike<T>) => Promise<Awaited<T>>) & { readonly activeCount: number, readonly pendingCount: number, concurrency: number, clearQueue(): void, onIdle(): Promise<void> }} Limiter
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for `queue()`.
|
|
34
|
+
* @typedef {object} QueueOptions
|
|
35
|
+
* @property {number} [concurrency] How many tasks run at the same time. Defaults to `1`.
|
|
36
|
+
* @property {boolean} [autoStart] Start right away. Defaults to `true`.
|
|
37
|
+
* @property {number | string} [timeout] Time limit per task, like `"30s"`. A slower task rejects with a `TimeoutError`.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options for `queue.add()`.
|
|
42
|
+
* @typedef {object} QueueAddOptions
|
|
43
|
+
* @property {number} [priority] Higher goes first; equal priorities keep their order. Defaults to `0`.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A queue from `queue()`.
|
|
48
|
+
* @typedef {object} Queue
|
|
49
|
+
* @property {<T>(task: () => T | PromiseLike<T>, options?: QueueAddOptions) => Promise<Awaited<T>>} add Adds a task; resolves with its result.
|
|
50
|
+
* @property {<T>(tasks: Array<() => T | PromiseLike<T>>, options?: QueueAddOptions) => Promise<Array<Awaited<T>>>} addAll Adds several tasks; resolves with all results.
|
|
51
|
+
* @property {() => Queue} pause Stops starting tasks. Running ones finish.
|
|
52
|
+
* @property {() => Queue} start Starts again.
|
|
53
|
+
* @property {() => void} clear Drops the tasks that haven't started. Their promises never settle.
|
|
54
|
+
* @property {() => Promise<void>} onIdle Resolves when nothing is waiting or running.
|
|
55
|
+
* @property {() => Promise<void>} onEmpty Resolves when nothing is waiting.
|
|
56
|
+
* @property {number} size Tasks waiting.
|
|
57
|
+
* @property {number} pending Tasks running.
|
|
58
|
+
* @property {boolean} isPaused
|
|
59
|
+
* @property {number} concurrency Can be changed on the fly.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A promise with its `resolve` and `reject`.
|
|
64
|
+
* @template T
|
|
65
|
+
* @typedef {object} Deferred
|
|
66
|
+
* @property {Promise<T>} promise
|
|
67
|
+
* @property {(value: T | PromiseLike<T>) => void} resolve
|
|
68
|
+
* @property {(reason?: unknown) => void} reject
|
|
69
|
+
* @property {boolean} settled Whether it's already resolved or rejected.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* What `settle()` returns.
|
|
74
|
+
* @template T
|
|
75
|
+
* @typedef {object} Settled
|
|
76
|
+
* @property {T[]} fulfilled Successful values, in order.
|
|
77
|
+
* @property {unknown[]} rejected Errors, in order.
|
|
78
|
+
* @property {Array<PromiseSettledResult<T>>} results Everything, like `Promise.allSettled`.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Options for `poll()`.
|
|
83
|
+
* @typedef {object} PollOptions
|
|
84
|
+
* @property {number | string} [interval] Time between tries. Defaults to 100 ms.
|
|
85
|
+
* @property {number | string} [timeout] Give up with a `TimeoutError` after this long.
|
|
86
|
+
* @property {AbortSignal} [signal] Stops with an `AbortError`.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A mutex from `mutex()`.
|
|
91
|
+
* @typedef {object} Mutex
|
|
92
|
+
* @property {<T>(fn: () => T | PromiseLike<T>) => Promise<Awaited<T>>} run Runs `fn` once no other `run` is in progress.
|
|
93
|
+
* @property {() => Promise<() => void>} lock Waits for the lock and gives you the function that releases it.
|
|
94
|
+
* @property {boolean} isLocked
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/** @param {number | string | undefined} value */
|
|
98
|
+
const _ms = (value) => {
|
|
99
|
+
if (value === undefined) return undefined;
|
|
100
|
+
if (typeof value === "number") return value;
|
|
101
|
+
const ms = require("./Time.js").parseDuration(value);
|
|
102
|
+
if (ms === null) throw new TypeError(`Invalid duration: ${value}`);
|
|
103
|
+
return ms;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Waits for a while. Same as `nc.wait()`.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* await async.sleep("250ms");
|
|
111
|
+
*
|
|
112
|
+
* @param {number | string} duration
|
|
113
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
function sleep(duration, options = {}) {
|
|
117
|
+
return require("./Utils.js").wait(duration, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Maps items through an async function, with at most `concurrency` calls
|
|
122
|
+
* at once. Results stay in order.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* const users = await async.map(ids, (id) => api.getUser(id), { concurrency: 10 });
|
|
126
|
+
*
|
|
127
|
+
* @template T
|
|
128
|
+
* @template R
|
|
129
|
+
* @param {Iterable<T> | AsyncIterable<T>} items
|
|
130
|
+
* @param {(item: T, index: number) => R | PromiseLike<R>} fn
|
|
131
|
+
* @param {MapOptions} [options]
|
|
132
|
+
* @returns {Promise<Array<Awaited<R>>>}
|
|
133
|
+
* @throws The first error, an `AggregateError` with `stopOnError: false`, or an `AbortError`.
|
|
134
|
+
*/
|
|
135
|
+
async function map(items, fn, options = {}) {
|
|
136
|
+
/** @type {T[]} */
|
|
137
|
+
const list = [];
|
|
138
|
+
if (Symbol.asyncIterator in Object(items)) for await (const item of /** @type {AsyncIterable<T>} */ (items)) list.push(item);
|
|
139
|
+
else for (const item of /** @type {Iterable<T>} */ (items)) list.push(item);
|
|
140
|
+
const concurrency = Math.max(1, options.concurrency ?? Infinity);
|
|
141
|
+
const stopOnError = options.stopOnError ?? true;
|
|
142
|
+
/** @type {Array<Awaited<R>>} */
|
|
143
|
+
const results = new Array(list.length);
|
|
144
|
+
/** @type {unknown[]} */
|
|
145
|
+
const errors = [];
|
|
146
|
+
let next = 0;
|
|
147
|
+
let failed = false;
|
|
148
|
+
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
if (!list.length) return resolve(undefined);
|
|
151
|
+
let active = 0;
|
|
152
|
+
let done = 0;
|
|
153
|
+
const onAbort = () => {
|
|
154
|
+
failed = true;
|
|
155
|
+
reject(new AbortError(undefined, { cause: options.signal?.reason }));
|
|
156
|
+
};
|
|
157
|
+
if (options.signal?.aborted) return onAbort();
|
|
158
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
159
|
+
const launch = () => {
|
|
160
|
+
while (active < concurrency && next < list.length && !failed) {
|
|
161
|
+
const index = next++;
|
|
162
|
+
active++;
|
|
163
|
+
Promise.resolve()
|
|
164
|
+
.then(() => fn(list[index], index))
|
|
165
|
+
.then(
|
|
166
|
+
(value) => {
|
|
167
|
+
results[index] = /** @type {Awaited<R>} */ (value);
|
|
168
|
+
},
|
|
169
|
+
(error) => {
|
|
170
|
+
if (stopOnError) {
|
|
171
|
+
failed = true;
|
|
172
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
173
|
+
reject(error);
|
|
174
|
+
} else {
|
|
175
|
+
errors[index] = error;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
.finally(() => {
|
|
180
|
+
active--;
|
|
181
|
+
done++;
|
|
182
|
+
if (failed) return;
|
|
183
|
+
if (done === list.length) {
|
|
184
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
185
|
+
if (errors.length) reject(new AggregateError(errors.filter((e) => e !== undefined), `${errors.filter((e) => e !== undefined).length} of ${list.length} tasks failed`));
|
|
186
|
+
else resolve(undefined);
|
|
187
|
+
} else {
|
|
188
|
+
launch();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
launch();
|
|
194
|
+
});
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Runs an async function for every item, with a concurrency limit.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* await async.forEach(images, (image) => optimize(image), { concurrency: 4 });
|
|
203
|
+
*
|
|
204
|
+
* @template T
|
|
205
|
+
* @param {Iterable<T> | AsyncIterable<T>} items
|
|
206
|
+
* @param {(item: T, index: number) => unknown} fn
|
|
207
|
+
* @param {MapOptions} [options]
|
|
208
|
+
* @returns {Promise<void>}
|
|
209
|
+
*/
|
|
210
|
+
async function forEach(items, fn, options = {}) {
|
|
211
|
+
await map(items, fn, options);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Keeps the items that pass an async test, with a concurrency limit.
|
|
216
|
+
* Order is preserved.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* const alive = await async.filter(hosts, (host) => ping(host), { concurrency: 20 });
|
|
220
|
+
*
|
|
221
|
+
* @template T
|
|
222
|
+
* @param {Iterable<T> | AsyncIterable<T>} items
|
|
223
|
+
* @param {(item: T, index: number) => unknown} predicate
|
|
224
|
+
* @param {MapOptions} [options]
|
|
225
|
+
* @returns {Promise<T[]>}
|
|
226
|
+
*/
|
|
227
|
+
async function filter(items, predicate, options = {}) {
|
|
228
|
+
/** @type {T[]} */
|
|
229
|
+
const list = [];
|
|
230
|
+
if (Symbol.asyncIterator in Object(items)) for await (const item of /** @type {AsyncIterable<T>} */ (items)) list.push(item);
|
|
231
|
+
else for (const item of /** @type {Iterable<T>} */ (items)) list.push(item);
|
|
232
|
+
const keep = await map(list, predicate, options);
|
|
233
|
+
return list.filter((_, i) => keep[i]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Runs async functions one after the other and returns their results.
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* await async.series([() => migrateUsers(), () => migrateOrders()]);
|
|
241
|
+
*
|
|
242
|
+
* @template T
|
|
243
|
+
* @param {Iterable<() => T | PromiseLike<T>>} tasks
|
|
244
|
+
* @returns {Promise<Array<Awaited<T>>>}
|
|
245
|
+
*/
|
|
246
|
+
async function series(tasks) {
|
|
247
|
+
/** @type {Array<Awaited<T>>} */
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const task of tasks) out.push(await task());
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* A limiter: wrap calls with it, and at most `concurrency` run at once
|
|
255
|
+
* while the rest wait their turn. Like `p-limit`.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* const limit = async.limit(3);
|
|
259
|
+
* const results = await Promise.all(urls.map((url) => limit(() => fetch(url))));
|
|
260
|
+
*
|
|
261
|
+
* @param {number} concurrency
|
|
262
|
+
* @returns {Limiter}
|
|
263
|
+
*/
|
|
264
|
+
function limit(concurrency) {
|
|
265
|
+
let max = Math.max(1, concurrency);
|
|
266
|
+
let active = 0;
|
|
267
|
+
/** @type {Array<() => void>} */
|
|
268
|
+
const waiting = [];
|
|
269
|
+
/** @type {Array<() => void>} */
|
|
270
|
+
let idleWaiters = [];
|
|
271
|
+
const next = () => {
|
|
272
|
+
if (active < max && waiting.length) {
|
|
273
|
+
active++;
|
|
274
|
+
/** @type {() => void} */ (waiting.shift())();
|
|
275
|
+
} else if (active === 0 && !waiting.length) {
|
|
276
|
+
for (const resolve of idleWaiters) resolve();
|
|
277
|
+
idleWaiters = [];
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
/** @type {any} */
|
|
281
|
+
const limiter = (/** @type {() => unknown} */ fn) =>
|
|
282
|
+
new Promise((resolve, reject) => {
|
|
283
|
+
waiting.push(() => {
|
|
284
|
+
Promise.resolve()
|
|
285
|
+
.then(fn)
|
|
286
|
+
.then(resolve, reject)
|
|
287
|
+
.finally(() => {
|
|
288
|
+
active--;
|
|
289
|
+
next();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
next();
|
|
293
|
+
});
|
|
294
|
+
Object.defineProperties(limiter, {
|
|
295
|
+
activeCount: { get: () => active },
|
|
296
|
+
pendingCount: { get: () => waiting.length },
|
|
297
|
+
concurrency: {
|
|
298
|
+
get: () => max,
|
|
299
|
+
set: (/** @type {number} */ value) => {
|
|
300
|
+
max = Math.max(1, value);
|
|
301
|
+
while (active < max && waiting.length) next();
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
clearQueue: { value: () => void waiting.splice(0) },
|
|
305
|
+
onIdle: { value: () => (active === 0 && !waiting.length ? Promise.resolve() : new Promise((resolve) => idleWaiters.push(() => resolve(undefined)))) },
|
|
306
|
+
});
|
|
307
|
+
return limiter;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A task queue with a concurrency limit, priorities, pause and per-task
|
|
312
|
+
* timeouts.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* const jobs = async.queue({ concurrency: 2, timeout: "30s" });
|
|
316
|
+
* jobs.add(() => sendEmail(a));
|
|
317
|
+
* jobs.add(() => sendEmail(vip), { priority: 10 }); // jumps the line
|
|
318
|
+
* await jobs.onIdle();
|
|
319
|
+
*
|
|
320
|
+
* @param {QueueOptions} [options]
|
|
321
|
+
* @returns {Queue}
|
|
322
|
+
*/
|
|
323
|
+
function queue(options = {}) {
|
|
324
|
+
let concurrency = Math.max(1, options.concurrency ?? 1);
|
|
325
|
+
let paused = options.autoStart === false;
|
|
326
|
+
let running = 0;
|
|
327
|
+
let order = 0;
|
|
328
|
+
const taskTimeout = _ms(options.timeout);
|
|
329
|
+
/** @type {Array<{ run: () => void, priority: number, order: number }>} */
|
|
330
|
+
const waiting = [];
|
|
331
|
+
/** @type {Array<() => void>} */
|
|
332
|
+
let idle = [];
|
|
333
|
+
/** @type {Array<() => void>} */
|
|
334
|
+
let empty = [];
|
|
335
|
+
|
|
336
|
+
const check = () => {
|
|
337
|
+
if (!waiting.length) {
|
|
338
|
+
for (const resolve of empty) resolve();
|
|
339
|
+
empty = [];
|
|
340
|
+
if (running === 0) {
|
|
341
|
+
for (const resolve of idle) resolve();
|
|
342
|
+
idle = [];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const pump = () => {
|
|
347
|
+
while (!paused && running < concurrency && waiting.length) {
|
|
348
|
+
const task = /** @type {{ run: () => void }} */ (waiting.shift());
|
|
349
|
+
running++;
|
|
350
|
+
task.run();
|
|
351
|
+
}
|
|
352
|
+
check();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/** @type {Queue} */
|
|
356
|
+
const self = {
|
|
357
|
+
add(task, addOptions = {}) {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
const entry = {
|
|
360
|
+
priority: addOptions.priority ?? 0,
|
|
361
|
+
order: order++,
|
|
362
|
+
run: () => {
|
|
363
|
+
const work = Promise.resolve().then(task);
|
|
364
|
+
(taskTimeout === undefined ? work : timeout(work, taskTimeout, `Queue task timed out after ${taskTimeout}ms`))
|
|
365
|
+
.then(/** @type {any} */ (resolve), reject)
|
|
366
|
+
.finally(() => {
|
|
367
|
+
running--;
|
|
368
|
+
pump();
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
const index = waiting.findIndex((w) => w.priority < entry.priority);
|
|
373
|
+
if (index === -1) waiting.push(entry);
|
|
374
|
+
else waiting.splice(index, 0, entry);
|
|
375
|
+
pump();
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
addAll(tasks, addOptions) {
|
|
379
|
+
return Promise.all(tasks.map((task) => self.add(task, addOptions)));
|
|
380
|
+
},
|
|
381
|
+
pause() {
|
|
382
|
+
paused = true;
|
|
383
|
+
return self;
|
|
384
|
+
},
|
|
385
|
+
start() {
|
|
386
|
+
paused = false;
|
|
387
|
+
pump();
|
|
388
|
+
return self;
|
|
389
|
+
},
|
|
390
|
+
clear() {
|
|
391
|
+
waiting.splice(0);
|
|
392
|
+
check();
|
|
393
|
+
},
|
|
394
|
+
onIdle() {
|
|
395
|
+
return !waiting.length && running === 0 ? Promise.resolve() : new Promise((resolve) => idle.push(() => resolve(undefined)));
|
|
396
|
+
},
|
|
397
|
+
onEmpty() {
|
|
398
|
+
return !waiting.length ? Promise.resolve() : new Promise((resolve) => empty.push(() => resolve(undefined)));
|
|
399
|
+
},
|
|
400
|
+
get size() {
|
|
401
|
+
return waiting.length;
|
|
402
|
+
},
|
|
403
|
+
get pending() {
|
|
404
|
+
return running;
|
|
405
|
+
},
|
|
406
|
+
get isPaused() {
|
|
407
|
+
return paused;
|
|
408
|
+
},
|
|
409
|
+
get concurrency() {
|
|
410
|
+
return concurrency;
|
|
411
|
+
},
|
|
412
|
+
set concurrency(value) {
|
|
413
|
+
concurrency = Math.max(1, value);
|
|
414
|
+
pump();
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
return self;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* A mutex: tasks given to `run()` never overlap. Put it around
|
|
422
|
+
* read-modify-write steps, like updating a file or refreshing a token.
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* const lock = async.mutex();
|
|
426
|
+
* await lock.run(async () => {
|
|
427
|
+
* const data = nc.readJSON("./counter.json", { n: 0 });
|
|
428
|
+
* nc.writeJSON("./counter.json", { n: data.n + 1 });
|
|
429
|
+
* });
|
|
430
|
+
*
|
|
431
|
+
* @returns {Mutex}
|
|
432
|
+
*/
|
|
433
|
+
function mutex() {
|
|
434
|
+
const limiter = limit(1);
|
|
435
|
+
let locked = false;
|
|
436
|
+
return {
|
|
437
|
+
run: (fn) => limiter(async () => {
|
|
438
|
+
locked = true;
|
|
439
|
+
try {
|
|
440
|
+
return await fn();
|
|
441
|
+
} finally {
|
|
442
|
+
locked = false;
|
|
443
|
+
}
|
|
444
|
+
}),
|
|
445
|
+
lock: () =>
|
|
446
|
+
new Promise((resolveLock) => {
|
|
447
|
+
void limiter(() => new Promise((release) => {
|
|
448
|
+
locked = true;
|
|
449
|
+
resolveLock(() => {
|
|
450
|
+
locked = false;
|
|
451
|
+
release(undefined);
|
|
452
|
+
});
|
|
453
|
+
}));
|
|
454
|
+
}),
|
|
455
|
+
get isLocked() {
|
|
456
|
+
return locked;
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* A promise along with its `resolve` and `reject`. Handy to connect
|
|
463
|
+
* callbacks or events to `await`.
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* const ready = async.deferred();
|
|
467
|
+
* socket.once("open", () => ready.resolve());
|
|
468
|
+
* await ready.promise;
|
|
469
|
+
*
|
|
470
|
+
* @template [T=void]
|
|
471
|
+
* @returns {Deferred<T>}
|
|
472
|
+
*/
|
|
473
|
+
function deferred() {
|
|
474
|
+
/** @type {(value: T | PromiseLike<T>) => void} */
|
|
475
|
+
let resolve = () => {};
|
|
476
|
+
/** @type {(reason?: unknown) => void} */
|
|
477
|
+
let reject = () => {};
|
|
478
|
+
/** @type {Deferred<T>} */
|
|
479
|
+
const out = {
|
|
480
|
+
promise: new Promise((res, rej) => {
|
|
481
|
+
resolve = res;
|
|
482
|
+
reject = rej;
|
|
483
|
+
}),
|
|
484
|
+
resolve: (value) => {
|
|
485
|
+
out.settled = true;
|
|
486
|
+
resolve(value);
|
|
487
|
+
},
|
|
488
|
+
reject: (reason) => {
|
|
489
|
+
out.settled = true;
|
|
490
|
+
reject(reason);
|
|
491
|
+
},
|
|
492
|
+
settled: false,
|
|
493
|
+
};
|
|
494
|
+
return out;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Waits for every promise and sorts successes from failures.
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* const { fulfilled, rejected } = await async.settle(emails.map(send));
|
|
502
|
+
* nc.info(`${fulfilled.length} sent, ${rejected.length} failed`);
|
|
503
|
+
*
|
|
504
|
+
* @template T
|
|
505
|
+
* @param {Iterable<T | PromiseLike<T>>} promises
|
|
506
|
+
* @returns {Promise<Settled<Awaited<T>>>}
|
|
507
|
+
*/
|
|
508
|
+
async function settle(promises) {
|
|
509
|
+
const results = /** @type {Array<PromiseSettledResult<Awaited<T>>>} */ (await Promise.allSettled(promises));
|
|
510
|
+
return {
|
|
511
|
+
fulfilled: results.filter((r) => r.status === "fulfilled").map((r) => /** @type {PromiseFulfilledResult<Awaited<T>>} */ (r).value),
|
|
512
|
+
rejected: results.filter((r) => r.status === "rejected").map((r) => /** @type {PromiseRejectedResult} */ (r).reason),
|
|
513
|
+
results,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Like `Promise.all`, for an object of promises.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* const { user, orders } = await async.props({ user: getUser(id), orders: getOrders(id) });
|
|
522
|
+
*
|
|
523
|
+
* @template {Record<string, unknown>} T
|
|
524
|
+
* @param {T} object
|
|
525
|
+
* @returns {Promise<{ [K in keyof T]: Awaited<T[K]> }>}
|
|
526
|
+
*/
|
|
527
|
+
async function props(object) {
|
|
528
|
+
const keys = Object.keys(object);
|
|
529
|
+
const values = await Promise.all(keys.map((key) => object[key]));
|
|
530
|
+
/** @type {any} */
|
|
531
|
+
const out = {};
|
|
532
|
+
keys.forEach((key, i) => {
|
|
533
|
+
out[key] = values[i];
|
|
534
|
+
});
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Calls `fn` until it returns something truthy, and resolves with it.
|
|
540
|
+
* Errors count as "not yet".
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* await async.poll(() => fetch(healthUrl).then((res) => res.ok), { interval: "500ms", timeout: "30s" });
|
|
544
|
+
*
|
|
545
|
+
* @template T
|
|
546
|
+
* @param {() => T | PromiseLike<T>} fn
|
|
547
|
+
* @param {PollOptions} [options]
|
|
548
|
+
* @returns {Promise<NonNullable<Awaited<T>>>}
|
|
549
|
+
* @throws {TimeoutError}
|
|
550
|
+
* @throws {AbortError}
|
|
551
|
+
*/
|
|
552
|
+
async function poll(fn, options = {}) {
|
|
553
|
+
const interval = _ms(options.interval) ?? 100;
|
|
554
|
+
const limitMs = _ms(options.timeout);
|
|
555
|
+
const deadline = limitMs === undefined ? Infinity : Date.now() + limitMs;
|
|
556
|
+
for (;;) {
|
|
557
|
+
if (options.signal?.aborted) throw new AbortError(undefined, { cause: options.signal.reason });
|
|
558
|
+
try {
|
|
559
|
+
const value = await fn();
|
|
560
|
+
if (value) return /** @type {NonNullable<Awaited<T>>} */ (value);
|
|
561
|
+
} catch {
|
|
562
|
+
// not ready yet
|
|
563
|
+
}
|
|
564
|
+
const remaining = deadline - Date.now();
|
|
565
|
+
if (remaining <= 0) throw new TimeoutError(`poll() timed out after ${limitMs}ms`, { timeout: limitMs });
|
|
566
|
+
await sleep(Math.min(interval, remaining), { signal: options.signal });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
module.exports = {
|
|
571
|
+
sleep,
|
|
572
|
+
map,
|
|
573
|
+
forEach,
|
|
574
|
+
filter,
|
|
575
|
+
series,
|
|
576
|
+
limit,
|
|
577
|
+
queue,
|
|
578
|
+
mutex,
|
|
579
|
+
deferred,
|
|
580
|
+
settle,
|
|
581
|
+
props,
|
|
582
|
+
poll,
|
|
583
|
+
retry,
|
|
584
|
+
timeout,
|
|
585
|
+
};
|