@@ -219,40 +219,101 @@ interface WaitUntilOptions {
219
219
readonly interval ?: number
220
220
/** Wait for "truthy" result, else wait for any defined result including `false` (default: true) */
221
221
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
222
231
}
223
232
233
+ export const waitUntilDefaultTimeout = 2000
234
+ export const waitUntilDefaultInterval = 500
235
+
224
236
/**
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.
226
239
*
227
240
* @param fn Function whose result is checked
228
241
* @param options See {@link WaitUntilOptions}
229
242
*
230
- * @returns Result of `fn()`, or `undefined` if timeout was reached .
243
+ * @returns Result of `fn()`, or possibly `undefined` depending on the arguments .
231
244
*/
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 >
232
254
export 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
+
234
270
for ( let i = 0 ; true ; i ++ ) {
235
271
const start : number = globals . clock . Date . now ( )
236
272
let result : T
237
273
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
243
296
}
244
297
245
298
// 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
+ }
247
306
248
- if ( ( opt . truthy && result ) || ( ! opt . truthy && result !== undefined ) ) {
249
- return result
307
+ throw lastError
250
308
}
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 )
253
313
}
254
314
255
- await sleep ( opt . interval )
315
+ elapsed += interval
316
+ interval = interval * opt . backoff
256
317
}
257
318
}
258
319
0 commit comments