in packages/core/src/shared/utilities/timeoutUtils.ts [256:334]
export async function waitUntil<T>(
fn: () => Promise<T>,
options: Omit<WaitUntilOptions, 'retryOnFail'>
): Promise<T | undefined>
export async function waitUntil<T>(fn: () => Promise<T>, options: WaitUntilOptions): Promise<T | undefined> {
// set default opts
const opt = {
timeout: waitUntilDefaultTimeout,
interval: waitUntilDefaultInterval,
truthy: true,
backoff: 1,
retryOnFail: false,
...options,
}
let interval = opt.interval
let lastError: Error | undefined
let elapsed: number = 0
let remaining = opt.timeout
// Internal helper to determine if we should retry
function shouldRetry(error: Error | undefined): boolean {
if (error === undefined) {
return typeof opt.retryOnFail === 'boolean' ? opt.retryOnFail : true
}
if (typeof opt.retryOnFail === 'function') {
return opt.retryOnFail(error)
}
return opt.retryOnFail
}
for (let i = 0; true; i++) {
const start: number = globals.clock.Date.now()
let result: T
try {
// Needed in case a caller uses a 0 timeout (function is only called once)
if (remaining > 0) {
result = await Promise.race([fn(), new Promise<T>((r) => globals.clock.setTimeout(r, remaining))])
} else {
result = await fn()
}
if (shouldRetry(lastError) || (opt.truthy && result) || (!opt.truthy && result !== undefined)) {
return result
}
} catch (e) {
// Unlikely to hit this, but exists for typing
if (!(e instanceof Error)) {
throw e
}
if (!shouldRetry(e)) {
throw e
}
lastError = e
}
// Ensures that we never overrun the timeout
remaining -= globals.clock.Date.now() - start
// If the sleep will exceed the timeout, abort early
if (elapsed + interval >= remaining) {
if (!shouldRetry(lastError)) {
return undefined
}
throw lastError
}
// when testing, this avoids the need to progress the stubbed clock
if (interval > 0) {
await sleep(interval)
}
elapsed += interval
interval = interval * opt.backoff
}
}