@@ -219,40 +219,101 @@ interface WaitUntilOptions {
219219 readonly interval ?: number
220220 /** Wait for "truthy" result, else wait for any defined result including `false` (default: true) */
221221 readonly truthy ?: boolean
222+ /** A backoff multiplier for how long the next interval will be (default: None, i.e 1) */
223+ readonly backoff ?: number
224+ /**
225+ * Only retries when an error is thrown, otherwise returning the immediate result.
226+ * - 'truthy' arg is ignored
227+ * - If the timeout is reached it throws the last error
228+ * - default: false
229+ */
230+ readonly retryOnFail ?: boolean
222231}
223232
233+ export const waitUntilDefaultTimeout = 2000
234+ export const waitUntilDefaultInterval = 500
235+
224236/**
225- * Invokes `fn()` until it returns a truthy value (or non-undefined if `truthy:false`).
237+ * Invokes `fn()` on an interval based on the given arguments. This can be used for retries, or until
238+ * an expected result is given. Read {@link WaitUntilOptions} carefully.
226239 *
227240 * @param fn Function whose result is checked
228241 * @param options See {@link WaitUntilOptions}
229242 *
230- * @returns Result of `fn()`, or `undefined` if timeout was reached .
243+ * @returns Result of `fn()`, or possibly `undefined` depending on the arguments .
231244 */
245+ export async function waitUntil < T > ( fn : ( ) => Promise < T > , options : WaitUntilOptions & { retryOnFail : true } ) : Promise < T >
246+ export async function waitUntil < T > (
247+ fn : ( ) => Promise < T > ,
248+ options : WaitUntilOptions & { retryOnFail : false }
249+ ) : Promise < T | undefined >
250+ export async function waitUntil < T > (
251+ fn : ( ) => Promise < T > ,
252+ options : Omit < WaitUntilOptions , 'retryOnFail' >
253+ ) : Promise < T | undefined >
232254export async function waitUntil < T > ( fn : ( ) => Promise < T > , options : WaitUntilOptions ) : Promise < T | undefined > {
233- const opt = { timeout : 5000 , interval : 500 , truthy : true , ...options }
255+ // set default opts
256+ const opt = {
257+ timeout : waitUntilDefaultTimeout ,
258+ interval : waitUntilDefaultInterval ,
259+ truthy : true ,
260+ backoff : 1 ,
261+ retryOnFail : false ,
262+ ...options ,
263+ }
264+
265+ let interval = opt . interval
266+ let lastError : Error | undefined
267+ let elapsed : number = 0
268+ let remaining = opt . timeout
269+
234270 for ( let i = 0 ; true ; i ++ ) {
235271 const start : number = globals . clock . Date . now ( )
236272 let result : T
237273
238- // Needed in case a caller uses a 0 timeout (function is only called once)
239- if ( opt . timeout > 0 ) {
240- result = await Promise . race ( [ fn ( ) , new Promise < T > ( ( r ) => globals . clock . setTimeout ( r , opt . timeout ) ) ] )
241- } else {
242- result = await fn ( )
274+ try {
275+ // Needed in case a caller uses a 0 timeout (function is only called once)
276+ if ( remaining > 0 ) {
277+ result = await Promise . race ( [ fn ( ) , new Promise < T > ( ( r ) => globals . clock . setTimeout ( r , remaining ) ) ] )
278+ } else {
279+ result = await fn ( )
280+ }
281+
282+ if ( opt . retryOnFail || ( opt . truthy && result ) || ( ! opt . truthy && result !== undefined ) ) {
283+ return result
284+ }
285+ } catch ( e ) {
286+ if ( ! opt . retryOnFail ) {
287+ throw e
288+ }
289+
290+ // Unlikely to hit this, but exists for typing
291+ if ( ! ( e instanceof Error ) ) {
292+ throw e
293+ }
294+
295+ lastError = e
243296 }
244297
245298 // Ensures that we never overrun the timeout
246- opt . timeout -= globals . clock . Date . now ( ) - start
299+ remaining -= globals . clock . Date . now ( ) - start
300+
301+ // If the sleep will exceed the timeout, abort early
302+ if ( elapsed + interval >= remaining ) {
303+ if ( ! opt . retryOnFail ) {
304+ return undefined
305+ }
247306
248- if ( ( opt . truthy && result ) || ( ! opt . truthy && result !== undefined ) ) {
249- return result
307+ throw lastError
250308 }
251- if ( i * opt . interval >= opt . timeout ) {
252- return undefined
309+
310+ // when testing, this avoids the need to progress the stubbed clock
311+ if ( interval > 0 ) {
312+ await sleep ( interval )
253313 }
254314
255- await sleep ( opt . interval )
315+ elapsed += interval
316+ interval = interval * opt . backoff
256317 }
257318}
258319
0 commit comments