Skip to content

Commit

Permalink
#16 add total delay information to checked function call (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
regevbr authored May 3, 2021
1 parent e3872f8 commit e2eda93
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 90 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Development
- nothing yet

## [v3.1.0](https://github.com/regevbr/busywait.js/compare/v3.0.0...v3.1.0)
### Added
- Added total delay information to the checked function call
### Fixed
- Stopped messing the global stop with the `__awaiter` helper function
- Fixed checkFn doc in readme

## [v3.0.0](https://github.com/regevbr/busywait.js/compare/v2.0.0...v3.0.0)
### Breaking changes
- Errors are now an instance of `Error` and not plain strings
Expand Down
117 changes: 69 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,25 @@
[![devDependencies Status](https://david-dm.org/regevbr/busywait.js/dev-status.svg)](https://david-dm.org/regevbr/busywait.js?type=dev)

# busywait.js

Simple Async busy wait module for Node.JS

## Main features
- Simple api to busy wait for a desired outcome
- Exponential backoff (with optional full jitter) support
- Slim library (single file, 100 lines of code, no dependencies)
- Full typescript support

- Simple api to busy wait for a desired outcome
- Exponential backoff (with optional full jitter) support
- Slim library (single file, 100 lines of code, no dependencies)
- Full typescript support

## Quick example

```typescript
import { busywait } from 'busywait';
import {busywait} from 'busywait';

const waitUntil = Date.now() + 2500;

const checkFn = async (iteration: number, delay: number): Promise<string> => {
console.log(`Running iteration ${iteration} after delay of ${delay}ms`);
const checkFn = async (iteration: number, delay: number, totalDelay: number): Promise<string> => {
console.log(`Running iteration ${iteration} after delay of ${delay}ms and total delay of ${totalDelay}`);
if (Date.now() > waitUntil) {
return `success`;
}
Expand All @@ -39,25 +42,26 @@ const checkFn = async (iteration: number, delay: number): Promise<string> => {
```

Will result in:

``` bash
Running iteration 1 after delay of 0ms
Running iteration 2 after delay of 500ms
Running iteration 3 after delay of 500ms
Running iteration 4 after delay of 500ms
Running iteration 5 after delay of 500ms
Running iteration 6 after delay of 500ms
Running iteration 1 after delay of 0ms and total delay of 1
Running iteration 2 after delay of 500ms and total delay of 504
Running iteration 3 after delay of 500ms and total delay of 1007
Running iteration 4 after delay of 500ms and total delay of 1508
Running iteration 5 after delay of 500ms and total delay of 2010
Running iteration 6 after delay of 500ms and total delay of 2511
Finished after 2511ms (6 iterations) with result success
```

### Exponential backoff

```typescript
import { busywait } from 'busywait';
import {busywait} from 'busywait';

const waitUntil = Date.now() + 2500;

const checkFn = async (iteration: number, delay: number): Promise<string> => {
console.log(`Running iteration ${iteration} after delay of ${delay}ms`);
const checkFn = async (iteration: number, delay: number, totalDelay: number): Promise<string> => {
console.log(`Running iteration ${iteration} after delay of ${delay}ms and total delay of ${totalDelay}`);
if (Date.now() > waitUntil) {
return `success`;
}
Expand All @@ -75,25 +79,26 @@ const checkFn = async (iteration: number, delay: number): Promise<string> => {
```

Will result in:

``` bash
Running iteration 1 after delay of 0ms
Running iteration 2 after delay of 100ms
Running iteration 3 after delay of 200ms
Running iteration 4 after delay of 400ms
Running iteration 5 after delay of 800ms
Running iteration 6 after delay of 1600ms
Finished after 3111ms (6 iterations) with result success
Running iteration 1 after delay of 0ms and total delay of 1
Running iteration 2 after delay of 100ms and total delay of 104
Running iteration 3 after delay of 200ms and total delay of 306
Running iteration 4 after delay of 400ms and total delay of 707
Running iteration 5 after delay of 800ms and total delay of 1509
Running iteration 6 after delay of 1600ms and total delay of 3110
Finished after 3110ms (6 iterations) with result success
```

### Exponential backoff with full jitter

```typescript
import { busywait } from 'busywait';
import {busywait} from 'busywait';

const waitUntil = Date.now() + 2500;

const checkFn = async (iteration: number, delay: number): Promise<string> => {
console.log(`Running iteration ${iteration} after delay of ${delay}ms`);
const checkFn = async (iteration: number, delay: number, totalDelay: number): Promise<string> => {
console.log(`Running iteration ${iteration} after delay of ${delay}ms and total delay of ${totalDelay}`);
if (Date.now() > waitUntil) {
return `success`;
}
Expand All @@ -112,17 +117,19 @@ const checkFn = async (iteration: number, delay: number): Promise<string> => {
```

Will result in:

``` bash
Running iteration 1 after delay of 78ms
Running iteration 2 after delay of 154ms
Running iteration 3 after delay of 228ms
Running iteration 4 after delay of 605ms
Running iteration 5 after delay of 136ms
Running iteration 6 after delay of 1652ms
Finished after 2863ms (6 iterations) with result success
Running iteration 1 after delay of 31ms and total delay of 31
Running iteration 2 after delay of 165ms and total delay of 199
Running iteration 3 after delay of 217ms and total delay of 417
Running iteration 4 after delay of 378ms and total delay of 796
Running iteration 5 after delay of 1397ms and total delay of 2195
Running iteration 6 after delay of 1656ms and total delay of 3853
Finished after 3853ms (6 iterations) with result success
```

## Install

```bash
npm install busywait
```
Expand All @@ -132,35 +139,49 @@ npm install busywait
#### checkFn

A function that takes a single optional argument, which is the current iteration number.
The function can either:
- return a non promised value (in which case, a failed check should throw an error)
- return promised value (in which case, a failed check should return a rejected promise)

##### Args

- `iteration` - The current iteration number (starting from 1)
- `delay` - The last delay (in ms) that was applied
- `totalDelay` - The total delay (in ms) applied so far

##### Return

Either:

- a non promised value (in which case, a failed check should throw an error)
- a promised value (in which case, a failed check should return a rejected promise)

#### options

##### mandatory

- `sleepTime` - Time in ms to wait between checks. In the exponential mode, will be the base sleep time.
- `sleepTime` - Time in ms to wait between checks. In the exponential mode, will be the base sleep time.

##### optional

- `multiplier` - The exponential multiplier. Set to 2 or higher to achieve exponential backoff (default: 1 - i.e. linear backoff)
- `maxDelay` - The max delay value between checks in ms (default: infinity)
- `maxChecks` - The max number of checks to perform before failing (default: infinity)
- `waitFirst` - Should we wait the `sleepTime` before performing the first check (default: false)
- `jitter` - ('none' | 'full') The [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) mode to use (default: none)
- `failMsg` - Custom error message to reject the promise with
- `multiplier` - The exponential multiplier. Set to 2 or higher to achieve exponential backoff (default: 1 - i.e. linear
backoff)
- `maxDelay` - The max delay value between checks in ms (default: infinity)
- `maxChecks` - The max number of checks to perform before failing (default: infinity)
- `waitFirst` - Should we wait the `sleepTime` before performing the first check (default: false)
- `jitter` - ('none' | 'full') The [jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)
mode to use (default: none)
- `failMsg` - Custom error message to reject the promise with

### Return value

Return value is a promise.
- The promise will be resolved if the `checkFn` was resolved within a legal number of checks.
- The promise will be rejected if the `checkFn` rejected (or threw an error) `maxChecks` times.

- The promise will be resolved if the `checkFn` was resolved within a legal number of checks.
- The promise will be rejected if the `checkFn` rejected (or threw an error) `maxChecks` times.

Promise resolved value:
- `backoff.iterations` - The number of iterations it took to finish
- `backoff.time` - The number of time it took to finish
- `result` - The resolved value of `checkFn`

- `backoff.iterations` - The number of iterations it took to finish
- `backoff.time` - The number of time it took to finish
- `result` - The resolved value of `checkFn`

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "busywait",
"version": "3.0.0",
"version": "3.1.0",
"description": "Async busy wait",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
108 changes: 69 additions & 39 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* istanbul ignore next */
// @ts-ignore
global.__awaiter = (this && this.__awaiter) || ((thisArg, _arguments, P, generator) => {
// tslint:disable-next-line:variable-name
const __awaiter = (thisArg, _arguments, P, generator) => {
function adopt(value: any) {
return value instanceof P ? value : new P((resolve: any) => {
resolve(value);
Expand Down Expand Up @@ -30,10 +31,11 @@ global.__awaiter = (this && this.__awaiter) || ((thisArg, _arguments, P, generat

step((generator = generator.apply(thisArg, _arguments || [])).next());
});
});
};

export type SyncCheckFn<T> = T extends Promise<any> ? never : (iteration: number, delay: number) => T;
export type ASyncCheckFn<T> = (iteration: number, delay: number) => Promise<T>;
type _CheckFn<T> = (iteration: number, delay: number, totalDelay: number) => T;
export type SyncCheckFn<T> = T extends Promise<any> ? never : _CheckFn<T>;
export type ASyncCheckFn<T> = _CheckFn<Promise<T>>;
export type CheckFn<T> = ASyncCheckFn<T> | SyncCheckFn<T>;

export type Jitter = 'none' | 'full';
Expand Down Expand Up @@ -61,8 +63,23 @@ type Reject = (error: Error) => void;
type Delayer = (iteration: number) => number;

const isFunction = (value: any): value is () => any => toString.call(value) === '[object Function]';

const isNumber = (value: any): value is number => typeof value === 'number';

const unwrapPromise = <T>() => {
let resolve: Resolve<T>;
let reject: Reject;
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-expect-error
return {promise, resolve, reject};
}

const wrapSyncMethod = <T>(checkFn: CheckFn<T>): ASyncCheckFn<T> =>
async (iteration: number, delay: number, totalDelay: number) => checkFn(iteration, delay, totalDelay);

const getAndValidateOptions = <T>(checkFn: CheckFn<T>, _options: IBusyWaitOptions): IBusyWaitOptions => {
const options = Object.assign({}, _options);
if (isNumber(options.maxChecks) && (isNaN(options.maxChecks) || options.maxChecks < 1)) {
Expand All @@ -85,20 +102,6 @@ const getAndValidateOptions = <T>(checkFn: CheckFn<T>, _options: IBusyWaitOption
return options;
};

const wrapSyncMethod = <T>(checkFn: CheckFn<T>): ASyncCheckFn<T> =>
async (iteration: number, delay: number) => checkFn(iteration, delay);

const unwrapPromise = <T>() => {
let resolve: Resolve<T>;
let reject: Reject;
const promise = new Promise<T>((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
// @ts-expect-error
return {promise, resolve, reject};
}

const getDelayer = (options: IBusyWaitOptions): Delayer => (iteration: number) => {
let delay = options.sleepTime * Math.pow(options.multiplier || 1, iteration);
delay = Math.min(delay, options.maxDelay || Infinity);
Expand All @@ -108,35 +111,62 @@ const getDelayer = (options: IBusyWaitOptions): Delayer => (iteration: number) =
return delay;
}

export const busywait = async <T>(_checkFn: CheckFn<T>, _options: IBusyWaitOptions): Promise<IBusyWaitResult<T>> => {
const options = getAndValidateOptions(_checkFn, _options);
interface IIterationState<T> {
iteration: number;
delayerIteration: number;
delay: number;
readonly startTime: number;
resolve: Resolve<IBusyWaitResult<T>>;
reject: Reject;
promise: Promise<IBusyWaitResult<T>>;
options: IBusyWaitOptions;
delayer: Delayer;
checkFn: ASyncCheckFn<T>;
}

const buildIterationState = <T>(checkFn: CheckFn<T>, _options: IBusyWaitOptions): IIterationState<T> => {
const options = getAndValidateOptions(checkFn, _options);
const delayer = getDelayer(options);
const checkFn = wrapSyncMethod(_checkFn);
let iteration = 0;
let delayerIteration = options.waitFirst ? 0 : -1;
const start = Date.now();
let delay = options.waitFirst ? delayer(delayerIteration) : 0;
const {promise, resolve, reject} = unwrapPromise<IBusyWaitResult<T>>();
const iterationCheck = async () => {
iteration++;
delayerIteration++;
const delayerIteration = options.waitFirst ? 0 : -1;
return {
iteration: 0,
delayerIteration,
startTime: Date.now(),
delay: options.waitFirst ? delayer(delayerIteration) : 0,
options,
delayer,
checkFn: wrapSyncMethod(checkFn),
...unwrapPromise<IBusyWaitResult<T>>(),
}
}

const iterationCheck = <T>(state: IIterationState<T>) => {
const iterationRun = async () => {
state.iteration++;
state.delayerIteration++;
try {
const result = await checkFn(iteration, delay);
return resolve({
const totalDelay = Date.now() - state.startTime;
const result = await state.checkFn(state.iteration, state.delay, totalDelay);
return state.resolve({
backoff: {
iterations: iteration,
time: Date.now() - start,
iterations: state.iteration,
time: totalDelay,
},
result,
});
} catch (e) {
if (options.maxChecks && iteration === options.maxChecks) {
return reject(new Error(options.failMsg || 'Exceeded number of iterations to wait'));
if (state.options.maxChecks && state.iteration === state.options.maxChecks) {
return state.reject(new Error(state.options.failMsg || 'Exceeded number of iterations to wait'));
}
delay = delayer(delayerIteration);
setTimeout(iterationCheck, delay);
state.delay = state.delayer(state.delayerIteration);
setTimeout(iterationRun, state.delay);
}
};
setTimeout(iterationCheck, delay);
return promise;
setTimeout(iterationRun, state.delay);
return state.promise;
}

export const busywait = async <T>(checkFn: CheckFn<T>, options: IBusyWaitOptions): Promise<IBusyWaitResult<T>> => {
const iterationState = buildIterationState(checkFn, options)
return iterationCheck(iterationState);
};
Loading

0 comments on commit e2eda93

Please sign in to comment.