ts-retry
Version:
A little retry tool to execute a function until the function is successful. Can also bind a timeout to a function. This lib is usable in typescript, in javascript, in node, in SPA tools (rest, Vue, Svelte...) and browser (available in ESM and common js fo
463 lines (365 loc) • 14.6 kB
Markdown
# ts-retry
A little retry tool to execute a function until the function is successful. Can also bind a timeout to a function.
This lib is usable in typescript, in javascript, in node, in SPA tools (rest, Vue, Svelte...) and browser (available in ESM and common js format).
---
**Breaking change**:
To migrate to 3.x: retryAsyncDecorator and retryAsync ahs been move in utils/decorators. These impact only
those that import those functions directly from decorator.ts file
Other 3.X items are new and implies no breaking change.
For those who are using 1.x in **typescript**, you may have to add a type to RetryOptions if you want to use
the new `until`function. This type is the called function returns type.
---
## How to
- to retry something:
```javascript
const result = await retry(
() => {
/* do something */
},
{ delay: 100, maxTry: 5 }
);
```
- to retry something async :
```javascript
const result = await retryAsync(
async () => {
/* do something */
},
{ delay: 100, maxTry: 5 }
);
```
- to retry until the answer is 42 :
```javascript
try {
await retryAsync(
async () => {
/* do something */
},
{
delay: 100,
maxTry: 5,
until: (lastResult) => lastResult === 42,
}
);
} catch (err) {
if (isTooManyTries(err)) {
// Did not get 42 after 'maxTry' calls
} else {
// something else goes wrong
}
}
```
- Need to call a function at multiple locations with same retryOptions ? Use decorators:
```javascript
const fn = (param1: string, param2:number) => /* do something */;
const decoratedFn = retryDecorator(
fn,
{ delay:100, maxTry:5 }
);
const title1 = await decoratedFn("value1", 1);
const title2 = await decoratedFn("valueXXX", 2);
const fn = async (name: string): Promise<any> => { /* something async */ };
const decoratedFn = retryAsyncDecorator(
fn,
{ delay:100, maxTry:5 }
);
const result1 = await decoratedFn("Smith");
const result2 = await decoratedFn("Doe");
```
- to wait:
```javascript
await wait(10000); // Wait for 10 seconds
```
- to set a timeout:
```javascript
try {
const result = await waitUntil(
()=> {/* do something */},
10000
);
} catch (err) {
if (isTimeoutError(error)) { {
// fn does not complete after 10 seconds
} else {
// fn throws an exception
}
}
```
- to set a timeout on something async:
```javascript
try {
const result = await waitUntilAsync(async () => {
/* do something */
}, 10000);
} catch (err) {
if (isTimeoutError(error)) {
// fn does not complete after 10 seconds
} else {
// fn throws an exception
}
}
```
- Need to call a function at multiple locations with same retryOptions ? Use decorators:
```javascript
const fn = (title: string, count:number) => /* a long task */;
const decoratedFn = waitUntilDecorator(
fn,
{ delay:100, maxTry:5 }
);
const title1 = await decoratedFn("Intro", 1);
const title2 = await decoratedFn("A chapter", 2);
```
```javascript
const fn = async (name: string): Promise<any> => {
/* a long task */
};
const decoratedFn = waitUntilAsyncDecorator(fn, { delay: 100, maxTry: 5 });
const result1 = await decoratedFn("John");
const result2 = await decoratedFn("Doe");
```
---
## Utils
`retry` comes with handy utilities function for common use case:
- to retry until a function returns something defined (aka not null neither not undefined):
```typescript
// in all cases results is a string and cannot be null or undefined
const result = await retryUntilDefined( (): string|undefined => { ... } ) );
const result = await retryUntilAsyncDefined( (): Promise<string|null> => { ... } );
const decorated = retryUntilDefinedDecorator( (p1: string): string|undefined => { ... } );
const result = await decorated('hello world');
const decorated = retryAsyncUntilDefinedDecorator( (p1: string): Promise<string|undefined> => { ... } );
const result = await decorated('hello world');
```
- to retry until a function returns something truthy:
```typescript
// in all cases results is a string and cannot be null or undefined
const result = await retryUntilTruthy( (): boolean|undefined => { ... } ) );
const result = await retryAsyncUntilTruthy( (): Promise<number|null> => { ... } );
const decorated = retryUntilTruthyDecorator( (p1: string): boolean|undefined => { ... } );
const result = await decorated('hello world');
const decorated = retryAsyncUntilTruthyDecorator( (p1: string): Promise<boolean|null> => { ... } );
const result = await decorated('hello world');
```
- to retry until fetch is successfully:
```typescript
const result = await retryAsyncUntilResponse( () => fetch(...) );
const decorated = retryAsyncUntilResponseDecorator( (param) => fetch(...) );
const result = await decorated('q=1');
```
---
## API
### Retry family
- `retry(fn, retryOptions?)`: call repeatedly `fn` until `fn` does not throw an exception. Stop after `retryOptions.maxTry` count. Between each call wait `retryOptions.delay` milliseconds.
if stop to call fn after `retryOptions.maxTry`, throws `fn` exception, otherwise returns fn return value.
- `retryAsync(fn, retryOptions?)`: same as retry, except `fn` is an asynchronous function.
- `retryOptions`:
- `maxTry`: [optional] maximum calls to fn.
- `delay`: [optional] delay between each call (in milliseconds). Could be either a number or a function (when delay time dependent from number of retrys, of previous result...), see below for explanation about delay
- `until`: [optional] (lastResult) => boolean: return false if last `fn` results is not the expected one: continue to call fn until `until` returns true. A `TooManyTries` is thrown after `maxTry` calls to fn;
- `onError`: [optional](err: Error, currentTry: number) => void: called on each error except the last one. Includes the current try for logging. To catch/log the last error use onMaxRetryFunc
- `onMaxRetryFunc`: [optional](err: Error) => void: called on the final error at the maxTry limit only
- `onSuccess`: [optional](currentTry: number) => void: called on success. Includes the current try for logging
When an option value is not provided, the default one is applied. The default options are:
```javascript
delay: 250,
maxTry: 4 * 60,
```
- `setDefaultRetryOptions<T>(retryOptions: RetryOptions<T>)`: change the default retryOptions.
- `getDefaultRetryOptions<T>()`: returns the current default retry options.
- `retryAsyncDecorator<T>(fn: T, retryOptions?: RetryOptions<T>)` and `retryDecorator<T>(fn: T, retryOptions?: RetryOptions<T>)`: decorators that return a function with same signature than the given function. On decorated call, fn is called repeteadly it does not throw an exception or until retryOptions.maxTry.
- `TooManyTries`: an error thrown by retry functions when `until` returns false after `maxTry` calls. It comes with a type guard and includes the last failed result:
```javascript
if (isTooManyTries(error)) {
// retry failed
console.error(`last error is ${error.getLastResult()}`)
}
```
### When delay can vary
When delay option is a function, it is called before each retry: this allow to have a delay that can change between retires (ex: delay can increase exponentially).
The function receives the following parameters:
```javascript
(parameter: {
currentTry: number,
maxTry: number,
lastDelay?: number
lastResult?: RETURN_TYPE
}) => number;
```
where:
- `currentTry`: the number of call to fn (first is 1, not 0).
- `maxTry`: maximum calls to fn.
- `lastDelay`: the previous delay, undefined when no delay has been computed yet.
- `lastResult`: the last result, undefined is last call to fn failed
## Until family
`retry` comes with handy utilities function for common use case:
**UntilDefined :**
To retry until we get a value which is neither null nor undefined.
For calling sync function:
```typescript
retryUntilDefined<RETURN_TYPE>(
fn: () => RETURN_TYPE | undefined | null,
retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```
```typescript
retryUntilDefinedDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE | undefined | null,
retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```
For calling async function:
```typescript
retryAsyncUntilDefined<RETURN_TYPE>(
fn: () => Promise<RETURN_TYPE | undefined | null>,
options?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```
```typescript
retryAsyncUntilDefinedDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE | undefined | null>,
retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```
**UntilTruthy :**
To retry until we get a value which javascript consider as truthy.
For calling sync function:
```typescript
retryUntilTruthy<PARAMETERS_TYPE, RETURN_TYPE>(
fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE,
retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```
```typescript
retryUntilTruthyDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
fn: (...args: PARAMETERS_TYPE) => RETURN_TYPE,
retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```
For calling async function:
```typescript
retryAsyncUntilTruthy<PARAMETERS_TYPE, RETURN_TYPE>(
fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>,
retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```
```typescript
retryAsyncUntilTruthyDecorator<PARAMETERS_TYPE, RETURN_TYPE>(
fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>,
retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```
**UntilResponse :**
To retry until fetch is sucessfull.
```typescript
retryAsyncUntilResponse<PARAMETERS_TYPE, RETURN_TYPE extends { ok: boolean }>(
fn: () => Promise<RETURN_TYPE>,
retryOptions?: RetryUtilsOptions,
): Promise<RETURN_TYPE>
```
```typescript
retryAsyncUntilResponseDecorator<PARAMETERS_TYPE, RETURN_TYPE extends { ok: boolean }>(
fn: (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>,
retryOptions?: RetryUtilsOptions,
): (...args: PARAMETERS_TYPE) => Promise<RETURN_TYPE>
```
`RetryUtilsOptions` type is the same than `RetryUtilsOptions` but without `until` option.
## Delay family
**createExponetialDelay**
Returns a delay function that provide exponetial delais
```javascript
const delay = createExponetialDelay(20);
const result = await retryAsync(
async () => {
/* do something */
},
{ delay, maxTry: 5 }
);
```
delay between each try will return 20, 400, 8000, 160000, 3200000
**createMutiplicableDelay**
Returns a delay function that provide multiplicated delais:
```typescript
createMutiplicableDelay<RETURN_TYPE>(initialDelay: number, multiplicator: number)
```
First delay retunrs initialDelay, second initialDelay*multiplicator, third multiplicator initialDelay*(multiplicator\*2) and so on
```javascript
const delay = createMutiplicableDelay(20);
const result = await retryAsync(
async () => {
/* do something */
},
{ delay, maxTry: 5 }
);
```
delay will be 20, 60, 120, 180, 240
**createRandomDelay**
Returns a delay function that provide radom delais between given min and max (included):
```typescript
createRandomDelay<RETURN_TYPE>(min: number, max: number)
```
Each time the created delay is called, a value between min and max (both included) is generated
```javascript
const delay = createRandomDelay(500, 10000);
const result = await retryAsync(
async () => {
/* do something */
},
{ delay, maxTry: 5 }
);
```
delay betewwen each try will be a random value between 500 and 1000 ms.
## Wait family
- `wait(duration?)`: Do nothing during "duration" milliseconds
- `waitUntil(fn, duration?, error?)`: waitUntil call asynchronously fn once. If fn complete within the duration (express in milliseconds), waitUntil returns the fn result. Otherwise, it throws the given error (if any) or a TimeoutError exception.
- `waitUntilAsync(fn, duration?, error?)`: same as waitUntil, except fn is an asynchronous function.
- `TimeoutError`: an error thrown by waitUntil and waitUntilAsync. It comes with a isTimeoutError type guard:
```javascript
if (isTimeoutError(error)) {
// fn does not complete within 10 seconds
}
```
In case of timeout fn is still executing. It is advised to add a mean to abort it.
- When duration is not provided, the default one is applied. The default is 60000ms.
- `setDefaultDuration(duration: number)`: change the default duration.
- `getDefaultDuration()`: returns the current default duration.
- `waitUntilAsyncDecorator(fn: T, duration?: number, error?: Error)` and `waitUntilDecorator(fn: T, duration?: number, error?: Error)`: decorators that return a function with same signature than the given function. On decorated call, fn is called bounded to the duration.
## Custom reaction when max retry is achieved
Sometimes, you need to perform some actions when max retry has achieved and the error is still there. For this `onMaxRetryFunc?: (err: Error) => void;` optional function was added to `RetryOptions`.
For example, you would like to store results of the error into the file in order to process it later. Here's how you can do it :
```typescript
export const runWithRetry = <T>(
message: string,
serviceUnderTest: ServiceUnderTest,
fn: () => T | Promise<T>,
delay = 1000,
maxTry = 10
) => {
const saveErrorReport = (err) => {
const errorDetails = {
serviceName: serviceUnderTest.connectorName,
error: err.message as string,
description: `Failed to ${message} because of ${err.message as string}`,
errorName: err.name as string,
stack: err.stack as string,
};
const path = resolve(
__dirname,
`../../../failed-service-report/${serviceUnderTest.connectorName}.json`
);
writeFile(path, Buffer.from(JSON.stringify(errorDetails)));
};
return retryAsync(
async () => {
logger.info(`${serviceUnderTest.description}: ${message}`);
return fn();
},
{
delay,
maxTry,
onMaxRetryFunc: saveErrorReport,
}
);
};
```
## Compatibility
This lib works with Deno (to import it,use the url `https://raw.githubusercontent.com/franckLdx/ts-retry/<version>/src/index.ts`). However, it's more convenient to use the specific port of this lib to Deno: see `https://deno.land/x/retry`