Created
June 1, 2025 11:20
-
-
Save iiison/5e915c82315e8133a838f4d534838f4e to your computer and use it in GitHub Desktop.
A RequestRetry class in JS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type PromiseFunc = (...args: any[]) => Promise<any>; | |
| type Params<Func extends PromiseFunc> = { | |
| func: Func; | |
| delay: number; | |
| maxTries: number; | |
| shouldJitter: boolean; | |
| signal?: AbortSignal | undefined; | |
| shouldRetry: (e: Error) => boolean; | |
| }; | |
| export class RequestRetry<Func extends PromiseFunc> { | |
| private func: Func; | |
| private delay: Params<Func>["delay"]; | |
| private maxTries: Params<Func>["maxTries"]; | |
| private shouldRetry: Params<Func>["shouldRetry"]; | |
| private shouldJitter: boolean; | |
| private signal?: AbortSignal | undefined; | |
| private tryCount = 0; | |
| constructor({ | |
| func, | |
| signal, | |
| delay = 1000, | |
| maxTries = 5, | |
| shouldJitter = true, | |
| shouldRetry = () => true, | |
| }: Params<Func>) { | |
| 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<Func> | |
| ): Promise<Awaited<ReturnType<Func>>> { | |
| 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<Func>): Promise<Awaited<ReturnType<Func>>> { | |
| 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment