From e2eda939fe6afb6ff9dbc61562e42429854dac40 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Mon, 3 May 2021 11:29:03 +0300 Subject: [PATCH] #16 add total delay information to checked function call (#17) --- CHANGELOG.md | 7 +++ README.md | 117 ++++++++++++++++++++++--------------- package.json | 2 +- src/index.ts | 108 +++++++++++++++++++++------------- src/test/src/index.spec.ts | 19 +++++- 5 files changed, 163 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9998370..ca4cdcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 44f7a32..7eea1cb 100644 --- a/README.md +++ b/README.md @@ -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 => { - console.log(`Running iteration ${iteration} after delay of ${delay}ms`); +const checkFn = async (iteration: number, delay: number, totalDelay: number): Promise => { + console.log(`Running iteration ${iteration} after delay of ${delay}ms and total delay of ${totalDelay}`); if (Date.now() > waitUntil) { return `success`; } @@ -39,25 +42,26 @@ const checkFn = async (iteration: number, delay: number): Promise => { ``` 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 => { - console.log(`Running iteration ${iteration} after delay of ${delay}ms`); +const checkFn = async (iteration: number, delay: number, totalDelay: number): Promise => { + console.log(`Running iteration ${iteration} after delay of ${delay}ms and total delay of ${totalDelay}`); if (Date.now() > waitUntil) { return `success`; } @@ -75,25 +79,26 @@ const checkFn = async (iteration: number, delay: number): Promise => { ``` 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 => { - console.log(`Running iteration ${iteration} after delay of ${delay}ms`); +const checkFn = async (iteration: number, delay: number, totalDelay: number): Promise => { + console.log(`Running iteration ${iteration} after delay of ${delay}ms and total delay of ${totalDelay}`); if (Date.now() > waitUntil) { return `success`; } @@ -112,17 +117,19 @@ const checkFn = async (iteration: number, delay: number): Promise => { ``` 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 ``` @@ -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 diff --git a/package.json b/package.json index 8591110..2a3eacb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 8d666bf..ba051ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); @@ -30,10 +31,11 @@ global.__awaiter = (this && this.__awaiter) || ((thisArg, _arguments, P, generat step((generator = generator.apply(thisArg, _arguments || [])).next()); }); -}); +}; -export type SyncCheckFn = T extends Promise ? never : (iteration: number, delay: number) => T; -export type ASyncCheckFn = (iteration: number, delay: number) => Promise; +type _CheckFn = (iteration: number, delay: number, totalDelay: number) => T; +export type SyncCheckFn = T extends Promise ? never : _CheckFn; +export type ASyncCheckFn = _CheckFn>; export type CheckFn = ASyncCheckFn | SyncCheckFn; export type Jitter = 'none' | 'full'; @@ -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 = () => { + let resolve: Resolve; + let reject: Reject; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + // @ts-expect-error + return {promise, resolve, reject}; +} + +const wrapSyncMethod = (checkFn: CheckFn): ASyncCheckFn => + async (iteration: number, delay: number, totalDelay: number) => checkFn(iteration, delay, totalDelay); + const getAndValidateOptions = (checkFn: CheckFn, _options: IBusyWaitOptions): IBusyWaitOptions => { const options = Object.assign({}, _options); if (isNumber(options.maxChecks) && (isNaN(options.maxChecks) || options.maxChecks < 1)) { @@ -85,20 +102,6 @@ const getAndValidateOptions = (checkFn: CheckFn, _options: IBusyWaitOption return options; }; -const wrapSyncMethod = (checkFn: CheckFn): ASyncCheckFn => - async (iteration: number, delay: number) => checkFn(iteration, delay); - -const unwrapPromise = () => { - let resolve: Resolve; - let reject: Reject; - const promise = new Promise((_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); @@ -108,35 +111,62 @@ const getDelayer = (options: IBusyWaitOptions): Delayer => (iteration: number) = return delay; } -export const busywait = async (_checkFn: CheckFn, _options: IBusyWaitOptions): Promise> => { - const options = getAndValidateOptions(_checkFn, _options); +interface IIterationState { + iteration: number; + delayerIteration: number; + delay: number; + readonly startTime: number; + resolve: Resolve>; + reject: Reject; + promise: Promise>; + options: IBusyWaitOptions; + delayer: Delayer; + checkFn: ASyncCheckFn; +} + +const buildIterationState = (checkFn: CheckFn, _options: IBusyWaitOptions): IIterationState => { + 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>(); - 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>(), + } +} + +const iterationCheck = (state: IIterationState) => { + 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 (checkFn: CheckFn, options: IBusyWaitOptions): Promise> => { + const iterationState = buildIterationState(checkFn, options) + return iterationCheck(iterationState); }; diff --git a/src/test/src/index.spec.ts b/src/test/src/index.spec.ts index 5aa523d..bf25d46 100644 --- a/src/test/src/index.spec.ts +++ b/src/test/src/index.spec.ts @@ -18,49 +18,64 @@ describe('busywait.js', function() { let waitUntil: number; let iterationsArray: number[]; let delaysArray: number[]; + let totalDelaysArray: number[]; beforeEach(() => { iterationsArray = []; delaysArray = []; + totalDelaysArray = []; waitUntil = Date.now() + waitTime; }); const checkIterationsArray = (iterations: number, delay: number, startDelay: boolean) => { iterationsArray.length.should.equal(iterations); delaysArray.length.should.equal(iterations); + totalDelaysArray.length.should.equal(iterations); + let totalDelay = 0; for (let i = 0; i < iterations; i++) { iterationsArray[i].should.equal(i + 1); delaysArray[i].should.equal((!startDelay && i === 0) ? 0 : delay); + totalDelay += delaysArray[i]; + totalDelaysArray[i].should.be.greaterThanOrEqual(totalDelay - 10); } }; const checkIterationsAndDelaysArray = (iterations: number, delays: number[]) => { iterationsArray.length.should.equal(iterations); delaysArray.length.should.equal(iterations); + totalDelaysArray.length.should.equal(iterations); + let totalDelay = 0; for (let i = 0; i < iterations; i++) { iterationsArray[i].should.equal(i + 1); delaysArray[i].should.equal(delays[i]); + totalDelay += delaysArray[i]; + totalDelaysArray[i].should.be.greaterThanOrEqual(totalDelay - 10); } }; const checkJitterDelaysArray = (delays: number[]) => { + let totalDelay = 0; for (let i = 0; i < iterationsArray.length; i++) { delaysArray[i].should.be.lessThan(delays[i] + 1); + totalDelay += delaysArray[i]; + totalDelaysArray[i].should.be.greaterThanOrEqual(totalDelay - 10); } }; - const syncCheck = (iteration: number, delay: number): string => { + const syncCheck = (iteration: number, delay: number, totalDelay: number): string => { iterationsArray.push(iteration); delaysArray.push(delay); + totalDelaysArray.push(totalDelay); if (Date.now() > waitUntil) { return successMessage; } throw new Error('not the time yet'); }; - const asyncCheck = (iteration: number, delay: number): Promise => { + const asyncCheck = (iteration: number, delay: number, totalDelay: number): Promise => { iterationsArray.push(iteration); delaysArray.push(delay); + totalDelaysArray.push(totalDelay); return new Promise((resolve, reject) => { if (Date.now() > waitUntil) { return resolve(successMessage);