/** * @param ms - the number of ms to sleep * @param signal - an aptional abort signal, to cancel the sleep * @returns a promise that resolves after given amount of time, and is interruptable with an abort signal. */ export async function sleep(ms: number, signal?: AbortSignal): Promise { signal?.throwIfAborted() const {resolve, reject, promise} = Promise.withResolvers() const timeout = setTimeout(resolve, ms) // without the signal, we can't cancel it, so just let the timeout resolve if (!signal) return promise const abortHandler = () => { clearTimeout(timeout) reject(signal.reason) } try { signal.addEventListener('abort', abortHandler) await promise return } finally { signal.removeEventListener('abort', abortHandler) } } export function backoff(options?: {maxAttempts?: number; baseDelay?: number; maxDelay?: number}) { const maxAttempts = options?.maxAttempts ?? 10 const baseDelay = options?.baseDelay ?? 1000 const maxDelay = options?.maxDelay ?? 30_000 let attempts = 0 const nextDelay = () => { return attempts === 0 ? attempts++ // immediate at 0 : Math.min(baseDelay * Math.pow(2, attempts++ - 1), maxDelay) } return async (signal?: AbortSignal) => { signal?.throwIfAborted() if (attempts > maxAttempts) throw new Error('exceeded max attempts!') await sleep(nextDelay(), signal) } }