A cold SAS session can answer startupservice before the libraries are assigned and return without Globvars, Sasdatasets, Saslibs and XLMaps. The app treated that first answer as final and showed "X are not present in the startupservice", while a reload a few seconds later worked - reported against staging. The request is now retried for a few seconds (five attempts, 1s/2s/3s/5s) and only the last answer is reported. An abort body is retried too, because that is the same transient case, and when it never clears its MSG is quoted in the dialog instead of the generic missing-props text. A malformed response - the adapter passing a plain-text job error through - is a verdict about configuration, so it is still shown immediately. `retryUntil` lives beside `retryOnce` in shared/utils and is unit tested.
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
export interface RetryUntilOptions {
|
|
/** How many times requestFn may be called. Default 5. */
|
|
maxAttempts?: number
|
|
/** Delay before the next attempt, given the attempt that just finished (1-based). */
|
|
delayMs?: (attempt: number) => number
|
|
/** Injectable for tests. Default setTimeout-based wait. */
|
|
wait?: (ms: number) => Promise<void>
|
|
}
|
|
|
|
const defaultWait = (ms: number): Promise<void> =>
|
|
new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
|
/** One second after the first attempt, then two, three, five, five. */
|
|
const defaultDelayMs = (attempt: number): number =>
|
|
Math.min(1000 * attempt, 5000)
|
|
|
|
/**
|
|
* Calls requestFn until isReady accepts the answer, then returns it.
|
|
*
|
|
* For a service that can answer before the thing it reports on exists - a cold
|
|
* SAS session, a library that has not been assigned yet - a first response is
|
|
* not a verdict, and an immediate error dialog makes the retry the user's job.
|
|
* The last answer is returned whether or not it was ready, so the caller keeps
|
|
* a single place to report failure.
|
|
*/
|
|
export const retryUntil = async <T>(
|
|
requestFn: () => Promise<T>,
|
|
isReady: (value: T) => boolean,
|
|
options?: RetryUntilOptions
|
|
): Promise<T> => {
|
|
const maxAttempts = options?.maxAttempts ?? 5
|
|
const delayMs = options?.delayMs ?? defaultDelayMs
|
|
const wait = options?.wait ?? defaultWait
|
|
|
|
let value = await requestFn()
|
|
|
|
for (let attempt = 1; attempt < maxAttempts; attempt++) {
|
|
if (isReady(value)) return value
|
|
await wait(delayMs(attempt))
|
|
value = await requestFn()
|
|
}
|
|
|
|
return value
|
|
}
|