| 1 | export interface ShowcaseRetryOptions { |
| 2 | readonly isCancelled: () => boolean; |
| 3 | readonly attemptTimeoutMs?: number; |
| 4 | readonly retryDelayMs?: number; |
| 5 | } |
| 6 | |
| 7 | const DEFAULT_ATTEMPT_TIMEOUT_MS = 10_000; |
| 8 | const DEFAULT_RETRY_DELAY_MS = 500; |
| 9 | |
| 10 | function delay(ms: number): Promise<void> { |
| 11 | return new Promise((resolve) => setTimeout(resolve, ms)); |
| 12 | } |
| 13 | |
| 14 | async function runAttemptWithTimeout( |
| 15 | operation: () => Promise<boolean>, |
| 16 | timeoutMs: number, |
| 17 | ): Promise<boolean> { |
| 18 | let timeout: ReturnType<typeof setTimeout> | null = null; |
| 19 | try { |
| 20 | return await Promise.race([ |
| 21 | operation(), |
| 22 | new Promise<boolean>((resolve) => { |
| 23 | timeout = setTimeout(() => resolve(false), timeoutMs); |
| 24 | }), |
| 25 | ]); |
| 26 | } catch { |
| 27 | return false; |
| 28 | } finally { |
| 29 | if (timeout !== null) clearTimeout(timeout); |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | /** Retry transient showcase setup work until it succeeds or the owning effect unmounts. */ |
| 34 | export async function retryShowcaseOperation( |
| 35 | operation: () => Promise<boolean>, |
| 36 | options: ShowcaseRetryOptions, |
| 37 | ): Promise<boolean> { |
| 38 | const attemptTimeoutMs = options.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS; |
| 39 | const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS; |
| 40 | |
| 41 | while (!options.isCancelled()) { |
| 42 | if (await runAttemptWithTimeout(operation, attemptTimeoutMs)) return true; |
| 43 | if (!options.isCancelled()) await delay(retryDelayMs); |
| 44 | } |
| 45 | return false; |
| 46 | } |