type PromiseFunc = (...args: any[]) => Promise; type Params = { func: Func; delay: number; maxTries: number; shouldJitter: boolean; signal?: AbortSignal | undefined; shouldRetry: (e: Error) => boolean; }; export class RequestRetry { private func: Func; private delay: Params["delay"]; private maxTries: Params["maxTries"]; private shouldRetry: Params["shouldRetry"]; private shouldJitter: boolean; private signal?: AbortSignal | undefined; private tryCount = 0; constructor({ func, signal, delay = 1000, maxTries = 5, shouldJitter = true, shouldRetry = () => true, }: Params) { this.func = func; this.delay = delay; this.maxTries = maxTries; this.shouldJitter = shouldJitter; this.shouldRetry = shouldRetry; this.signal = signal; } private getDelayWithJitter(): number { const exponentialDelay = this.delay * 1.5; if (!this.shouldJitter) return exponentialDelay; // Apply jitter: ±30% const jitterPercentage = 0.3; const jitter = (Math.random() * 2 - 1) * jitterPercentage * exponentialDelay; // range: [-30%, +30%] const finalDelay = exponentialDelay + jitter; return finalDelay; } private sleep(time: number) { return new Promise((resolve, reject) => { const timeout = setTimeout(resolve, time); if (this.signal) { this.signal.addEventListener( "abort", () => { clearTimeout(timeout); reject(new Error("❌ Aborted")); }, { once: true } ); } }); } private async retry( e: Error, args: Parameters ): Promise>> { if (this.signal?.aborted) throw new Error("❌ Aborted"); const shouldTry = await this.shouldRetry(e); if (!shouldTry || this.maxTries < this.tryCount) { throw e; } try { this.tryCount += 1; await this.sleep(this.delay); const result = await this.func(...args); return result; } catch (error: unknown) { this.delay = Math.round(this.getDelayWithJitter()); return await this.retry(error as Error, args); } } async run(...args: Parameters): Promise>> { try { const res = await this.func(...args); return res; } catch (e: unknown) { return await this.retry(e as Error, args); } } } function flakyFetch() { return new Promise((resolve, reject) => { const success = Math.random() > 0.7; // 30% success rate setTimeout(() => { if (success) { resolve("✅ Success on attempt!"); } else { reject("❌ Random failure"); } }, 200); }); } const controller = new AbortController() const retry = new RequestRetry({ func: flakyFetch, delay: 500, maxTries: 15, signal: controller.signal, shouldRetry: (e) => { console.log("Retry condition evaluated for:", e); return true; } , }); retry .run("this is sparta!") .then( (result) => console.log("Final result:", result)) .catch( (error) => console.error("Final failure:", error)); setTimeout( () => { controller.abort() }, 1000)