@naturalcycles/js-lib 14.80.1 → 14.81.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.
@@ -6,7 +6,7 @@ const __1 = require("..");
6
6
  function _Retry(opt = {}) {
7
7
  return (target, key, descriptor) => {
8
8
  const originalFn = descriptor.value;
9
- descriptor.value = (0, __1.pRetry)(originalFn, opt);
9
+ descriptor.value = (0, __1.pRetryFn)(originalFn, opt);
10
10
  return descriptor;
11
11
  };
12
12
  }
package/dist/index.d.ts CHANGED
@@ -41,7 +41,7 @@ export * from './promise/pFilter';
41
41
  export * from './promise/pHang';
42
42
  import { pMap, PMapOptions } from './promise/pMap';
43
43
  export * from './promise/pProps';
44
- import { pRetry, PRetryOptions } from './promise/pRetry';
44
+ import { pRetry, pRetryFn, PRetryOptions } from './promise/pRetry';
45
45
  export * from './promise/pState';
46
46
  import { pTimeout, pTimeoutFn, PTimeoutOptions } from './promise/pTimeout';
47
47
  export * from './promise/pTuple';
@@ -60,4 +60,4 @@ import { PQueue, PQueueCfg } from './promise/pQueue';
60
60
  export * from './seq/seq';
61
61
  export * from './math/stack.util';
62
62
  export type { AbortableMapper, AbortablePredicate, AbortableAsyncPredicate, AbortableAsyncMapper, PQueueCfg, MemoCache, PromiseDecoratorCfg, PromiseDecoratorResp, ErrorData, ErrorObject, HttpErrorData, HttpErrorResponse, Admin401ErrorData, Admin403ErrorData, StringMap, PromiseMap, AnyObject, AnyFunction, ValuesOf, ValueOf, KeyValueTuple, ObjectMapper, ObjectPredicate, InstanceId, IsoDate, IsoDateTime, Reviver, PMapOptions, Mapper, AsyncMapper, Predicate, AsyncPredicate, BatchResult, DeferredPromise, PRetryOptions, PTimeoutOptions, TryCatchOptions, StringifyAnyOptions, JsonStringifyFunction, Merge, ReadonlyDeep, Promisable, Simplify, ConditionalPick, ConditionalExcept, Class, UnixTimestamp, BaseDBEntity, SavedDBEntity, Saved, Unsaved, CreatedUpdated, CreatedUpdatedId, ObjectWithId, AnyObjectWithId, JsonSchema, JsonSchemaAny, JsonSchemaOneOf, JsonSchemaAllOf, JsonSchemaAnyOf, JsonSchemaNot, JsonSchemaRef, JsonSchemaConst, JsonSchemaEnum, JsonSchemaString, JsonSchemaNumber, JsonSchemaBoolean, JsonSchemaNull, JsonSchemaRootObject, JsonSchemaObject, JsonSchemaArray, JsonSchemaTuple, JsonSchemaBuilder, CommonLogLevel, CommonLogWithLevelFunction, CommonLogFunction, CommonLogger, };
63
- export { is, _createPromiseDecorator, _stringMapValues, _stringMapEntries, _objectKeys, pMap, _passthroughMapper, _passUndefinedMapper, _passthroughPredicate, _passNothingPredicate, _noop, ErrorMode, pDefer, AggregatedError, pRetry, pTimeout, pTimeoutFn, _tryCatch, _TryCatch, _stringifyAny, jsonSchema, JsonSchemaAnyBuilder, commonLoggerMinLevel, commonLoggerNoop, commonLogLevelNumber, commonLoggerPipe, commonLoggerPrefix, commonLoggerCreate, PQueue, END, SKIP, };
63
+ export { is, _createPromiseDecorator, _stringMapValues, _stringMapEntries, _objectKeys, pMap, _passthroughMapper, _passUndefinedMapper, _passthroughPredicate, _passNothingPredicate, _noop, ErrorMode, pDefer, AggregatedError, pRetry, pRetryFn, pTimeout, pTimeoutFn, _tryCatch, _TryCatch, _stringifyAny, jsonSchema, JsonSchemaAnyBuilder, commonLoggerMinLevel, commonLoggerNoop, commonLogLevelNumber, commonLoggerPipe, commonLoggerPrefix, commonLoggerCreate, PQueue, END, SKIP, };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SKIP = exports.END = exports.PQueue = exports.commonLoggerCreate = exports.commonLoggerPrefix = exports.commonLoggerPipe = exports.commonLogLevelNumber = exports.commonLoggerNoop = exports.commonLoggerMinLevel = exports.JsonSchemaAnyBuilder = exports.jsonSchema = exports._stringifyAny = exports._TryCatch = exports._tryCatch = exports.pTimeoutFn = exports.pTimeout = exports.pRetry = exports.AggregatedError = exports.pDefer = exports.ErrorMode = exports._noop = exports._passNothingPredicate = exports._passthroughPredicate = exports._passUndefinedMapper = exports._passthroughMapper = exports.pMap = exports._objectKeys = exports._stringMapEntries = exports._stringMapValues = exports._createPromiseDecorator = exports.is = void 0;
3
+ exports.SKIP = exports.END = exports.PQueue = exports.commonLoggerCreate = exports.commonLoggerPrefix = exports.commonLoggerPipe = exports.commonLogLevelNumber = exports.commonLoggerNoop = exports.commonLoggerMinLevel = exports.JsonSchemaAnyBuilder = exports.jsonSchema = exports._stringifyAny = exports._TryCatch = exports._tryCatch = exports.pTimeoutFn = exports.pTimeout = exports.pRetryFn = exports.pRetry = exports.AggregatedError = exports.pDefer = exports.ErrorMode = exports._noop = exports._passNothingPredicate = exports._passthroughPredicate = exports._passUndefinedMapper = exports._passthroughMapper = exports.pMap = exports._objectKeys = exports._stringMapEntries = exports._stringMapValues = exports._createPromiseDecorator = exports.is = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  (0, tslib_1.__exportStar)(require("./array/array.util"), exports);
6
6
  (0, tslib_1.__exportStar)(require("./lazy"), exports);
@@ -53,6 +53,7 @@ Object.defineProperty(exports, "pMap", { enumerable: true, get: function () { re
53
53
  (0, tslib_1.__exportStar)(require("./promise/pProps"), exports);
54
54
  const pRetry_1 = require("./promise/pRetry");
55
55
  Object.defineProperty(exports, "pRetry", { enumerable: true, get: function () { return pRetry_1.pRetry; } });
56
+ Object.defineProperty(exports, "pRetryFn", { enumerable: true, get: function () { return pRetry_1.pRetryFn; } });
56
57
  (0, tslib_1.__exportStar)(require("./promise/pState"), exports);
57
58
  const pTimeout_1 = require("./promise/pTimeout");
58
59
  Object.defineProperty(exports, "pTimeout", { enumerable: true, get: function () { return pTimeout_1.pTimeout; } });
@@ -5,6 +5,12 @@ export interface PRetryOptions {
5
5
  * Can be used to identify the place in the code that failed.
6
6
  */
7
7
  name?: string;
8
+ /**
9
+ * Timeout for each Try, in milliseconds.
10
+ *
11
+ * Defaults to 60_000
12
+ */
13
+ timeout?: number;
8
14
  /**
9
15
  * How many attempts to try.
10
16
  * First attempt is not a retry, but "initial try". It still counts.
@@ -29,7 +35,7 @@ export interface PRetryOptions {
29
35
  *
30
36
  * @default () => true
31
37
  */
32
- predicate?: (err: unknown, attempt: number, maxAttempts: number) => boolean;
38
+ predicate?: (err: Error, attempt: number, maxAttempts: number) => boolean;
33
39
  /**
34
40
  * Log the first attempt (which is not a "retry" yet).
35
41
  *
@@ -62,9 +68,18 @@ export interface PRetryOptions {
62
68
  * Default to `console`
63
69
  */
64
70
  logger?: CommonLogger;
71
+ /**
72
+ * Defaults to true.
73
+ * If true - preserves the stack trace in case of a Timeout (usually - very useful!).
74
+ * It has a certain perf cost.
75
+ *
76
+ * @experimental
77
+ */
78
+ keepStackTrace?: boolean;
65
79
  }
66
80
  /**
67
81
  * Returns a Function (!), enhanced with retry capabilities.
68
82
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
69
83
  */
70
- export declare function pRetry<T extends AnyFunction>(fn: T, opt?: PRetryOptions): T;
84
+ export declare function pRetryFn<T extends AnyFunction>(fn: T, opt?: PRetryOptions): T;
85
+ export declare function pRetry<T>(fn: (attempt: number) => Promise<T>, opt?: PRetryOptions): Promise<T>;
@@ -1,58 +1,90 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pRetry = void 0;
3
+ exports.pRetry = exports.pRetryFn = void 0;
4
4
  const __1 = require("..");
5
+ const pTimeout_1 = require("./pTimeout");
5
6
  /**
6
7
  * Returns a Function (!), enhanced with retry capabilities.
7
8
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
8
9
  */
9
- // eslint-disable-next-line @typescript-eslint/ban-types
10
- function pRetry(fn, opt = {}) {
11
- const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name = fn.name, } = opt;
10
+ function pRetryFn(fn, opt = {}) {
11
+ return async function pRetryFunction(...args) {
12
+ return await pRetry(() => fn.call(this, ...args), opt);
13
+ };
14
+ }
15
+ exports.pRetryFn = pRetryFn;
16
+ async function pRetry(fn, opt = {}) {
17
+ const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name, keepStackTrace = true, timeout, } = opt;
18
+ const fakeError = keepStackTrace ? new Error('RetryError') : undefined;
12
19
  let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt;
13
20
  if (opt.logAll) {
14
- logFirstAttempt = logRetries = logFailures = true;
21
+ logSuccess = logFirstAttempt = logRetries = logFailures = true;
15
22
  }
16
23
  if (opt.logNone) {
17
24
  logSuccess = logFirstAttempt = logRetries = logFailures = false;
18
25
  }
19
- const fname = ['pRetry', name].filter(Boolean).join('.');
20
- return async function (...args) {
21
- let delay = initialDelay;
22
- let attempt = 0;
23
- return await new Promise((resolve, reject) => {
24
- const next = async () => {
25
- const started = Date.now();
26
- try {
27
- attempt++;
28
- if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
29
- logger.log(`${fname} attempt #${attempt}...`);
30
- }
31
- const r = await fn.apply(this, args);
32
- if (logSuccess) {
33
- logger.log(`${fname} attempt #${attempt} succeeded in ${(0, __1._since)(started)}`);
34
- }
35
- resolve(r);
26
+ const fname = name || fn.name || 'pRetry function';
27
+ let delay = initialDelay;
28
+ let attempt = 0;
29
+ let timer;
30
+ let timedOut = false;
31
+ return await new Promise((resolve, reject) => {
32
+ const rejectWithTimeout = () => {
33
+ timedOut = true; // to prevent more tries
34
+ const err = new pTimeout_1.TimeoutError(`"${fname}" timed out after ${timeout} ms`);
35
+ if (fakeError) {
36
+ // keep original stack
37
+ err.stack = fakeError.stack.replace('Error: RetryError', 'TimeoutError');
38
+ }
39
+ reject(err);
40
+ };
41
+ const next = async () => {
42
+ if (timedOut)
43
+ return;
44
+ if (timeout) {
45
+ timer = setTimeout(rejectWithTimeout, timeout);
46
+ }
47
+ const started = Date.now();
48
+ try {
49
+ attempt++;
50
+ if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
51
+ logger.log(`${fname} attempt #${attempt}...`);
36
52
  }
37
- catch (err) {
38
- if (logFailures) {
39
- logger.warn(`${fname} attempt #${attempt} error in ${(0, __1._since)(started)}:`, (0, __1._stringifyAny)(err, {
40
- includeErrorData: true,
41
- }));
42
- }
43
- if (attempt >= maxAttempts || (predicate && !predicate(err, attempt, maxAttempts))) {
44
- // Give up
45
- reject(err);
46
- }
47
- else {
48
- // Retry after delay
49
- delay *= delayMultiplier;
50
- setTimeout(next, delay);
53
+ const r = await fn(attempt);
54
+ clearTimeout(timer);
55
+ if (logSuccess) {
56
+ logger.log(`${fname} attempt #${attempt} succeeded in ${(0, __1._since)(started)}`);
57
+ }
58
+ resolve(r);
59
+ }
60
+ catch (err) {
61
+ clearTimeout(timer);
62
+ if (logFailures) {
63
+ logger.warn(`${fname} attempt #${attempt} error in ${(0, __1._since)(started)}:`, (0, __1._stringifyAny)(err, {
64
+ includeErrorData: true,
65
+ }));
66
+ }
67
+ if (attempt >= maxAttempts ||
68
+ (predicate && !predicate(err, attempt, maxAttempts))) {
69
+ // Give up
70
+ if (fakeError) {
71
+ // Preserve the original call stack
72
+ Object.defineProperty(err, 'stack', {
73
+ value: err.stack +
74
+ '\n --' +
75
+ fakeError.stack.replace('Error: RetryError', ''),
76
+ });
51
77
  }
78
+ reject(err);
52
79
  }
53
- };
54
- void next();
55
- });
56
- };
80
+ else {
81
+ // Retry after delay
82
+ delay *= delayMultiplier;
83
+ setTimeout(next, delay);
84
+ }
85
+ }
86
+ };
87
+ void next();
88
+ });
57
89
  }
58
90
  exports.pRetry = pRetry;
@@ -1,9 +1,9 @@
1
- import { pRetry } from '..';
1
+ import { pRetryFn } from '..';
2
2
  // eslint-disable-next-line @typescript-eslint/naming-convention
3
3
  export function _Retry(opt = {}) {
4
4
  return (target, key, descriptor) => {
5
5
  const originalFn = descriptor.value;
6
- descriptor.value = pRetry(originalFn, opt);
6
+ descriptor.value = pRetryFn(originalFn, opt);
7
7
  return descriptor;
8
8
  };
9
9
  }
package/dist-esm/index.js CHANGED
@@ -38,7 +38,7 @@ export * from './promise/pFilter';
38
38
  export * from './promise/pHang';
39
39
  import { pMap } from './promise/pMap';
40
40
  export * from './promise/pProps';
41
- import { pRetry } from './promise/pRetry';
41
+ import { pRetry, pRetryFn } from './promise/pRetry';
42
42
  export * from './promise/pState';
43
43
  import { pTimeout, pTimeoutFn } from './promise/pTimeout';
44
44
  export * from './promise/pTuple';
@@ -55,4 +55,4 @@ export * from './string/safeJsonStringify';
55
55
  import { PQueue } from './promise/pQueue';
56
56
  export * from './seq/seq';
57
57
  export * from './math/stack.util';
58
- export { is, _createPromiseDecorator, _stringMapValues, _stringMapEntries, _objectKeys, pMap, _passthroughMapper, _passUndefinedMapper, _passthroughPredicate, _passNothingPredicate, _noop, ErrorMode, pDefer, AggregatedError, pRetry, pTimeout, pTimeoutFn, _tryCatch, _TryCatch, _stringifyAny, jsonSchema, JsonSchemaAnyBuilder, commonLoggerMinLevel, commonLoggerNoop, commonLogLevelNumber, commonLoggerPipe, commonLoggerPrefix, commonLoggerCreate, PQueue, END, SKIP, };
58
+ export { is, _createPromiseDecorator, _stringMapValues, _stringMapEntries, _objectKeys, pMap, _passthroughMapper, _passUndefinedMapper, _passthroughPredicate, _passNothingPredicate, _noop, ErrorMode, pDefer, AggregatedError, pRetry, pRetryFn, pTimeout, pTimeoutFn, _tryCatch, _TryCatch, _stringifyAny, jsonSchema, JsonSchemaAnyBuilder, commonLoggerMinLevel, commonLoggerNoop, commonLogLevelNumber, commonLoggerPipe, commonLoggerPrefix, commonLoggerCreate, PQueue, END, SKIP, };
@@ -1,54 +1,85 @@
1
1
  import { _since, _stringifyAny } from '..';
2
+ import { TimeoutError } from './pTimeout';
2
3
  /**
3
4
  * Returns a Function (!), enhanced with retry capabilities.
4
5
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
5
6
  */
6
- // eslint-disable-next-line @typescript-eslint/ban-types
7
- export function pRetry(fn, opt = {}) {
8
- const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name = fn.name, } = opt;
7
+ export function pRetryFn(fn, opt = {}) {
8
+ return async function pRetryFunction(...args) {
9
+ return await pRetry(() => fn.call(this, ...args), opt);
10
+ };
11
+ }
12
+ export async function pRetry(fn, opt = {}) {
13
+ const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name, keepStackTrace = true, timeout, } = opt;
14
+ const fakeError = keepStackTrace ? new Error('RetryError') : undefined;
9
15
  let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt;
10
16
  if (opt.logAll) {
11
- logFirstAttempt = logRetries = logFailures = true;
17
+ logSuccess = logFirstAttempt = logRetries = logFailures = true;
12
18
  }
13
19
  if (opt.logNone) {
14
20
  logSuccess = logFirstAttempt = logRetries = logFailures = false;
15
21
  }
16
- const fname = ['pRetry', name].filter(Boolean).join('.');
17
- return async function (...args) {
18
- let delay = initialDelay;
19
- let attempt = 0;
20
- return await new Promise((resolve, reject) => {
21
- const next = async () => {
22
- const started = Date.now();
23
- try {
24
- attempt++;
25
- if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
26
- logger.log(`${fname} attempt #${attempt}...`);
27
- }
28
- const r = await fn.apply(this, args);
29
- if (logSuccess) {
30
- logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`);
31
- }
32
- resolve(r);
22
+ const fname = name || fn.name || 'pRetry function';
23
+ let delay = initialDelay;
24
+ let attempt = 0;
25
+ let timer;
26
+ let timedOut = false;
27
+ return await new Promise((resolve, reject) => {
28
+ const rejectWithTimeout = () => {
29
+ timedOut = true; // to prevent more tries
30
+ const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`);
31
+ if (fakeError) {
32
+ // keep original stack
33
+ err.stack = fakeError.stack.replace('Error: RetryError', 'TimeoutError');
34
+ }
35
+ reject(err);
36
+ };
37
+ const next = async () => {
38
+ if (timedOut)
39
+ return;
40
+ if (timeout) {
41
+ timer = setTimeout(rejectWithTimeout, timeout);
42
+ }
43
+ const started = Date.now();
44
+ try {
45
+ attempt++;
46
+ if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
47
+ logger.log(`${fname} attempt #${attempt}...`);
33
48
  }
34
- catch (err) {
35
- if (logFailures) {
36
- logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, _stringifyAny(err, {
37
- includeErrorData: true,
38
- }));
39
- }
40
- if (attempt >= maxAttempts || (predicate && !predicate(err, attempt, maxAttempts))) {
41
- // Give up
42
- reject(err);
43
- }
44
- else {
45
- // Retry after delay
46
- delay *= delayMultiplier;
47
- setTimeout(next, delay);
49
+ const r = await fn(attempt);
50
+ clearTimeout(timer);
51
+ if (logSuccess) {
52
+ logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`);
53
+ }
54
+ resolve(r);
55
+ }
56
+ catch (err) {
57
+ clearTimeout(timer);
58
+ if (logFailures) {
59
+ logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, _stringifyAny(err, {
60
+ includeErrorData: true,
61
+ }));
62
+ }
63
+ if (attempt >= maxAttempts ||
64
+ (predicate && !predicate(err, attempt, maxAttempts))) {
65
+ // Give up
66
+ if (fakeError) {
67
+ // Preserve the original call stack
68
+ Object.defineProperty(err, 'stack', {
69
+ value: err.stack +
70
+ '\n --' +
71
+ fakeError.stack.replace('Error: RetryError', ''),
72
+ });
48
73
  }
74
+ reject(err);
49
75
  }
50
- };
51
- void next();
52
- });
53
- };
76
+ else {
77
+ // Retry after delay
78
+ delay *= delayMultiplier;
79
+ setTimeout(next, delay);
80
+ }
81
+ }
82
+ };
83
+ void next();
84
+ });
54
85
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.80.1",
3
+ "version": "14.81.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -1,10 +1,10 @@
1
- import { pRetry, PRetryOptions } from '..'
1
+ import { pRetryFn, PRetryOptions } from '..'
2
2
 
3
3
  // eslint-disable-next-line @typescript-eslint/naming-convention
4
4
  export function _Retry(opt: PRetryOptions = {}): MethodDecorator {
5
5
  return (target, key, descriptor) => {
6
6
  const originalFn = descriptor.value
7
- descriptor.value = pRetry(originalFn as any, opt)
7
+ descriptor.value = pRetryFn(originalFn as any, opt)
8
8
  return descriptor
9
9
  }
10
10
  }
package/src/index.ts CHANGED
@@ -74,7 +74,7 @@ export * from './promise/pFilter'
74
74
  export * from './promise/pHang'
75
75
  import { pMap, PMapOptions } from './promise/pMap'
76
76
  export * from './promise/pProps'
77
- import { pRetry, PRetryOptions } from './promise/pRetry'
77
+ import { pRetry, pRetryFn, PRetryOptions } from './promise/pRetry'
78
78
  export * from './promise/pState'
79
79
  import { pTimeout, pTimeoutFn, PTimeoutOptions } from './promise/pTimeout'
80
80
  export * from './promise/pTuple'
@@ -250,6 +250,7 @@ export {
250
250
  pDefer,
251
251
  AggregatedError,
252
252
  pRetry,
253
+ pRetryFn,
253
254
  pTimeout,
254
255
  pTimeoutFn,
255
256
  _tryCatch,
@@ -1,4 +1,5 @@
1
1
  import { _since, _stringifyAny, AnyFunction, CommonLogger } from '..'
2
+ import { TimeoutError } from './pTimeout'
2
3
 
3
4
  export interface PRetryOptions {
4
5
  /**
@@ -7,6 +8,13 @@ export interface PRetryOptions {
7
8
  */
8
9
  name?: string
9
10
 
11
+ /**
12
+ * Timeout for each Try, in milliseconds.
13
+ *
14
+ * Defaults to 60_000
15
+ */
16
+ timeout?: number
17
+
10
18
  /**
11
19
  * How many attempts to try.
12
20
  * First attempt is not a retry, but "initial try". It still counts.
@@ -34,7 +42,7 @@ export interface PRetryOptions {
34
42
  *
35
43
  * @default () => true
36
44
  */
37
- predicate?: (err: unknown, attempt: number, maxAttempts: number) => boolean
45
+ predicate?: (err: Error, attempt: number, maxAttempts: number) => boolean
38
46
 
39
47
  /**
40
48
  * Log the first attempt (which is not a "retry" yet).
@@ -74,76 +82,132 @@ export interface PRetryOptions {
74
82
  * Default to `console`
75
83
  */
76
84
  logger?: CommonLogger
85
+
86
+ /**
87
+ * Defaults to true.
88
+ * If true - preserves the stack trace in case of a Timeout (usually - very useful!).
89
+ * It has a certain perf cost.
90
+ *
91
+ * @experimental
92
+ */
93
+ keepStackTrace?: boolean
77
94
  }
78
95
 
79
96
  /**
80
97
  * Returns a Function (!), enhanced with retry capabilities.
81
98
  * Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
82
99
  */
83
- // eslint-disable-next-line @typescript-eslint/ban-types
84
- export function pRetry<T extends AnyFunction>(fn: T, opt: PRetryOptions = {}): T {
100
+ export function pRetryFn<T extends AnyFunction>(fn: T, opt: PRetryOptions = {}): T {
101
+ return async function pRetryFunction(this: any, ...args: any[]) {
102
+ return await pRetry(() => fn.call(this, ...args), opt)
103
+ } as any
104
+ }
105
+
106
+ export async function pRetry<T>(
107
+ fn: (attempt: number) => Promise<T>,
108
+ opt: PRetryOptions = {},
109
+ ): Promise<T> {
85
110
  const {
86
111
  maxAttempts = 4,
87
112
  delay: initialDelay = 1000,
88
113
  delayMultiplier = 2,
89
114
  predicate,
90
115
  logger = console,
91
- name = fn.name,
116
+ name,
117
+ keepStackTrace = true,
118
+ timeout,
92
119
  } = opt
93
120
 
121
+ const fakeError = keepStackTrace ? new Error('RetryError') : undefined
122
+
94
123
  let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt
95
124
 
96
125
  if (opt.logAll) {
97
- logFirstAttempt = logRetries = logFailures = true
126
+ logSuccess = logFirstAttempt = logRetries = logFailures = true
98
127
  }
99
128
  if (opt.logNone) {
100
129
  logSuccess = logFirstAttempt = logRetries = logFailures = false
101
130
  }
102
131
 
103
- const fname = ['pRetry', name].filter(Boolean).join('.')
132
+ const fname = name || fn.name || 'pRetry function'
104
133
 
105
- return async function (this: any, ...args: any[]) {
106
- let delay = initialDelay
107
- let attempt = 0
134
+ let delay = initialDelay
135
+ let attempt = 0
136
+ let timer: NodeJS.Timeout | undefined
137
+ let timedOut = false
108
138
 
109
- return await new Promise((resolve, reject) => {
110
- const next = async () => {
111
- const started = Date.now()
139
+ return await new Promise((resolve, reject) => {
140
+ const rejectWithTimeout = () => {
141
+ timedOut = true // to prevent more tries
142
+ const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`)
143
+ if (fakeError) {
144
+ // keep original stack
145
+ err.stack = fakeError.stack!.replace('Error: RetryError', 'TimeoutError')
146
+ }
147
+ reject(err)
148
+ }
112
149
 
113
- try {
114
- attempt++
115
- if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
116
- logger.log(`${fname} attempt #${attempt}...`)
117
- }
150
+ const next = async () => {
151
+ if (timedOut) return
118
152
 
119
- const r = await fn.apply(this, args)
153
+ if (timeout) {
154
+ timer = setTimeout(rejectWithTimeout, timeout)
155
+ }
120
156
 
121
- if (logSuccess) {
122
- logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`)
123
- }
124
- resolve(r)
125
- } catch (err) {
126
- if (logFailures) {
127
- logger.warn(
128
- `${fname} attempt #${attempt} error in ${_since(started)}:`,
129
- _stringifyAny(err, {
130
- includeErrorData: true,
131
- }),
132
- )
133
- }
157
+ const started = Date.now()
158
+
159
+ try {
160
+ attempt++
161
+ if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
162
+ logger.log(`${fname} attempt #${attempt}...`)
163
+ }
164
+
165
+ const r = await fn(attempt)
166
+
167
+ clearTimeout(timer!)
134
168
 
135
- if (attempt >= maxAttempts || (predicate && !predicate(err, attempt, maxAttempts))) {
136
- // Give up
137
- reject(err)
138
- } else {
139
- // Retry after delay
140
- delay *= delayMultiplier
141
- setTimeout(next, delay)
169
+ if (logSuccess) {
170
+ logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`)
171
+ }
172
+
173
+ resolve(r)
174
+ } catch (err) {
175
+ clearTimeout(timer!)
176
+
177
+ if (logFailures) {
178
+ logger.warn(
179
+ `${fname} attempt #${attempt} error in ${_since(started)}:`,
180
+ _stringifyAny(err, {
181
+ includeErrorData: true,
182
+ }),
183
+ )
184
+ }
185
+
186
+ if (
187
+ attempt >= maxAttempts ||
188
+ (predicate && !predicate(err as Error, attempt, maxAttempts))
189
+ ) {
190
+ // Give up
191
+
192
+ if (fakeError) {
193
+ // Preserve the original call stack
194
+ Object.defineProperty(err, 'stack', {
195
+ value:
196
+ (err as Error).stack +
197
+ '\n --' +
198
+ fakeError.stack!.replace('Error: RetryError', ''),
199
+ })
142
200
  }
201
+
202
+ reject(err)
203
+ } else {
204
+ // Retry after delay
205
+ delay *= delayMultiplier
206
+ setTimeout(next, delay)
143
207
  }
144
208
  }
209
+ }
145
210
 
146
- void next()
147
- })
148
- } as any
211
+ void next()
212
+ })
149
213
  }