@naturalcycles/js-lib 15.82.0 → 15.83.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.
- package/dist/http/fetcher.d.ts +13 -0
- package/dist/http/fetcher.js +39 -1
- package/dist/http/fetcher.model.d.ts +16 -0
- package/package.json +1 -1
- package/src/http/fetcher.model.ts +16 -0
- package/src/http/fetcher.ts +46 -1
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -35,8 +35,15 @@ export declare class Fetcher {
|
|
|
35
35
|
* See FetcherInitHook docs.
|
|
36
36
|
*/
|
|
37
37
|
onInit(hook: FetcherInitHook): this;
|
|
38
|
+
/**
|
|
39
|
+
* Clears the cached result of the init hooks, so they re-run before the next request.
|
|
40
|
+
* Useful when the state acquired during init (e.g an auth token) becomes stale.
|
|
41
|
+
* See also `cfg.hooks.shouldReinit`, which calls this automatically.
|
|
42
|
+
*/
|
|
43
|
+
resetInit(): void;
|
|
38
44
|
cfg: FetcherNormalizedCfg;
|
|
39
45
|
private initPromise?;
|
|
46
|
+
private initGeneration;
|
|
40
47
|
static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
|
|
41
48
|
get: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
42
49
|
post: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
@@ -110,6 +117,12 @@ export declare class Fetcher {
|
|
|
110
117
|
* Note: responseType defaults to the Fetcher's cfg responseType (`json`, unless overridden).
|
|
111
118
|
*/
|
|
112
119
|
doFetch<T = unknown>(opt: FetcherOptions): Promise<FetcherResponse<T>>;
|
|
120
|
+
/**
|
|
121
|
+
* Consults cfg.hooks.shouldReinit to detect "stale init" (e.g expired auth token).
|
|
122
|
+
* If detected - resets the init (so it re-runs) and returns true,
|
|
123
|
+
* telling the caller to retry the request. At most once per request.
|
|
124
|
+
*/
|
|
125
|
+
private detectStaleInit;
|
|
113
126
|
private runInitHooks;
|
|
114
127
|
private onOkResponse;
|
|
115
128
|
/**
|
package/dist/http/fetcher.js
CHANGED
|
@@ -101,8 +101,18 @@ export class Fetcher {
|
|
|
101
101
|
(this.cfg.hooks.init ||= []).push(hook);
|
|
102
102
|
return this;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Clears the cached result of the init hooks, so they re-run before the next request.
|
|
106
|
+
* Useful when the state acquired during init (e.g an auth token) becomes stale.
|
|
107
|
+
* See also `cfg.hooks.shouldReinit`, which calls this automatically.
|
|
108
|
+
*/
|
|
109
|
+
resetInit() {
|
|
110
|
+
this.initPromise = undefined;
|
|
111
|
+
this.initGeneration++;
|
|
112
|
+
}
|
|
104
113
|
cfg;
|
|
105
114
|
initPromise;
|
|
115
|
+
initGeneration = 0;
|
|
106
116
|
static create(cfg = {}) {
|
|
107
117
|
return new Fetcher(cfg);
|
|
108
118
|
}
|
|
@@ -271,15 +281,17 @@ export class Fetcher {
|
|
|
271
281
|
* Note: responseType defaults to the Fetcher's cfg responseType (`json`, unless overridden).
|
|
272
282
|
*/
|
|
273
283
|
async doFetch(opt) {
|
|
284
|
+
let initGeneration = 0;
|
|
274
285
|
if (this.cfg.hooks.init) {
|
|
275
286
|
try {
|
|
276
287
|
await (this.initPromise ??= this.runInitHooks());
|
|
277
288
|
}
|
|
278
289
|
catch (err) {
|
|
279
290
|
// Reset, so init hooks are re-attempted on the next request
|
|
280
|
-
this.
|
|
291
|
+
this.resetInit();
|
|
281
292
|
throw err;
|
|
282
293
|
}
|
|
294
|
+
initGeneration = this.initGeneration;
|
|
283
295
|
}
|
|
284
296
|
const req = this.normalizeOptions(opt);
|
|
285
297
|
const { logger } = this.cfg;
|
|
@@ -368,8 +380,31 @@ export class Fetcher {
|
|
|
368
380
|
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
369
381
|
await hook(res);
|
|
370
382
|
}
|
|
383
|
+
if (res.err && (await this.detectStaleInit(opt, res, initGeneration))) {
|
|
384
|
+
// Stale init (e.g expired auth token) was detected and reset - retry the request once
|
|
385
|
+
return await this.doFetch({ ...opt, [reinitAttempted]: true });
|
|
386
|
+
}
|
|
371
387
|
return res;
|
|
372
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Consults cfg.hooks.shouldReinit to detect "stale init" (e.g expired auth token).
|
|
391
|
+
* If detected - resets the init (so it re-runs) and returns true,
|
|
392
|
+
* telling the caller to retry the request. At most once per request.
|
|
393
|
+
*/
|
|
394
|
+
async detectStaleInit(opt, res, initGeneration) {
|
|
395
|
+
const { shouldReinit, init } = this.cfg.hooks;
|
|
396
|
+
if (!shouldReinit || !init)
|
|
397
|
+
return false;
|
|
398
|
+
if (opt[reinitAttempted])
|
|
399
|
+
return false;
|
|
400
|
+
if (!(await shouldReinit(res)))
|
|
401
|
+
return false;
|
|
402
|
+
// Generation check prevents concurrent stale requests from resetting the already-fresh init
|
|
403
|
+
if (initGeneration === this.initGeneration) {
|
|
404
|
+
this.resetInit();
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
373
408
|
async runInitHooks() {
|
|
374
409
|
for (const hook of this.cfg.hooks.init || []) {
|
|
375
410
|
await hook(this.cfg);
|
|
@@ -816,6 +851,9 @@ export class Fetcher {
|
|
|
816
851
|
export function getFetcher(cfg = {}) {
|
|
817
852
|
return Fetcher.create(cfg);
|
|
818
853
|
}
|
|
854
|
+
// Marks FetcherOptions of a request that was already retried due to a reinit,
|
|
855
|
+
// to guarantee at most one reinit per request
|
|
856
|
+
const reinitAttempted = Symbol('reinitAttempted');
|
|
819
857
|
const acceptByResponseType = {
|
|
820
858
|
text: 'text/plain',
|
|
821
859
|
json: 'application/json',
|
|
@@ -30,6 +30,15 @@ export type FetcherOnErrorHook = (err: Error) => Promisable<void>;
|
|
|
30
30
|
* and set `cfg.init.headers` based on it.
|
|
31
31
|
*/
|
|
32
32
|
export type FetcherInitHook = (cfg: FetcherNormalizedCfg) => Promisable<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Decides, based on the error response, if the Fetcher init state is "stale",
|
|
35
|
+
* e.g the auth token acquired in the init hook has expired.
|
|
36
|
+
* Returning true triggers `resetInit()`, so the init hooks re-run,
|
|
37
|
+
* and the request is then retried (at most once per request).
|
|
38
|
+
*
|
|
39
|
+
* Only consulted when init hooks are defined and the request resulted in an error.
|
|
40
|
+
*/
|
|
41
|
+
export type FetcherShouldReinitFn = (res: FetcherErrorResponse) => Promisable<boolean>;
|
|
33
42
|
/**
|
|
34
43
|
* FetcherCfg: configuration of the Fetcher instance. One per instance.
|
|
35
44
|
* FetcherOptions: options for a single request. One per request.
|
|
@@ -56,6 +65,13 @@ export interface FetcherCfg {
|
|
|
56
65
|
* See FetcherInitHook docs.
|
|
57
66
|
*/
|
|
58
67
|
init?: FetcherInitHook[];
|
|
68
|
+
/**
|
|
69
|
+
* Allows to detect "stale init" (e.g the auth token acquired in the init hook
|
|
70
|
+
* has expired) based on the error response, and trigger reinit+retry.
|
|
71
|
+
* Unlike other hooks, it's a single decision predicate, not an array.
|
|
72
|
+
* See FetcherShouldReinitFn docs.
|
|
73
|
+
*/
|
|
74
|
+
shouldReinit?: FetcherShouldReinitFn;
|
|
59
75
|
/**
|
|
60
76
|
* Allows to mutate req.
|
|
61
77
|
*/
|
package/package.json
CHANGED
|
@@ -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
|
*/
|
package/src/http/fetcher.ts
CHANGED
|
@@ -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.
|
|
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',
|