Files
dc/client/src/app/shared/utils/retry-until.ts
T
dc 6c4d66a57c
Build / Build-and-ng-test (pull_request) Successful in 4m10s
Build / lighthouse (pull_request) Successful in 6m54s
Build / Build-and-test-development (pull_request) Successful in 21m54s
fix(startup): retry startupservice while the session is still loading
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.
2026-11-09 00:21:08 +00:00

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
}