@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.
@@ -42,6 +42,7 @@ import type {
42
42
  FetcherBeforeRequestHook,
43
43
  FetcherBeforeRetryHook,
44
44
  FetcherCfg,
45
+ FetcherErrorResponse,
45
46
  FetcherGraphQLOptions,
46
47
  FetcherInitHook,
47
48
  FetcherNormalizedCfg,
@@ -154,9 +155,20 @@ export class Fetcher {
154
155
  return this
155
156
  }
156
157
 
158
+ /**
159
+ * Clears the cached result of the init hooks, so they re-run before the next request.
160
+ * Useful when the state acquired during init (e.g an auth token) becomes stale.
161
+ * See also `cfg.hooks.shouldReinit`, which calls this automatically.
162
+ */
163
+ resetInit(): void {
164
+ this.initPromise = undefined
165
+ this.initGeneration++
166
+ }
167
+
157
168
  cfg: FetcherNormalizedCfg
158
169
 
159
170
  private initPromise?: Promise<void>
171
+ private initGeneration = 0
160
172
 
161
173
  static create(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
162
174
  return new Fetcher(cfg)
@@ -349,14 +361,16 @@ export class Fetcher {
349
361
  * Note: responseType defaults to the Fetcher's cfg responseType (`json`, unless overridden).
350
362
  */
351
363
  async doFetch<T = unknown>(opt: FetcherOptions): Promise<FetcherResponse<T>> {
364
+ let initGeneration = 0
352
365
  if (this.cfg.hooks.init) {
353
366
  try {
354
367
  await (this.initPromise ??= this.runInitHooks())
355
368
  } catch (err) {
356
369
  // Reset, so init hooks are re-attempted on the next request
357
- this.initPromise = undefined
370
+ this.resetInit()
358
371
  throw err
359
372
  }
373
+ initGeneration = this.initGeneration
360
374
  }
361
375
 
362
376
  const req = this.normalizeOptions(opt)
@@ -469,9 +483,36 @@ export class Fetcher {
469
483
  await hook(res)
470
484
  }
471
485
 
486
+ if (res.err && (await this.detectStaleInit(opt, res, initGeneration))) {
487
+ // Stale init (e.g expired auth token) was detected and reset - retry the request once
488
+ return await this.doFetch<T>({ ...opt, [reinitAttempted]: true } as FetcherOptions)
489
+ }
490
+
472
491
  return res
473
492
  }
474
493
 
494
+ /**
495
+ * Consults cfg.hooks.shouldReinit to detect "stale init" (e.g expired auth token).
496
+ * If detected - resets the init (so it re-runs) and returns true,
497
+ * telling the caller to retry the request. At most once per request.
498
+ */
499
+ private async detectStaleInit(
500
+ opt: FetcherOptions,
501
+ res: FetcherErrorResponse,
502
+ initGeneration: number,
503
+ ): Promise<boolean> {
504
+ const { shouldReinit, init } = this.cfg.hooks
505
+ if (!shouldReinit || !init) return false
506
+ if ((opt as any)[reinitAttempted]) return false
507
+ if (!(await shouldReinit(res))) return false
508
+
509
+ // Generation check prevents concurrent stale requests from resetting the already-fresh init
510
+ if (initGeneration === this.initGeneration) {
511
+ this.resetInit()
512
+ }
513
+ return true
514
+ }
515
+
475
516
  private async runInitHooks(): Promise<void> {
476
517
  for (const hook of this.cfg.hooks.init || []) {
477
518
  await hook(this.cfg)
@@ -970,6 +1011,10 @@ export function getFetcher(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
970
1011
  return Fetcher.create(cfg)
971
1012
  }
972
1013
 
1014
+ // Marks FetcherOptions of a request that was already retried due to a reinit,
1015
+ // to guarantee at most one reinit per request
1016
+ const reinitAttempted = Symbol('reinitAttempted')
1017
+
973
1018
  const acceptByResponseType: Record<FetcherResponseType, string> = {
974
1019
  text: 'text/plain',
975
1020
  json: 'application/json',