@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. 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
+ };