@naturalcycles/js-lib 15.72.0 → 15.73.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/dist/abort.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { NumberOfMilliseconds } from './types.js';
1
2
  /**
2
3
  * Like AbortSignal, but it can "abort itself" via the `.abort()` method.
3
4
  *
@@ -22,3 +23,32 @@ export interface AbortableSignal extends AbortSignal {
22
23
  * @experimental
23
24
  */
24
25
  export declare function createAbortableSignal(): AbortableSignal;
26
+ /**
27
+ * Returns AbortSignal if ms is defined.
28
+ * Otherwise returns undefined.
29
+ */
30
+ export declare function abortSignalTimeoutOrUndefined(ms: NumberOfMilliseconds | undefined): AbortSignal | undefined;
31
+ /**
32
+ * Returns an AbortSignal that aborts after the given number of milliseconds.
33
+ * Uses native `AbortSignal.timeout()` when available, falls back to a polyfill.
34
+ *
35
+ * The abort reason is a DOMException with name "TimeoutError".
36
+ */
37
+ export declare function abortSignalTimeout(ms: NumberOfMilliseconds): AbortSignal;
38
+ export declare function polyfilledAbortSignalTimeout(ms: NumberOfMilliseconds): AbortSignal;
39
+ /**
40
+ * Returns AbortSignal.any(signals) is the array (after filtering undefined inputs) is not empty,
41
+ * otherwise undefined.
42
+ */
43
+ export declare function abortSignalAnyOrUndefined(signals: (AbortSignal | undefined)[]): AbortSignal | undefined;
44
+ /**
45
+ * Returns an AbortSignal that aborts when any of the given signals abort.
46
+ * Uses native `AbortSignal.any()` when available, falls back to a polyfill.
47
+ *
48
+ * The abort reason is taken from the first signal that aborts.
49
+ * If any input signal is already aborted, the returned signal is immediately aborted.
50
+ *
51
+ * If only 1 signal is passed in the input array - that Signal is returned as-is.
52
+ */
53
+ export declare function abortSignalAny(signals: AbortSignal[]): AbortSignal;
54
+ export declare function polyfilledAbortSignalAny(signals: AbortSignal[]): AbortSignal;
package/dist/abort.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { _isTruthy } from './is.util.js';
1
2
  /**
2
3
  * Creates AbortableSignal,
3
4
  * which is like AbortSignal, but can "abort itself" with `.abort()` method.
@@ -10,3 +11,69 @@ export function createAbortableSignal() {
10
11
  abort: ac.abort.bind(ac),
11
12
  });
12
13
  }
14
+ /**
15
+ * Returns AbortSignal if ms is defined.
16
+ * Otherwise returns undefined.
17
+ */
18
+ export function abortSignalTimeoutOrUndefined(ms) {
19
+ return ms ? abortSignalTimeout(ms) : undefined;
20
+ }
21
+ /**
22
+ * Returns an AbortSignal that aborts after the given number of milliseconds.
23
+ * Uses native `AbortSignal.timeout()` when available, falls back to a polyfill.
24
+ *
25
+ * The abort reason is a DOMException with name "TimeoutError".
26
+ */
27
+ export function abortSignalTimeout(ms) {
28
+ return typeof AbortSignal.timeout === 'function'
29
+ ? AbortSignal.timeout(ms)
30
+ : polyfilledAbortSignalTimeout(ms);
31
+ }
32
+ export function polyfilledAbortSignalTimeout(ms) {
33
+ const ac = new AbortController();
34
+ setTimeout(() => {
35
+ ac.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'));
36
+ }, ms);
37
+ return ac.signal;
38
+ }
39
+ /**
40
+ * Returns AbortSignal.any(signals) is the array (after filtering undefined inputs) is not empty,
41
+ * otherwise undefined.
42
+ */
43
+ export function abortSignalAnyOrUndefined(signals) {
44
+ const filtered = signals.filter(_isTruthy);
45
+ return filtered.length ? abortSignalAny(filtered) : undefined;
46
+ }
47
+ /**
48
+ * Returns an AbortSignal that aborts when any of the given signals abort.
49
+ * Uses native `AbortSignal.any()` when available, falls back to a polyfill.
50
+ *
51
+ * The abort reason is taken from the first signal that aborts.
52
+ * If any input signal is already aborted, the returned signal is immediately aborted.
53
+ *
54
+ * If only 1 signal is passed in the input array - that Signal is returned as-is.
55
+ */
56
+ export function abortSignalAny(signals) {
57
+ if (signals.length === 1) {
58
+ return signals[0];
59
+ }
60
+ return typeof AbortSignal.any === 'function'
61
+ ? AbortSignal.any(signals)
62
+ : polyfilledAbortSignalAny(signals);
63
+ }
64
+ export function polyfilledAbortSignalAny(signals) {
65
+ const ac = new AbortController();
66
+ for (const signal of signals) {
67
+ if (signal.aborted) {
68
+ ac.abort(signal.reason);
69
+ return ac.signal;
70
+ }
71
+ }
72
+ for (const signal of signals) {
73
+ signal.addEventListener('abort', () => ac.abort(signal.reason), {
74
+ once: true,
75
+ signal: ac.signal,
76
+ });
77
+ }
78
+ return ac.signal;
79
+ }
@@ -16,7 +16,7 @@ export declare class Fetcher {
16
16
  *
17
17
  * Version is to be incremented every time a difference in behaviour (or a bugfix) is done.
18
18
  */
19
- static readonly VERSION = 3;
19
+ static readonly VERSION = 4;
20
20
  /**
21
21
  * userAgent is statically exposed as Fetcher.userAgent.
22
22
  * It can be modified globally, and will be used (read) at the start of every request.
@@ -1,10 +1,11 @@
1
1
  /// <reference lib="es2023" preserve="true" />
2
2
  /// <reference lib="dom" preserve="true" />
3
3
  /// <reference lib="dom.iterable" preserve="true" />
4
+ import { abortSignalAnyOrUndefined, abortSignalTimeoutOrUndefined } from '../abort.js';
4
5
  import { _ms, _since } from '../datetime/time.util.js';
5
6
  import { isServerSide } from '../env.js';
6
7
  import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert.js';
7
- import { _anyToError, _anyToErrorObject, _errorDataAppend, _errorLikeToErrorObject, HttpRequestError, TimeoutError, UnexpectedPassError, } from '../error/error.util.js';
8
+ import { _anyToError, _anyToErrorObject, _errorDataAppend, _errorLikeToErrorObject, HttpRequestError, UnexpectedPassError, } from '../error/error.util.js';
8
9
  import { _clamp } from '../number/number.util.js';
9
10
  import { _filterFalsyValues, _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, _pick, } from '../object/object.util.js';
10
11
  import { pDelay } from '../promise/pDelay.js';
@@ -24,7 +25,7 @@ export class Fetcher {
24
25
  *
25
26
  * Version is to be incremented every time a difference in behaviour (or a bugfix) is done.
26
27
  */
27
- static VERSION = 3;
28
+ static VERSION = 4;
28
29
  /**
29
30
  * userAgent is statically exposed as Fetcher.userAgent.
30
31
  * It can be modified globally, and will be used (read) at the start of every request.
@@ -232,7 +233,8 @@ export class Fetcher {
232
233
  async doFetch(opt) {
233
234
  const req = this.normalizeOptions(opt);
234
235
  const { logger } = this.cfg;
235
- const { timeoutSeconds, init: { method }, } = req;
236
+ const { init: { method }, } = req;
237
+ const timeoutMillis = req.timeoutSeconds ? req.timeoutSeconds * 1000 : undefined;
236
238
  for (const hook of this.cfg.hooks.beforeRequest || []) {
237
239
  await hook(req);
238
240
  }
@@ -251,21 +253,10 @@ export class Fetcher {
251
253
  };
252
254
  while (!res.retryStatus.retryStopped) {
253
255
  req.started = Date.now();
254
- // setup timeout
255
- let timeoutId;
256
- if (timeoutSeconds) {
257
- // Used for Request timeout (when timeoutSeconds is set),
258
- // but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
259
- // UPD: no, not using for "downloadBody" currently
260
- const abortController = new AbortController();
261
- req.init.signal = abortController.signal;
262
- timeoutId = setTimeout(() => {
263
- // console.log(`actual request timed out in ${_since(req.started)}`)
264
- // Apparently, providing a `string` reason to abort() causes Undici to throw `invalid_argument` error,
265
- // so, we're wrapping it in a TimeoutError instance
266
- abortController.abort(new TimeoutError(`request timed out after ${timeoutSeconds} sec`));
267
- }, timeoutSeconds * 1000);
268
- }
256
+ req.init.signal = abortSignalAnyOrUndefined([
257
+ abortSignalTimeoutOrUndefined(timeoutMillis),
258
+ opt.signal,
259
+ ]);
269
260
  if (req.logRequest) {
270
261
  const { retryAttempt } = res.retryStatus;
271
262
  logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
@@ -284,22 +275,19 @@ export class Fetcher {
284
275
  catch (err) {
285
276
  // For example, CORS error would result in "TypeError: failed to fetch" here
286
277
  // or, `fetch failed` with the cause of `unexpected redirect`
278
+ // AbortSignal.timeout() throws a DOMException with name "TimeoutError"
287
279
  res.err = _anyToError(err);
288
280
  res.ok = false;
289
281
  // important to set it to undefined, otherwise it can keep the previous value (from previous try)
290
282
  res.fetchResponse = undefined;
291
283
  }
292
- finally {
293
- clearTimeout(timeoutId);
294
- // Separate Timeout will be introduced to "download and parse the body"
295
- }
296
284
  res.statusFamily = this.getStatusFamily(res);
297
285
  res.statusCode = res.fetchResponse?.status;
298
286
  if (res.fetchResponse?.ok || !req.throwHttpErrors) {
299
287
  try {
300
288
  // We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
301
289
  await pTimeout(async () => await this.onOkResponse(res), {
302
- timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
290
+ timeout: timeoutMillis || Number.POSITIVE_INFINITY,
303
291
  name: 'Fetcher.downloadBody',
304
292
  });
305
293
  }
@@ -169,6 +169,12 @@ export interface FetcherOptions {
169
169
  * so both should finish within this single timeout (not each).
170
170
  */
171
171
  timeoutSeconds?: number;
172
+ /**
173
+ * AbortSignal to allow the caller to abort the request.
174
+ * If `timeoutSeconds` is also set, the signals are combined via `AbortSignal.any()`,
175
+ * so the request aborts on whichever fires first.
176
+ */
177
+ signal?: AbortSignal;
172
178
  /**
173
179
  * Supports all the types that RequestInit.body supports.
174
180
  *
@@ -1,4 +1,3 @@
1
- export * from './abortable.js';
2
1
  export * from './pDefer.js';
3
2
  export * from './pDelay.js';
4
3
  export * from './pFilter.js';
@@ -1,4 +1,3 @@
1
- export * from './abortable.js';
2
1
  export * from './pDefer.js';
3
2
  export * from './pDelay.js';
4
3
  export * from './pFilter.js';
@@ -1,6 +1,6 @@
1
1
  import type { ErrorData } from '../error/error.model.js';
2
2
  import type { CommonLogger } from '../log/commonLogger.js';
3
- import type { AnyFunction } from '../types.js';
3
+ import type { AnyAsyncFunction } from '../types.js';
4
4
  export interface PRetryOptions {
5
5
  /**
6
6
  * If set - will be included in the error message.
@@ -79,5 +79,5 @@ export interface PRetryOptions {
79
79
  * Returns a Function (!), enhanced with retry capabilities.
80
80
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
81
81
  */
82
- export declare function pRetryFn<T extends AnyFunction>(fn: T, opt?: PRetryOptions): T;
82
+ export declare function pRetryFn<T extends AnyAsyncFunction>(fn: T, opt?: PRetryOptions): T;
83
83
  export declare function pRetry<T>(fn: (attempt: number) => Promise<T>, opt?: PRetryOptions): Promise<T>;
@@ -46,4 +46,4 @@ export declare function pTimeoutFn<T extends AnyAsyncFunction>(fn: T, opt: PTime
46
46
  * Throws an Error if the Function is not resolved in a certain time.
47
47
  * If the Function rejects - passes this rejection further.
48
48
  */
49
- export declare function pTimeout<T>(fn: AnyAsyncFunction<T>, opt: PTimeoutOptions): Promise<T>;
49
+ export declare function pTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, opt: PTimeoutOptions): Promise<T>;
@@ -23,9 +23,11 @@ export function pTimeoutFn(fn, opt) {
23
23
  * If the Function rejects - passes this rejection further.
24
24
  */
25
25
  export async function pTimeout(fn, opt) {
26
+ const ac = new AbortController();
27
+ const { signal } = ac;
26
28
  if (!opt.timeout) {
27
29
  // short-circuit to direct execution if 0 timeout is passed
28
- return await fn();
30
+ return await fn(signal);
29
31
  }
30
32
  const { timeout, name = fn.name || 'pTimeout function', onTimeout } = opt;
31
33
  const fakeError = opt.fakeError || new Error('TimeoutError');
@@ -46,13 +48,15 @@ export async function pTimeout(fn, opt) {
46
48
  // oxlint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
47
49
  reject(_errorDataAppend(err, opt.errorData));
48
50
  }
51
+ ac.abort(err);
49
52
  return;
50
53
  }
51
54
  reject(err);
55
+ ac.abort(err);
52
56
  }, timeout);
53
57
  // Execute the Function
54
58
  try {
55
- resolve(await fn());
59
+ resolve(await fn(signal));
56
60
  }
57
61
  catch (err) {
58
62
  reject(err);
package/dist/types.d.ts CHANGED
@@ -298,15 +298,22 @@ export declare const _stringMapEntries: <T>(map: StringMap<T>) => [k: string, v:
298
298
  /**
299
299
  * Alias of `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
300
300
  * This is how TypeScript should work, actually.
301
+ *
302
+ * Object.keys always returns strings, so numeric keys are stringified via `${K}`.
303
+ * Symbol keys are excluded (Object.keys does not return symbols).
301
304
  */
302
- export declare const _objectKeys: <K extends PropertyKey>(obj: Partial<Record<K, any>>) => K[];
305
+ export declare const _objectKeys: <K extends PropertyKey>(obj: Partial<Record<K, any>>) => (K extends string ? K : K extends number | bigint ? `${K}` : never)[];
303
306
  /**
304
307
  * Alias of `Object.entries`, but returns better-typed output.
305
308
  *
309
+ * Difference with _stringMapEntries?
310
+ * Use _stringMapEntries when the object is a StringMap<T> - it'll correctly infer T being not undefined.
311
+ * If the object is not a StringMap - use _objectEntries - it'll correctly infer object keys, which can be typed as Enum.
312
+ *
306
313
  * So e.g you can use _objectEntries(obj).map([k, v] => {})
307
314
  * and `k` will be `keyof obj` instead of generic `string`.
308
315
  */
309
- export declare const _objectEntries: <K extends PropertyKey, V>(obj: Partial<Record<K, V>>) => [k: K, v: V][];
316
+ export declare const _objectEntries: <K extends PropertyKey, V>(obj: Partial<Record<K, V>>) => [k: K extends string ? K : K extends number | bigint ? `${K}` : never, v: V][];
310
317
  export type NullishValue = null | undefined;
311
318
  export type FalsyValue = false | '' | 0 | null | undefined;
312
319
  /**
package/dist/types.js CHANGED
@@ -33,11 +33,18 @@ export const _stringMapEntries = Object.entries;
33
33
  /**
34
34
  * Alias of `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
35
35
  * This is how TypeScript should work, actually.
36
+ *
37
+ * Object.keys always returns strings, so numeric keys are stringified via `${K}`.
38
+ * Symbol keys are excluded (Object.keys does not return symbols).
36
39
  */
37
40
  export const _objectKeys = Object.keys;
38
41
  /**
39
42
  * Alias of `Object.entries`, but returns better-typed output.
40
43
  *
44
+ * Difference with _stringMapEntries?
45
+ * Use _stringMapEntries when the object is a StringMap<T> - it'll correctly infer T being not undefined.
46
+ * If the object is not a StringMap - use _objectEntries - it'll correctly infer object keys, which can be typed as Enum.
47
+ *
41
48
  * So e.g you can use _objectEntries(obj).map([k, v] => {})
42
49
  * and `k` will be `keyof obj` instead of generic `string`.
43
50
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
3
  "type": "module",
4
- "version": "15.72.0",
4
+ "version": "15.73.0",
5
5
  "dependencies": {
6
6
  "tslib": "^2"
7
7
  },
@@ -20,7 +20,7 @@
20
20
  "@typescript/native-preview": "7.0.0-dev.20260401.1",
21
21
  "crypto-js": "^4",
22
22
  "dayjs": "^1",
23
- "@naturalcycles/dev-lib": "20.42.0"
23
+ "@naturalcycles/dev-lib": "18.4.2"
24
24
  },
25
25
  "exports": {
26
26
  ".": "./dist/index.js",
package/src/abort.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { _isTruthy } from './is.util.js'
2
+ import type { NumberOfMilliseconds } from './types.js'
3
+
1
4
  /**
2
5
  * Like AbortSignal, but it can "abort itself" via the `.abort()` method.
3
6
  *
@@ -28,3 +31,83 @@ export function createAbortableSignal(): AbortableSignal {
28
31
  abort: ac.abort.bind(ac),
29
32
  })
30
33
  }
34
+
35
+ /**
36
+ * Returns AbortSignal if ms is defined.
37
+ * Otherwise returns undefined.
38
+ */
39
+ export function abortSignalTimeoutOrUndefined(
40
+ ms: NumberOfMilliseconds | undefined,
41
+ ): AbortSignal | undefined {
42
+ return ms ? abortSignalTimeout(ms) : undefined
43
+ }
44
+
45
+ /**
46
+ * Returns an AbortSignal that aborts after the given number of milliseconds.
47
+ * Uses native `AbortSignal.timeout()` when available, falls back to a polyfill.
48
+ *
49
+ * The abort reason is a DOMException with name "TimeoutError".
50
+ */
51
+ export function abortSignalTimeout(ms: NumberOfMilliseconds): AbortSignal {
52
+ return typeof AbortSignal.timeout === 'function'
53
+ ? AbortSignal.timeout(ms)
54
+ : polyfilledAbortSignalTimeout(ms)
55
+ }
56
+
57
+ export function polyfilledAbortSignalTimeout(ms: NumberOfMilliseconds): AbortSignal {
58
+ const ac = new AbortController()
59
+ setTimeout(() => {
60
+ ac.abort(new DOMException('The operation was aborted due to timeout', 'TimeoutError'))
61
+ }, ms)
62
+ return ac.signal
63
+ }
64
+
65
+ /**
66
+ * Returns AbortSignal.any(signals) is the array (after filtering undefined inputs) is not empty,
67
+ * otherwise undefined.
68
+ */
69
+ export function abortSignalAnyOrUndefined(
70
+ signals: (AbortSignal | undefined)[],
71
+ ): AbortSignal | undefined {
72
+ const filtered = signals.filter(_isTruthy)
73
+ return filtered.length ? abortSignalAny(filtered) : undefined
74
+ }
75
+
76
+ /**
77
+ * Returns an AbortSignal that aborts when any of the given signals abort.
78
+ * Uses native `AbortSignal.any()` when available, falls back to a polyfill.
79
+ *
80
+ * The abort reason is taken from the first signal that aborts.
81
+ * If any input signal is already aborted, the returned signal is immediately aborted.
82
+ *
83
+ * If only 1 signal is passed in the input array - that Signal is returned as-is.
84
+ */
85
+ export function abortSignalAny(signals: AbortSignal[]): AbortSignal {
86
+ if (signals.length === 1) {
87
+ return signals[0]!
88
+ }
89
+
90
+ return typeof AbortSignal.any === 'function'
91
+ ? AbortSignal.any(signals)
92
+ : polyfilledAbortSignalAny(signals)
93
+ }
94
+
95
+ export function polyfilledAbortSignalAny(signals: AbortSignal[]): AbortSignal {
96
+ const ac = new AbortController()
97
+
98
+ for (const signal of signals) {
99
+ if (signal.aborted) {
100
+ ac.abort(signal.reason)
101
+ return ac.signal
102
+ }
103
+ }
104
+
105
+ for (const signal of signals) {
106
+ signal.addEventListener('abort', () => ac.abort(signal.reason), {
107
+ once: true,
108
+ signal: ac.signal,
109
+ })
110
+ }
111
+
112
+ return ac.signal
113
+ }
@@ -221,6 +221,13 @@ export interface FetcherOptions {
221
221
  */
222
222
  timeoutSeconds?: number
223
223
 
224
+ /**
225
+ * AbortSignal to allow the caller to abort the request.
226
+ * If `timeoutSeconds` is also set, the signals are combined via `AbortSignal.any()`,
227
+ * so the request aborts on whichever fires first.
228
+ */
229
+ signal?: AbortSignal
230
+
224
231
  /**
225
232
  * Supports all the types that RequestInit.body supports.
226
233
  *
@@ -3,6 +3,7 @@
3
3
  /// <reference lib="dom.iterable" preserve="true" />
4
4
 
5
5
  import type { ReadableStream as WebReadableStream } from 'node:stream/web'
6
+ import { abortSignalAnyOrUndefined, abortSignalTimeoutOrUndefined } from '../abort.js'
6
7
  import { _ms, _since } from '../datetime/time.util.js'
7
8
  import { isServerSide } from '../env.js'
8
9
  import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert.js'
@@ -13,7 +14,6 @@ import {
13
14
  _errorDataAppend,
14
15
  _errorLikeToErrorObject,
15
16
  HttpRequestError,
16
- TimeoutError,
17
17
  UnexpectedPassError,
18
18
  } from '../error/error.util.js'
19
19
  import { _clamp } from '../number/number.util.js'
@@ -68,7 +68,7 @@ export class Fetcher {
68
68
  *
69
69
  * Version is to be incremented every time a difference in behaviour (or a bugfix) is done.
70
70
  */
71
- static readonly VERSION = 3
71
+ static readonly VERSION = 4
72
72
  /**
73
73
  * userAgent is statically exposed as Fetcher.userAgent.
74
74
  * It can be modified globally, and will be used (read) at the start of every request.
@@ -306,9 +306,9 @@ export class Fetcher {
306
306
  const req = this.normalizeOptions(opt)
307
307
  const { logger } = this.cfg
308
308
  const {
309
- timeoutSeconds,
310
309
  init: { method },
311
310
  } = req
311
+ const timeoutMillis = req.timeoutSeconds ? req.timeoutSeconds * 1000 : undefined
312
312
 
313
313
  for (const hook of this.cfg.hooks.beforeRequest || []) {
314
314
  await hook(req)
@@ -332,21 +332,10 @@ export class Fetcher {
332
332
  while (!res.retryStatus.retryStopped) {
333
333
  req.started = Date.now() as UnixTimestampMillis
334
334
 
335
- // setup timeout
336
- let timeoutId: number | undefined
337
- if (timeoutSeconds) {
338
- // Used for Request timeout (when timeoutSeconds is set),
339
- // but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
340
- // UPD: no, not using for "downloadBody" currently
341
- const abortController = new AbortController()
342
- req.init.signal = abortController.signal
343
- timeoutId = setTimeout(() => {
344
- // console.log(`actual request timed out in ${_since(req.started)}`)
345
- // Apparently, providing a `string` reason to abort() causes Undici to throw `invalid_argument` error,
346
- // so, we're wrapping it in a TimeoutError instance
347
- abortController.abort(new TimeoutError(`request timed out after ${timeoutSeconds} sec`))
348
- }, timeoutSeconds * 1000) as any as number
349
- }
335
+ req.init.signal = abortSignalAnyOrUndefined([
336
+ abortSignalTimeoutOrUndefined(timeoutMillis),
337
+ opt.signal,
338
+ ])
350
339
 
351
340
  if (req.logRequest) {
352
341
  const { retryAttempt } = res.retryStatus
@@ -372,13 +361,11 @@ export class Fetcher {
372
361
  } catch (err) {
373
362
  // For example, CORS error would result in "TypeError: failed to fetch" here
374
363
  // or, `fetch failed` with the cause of `unexpected redirect`
364
+ // AbortSignal.timeout() throws a DOMException with name "TimeoutError"
375
365
  res.err = _anyToError(err)
376
366
  res.ok = false
377
367
  // important to set it to undefined, otherwise it can keep the previous value (from previous try)
378
368
  res.fetchResponse = undefined
379
- } finally {
380
- clearTimeout(timeoutId)
381
- // Separate Timeout will be introduced to "download and parse the body"
382
369
  }
383
370
  res.statusFamily = this.getStatusFamily(res)
384
371
  res.statusCode = res.fetchResponse?.status
@@ -390,7 +377,7 @@ export class Fetcher {
390
377
  async () =>
391
378
  await this.onOkResponse(res as FetcherResponse<T> & { fetchResponse: Response }),
392
379
  {
393
- timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
380
+ timeout: timeoutMillis || Number.POSITIVE_INFINITY,
394
381
  name: 'Fetcher.downloadBody',
395
382
  },
396
383
  )
@@ -1,4 +1,3 @@
1
- export * from './abortable.js'
2
1
  export * from './pDefer.js'
3
2
  export * from './pDelay.js'
4
3
  export * from './pFilter.js'
@@ -2,7 +2,7 @@ import { _since } from '../datetime/time.util.js'
2
2
  import type { ErrorData } from '../error/error.model.js'
3
3
  import { _errorDataAppend } from '../error/error.util.js'
4
4
  import type { CommonLogger } from '../log/commonLogger.js'
5
- import type { AnyFunction, UnixTimestampMillis } from '../types.js'
5
+ import type { AnyAsyncFunction, UnixTimestampMillis } from '../types.js'
6
6
  import { pDelay } from './pDelay.js'
7
7
  import { pTimeout } from './pTimeout.js'
8
8
 
@@ -98,7 +98,7 @@ export interface PRetryOptions {
98
98
  * Returns a Function (!), enhanced with retry capabilities.
99
99
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
100
100
  */
101
- export function pRetryFn<T extends AnyFunction>(fn: T, opt: PRetryOptions = {}): T {
101
+ export function pRetryFn<T extends AnyAsyncFunction>(fn: T, opt: PRetryOptions = {}): T {
102
102
  return async function pRetryFunction(this: any, ...args: any[]) {
103
103
  return await pRetry(() => fn.call(this, ...args), opt)
104
104
  } as any
@@ -64,10 +64,16 @@ export function pTimeoutFn<T extends AnyAsyncFunction>(fn: T, opt: PTimeoutOptio
64
64
  * Throws an Error if the Function is not resolved in a certain time.
65
65
  * If the Function rejects - passes this rejection further.
66
66
  */
67
- export async function pTimeout<T>(fn: AnyAsyncFunction<T>, opt: PTimeoutOptions): Promise<T> {
67
+ export async function pTimeout<T>(
68
+ fn: (signal: AbortSignal) => Promise<T>,
69
+ opt: PTimeoutOptions,
70
+ ): Promise<T> {
71
+ const ac = new AbortController()
72
+ const { signal } = ac
73
+
68
74
  if (!opt.timeout) {
69
75
  // short-circuit to direct execution if 0 timeout is passed
70
- return await fn()
76
+ return await fn(signal)
71
77
  }
72
78
 
73
79
  const { timeout, name = fn.name || 'pTimeout function', onTimeout } = opt
@@ -90,15 +96,17 @@ export async function pTimeout<T>(fn: AnyAsyncFunction<T>, opt: PTimeoutOptions)
90
96
  // oxlint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
91
97
  reject(_errorDataAppend(err, opt.errorData))
92
98
  }
99
+ ac.abort(err)
93
100
  return
94
101
  }
95
102
 
96
103
  reject(err)
104
+ ac.abort(err)
97
105
  }, timeout)
98
106
 
99
107
  // Execute the Function
100
108
  try {
101
- resolve(await fn())
109
+ resolve(await fn(signal))
102
110
  } catch (err) {
103
111
  reject(err as Error)
104
112
  } finally {
package/src/types.ts CHANGED
@@ -373,20 +373,27 @@ export const _stringMapEntries = Object.entries as <T>(map: StringMap<T>) => [k:
373
373
  /**
374
374
  * Alias of `Object.keys`, but returns keys typed as `keyof T`, not as just `string`.
375
375
  * This is how TypeScript should work, actually.
376
+ *
377
+ * Object.keys always returns strings, so numeric keys are stringified via `${K}`.
378
+ * Symbol keys are excluded (Object.keys does not return symbols).
376
379
  */
377
380
  export const _objectKeys = Object.keys as <K extends PropertyKey>(
378
381
  obj: Partial<Record<K, any>>,
379
- ) => K[]
382
+ ) => (K extends string ? K : K extends number | bigint ? `${K}` : never)[]
380
383
 
381
384
  /**
382
385
  * Alias of `Object.entries`, but returns better-typed output.
383
386
  *
387
+ * Difference with _stringMapEntries?
388
+ * Use _stringMapEntries when the object is a StringMap<T> - it'll correctly infer T being not undefined.
389
+ * If the object is not a StringMap - use _objectEntries - it'll correctly infer object keys, which can be typed as Enum.
390
+ *
384
391
  * So e.g you can use _objectEntries(obj).map([k, v] => {})
385
392
  * and `k` will be `keyof obj` instead of generic `string`.
386
393
  */
387
394
  export const _objectEntries = Object.entries as <K extends PropertyKey, V>(
388
395
  obj: Partial<Record<K, V>>,
389
- ) => [k: K, v: V][]
396
+ ) => [k: K extends string ? K : K extends number | bigint ? `${K}` : never, v: V][]
390
397
 
391
398
  export type NullishValue = null | undefined
392
399
  export type FalsyValue = false | '' | 0 | null | undefined
@@ -1,20 +0,0 @@
1
- import type { AnyFunction } from '../types.js';
2
- /**
3
- * Similar to AbortController and AbortSignal.
4
- * Similar to pDefer and Promise.
5
- * Similar to Subject and Observable.
6
- *
7
- * Minimal interface for something that can be aborted in the future,
8
- * but not necessary.
9
- * Allows to listen to `onAbort` event.
10
- *
11
- * @experimental
12
- */
13
- export declare class Abortable {
14
- onAbort?: AnyFunction | undefined;
15
- constructor(onAbort?: AnyFunction | undefined);
16
- aborted: boolean;
17
- abort(): void;
18
- clear(): void;
19
- }
20
- export declare function abortable(onAbort?: AnyFunction): Abortable;
@@ -1,32 +0,0 @@
1
- /**
2
- * Similar to AbortController and AbortSignal.
3
- * Similar to pDefer and Promise.
4
- * Similar to Subject and Observable.
5
- *
6
- * Minimal interface for something that can be aborted in the future,
7
- * but not necessary.
8
- * Allows to listen to `onAbort` event.
9
- *
10
- * @experimental
11
- */
12
- export class Abortable {
13
- onAbort;
14
- constructor(onAbort) {
15
- this.onAbort = onAbort;
16
- }
17
- aborted = false;
18
- abort() {
19
- if (this.aborted)
20
- return;
21
- this.aborted = true;
22
- this.onAbort?.();
23
- this.onAbort = undefined; // cleanup listener
24
- }
25
- clear() {
26
- this.onAbort = undefined;
27
- }
28
- }
29
- // convenience function
30
- export function abortable(onAbort) {
31
- return new Abortable(onAbort);
32
- }
@@ -1,34 +0,0 @@
1
- import type { AnyFunction } from '../types.js'
2
-
3
- /**
4
- * Similar to AbortController and AbortSignal.
5
- * Similar to pDefer and Promise.
6
- * Similar to Subject and Observable.
7
- *
8
- * Minimal interface for something that can be aborted in the future,
9
- * but not necessary.
10
- * Allows to listen to `onAbort` event.
11
- *
12
- * @experimental
13
- */
14
- export class Abortable {
15
- constructor(public onAbort?: AnyFunction) {}
16
-
17
- aborted = false
18
-
19
- abort(): void {
20
- if (this.aborted) return
21
- this.aborted = true
22
- this.onAbort?.()
23
- this.onAbort = undefined // cleanup listener
24
- }
25
-
26
- clear(): void {
27
- this.onAbort = undefined
28
- }
29
- }
30
-
31
- // convenience function
32
- export function abortable(onAbort?: AnyFunction): Abortable {
33
- return new Abortable(onAbort)
34
- }