1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
| import { useEffect, useMemo } from "react";
type AsyncFunc<T, U> = (...args: T[]) => Promise<U>;
type FunctionStatus = 'init' | 'running' | 'stoped' | 'success' | 'fail';
type FunctionStatusChangeCallback<R> = (status: FunctionStatus, result?: R) => void;
interface RetryableFunctionParams<R> { count?: number; timeout?: number; retryDuration?: number; errorConsumeCount?: boolean; callback?: FunctionStatusChangeCallback<R> }
class Retryable<T, U> { private fn: AsyncFunc<T, U>; private maxRetries: number; private retries: number; private errorConsumeCount: boolean; private startTime: number; private timeout: number; private args: T[]; private retryDuration: number; private planToStop: boolean; private resolve: ((value: U | PromiseLike<U>) => void) | null; private reject: ((reason?: any) => void) | null; private status: FunctionStatus; private callback?: FunctionStatusChangeCallback<U>;
constructor(fn: AsyncFunc<T, U>, params: RetryableFunctionParams<U>) { this.fn = fn; this.callback = params.callback; this.maxRetries = params.count ?? 3; this.retries = params.count ?? 3; this.retryDuration = params.retryDuration ?? 500; this.errorConsumeCount = params.errorConsumeCount; this.startTime = new Date().getTime(); this.timeout = params.timeout ?? 1000; this.args = []; this.planToStop = false; this.resolve = null; this.reject = null; this.changeStatus('init'); }
getStatus = () => { return this.status; };
stop = (): void => { this.planToStop = true; if (this.status !== 'running') { this.changeStatus('stoped'); } };
continueRun = (resetTimeout?: boolean): boolean => { if (resetTimeout) { this.startTime = new Date().getTime(); } if (this.canRetry() && this.status === 'success') { setTimeout(() => { this.execute(); }, this.retryDuration); return true; } return false; };
restart = (...args: T[]): Promise<U> => { if (this.planToStop) return; this.retries = this.maxRetries; this.startTime = new Date().getTime(); this.args = args; return new Promise<U>((resolve, reject) => { this.resolve = resolve; this.reject = reject; this.execute(); }); };
resetFunc = (fn: AsyncFunc<T, U>): void => { this.fn = fn; };
private canRetry() { return !this.planToStop && this.retries > 0 && (new Date().getTime() - this.startTime < this.timeout); }
private changeStatus: FunctionStatusChangeCallback<U> = (status, result) => { this.status = status; this.callback?.(status, result); };
private execute = async (): Promise<void> => { try { if (this.planToStop) { this.changeStatus('stoped'); return; } this.changeStatus('running'); this.retries--; const result = await this.fn(...this.args); this.resolve && this.resolve(result); if (this.planToStop) { this.changeStatus('stoped', result); return; } this.changeStatus('success', result); } catch (error) { if (!this.errorConsumeCount) { this.retries++; } if (this.planToStop) { this.changeStatus('stoped'); return; } if (this.canRetry()) { setTimeout(() => { this.execute(); }, this.retryDuration); } else { this.reject && this.reject(error); this.changeStatus('fail'); } } }; }
export function useRetryableAsyncFunc<T, U>(fn: AsyncFunc<T, U>, params: RetryableFunctionParams<U>) { const retryAble = useMemo(() => { return new Retryable(fn, params); }, []);
useEffect(() => { retryAble.resetFunc(fn); }, [fn]);
return retryAble; }
|