From 16096b135db5ebc57671b2091e584ff4c222d378 Mon Sep 17 00:00:00 2001 From: Igor Savin Date: Thu, 10 Aug 2023 16:43:46 +0300 Subject: [PATCH] Implement waitAndRetry (#67) --- README.md | 19 ++++++++++ index.ts | 2 ++ src/utils/waitUtils.spec.ts | 70 +++++++++++++++++++++++++++++++++++++ src/utils/waitUtils.ts | 34 ++++++++++++++++++ 4 files changed, 125 insertions(+) create mode 100644 src/utils/waitUtils.spec.ts create mode 100644 src/utils/waitUtils.ts diff --git a/README.md b/README.md index e1defa6..80a81ad 100644 --- a/README.md +++ b/README.md @@ -201,3 +201,22 @@ It's up to the caller of the function to handle the received error or throw an e Read [this article](https://antman-does-software.com/stop-catching-errors-in-typescript-use-the-either-type-to-make-your-code-predictable) for more information on how `Either` works and its benefits. Additionally, `DefineEither` is also provided. It is a variation of the aforementioned `Either`, which may or may not have `error` set, but always has `result`. + +### waitAndRetry + +There is helper function available for writing event-driven assertions in automated tests, which rely on something eventually happening: + +```ts +import {waitAndRetry} from "@lokalise/node-core"; + +const result = await waitAndRetry( + () => { + return someEventEmitter.emittedEvents.length > 0 + }, + 20, // sleepTime between attempts + 30, // maxRetryCount before timeout +) + +expect(result).toBe(false) // resolves to what the last attempt has returned +expect(someEventEmitter.emittedEvents.length).toBe(1) +``` diff --git a/index.ts b/index.ts index eff9170..19d26dd 100644 --- a/index.ts +++ b/index.ts @@ -64,3 +64,5 @@ export { } from './src/errors/globalErrorHandler' export type { MayOmit } from './src/common/may-omit' + +export { waitAndRetry } from './src/utils/waitUtils' diff --git a/src/utils/waitUtils.spec.ts b/src/utils/waitUtils.spec.ts new file mode 100644 index 0000000..d61052a --- /dev/null +++ b/src/utils/waitUtils.spec.ts @@ -0,0 +1,70 @@ +import { describe, expect } from 'vitest' + +import { waitAndRetry } from './waitUtils' + +class Counter { + private readonly timeOfSucces: number + public executionCounter: number + constructor(msecsTillSuccess: number) { + this.timeOfSucces = Date.now() + msecsTillSuccess + this.executionCounter = 0 + } + + process() { + this.executionCounter++ + return Date.now() >= this.timeOfSucces + } +} + +describe('waitUtils', () => { + describe('waitAndRetry', () => { + it('executes once if there is an instant condition match', async () => { + const counter = new Counter(0) + + const result = await waitAndRetry(() => { + return counter.process() + }) + + expect(result).toBe(true) + expect(counter.executionCounter).toBe(1) + }) + + it('executes until there is a condition match', async () => { + const counter = new Counter(1000) + + const result = await waitAndRetry( + () => { + return counter.process() + }, + 50, + 30, + ) + + expect(result).toBe(true) + expect(counter.executionCounter > 0).toBe(true) + }) + + it('times out of there is never a condition match', async () => { + const counter = new Counter(1000) + + const result = await waitAndRetry( + () => { + return counter.process() + }, + 20, + 30, + ) + + expect(result).toBe(false) + expect(counter.executionCounter > 0).toBe(true) + }) + + it('handles an error', async () => { + await expect( + waitAndRetry(() => { + throw new Error('it broke') + }), + ).rejects.toThrowError('it broke') + }) + }) +}) diff --git a/src/utils/waitUtils.ts b/src/utils/waitUtils.ts new file mode 100644 index 0000000..c21c1d6 --- /dev/null +++ b/src/utils/waitUtils.ts @@ -0,0 +1,34 @@ +export const waitAndRetry = async ( + predicateFn: () => T, + sleepTime = 20, + maxRetryCount = 15, +): Promise => { + return new Promise((resolve, reject) => { + let retryCount = 0 + function performCheck() { + // amount of retries exceeded + if (maxRetryCount !== 0 && retryCount > maxRetryCount) { + resolve(predicateFn()) + } + + // Try executing predicateFn + Promise.resolve() + .then(() => { + return predicateFn() + }) + .then((result) => { + if (result) { + resolve(result) + } else { + retryCount++ + setTimeout(performCheck, sleepTime) + } + }) + .catch((err) => { + reject(err) + }) + } + + performCheck() + }) +}