@ix-xs/node-comfort 2.0.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 (77) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +32 -30
  3. package/index.js +3 -3
  4. package/package.json +8 -7
  5. package/src/Arr.js +222 -58
  6. package/src/Async.js +322 -90
  7. package/src/Cache.js +148 -44
  8. package/src/Checker.js +257 -26
  9. package/src/Cli.js +404 -145
  10. package/src/Color.js +224 -83
  11. package/src/Crypto.js +481 -106
  12. package/src/Emitter.js +104 -37
  13. package/src/Env.js +176 -42
  14. package/src/FS.js +203 -60
  15. package/src/Func.js +226 -76
  16. package/src/Http.js +311 -149
  17. package/src/Id.js +281 -96
  18. package/src/Logger.js +302 -78
  19. package/src/Num.js +182 -60
  20. package/src/Obj.js +340 -125
  21. package/src/SQLite.js +426 -105
  22. package/src/Schema.js +602 -87
  23. package/src/Str.js +316 -86
  24. package/src/Sys.js +197 -54
  25. package/src/Time.js +572 -317
  26. package/src/Utils.js +62 -41
  27. package/src/errors.js +54 -11
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +59 -17
  30. package/types/src/Arr.d.ts +78 -0
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +52 -1
  33. package/types/src/Async.d.ts.map +1 -1
  34. package/types/src/Cache.d.ts +33 -7
  35. package/types/src/Cache.d.ts.map +1 -1
  36. package/types/src/Checker.d.ts +155 -2
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +97 -15
  39. package/types/src/Cli.d.ts.map +1 -1
  40. package/types/src/Color.d.ts +7 -1
  41. package/types/src/Color.d.ts.map +1 -1
  42. package/types/src/Crypto.d.ts +99 -13
  43. package/types/src/Crypto.d.ts.map +1 -1
  44. package/types/src/Emitter.d.ts +36 -17
  45. package/types/src/Emitter.d.ts.map +1 -1
  46. package/types/src/Env.d.ts +31 -1
  47. package/types/src/Env.d.ts.map +1 -1
  48. package/types/src/FS.d.ts +36 -5
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +147 -19
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +113 -43
  53. package/types/src/Http.d.ts.map +1 -1
  54. package/types/src/Id.d.ts +60 -8
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +53 -2
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +48 -7
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +67 -4
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +123 -14
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +256 -7
  65. package/types/src/Schema.d.ts.map +1 -1
  66. package/types/src/Str.d.ts +49 -1
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +51 -6
  69. package/types/src/Sys.d.ts.map +1 -1
  70. package/types/src/Time.d.ts +130 -34
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +15 -11
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +27 -3
  75. package/types/src/errors.d.ts.map +1 -1
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
package/src/Async.js CHANGED
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  const { AbortError, TimeoutError } = require("./errors.js");
17
+ const { toMs } = require("./internal/shared.js");
17
18
  const { retry, timeout } = require("./Func.js");
18
19
 
19
20
  /**
@@ -24,6 +25,12 @@ const { retry, timeout } = require("./Func.js");
24
25
  * @property {AbortSignal} [signal] Stops starting new work and rejects with an `AbortError`.
25
26
  */
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
+
27
34
  /**
28
35
  * A limiter from `limit()`. Call it with a function to run it when there's room.
29
36
  * @typedef {(<T>(fn: () => T | PromiseLike<T>) => Promise<Awaited<T>>) & { readonly activeCount: number, readonly pendingCount: number, concurrency: number, clearQueue(): void, onIdle(): Promise<void> }} Limiter
@@ -95,13 +102,104 @@ const { retry, timeout } = require("./Func.js");
95
102
  */
96
103
 
97
104
  /** @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
+ 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
+ }
105
203
 
106
204
  /**
107
205
  * Waits for a while. Same as `nc.wait()`.
@@ -119,10 +217,12 @@ function sleep(duration, options = {}) {
119
217
 
120
218
  /**
121
219
  * Maps items through an async function, with at most `concurrency` calls
122
- * at once. Results stay in order.
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.
123
222
  *
124
223
  * @example
125
224
  * const users = await async.map(ids, (id) => api.getUser(id), { concurrency: 10 });
225
+ * await async.forEach(readline.createInterface({ input }), importLine, { concurrency: 20 });
126
226
  *
127
227
  * @template T
128
228
  * @template R
@@ -132,67 +232,108 @@ function sleep(duration, options = {}) {
132
232
  * @returns {Promise<Array<Awaited<R>>>}
133
233
  * @throws The first error, an `AggregateError` with `stopOnError: false`, or an `AbortError`.
134
234
  */
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);
235
+ function map(items, fn, options = {}) {
140
236
  const concurrency = Math.max(1, options.concurrency ?? Infinity);
141
237
  const stopOnError = options.stopOnError ?? true;
238
+ const signal = options.signal;
142
239
  /** @type {Array<Awaited<R>>} */
143
- const results = new Array(list.length);
240
+ const results = [];
144
241
  /** @type {unknown[]} */
145
242
  const errors = [];
146
- let next = 0;
147
- let failed = false;
148
243
 
149
- await new Promise((resolve, reject) => {
150
- if (!list.length) return resolve(undefined);
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;
151
255
  let active = 0;
152
- let done = 0;
153
- const onAbort = () => {
154
- failed = true;
155
- reject(new AbortError(undefined, { cause: options.signal?.reason }));
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);
156
272
  };
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
- }
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();
177
321
  },
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
- });
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;
191
329
  }
330
+ if (exhausted && active === 0) end();
192
331
  };
193
- launch();
332
+
333
+ if (signal?.aborted) return onAbort();
334
+ signal?.addEventListener("abort", onAbort, { once: true });
335
+ pump();
194
336
  });
195
- return results;
196
337
  }
197
338
 
198
339
  /**
@@ -227,9 +368,10 @@ async function forEach(items, fn, options = {}) {
227
368
  async function filter(items, predicate, options = {}) {
228
369
  /** @type {T[]} */
229
370
  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);
371
+ const keep = await map(items, (item, index) => {
372
+ list[index] = item;
373
+ return predicate(item, index);
374
+ }, options);
233
375
  return list.filter((_, i) => keep[i]);
234
376
  }
235
377
 
@@ -250,6 +392,23 @@ async function series(tasks) {
250
392
  return out;
251
393
  }
252
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
+
253
412
  /**
254
413
  * A limiter: wrap calls with it, and at most `concurrency` run at once
255
414
  * while the rest wait their turn. Like `p-limit`.
@@ -264,17 +423,19 @@ async function series(tasks) {
264
423
  function limit(concurrency) {
265
424
  let max = Math.max(1, concurrency);
266
425
  let active = 0;
267
- /** @type {Array<() => void>} */
268
- const waiting = [];
426
+ /** @type {_Fifo<() => void>} */
427
+ const waiting = new _Fifo();
269
428
  /** @type {Array<() => void>} */
270
429
  let idleWaiters = [];
271
430
  const next = () => {
272
- if (active < max && waiting.length) {
431
+ while (active < max && waiting.size) {
273
432
  active++;
274
433
  /** @type {() => void} */ (waiting.shift())();
275
- } else if (active === 0 && !waiting.length) {
276
- for (const resolve of idleWaiters) resolve();
434
+ }
435
+ if (active === 0 && !waiting.size && idleWaiters.length) {
436
+ const resolvers = idleWaiters;
277
437
  idleWaiters = [];
438
+ for (const resolve of resolvers) resolve();
278
439
  }
279
440
  };
280
441
  /** @type {any} */
@@ -293,20 +454,65 @@ function limit(concurrency) {
293
454
  });
294
455
  Object.defineProperties(limiter, {
295
456
  activeCount: { get: () => active },
296
- pendingCount: { get: () => waiting.length },
457
+ pendingCount: { get: () => waiting.size },
297
458
  concurrency: {
298
459
  get: () => max,
299
460
  set: (/** @type {number} */ value) => {
300
461
  max = Math.max(1, value);
301
- while (active < max && waiting.length) next();
462
+ next();
302
463
  },
303
464
  },
304
- clearQueue: { value: () => void waiting.splice(0) },
305
- onIdle: { value: () => (active === 0 && !waiting.length ? Promise.resolve() : new Promise((resolve) => idleWaiters.push(() => resolve(undefined)))) },
465
+ clearQueue: { value: () => waiting.clear() },
466
+ onIdle: { value: () => (active === 0 && !waiting.size ? Promise.resolve() : new Promise((resolve) => idleWaiters.push(() => resolve(undefined)))) },
306
467
  });
307
468
  return limiter;
308
469
  }
309
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
+
310
516
  /**
311
517
  * A task queue with a concurrency limit, priorities, pause and per-task
312
518
  * timeouts.
@@ -326,15 +532,15 @@ function queue(options = {}) {
326
532
  let running = 0;
327
533
  let order = 0;
328
534
  const taskTimeout = _ms(options.timeout);
329
- /** @type {Array<{ run: () => void, priority: number, order: number }>} */
330
- const waiting = [];
535
+ /** @type {_Heap<{ run: () => void, priority: number, order: number }>} */
536
+ const waiting = new _Heap();
331
537
  /** @type {Array<() => void>} */
332
538
  let idle = [];
333
539
  /** @type {Array<() => void>} */
334
540
  let empty = [];
335
541
 
336
542
  const check = () => {
337
- if (!waiting.length) {
543
+ if (!waiting.size) {
338
544
  for (const resolve of empty) resolve();
339
545
  empty = [];
340
546
  if (running === 0) {
@@ -344,8 +550,8 @@ function queue(options = {}) {
344
550
  }
345
551
  };
346
552
  const pump = () => {
347
- while (!paused && running < concurrency && waiting.length) {
348
- const task = /** @type {{ run: () => void }} */ (waiting.shift());
553
+ while (!paused && running < concurrency && waiting.size) {
554
+ const task = /** @type {{ run: () => void }} */ (waiting.pop());
349
555
  running++;
350
556
  task.run();
351
557
  }
@@ -369,9 +575,7 @@ function queue(options = {}) {
369
575
  });
370
576
  },
371
577
  };
372
- const index = waiting.findIndex((w) => w.priority < entry.priority);
373
- if (index === -1) waiting.push(entry);
374
- else waiting.splice(index, 0, entry);
578
+ waiting.push(entry);
375
579
  pump();
376
580
  });
377
581
  },
@@ -388,17 +592,17 @@ function queue(options = {}) {
388
592
  return self;
389
593
  },
390
594
  clear() {
391
- waiting.splice(0);
595
+ waiting.clear();
392
596
  check();
393
597
  },
394
598
  onIdle() {
395
- return !waiting.length && running === 0 ? Promise.resolve() : new Promise((resolve) => idle.push(() => resolve(undefined)));
599
+ return !waiting.size && running === 0 ? Promise.resolve() : new Promise((resolve) => idle.push(() => resolve(undefined)));
396
600
  },
397
601
  onEmpty() {
398
- return !waiting.length ? Promise.resolve() : new Promise((resolve) => empty.push(() => resolve(undefined)));
602
+ return !waiting.size ? Promise.resolve() : new Promise((resolve) => empty.push(() => resolve(undefined)));
399
603
  },
400
604
  get size() {
401
- return waiting.length;
605
+ return waiting.size;
402
606
  },
403
607
  get pending() {
404
608
  return running;
@@ -527,12 +731,7 @@ async function settle(promises) {
527
731
  async function props(object) {
528
732
  const keys = Object.keys(object);
529
733
  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;
734
+ return /** @type {any} */ (Object.fromEntries(keys.map((key, i) => [key, values[i]])));
536
735
  }
537
736
 
538
737
  /**
@@ -553,33 +752,66 @@ async function poll(fn, options = {}) {
553
752
  const interval = _ms(options.interval) ?? 100;
554
753
  const limitMs = _ms(options.timeout);
555
754
  const deadline = limitMs === undefined ? Infinity : Date.now() + limitMs;
755
+ /** @type {unknown} */
756
+ let lastError;
556
757
  for (;;) {
557
758
  if (options.signal?.aborted) throw new AbortError(undefined, { cause: options.signal.reason });
558
759
  try {
559
760
  const value = await fn();
560
761
  if (value) return /** @type {NonNullable<Awaited<T>>} */ (value);
561
- } catch {
562
- // not ready yet
762
+ lastError = undefined;
763
+ } catch (error) {
764
+ // Not ready yet. Kept, so a timeout can say why.
765
+ lastError = error;
563
766
  }
564
767
  const remaining = deadline - Date.now();
565
- if (remaining <= 0) throw new TimeoutError(`poll() timed out after ${limitMs}ms`, { timeout: limitMs });
768
+ if (remaining <= 0) throw new TimeoutError(`poll() timed out after ${limitMs}ms`, { timeout: limitMs, cause: lastError });
566
769
  await sleep(Math.min(interval, remaining), { signal: options.signal });
567
770
  }
568
771
  }
569
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
+
570
799
  module.exports = {
571
800
  sleep,
572
801
  map,
573
802
  forEach,
574
803
  filter,
575
804
  series,
805
+ parallel,
576
806
  limit,
807
+ rateLimit,
577
808
  queue,
578
809
  mutex,
579
810
  deferred,
580
811
  settle,
581
812
  props,
582
813
  poll,
814
+ abortable,
583
815
  retry,
584
816
  timeout,
585
817
  };