@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. 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
+ };