export class Backoff { // For testing purposes, assign to overwrite the provided value by param static __TEST__BASE_MILLIS?: number; static __TEST__MAX_MILLIS?: number; static DEFAULT_BASE_MILLIS = 1000; // 1 second static DEFAULT_MAX_MILLIS = 1800000; // 30 minutes baseMillis: number; maxMillis: number; attempts: number; cb: (...args: any[]) => any; timeoutID: ReturnType | undefined; /** * Schedule function calls with exponential backoff */ constructor(cb: (...args: any[]) => any, baseMillis?: number, maxMillis?: number) { this.baseMillis = Backoff.__TEST__BASE_MILLIS || baseMillis || Backoff.DEFAULT_BASE_MILLIS; this.maxMillis = Backoff.__TEST__MAX_MILLIS || maxMillis || Backoff.DEFAULT_MAX_MILLIS; this.attempts = 0; this.cb = cb; } /** * Schedule a next call to `cb` * @returns scheduled delay in milliseconds */ scheduleCall() { let delayInMillis = Math.min(this.baseMillis * Math.pow(2, this.attempts), this.maxMillis); if (this.timeoutID) clearTimeout(this.timeoutID); this.timeoutID = setTimeout(() => { this.timeoutID = undefined; this.cb(); }, delayInMillis); this.attempts++; return delayInMillis; } /** * Schedule a delayed call to `cb` * @returns a promise that resolves/rejects with the result of the `cb` function, which must return a promise. */ scheduleCallAsync(): Promise { const delayInMillis = Math.min(this.baseMillis * Math.pow(2, this.attempts), this.maxMillis); if (this.timeoutID) clearTimeout(this.timeoutID); this.attempts++; return new Promise((resolve, reject) => { this.timeoutID = setTimeout(() => { this.timeoutID = undefined; this.cb().then(resolve, reject); }, delayInMillis); }); } /** * Reset the backoff attempts */ reset() { this.attempts = 0; if (this.timeoutID) { clearTimeout(this.timeoutID); this.timeoutID = undefined; } } }