import { AbortError } from './abort'; const ERRORS = { NOT_FUNCTION: 'Your executor is not a function. functions and promises are valid.', FAILED_TO_WAIT: 'Failed to wait', }; function promisify(fn: any) { return async () => { const result = await fn(); return result; }; } export type IExecuteFunction = any; function validateExecution(executeFn: IExecuteFunction) { if (typeof executeFn !== 'function') { throw new Error(ERRORS.NOT_FUNCTION); } } export interface IWaitForOptions { timeout?: number interval?: number message?: string stopOnFailure?: boolean backoffFactor?: number backoffMaxInterval?: number verbose?: boolean maxAttempts?: number } export class PollUntil { _interval: number; _timeout: number; _executedAttempts: number; private _stopOnFailure: boolean; private readonly _backoffFactor: number; private readonly _backoffMaxInterval: number; private readonly _Console: Console; private readonly originalStacktraceError: Error; private readonly _userMessage: string; private readonly _verbose: boolean; private readonly _maxAttempts: number | undefined; private _isWaiting: boolean; private _isResolved: boolean; private _executeFn: IExecuteFunction; private start: number; private promise: Promise | undefined; private resolve: ((value: any) => void) | undefined; private reject: ((reason?: any) => void) | undefined; private _lastError: Error | undefined; constructor({ interval = 100, timeout = 1000, stopOnFailure = false, verbose = false, backoffFactor = 1, backoffMaxInterval, message = '', maxAttempts, }:IWaitForOptions = {}) { this._interval = interval; this._timeout = timeout; this._executedAttempts = 0; this._stopOnFailure = stopOnFailure; this._isWaiting = false; this._isResolved = false; this._verbose = verbose; this._userMessage = message; this.originalStacktraceError = new Error(); this._Console = console; this._backoffFactor = backoffFactor; this._backoffMaxInterval = backoffMaxInterval || timeout; this._maxAttempts = maxAttempts; this.start = +Date.now(); } tryEvery(interval: number): PollUntil { this._interval = interval; return this; } stopAfter(timeout: number): PollUntil { this._timeout = timeout; return this; } execute(executeFn: IExecuteFunction): Promise { this._applyPromiseHandlers(); validateExecution(executeFn); this._executeFn = promisify(executeFn); this.start = Date.now(); this._isWaiting = true; this._log('starting to execute'); this._runFunction(); return this.promise!; } getPromise(): Promise { return this.promise!; } isResolved() { return this._isResolved; } isWaiting() { return this._isWaiting; } stopOnFailure(stop: boolean): PollUntil { this._stopOnFailure = stop; return this; } _applyPromiseHandlers() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } _timeFromStart() { return Date.now() - this.start; } _shouldStopTrying() { return this._timeFromStart() > this._timeout || this._attemptsExhausted(); } _attemptsExhausted() { return this._maxAttempts !== undefined && this._executedAttempts >= this._maxAttempts; } _executeAgain() { this._log('executing again'); const currentInterval = this._interval; const nextInterval = currentInterval * this._backoffFactor; this._interval = (nextInterval > this._backoffMaxInterval) ? this._backoffMaxInterval : nextInterval; this._executedAttempts += 1; setTimeout(this._runFunction.bind(this), currentInterval); } _failedToWait() { const timeFromStartStr = `${this._timeFromStart()}ms`; let waitErrorText = this._attemptsExhausted() ? `Operation unsuccessful after ${this._executedAttempts} attempts (total of ${timeFromStartStr})` : `${ERRORS.FAILED_TO_WAIT} after ${timeFromStartStr} (total of ${this._executedAttempts} attempts)`; if (this._userMessage) waitErrorText = `${waitErrorText}: ${this._userMessage}`; if (this._lastError) { this._lastError.message = `${waitErrorText}\n${this._lastError.message}`; const originalStack = this.originalStacktraceError.stack; if (originalStack) { this._lastError.stack += originalStack.substring(originalStack.indexOf('\n') + 1); } } else { this._lastError = this.originalStacktraceError; this._lastError.message = waitErrorText; } this._log(this._lastError); return this._lastError; } _runFunction() { if (this._shouldStopTrying()) { this._isWaiting = false; this.reject?.(this._failedToWait()); return; } this._executeFn() .then((result: any) => { if (result === false) { this._log(`then execute again with result: ${result}`); this._executeAgain(); return; } this.resolve?.(result); this._isWaiting = false; this._isResolved = true; this._log(`then done waiting with result: ${result}`); }) .catch((err: Error) => { if (err instanceof AbortError) { this._log(`aborted with err: ${err.cause}`); return this.reject?.(err.cause); } if (this._stopOnFailure) { this._log(`stopped on failure with err: ${err}`); return this.reject?.(err); } this._lastError = err; this._log(`catch with err: ${err}`); return this._executeAgain(); }); } _log(message: Error | string) { if (this._verbose && this._Console && this._Console.log) this._Console.log(message); } } export const waitFor = (waitForFunction:IExecuteFunction, options?: IWaitForOptions) => new PollUntil(options).execute(waitForFunction);