@naturalcycles/js-lib 15.82.0 → 15.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,69 @@
1
- import type { DebounceOptions, ThrottleOptions } from './debounce.js'
2
- import { _debounce, _throttle } from './debounce.js'
1
+ import type { AnyAsyncFunction, AnyFunction } from '../types.js'
2
+ import type {
3
+ AsyncDebounceOptions,
4
+ AsyncThrottleOptions,
5
+ DebounceOptions,
6
+ ThrottleOptions,
7
+ } from './debounce.js'
8
+ import { _asyncDebounce, _asyncThrottle, _debounce, _throttle } from './debounce.js'
9
+ import type { MethodDecorator } from './decorator.util.js'
3
10
 
4
- export function _Debounce(wait: number, opt: DebounceOptions = {}): MethodDecorator {
11
+ export function _Debounce<T extends AnyFunction>(
12
+ wait: number,
13
+ opt: DebounceOptions = {},
14
+ ): MethodDecorator<T> {
5
15
  return (_target, _key, descriptor) => {
6
- const originalFn = descriptor.value
7
- descriptor.value = _debounce(originalFn as any, wait, opt)
16
+ const originalFn = descriptor.value!
17
+ descriptor.value = _debounce<T>(originalFn, wait, opt)
8
18
  return descriptor
9
19
  }
10
20
  }
11
21
 
12
- export function _Throttle(wait: number, opt: ThrottleOptions = {}): MethodDecorator {
22
+ export function _Throttle<T extends AnyFunction>(
23
+ wait: number,
24
+ opt: ThrottleOptions = {},
25
+ ): MethodDecorator<T> {
13
26
  return (_target, _key, descriptor) => {
14
- const originalFn = descriptor.value
15
- descriptor.value = _throttle(originalFn as any, wait, opt)
27
+ const originalFn = descriptor.value!
28
+ descriptor.value = _throttle<T>(originalFn, wait, opt)
29
+ return descriptor
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Like `@_Debounce`, but for async methods: every call returns a real Promise resolving with the
35
+ * coalesced invocation's result, so `await`-ing never yields a stale value or a non-promise
36
+ *
37
+ * Be aware that the decorated method may resolve `undefined` if the config yields no invocation,
38
+ * which the method's `T` type can't express.
39
+ *
40
+ * @experimental
41
+ */
42
+ export function _AsyncDebounce<T extends AnyAsyncFunction>(
43
+ wait: number,
44
+ opt: AsyncDebounceOptions = {},
45
+ ): MethodDecorator<T> {
46
+ return (_target, _key, descriptor) => {
47
+ const originalFn = descriptor.value!
48
+ descriptor.value = _asyncDebounce<T>(originalFn, wait, opt) as any
49
+ return descriptor
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Like `@_Throttle`, but for async methods.
55
+ *
56
+ * @see {@link _AsyncDebounce}
57
+ *
58
+ * @experimental
59
+ */
60
+ export function _AsyncThrottle<T extends AnyAsyncFunction>(
61
+ wait: number,
62
+ opt: AsyncThrottleOptions = {},
63
+ ): MethodDecorator<T> {
64
+ return (_target, _key, descriptor) => {
65
+ const originalFn = descriptor.value!
66
+ descriptor.value = _asyncThrottle<T>(originalFn, wait, opt) as any
16
67
  return descriptor
17
68
  }
18
69
  }
@@ -1,116 +1,58 @@
1
- import type { AnyFunction } from '../types.js'
2
-
3
- export interface Cancelable {
4
- cancel: () => void
5
- flush: () => void
6
- }
7
-
8
- export interface ThrottleOptions {
9
- /**
10
- * @default true
11
- */
12
- leading?: boolean
13
-
14
- /**
15
- * @default true
16
- */
17
- trailing?: boolean
18
- }
19
-
20
- export interface DebounceOptions {
21
- /**
22
- * @default false
23
- */
24
- leading?: boolean
25
-
26
- /**
27
- * @default true
28
- */
29
- trailing?: boolean
30
-
31
- /**
32
- *
33
- */
34
- maxWait?: number
35
- }
1
+ import { _anyToError } from '../error/error.util.js'
2
+ import { pDefer } from '../promise/pDefer.js'
3
+ import type { DeferredPromise } from '../promise/pDefer.js'
4
+ import type { AnyAsyncFunction, AnyFunction } from '../types.js'
36
5
 
37
6
  export function _debounce<T extends AnyFunction>(
38
7
  func: T,
39
8
  wait: number,
40
9
  opt: DebounceOptions = {},
41
10
  ): T & Cancelable {
42
- let lastArgs: any
43
- let lastThis: any
44
- let result: any
11
+ let lastArgs: Parameters<T> | undefined
12
+ let lastThis: ThisParameterType<T> | undefined
13
+ let result: ReturnType<T>
45
14
  let timerId: number | undefined
46
- let lastCallTime: number | undefined
47
15
 
48
- let lastInvokeTime = 0
49
16
  const maxing = 'maxWait' in opt
50
-
51
17
  const { leading = false, trailing = true } = opt
52
- const maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait
18
+ const state: DebounceTimerState = {
19
+ lastCallTime: undefined,
20
+ lastInvokeTime: 0,
21
+ wait,
22
+ maxing,
23
+ maxWait: maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait,
24
+ }
53
25
 
54
- function invokeFunc(time: number): any {
26
+ function invokeFunc(time: number): ReturnType<T> {
55
27
  const args = lastArgs
56
28
  const thisArg = lastThis
57
29
 
58
30
  lastArgs = lastThis = undefined
59
- lastInvokeTime = time
60
- result = func.apply(thisArg, args)
31
+ state.lastInvokeTime = time
32
+ result = func.apply(thisArg, args!)
61
33
  return result
62
34
  }
63
35
 
64
- function startTimer(pendingFunc: AnyFunction, wait: number): number {
65
- return setTimeout(pendingFunc, wait)
66
- }
67
-
68
- function cancelTimer(id: number): void {
69
- clearTimeout(id)
70
- }
71
-
72
- function leadingEdge(time: number): any {
36
+ function leadingEdge(time: number): ReturnType<T> {
73
37
  // Reset any `maxWait` timer.
74
- lastInvokeTime = time
38
+ state.lastInvokeTime = time
75
39
  // Start the timer for the trailing edge.
76
- timerId = startTimer(timerExpired, wait)
40
+ timerId = startTimer(timerExpired, state.wait)
77
41
  // Invoke the leading edge.
78
42
  return leading ? invokeFunc(time) : result
79
43
  }
80
44
 
81
- function remainingWait(time: number): number {
82
- const timeSinceLastCall = time - lastCallTime!
83
- const timeSinceLastInvoke = time - lastInvokeTime
84
- const timeWaiting = wait - timeSinceLastCall
85
-
86
- return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting
87
- }
88
-
89
- function shouldInvoke(time: number): boolean {
90
- const timeSinceLastCall = time - lastCallTime!
91
- const timeSinceLastInvoke = time - lastInvokeTime
92
-
93
- // Either this is the first call, activity has stopped and we're at the
94
- // trailing edge, the system time has gone backwards and we're treating
95
- // it as the trailing edge, or we've hit the `maxWait` limit.
96
- return (
97
- lastCallTime === undefined ||
98
- timeSinceLastCall >= wait ||
99
- timeSinceLastCall < 0 ||
100
- (maxing && timeSinceLastInvoke >= maxWait!)
101
- )
102
- }
103
-
104
- function timerExpired(): any {
45
+ function timerExpired(): void {
105
46
  const time = Date.now()
106
- if (shouldInvoke(time)) {
107
- return trailingEdge(time)
47
+ if (shouldInvoke(time, state)) {
48
+ trailingEdge(time)
49
+ return
108
50
  }
109
51
  // Restart the timer.
110
- timerId = startTimer(timerExpired, remainingWait(time))
52
+ timerId = startTimer(timerExpired, remainingWait(time, state))
111
53
  }
112
54
 
113
- function trailingEdge(time: number): any {
55
+ function trailingEdge(time: number): ReturnType<T> {
114
56
  timerId = undefined
115
57
 
116
58
  // Only invoke if we have `lastArgs` which means `func` has been
@@ -126,11 +68,12 @@ export function _debounce<T extends AnyFunction>(
126
68
  if (timerId !== undefined) {
127
69
  cancelTimer(timerId)
128
70
  }
129
- lastInvokeTime = 0
130
- lastArgs = lastCallTime = lastThis = timerId = undefined
71
+ state.lastInvokeTime = 0
72
+ state.lastCallTime = undefined
73
+ lastArgs = lastThis = timerId = undefined
131
74
  }
132
75
 
133
- function flush(): any {
76
+ function flush(): ReturnType<T> {
134
77
  return timerId === undefined ? result : trailingEdge(Date.now())
135
78
  }
136
79
 
@@ -138,26 +81,26 @@ export function _debounce<T extends AnyFunction>(
138
81
  return timerId !== undefined
139
82
  }
140
83
 
141
- function debounced(this: any, ...args: any[]): any {
84
+ function debounced(this: ThisParameterType<T>, ...args: Parameters<T>): ReturnType<T> {
142
85
  const time = Date.now()
143
- const isInvoking = shouldInvoke(time)
86
+ const isInvoking = shouldInvoke(time, state)
144
87
 
145
88
  lastArgs = args
146
89
  lastThis = this
147
- lastCallTime = time
90
+ state.lastCallTime = time
148
91
 
149
92
  if (isInvoking) {
150
93
  if (timerId === undefined) {
151
- return leadingEdge(lastCallTime)
94
+ return leadingEdge(state.lastCallTime)
152
95
  }
153
- if (maxing) {
96
+ if (state.maxing) {
154
97
  // Handle invocations in a tight loop.
155
- timerId = startTimer(timerExpired, wait)
156
- return invokeFunc(lastCallTime)
98
+ timerId = startTimer(timerExpired, state.wait)
99
+ return invokeFunc(state.lastCallTime)
157
100
  }
158
101
  }
159
102
  if (timerId === undefined) {
160
- timerId = startTimer(timerExpired, wait)
103
+ timerId = startTimer(timerExpired, state.wait)
161
104
  }
162
105
  return result
163
106
  }
@@ -180,3 +123,319 @@ export function _throttle<T extends AnyFunction>(
180
123
  maxWait: wait,
181
124
  })
182
125
  }
126
+
127
+ /**
128
+ * Like `_debounce`, but for async functions.
129
+ *
130
+ * Unlike `_debounce` (which returns a stale `result`/`undefined` for suppressed calls),
131
+ * `_asyncDebounce` always returns a real Promise, resolving with the coalesced invocation's result.
132
+ *
133
+ * @experimental
134
+ */
135
+ export function _asyncDebounce<T extends AnyAsyncFunction>(
136
+ func: T,
137
+ wait: number,
138
+ opt: AsyncDebounceOptions = {},
139
+ ): AsyncDebounced<T> {
140
+ let lastArgs: Parameters<T> | undefined
141
+ let lastThis: ThisParameterType<T> | undefined
142
+ let timerId: number | undefined
143
+ // Promise shared by all calls coalesced into the next invocation (undefined when none pending).
144
+ let deferred: DeferredPromise<Awaited<ReturnType<T>> | undefined> | undefined
145
+
146
+ const { leading = false, trailing = true } = opt
147
+
148
+ const maxing = 'maxWait' in opt
149
+ const maxWait = maxing ? Math.max(Number(opt.maxWait) || 0, wait) : opt.maxWait
150
+
151
+ const state: DebounceTimerState = {
152
+ lastCallTime: undefined,
153
+ lastInvokeTime: 0,
154
+ wait,
155
+ maxing,
156
+ maxWait,
157
+ }
158
+
159
+ async function invokeFunc(time: number): Promise<void> {
160
+ const args = lastArgs
161
+ const thisArg = lastThis
162
+
163
+ lastArgs = lastThis = undefined
164
+ state.lastInvokeTime = time
165
+
166
+ // Detach and settle the current batch.
167
+ const d = deferred!
168
+ deferred = undefined
169
+ try {
170
+ const result = await func.apply(thisArg, args!)
171
+ d.resolve(result)
172
+ } catch (err) {
173
+ d.reject(_anyToError(err))
174
+ }
175
+ }
176
+
177
+ async function leadingEdge(time: number): Promise<Awaited<ReturnType<T>> | undefined> {
178
+ // Reset any `maxWait` timer.
179
+ state.lastInvokeTime = time
180
+ // Start the timer for the trailing edge.
181
+ timerId = startTimer(timerExpired, state.wait)
182
+ // Capture the batch before `invokeFunc` detaches it.
183
+ const d = deferred!
184
+ // Invoke the leading edge.
185
+ if (leading) {
186
+ await invokeFunc(time)
187
+ }
188
+ return d
189
+ }
190
+
191
+ function timerExpired(): void {
192
+ const time = Date.now()
193
+ if (shouldInvoke(time, state)) {
194
+ void trailingEdge(time)
195
+ return
196
+ }
197
+ // Restart the timer.
198
+ timerId = startTimer(timerExpired, remainingWait(time, state))
199
+ }
200
+
201
+ async function trailingEdge(time: number): Promise<void> {
202
+ timerId = undefined
203
+
204
+ // Only invoke if we have `lastArgs` which means `func` has been
205
+ // debounced at least once.
206
+ if (trailing && lastArgs && deferred) {
207
+ await invokeFunc(time)
208
+ return
209
+ }
210
+ lastArgs = lastThis = undefined
211
+ // Dropped batch (only reachable with `trailing: false`): no invocation, so resolve `undefined`.
212
+ if (deferred) {
213
+ const d = deferred
214
+ deferred = undefined
215
+ d.resolve(undefined)
216
+ }
217
+ }
218
+
219
+ function cancel(): void {
220
+ if (timerId !== undefined) {
221
+ cancelTimer(timerId)
222
+ }
223
+ state.lastInvokeTime = 0
224
+ state.lastCallTime = undefined
225
+ lastArgs = lastThis = timerId = undefined
226
+ // Reject pending awaiters.
227
+ if (deferred) {
228
+ const d = deferred
229
+ deferred = undefined
230
+ d.reject(new Error('asyncDebounce cancelled'))
231
+ }
232
+ }
233
+
234
+ async function flush(): Promise<Awaited<ReturnType<T>> | undefined> {
235
+ if (timerId === undefined || !deferred) {
236
+ return undefined
237
+ }
238
+ const d = deferred
239
+ cancelTimer(timerId)
240
+ await trailingEdge(Date.now())
241
+ return d
242
+ }
243
+
244
+ function pending(): boolean {
245
+ return timerId !== undefined
246
+ }
247
+
248
+ async function debounced(
249
+ this: ThisParameterType<T>,
250
+ ...args: Parameters<T>
251
+ ): Promise<Awaited<ReturnType<T>> | undefined> {
252
+ const time = Date.now()
253
+ const isInvoking = shouldInvoke(time, state)
254
+
255
+ lastArgs = args
256
+ lastThis = this
257
+ state.lastCallTime = time
258
+
259
+ deferred ||= pDefer<Awaited<ReturnType<T>> | undefined>()
260
+ const currentDeferred = deferred
261
+
262
+ if (isInvoking) {
263
+ if (timerId === undefined) {
264
+ return leadingEdge(state.lastCallTime)
265
+ }
266
+ if (state.maxing) {
267
+ // Handle invocations in a tight loop.
268
+ timerId = startTimer(timerExpired, state.wait)
269
+ await invokeFunc(state.lastCallTime)
270
+ return currentDeferred
271
+ }
272
+ }
273
+ if (timerId === undefined) {
274
+ timerId = startTimer(timerExpired, state.wait)
275
+ }
276
+ return currentDeferred
277
+ }
278
+
279
+ debounced.cancel = cancel
280
+ debounced.flush = flush
281
+ debounced.pending = pending
282
+ return debounced as any
283
+ }
284
+
285
+ /**
286
+ * Like `_throttle`, but for async functions. See `_asyncDebounce` for the Promise semantics.
287
+ *
288
+ * @experimental
289
+ */
290
+ export function _asyncThrottle<T extends AnyAsyncFunction>(
291
+ func: T,
292
+ wait: number,
293
+ opt: AsyncThrottleOptions = {},
294
+ ): AsyncDebounced<T> {
295
+ return _asyncDebounce(func, wait, {
296
+ leading: true,
297
+ trailing: true,
298
+ ...opt,
299
+ maxWait: wait,
300
+ })
301
+ }
302
+
303
+ function remainingWait(time: number, state: DebounceTimerState): number {
304
+ const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state
305
+ const timeSinceLastCall = time - lastCallTime!
306
+ const timeSinceLastInvoke = time - lastInvokeTime
307
+ const timeWaiting = wait - timeSinceLastCall
308
+
309
+ return maxing ? Math.min(timeWaiting, maxWait! - timeSinceLastInvoke) : timeWaiting
310
+ }
311
+
312
+ function shouldInvoke(time: number, state: DebounceTimerState): boolean {
313
+ const { lastCallTime, lastInvokeTime, wait, maxing, maxWait } = state
314
+ const timeSinceLastCall = time - lastCallTime!
315
+ const timeSinceLastInvoke = time - lastInvokeTime
316
+
317
+ // Either this is the first call, activity has stopped and we're at the
318
+ // trailing edge, the system time has gone backwards and we're treating
319
+ // it as the trailing edge, or we've hit the `maxWait` limit.
320
+ return (
321
+ lastCallTime === undefined ||
322
+ timeSinceLastCall >= wait ||
323
+ timeSinceLastCall < 0 ||
324
+ (maxing && timeSinceLastInvoke >= maxWait!)
325
+ )
326
+ }
327
+
328
+ function startTimer(pendingFunc: AnyFunction, wait: number): number {
329
+ return setTimeout(pendingFunc, wait)
330
+ }
331
+
332
+ function cancelTimer(id: number): void {
333
+ clearTimeout(id)
334
+ }
335
+
336
+ interface DebounceTimerState {
337
+ lastCallTime: number | undefined
338
+ lastInvokeTime: number
339
+ wait: number
340
+ maxing: boolean
341
+ maxWait: number | undefined
342
+ }
343
+
344
+ export interface Cancelable {
345
+ cancel: () => void
346
+ flush: () => void
347
+ }
348
+
349
+ /**
350
+ * Like `Cancelable`, but for the async debounced functions: `flush()` resolves with the flushed
351
+ * invocation's result (or `undefined` if nothing was pending), and `pending()` is exposed.
352
+ */
353
+ export interface AsyncCancelable<T> {
354
+ cancel: () => void
355
+ flush: () => Promise<T | undefined>
356
+ pending: () => boolean
357
+ }
358
+
359
+ export interface ThrottleOptions {
360
+ /**
361
+ * Invoke on the leading edge of the window (immediately, on the first call).
362
+ *
363
+ * @default true
364
+ */
365
+ leading?: boolean
366
+
367
+ /**
368
+ * Invoke on the trailing edge of the window (after `wait` has elapsed).
369
+ *
370
+ * @default true
371
+ */
372
+ trailing?: boolean
373
+ }
374
+
375
+ export interface DebounceOptions {
376
+ /**
377
+ * Invoke on the leading edge of the window (immediately, on the first call).
378
+ *
379
+ * @default false
380
+ */
381
+ leading?: boolean
382
+
383
+ /**
384
+ * Invoke on the trailing edge of the window (after `wait` has elapsed).
385
+ *
386
+ * @default true
387
+ */
388
+ trailing?: boolean
389
+
390
+ /**
391
+ * Maximum time `func` is allowed to be delayed before it's forcibly invoked.
392
+ */
393
+ maxWait?: number
394
+ }
395
+
396
+ export interface AsyncThrottleOptions {
397
+ /**
398
+ * Invoke on the leading edge of the window (immediately, on the first call).
399
+ *
400
+ * @default true
401
+ */
402
+ leading?: boolean
403
+
404
+ /**
405
+ * Invoke on the trailing edge of the window (after `wait` has elapsed).
406
+ *
407
+ * When `false`, calls dropped within the window (not served by a leading invocation) resolve
408
+ * with `undefined`. There's no invocation for them to await. This is why the returned function
409
+ * can resolve with `undefined` even though the original function's return type doesn't express that.
410
+ *
411
+ * @default true
412
+ */
413
+ trailing?: boolean
414
+ }
415
+
416
+ export interface AsyncDebounceOptions extends AsyncThrottleOptions {
417
+ /**
418
+ * Invoke on the leading edge of the window (immediately, on the first call).
419
+ *
420
+ * @default false
421
+ */
422
+ leading?: boolean
423
+
424
+ /**
425
+ * Maximum time `func` is allowed to be delayed before it's forcibly invoked.
426
+ */
427
+ maxWait?: number
428
+ }
429
+
430
+ /**
431
+ * The function returned by `_asyncDebounce`/`_asyncThrottle`. Same signature as `T`, but resolves
432
+ * `Awaited<ReturnType<T>> | undefined`
433
+ *
434
+ * This is because some configurations (e.g. `trailing: false`) may drop calls without invoking
435
+ * the original function, so there's no result to await.
436
+ */
437
+ export type AsyncDebounced<T extends AnyAsyncFunction> = ((
438
+ this: ThisParameterType<T>,
439
+ ...args: Parameters<T>
440
+ ) => Promise<Awaited<ReturnType<T>> | undefined>) &
441
+ AsyncCancelable<Awaited<ReturnType<T>>>
@@ -1,5 +1,16 @@
1
1
  import type { AnyObject, InstanceId } from '../types.js'
2
2
 
3
+ /**
4
+ * Generic override of TypeScript's built-in legacy MethodDecorator, that
5
+ * allows us to infer the parameters of the decorated method from the parameters
6
+ * of a decorator.
7
+ */
8
+ export type MethodDecorator<T> = (
9
+ target: AnyObject,
10
+ propertyKey: string | symbol,
11
+ descriptor: TypedPropertyDescriptor<T>,
12
+ ) => TypedPropertyDescriptor<T> | undefined
13
+
3
14
  /**
4
15
  * @returns
5
16
  * e.g `NameOfYourClass.methodName`
@@ -2,8 +2,9 @@ import { _assert, _assertTypeOf } from '../error/assert.js'
2
2
  import type { CommonLogger } from '../log/commonLogger.js'
3
3
  import type { AnyFunction, AnyObject, MaybeParameters } from '../types.js'
4
4
  import { _objectAssign } from '../types.js'
5
+ import type { MethodDecorator } from './decorator.util.js'
5
6
  import { _getTargetMethodSignature } from './decorator.util.js'
6
- import type { MemoCache, MethodDecorator } from './memo.util.js'
7
+ import type { MemoCache } from './memo.util.js'
7
8
  import { jsonMemoSerializer, MapMemoCache } from './memo.util.js'
8
9
 
9
10
  export interface MemoOptions<FN> {
@@ -1,6 +1,6 @@
1
1
  import { _isPrimitive } from '../is.util.js'
2
2
  import { pDelay } from '../promise/pDelay.js'
3
- import type { AnyObject, UnixTimestamp } from '../types.js'
3
+ import type { UnixTimestamp } from '../types.js'
4
4
  import { MISS } from '../types.js'
5
5
 
6
6
  export type MemoSerializer = (args: any[]) => any
@@ -147,14 +147,3 @@ export class MapAsyncMemoCache<KEY = any, VALUE = any> implements AsyncMemoCache
147
147
  this.m.clear()
148
148
  }
149
149
  }
150
-
151
- /**
152
- * Generic override of Typescript's built in legacy MethodDecorator, that
153
- * allows us to infer the parameters of the decorated method from the parameters
154
- * of a decorator.
155
- */
156
- export type MethodDecorator<T> = (
157
- target: AnyObject,
158
- propertyKey: string | symbol,
159
- descriptor: TypedPropertyDescriptor<T>,
160
- ) => TypedPropertyDescriptor<T> | undefined
@@ -59,6 +59,15 @@ export type FetcherOnErrorHook = (err: Error) => Promisable<void>
59
59
  * and set `cfg.init.headers` based on it.
60
60
  */
61
61
  export type FetcherInitHook = (cfg: FetcherNormalizedCfg) => Promisable<void>
62
+ /**
63
+ * Decides, based on the error response, if the Fetcher init state is "stale",
64
+ * e.g the auth token acquired in the init hook has expired.
65
+ * Returning true triggers `resetInit()`, so the init hooks re-run,
66
+ * and the request is then retried (at most once per request).
67
+ *
68
+ * Only consulted when init hooks are defined and the request resulted in an error.
69
+ */
70
+ export type FetcherShouldReinitFn = (res: FetcherErrorResponse) => Promisable<boolean>
62
71
 
63
72
  /**
64
73
  * FetcherCfg: configuration of the Fetcher instance. One per instance.
@@ -88,6 +97,13 @@ export interface FetcherCfg {
88
97
  * See FetcherInitHook docs.
89
98
  */
90
99
  init?: FetcherInitHook[]
100
+ /**
101
+ * Allows to detect "stale init" (e.g the auth token acquired in the init hook
102
+ * has expired) based on the error response, and trigger reinit+retry.
103
+ * Unlike other hooks, it's a single decision predicate, not an array.
104
+ * See FetcherShouldReinitFn docs.
105
+ */
106
+ shouldReinit?: FetcherShouldReinitFn
91
107
  /**
92
108
  * Allows to mutate req.
93
109
  */