@naturalcycles/js-lib 14.119.1 → 14.121.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 +39 -11
- package/dist/http/fetcher.js +66 -69
- package/dist/http/http.model.d.ts +2 -1
- package/dist/http/http.model.js +2 -0
- package/dist-esm/http/fetcher.js +113 -50
- package/dist-esm/http/http.model.js +1 -1
- package/package.json +1 -1
- package/src/http/fetcher.ts +120 -77
- package/src/http/http.model.ts +3 -1
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
import { CommonLogger } from '../log/commonLogger';
|
|
3
3
|
import type { Promisable } from '../typeFest';
|
|
4
4
|
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
5
|
-
export interface FetcherNormalizedCfg extends FetcherCfg
|
|
5
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
6
6
|
logger: CommonLogger;
|
|
7
7
|
searchParams: Record<string, any>;
|
|
8
8
|
}
|
|
9
|
+
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>;
|
|
10
|
+
export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>;
|
|
11
|
+
export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>;
|
|
9
12
|
export interface FetcherCfg {
|
|
13
|
+
/**
|
|
14
|
+
* Should **not** contain trailing slash.
|
|
15
|
+
*/
|
|
10
16
|
baseUrl?: string;
|
|
11
17
|
/**
|
|
12
18
|
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
|
|
@@ -17,22 +23,28 @@ export interface FetcherCfg {
|
|
|
17
23
|
/**
|
|
18
24
|
* Allows to mutate req.
|
|
19
25
|
*/
|
|
20
|
-
beforeRequest
|
|
26
|
+
beforeRequest?: FetcherBeforeRequestHook[];
|
|
21
27
|
/**
|
|
22
28
|
* Allows to mutate res.
|
|
23
29
|
* If you set `res.err` - it will be thrown.
|
|
24
30
|
*/
|
|
25
|
-
|
|
31
|
+
afterResponse?: FetcherAfterResponseHook[];
|
|
26
32
|
/**
|
|
27
33
|
* Allows to mutate res.retryStatus to override retry behavior.
|
|
28
34
|
*/
|
|
29
|
-
beforeRetry
|
|
35
|
+
beforeRetry?: FetcherBeforeRetryHook[];
|
|
30
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* If true - enables all possible logging.
|
|
39
|
+
*/
|
|
31
40
|
debug?: boolean;
|
|
32
41
|
logRequest?: boolean;
|
|
33
42
|
logRequestBody?: boolean;
|
|
34
43
|
logResponse?: boolean;
|
|
35
44
|
logResponseBody?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Defaults to `console`.
|
|
47
|
+
*/
|
|
36
48
|
logger?: CommonLogger;
|
|
37
49
|
}
|
|
38
50
|
export interface FetcherRetryStatus {
|
|
@@ -68,7 +80,7 @@ export interface FetcherOptions {
|
|
|
68
80
|
timeoutSeconds?: number;
|
|
69
81
|
json?: any;
|
|
70
82
|
text?: string;
|
|
71
|
-
|
|
83
|
+
credentials?: RequestCredentials;
|
|
72
84
|
headers?: Record<string, any>;
|
|
73
85
|
mode?: FetcherMode;
|
|
74
86
|
searchParams?: Record<string, any>;
|
|
@@ -120,14 +132,30 @@ export type FetcherMode = 'json' | 'text';
|
|
|
120
132
|
*/
|
|
121
133
|
export declare class Fetcher {
|
|
122
134
|
private constructor();
|
|
135
|
+
/**
|
|
136
|
+
* Add BeforeRequest hook at the end of the hooks list.
|
|
137
|
+
*/
|
|
138
|
+
onBeforeRequest(hook: FetcherBeforeRequestHook): this;
|
|
139
|
+
onAfterResponse(hook: FetcherAfterResponseHook): this;
|
|
140
|
+
onBeforeRetry(hook: FetcherBeforeRetryHook): this;
|
|
123
141
|
cfg: FetcherNormalizedCfg;
|
|
124
142
|
static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
get: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
144
|
+
post: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
145
|
+
put: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
146
|
+
patch: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
147
|
+
delete: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
148
|
+
head: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
149
|
+
getText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
150
|
+
postText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
151
|
+
putText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
152
|
+
patchText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
153
|
+
deleteText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
154
|
+
getJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
155
|
+
postJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
156
|
+
putJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
157
|
+
patchJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
158
|
+
deleteJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
131
159
|
fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
132
160
|
rawFetch<T = unknown>(url: string, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
|
|
133
161
|
private processRetry;
|
package/dist/http/fetcher.js
CHANGED
|
@@ -9,6 +9,7 @@ const object_util_1 = require("../object/object.util");
|
|
|
9
9
|
const pDelay_1 = require("../promise/pDelay");
|
|
10
10
|
const json_util_1 = require("../string/json.util");
|
|
11
11
|
const time_util_1 = require("../time/time.util");
|
|
12
|
+
const http_model_1 = require("./http.model");
|
|
12
13
|
const defRetryOptions = {
|
|
13
14
|
count: 2,
|
|
14
15
|
timeout: 500,
|
|
@@ -24,50 +25,53 @@ const defRetryOptions = {
|
|
|
24
25
|
class Fetcher {
|
|
25
26
|
constructor(cfg = {}) {
|
|
26
27
|
this.cfg = this.normalizeCfg(cfg);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
// Dynamically create all helper methods
|
|
29
|
+
http_model_1.HTTP_METHODS.forEach(method => {
|
|
30
|
+
// mode=void
|
|
31
|
+
this[method] = async (url, opt) => {
|
|
32
|
+
return await this.fetch(url, {
|
|
33
|
+
...opt,
|
|
34
|
+
method,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
this[`${method}Text`] = async (url, opt) => {
|
|
38
|
+
return await this.fetch(url, {
|
|
39
|
+
...opt,
|
|
40
|
+
method,
|
|
41
|
+
mode: 'text',
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
this[`${method}Json`] = async (url, opt) => {
|
|
45
|
+
return await this.fetch(url, {
|
|
46
|
+
...opt,
|
|
47
|
+
method,
|
|
48
|
+
mode: 'json',
|
|
49
|
+
});
|
|
50
|
+
};
|
|
35
51
|
});
|
|
36
52
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Add BeforeRequest hook at the end of the hooks list.
|
|
55
|
+
*/
|
|
56
|
+
onBeforeRequest(hook) {
|
|
57
|
+
;
|
|
58
|
+
(this.cfg.hooks.beforeRequest ||= []).push(hook);
|
|
59
|
+
return this;
|
|
43
60
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
mode: 'json',
|
|
49
|
-
});
|
|
61
|
+
onAfterResponse(hook) {
|
|
62
|
+
;
|
|
63
|
+
(this.cfg.hooks.afterResponse ||= []).push(hook);
|
|
64
|
+
return this;
|
|
50
65
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
mode: 'json',
|
|
56
|
-
});
|
|
66
|
+
onBeforeRetry(hook) {
|
|
67
|
+
;
|
|
68
|
+
(this.cfg.hooks.beforeRetry ||= []).push(hook);
|
|
69
|
+
return this;
|
|
57
70
|
}
|
|
58
|
-
|
|
59
|
-
return
|
|
60
|
-
...opt,
|
|
61
|
-
method: 'delete',
|
|
62
|
-
mode: 'json',
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
async getText(url, opt) {
|
|
66
|
-
return await this.fetch(url, {
|
|
67
|
-
...opt,
|
|
68
|
-
mode: 'text',
|
|
69
|
-
});
|
|
71
|
+
static create(cfg = {}) {
|
|
72
|
+
return new Fetcher(cfg);
|
|
70
73
|
}
|
|
74
|
+
// headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
71
75
|
async fetch(url, opt) {
|
|
72
76
|
const res = await this.rawFetch(url, opt);
|
|
73
77
|
if (res.err) {
|
|
@@ -90,7 +94,9 @@ class Fetcher {
|
|
|
90
94
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
91
95
|
}, timeoutSeconds * 1000);
|
|
92
96
|
}
|
|
93
|
-
await this.cfg.hooks
|
|
97
|
+
for await (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
98
|
+
await hook(req);
|
|
99
|
+
}
|
|
94
100
|
const res = {
|
|
95
101
|
req,
|
|
96
102
|
retryStatus: {
|
|
@@ -159,29 +165,12 @@ class Fetcher {
|
|
|
159
165
|
url: req.url,
|
|
160
166
|
// tryCount: req.tryCount,
|
|
161
167
|
}));
|
|
162
|
-
// We don't log errors when they are also thrown,
|
|
163
|
-
// otherwise it gets logged twice: here, and upstream
|
|
164
|
-
// if (this.cfg.logResponse) {
|
|
165
|
-
// const { retryAttempt } = res.retryStatus
|
|
166
|
-
// logger.error(
|
|
167
|
-
// [
|
|
168
|
-
// [
|
|
169
|
-
// ' <<',
|
|
170
|
-
// res.fetchResponse.status,
|
|
171
|
-
// signature,
|
|
172
|
-
// retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
173
|
-
// _since(started),
|
|
174
|
-
// ]
|
|
175
|
-
// .filter(Boolean)
|
|
176
|
-
// .join(' '),
|
|
177
|
-
// _stringifyAny(body),
|
|
178
|
-
// ].join('\n'),
|
|
179
|
-
// )
|
|
180
|
-
// }
|
|
181
168
|
await this.processRetry(res);
|
|
182
169
|
}
|
|
183
170
|
}
|
|
184
|
-
await this.cfg.hooks
|
|
171
|
+
for await (const hook of this.cfg.hooks.afterResponse || []) {
|
|
172
|
+
await hook(res);
|
|
173
|
+
}
|
|
185
174
|
return res;
|
|
186
175
|
}
|
|
187
176
|
async processRetry(res) {
|
|
@@ -189,7 +178,9 @@ class Fetcher {
|
|
|
189
178
|
if (!this.shouldRetry(res)) {
|
|
190
179
|
retryStatus.retryStopped = true;
|
|
191
180
|
}
|
|
192
|
-
await this.cfg.hooks
|
|
181
|
+
for await (const hook of this.cfg.hooks.beforeRetry || []) {
|
|
182
|
+
await hook(res);
|
|
183
|
+
}
|
|
193
184
|
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
194
185
|
if (retryStatus.retryAttempt >= count) {
|
|
195
186
|
retryStatus.retryStopped = true;
|
|
@@ -210,9 +201,9 @@ class Fetcher {
|
|
|
210
201
|
if (method === 'post' && !retryPost)
|
|
211
202
|
return false;
|
|
212
203
|
const { statusFamily } = res;
|
|
213
|
-
if (statusFamily ===
|
|
204
|
+
if (statusFamily === 5 && !retry5xx)
|
|
214
205
|
return false;
|
|
215
|
-
if (statusFamily ===
|
|
206
|
+
if (statusFamily === 4 && !retry4xx)
|
|
216
207
|
return false;
|
|
217
208
|
return true; // default is true
|
|
218
209
|
}
|
|
@@ -221,15 +212,15 @@ class Fetcher {
|
|
|
221
212
|
if (!status)
|
|
222
213
|
return;
|
|
223
214
|
if (status >= 500)
|
|
224
|
-
return
|
|
215
|
+
return 5;
|
|
225
216
|
if (status >= 400)
|
|
226
|
-
return
|
|
217
|
+
return 4;
|
|
227
218
|
if (status >= 300)
|
|
228
|
-
return
|
|
219
|
+
return 3;
|
|
229
220
|
if (status >= 200)
|
|
230
|
-
return
|
|
221
|
+
return 2;
|
|
231
222
|
if (status >= 100)
|
|
232
|
-
return
|
|
223
|
+
return 1;
|
|
233
224
|
}
|
|
234
225
|
/**
|
|
235
226
|
* Returns url without baseUrl and before ?queryString
|
|
@@ -245,8 +236,9 @@ class Fetcher {
|
|
|
245
236
|
console.warn(`Fetcher: baseUrl should not end with /`);
|
|
246
237
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
247
238
|
}
|
|
248
|
-
const { debug } = cfg;
|
|
239
|
+
const { debug = false } = cfg;
|
|
249
240
|
const norm = (0, object_util_1._merge)({
|
|
241
|
+
baseUrl: '',
|
|
250
242
|
url: '',
|
|
251
243
|
searchParams: {},
|
|
252
244
|
timeoutSeconds: 30,
|
|
@@ -255,6 +247,7 @@ class Fetcher {
|
|
|
255
247
|
retry4xx: false,
|
|
256
248
|
retry5xx: true,
|
|
257
249
|
logger: console,
|
|
250
|
+
debug,
|
|
258
251
|
logRequest: debug,
|
|
259
252
|
logRequestBody: debug,
|
|
260
253
|
logResponse: debug,
|
|
@@ -264,6 +257,7 @@ class Fetcher {
|
|
|
264
257
|
method: 'get',
|
|
265
258
|
headers: {},
|
|
266
259
|
},
|
|
260
|
+
hooks: {},
|
|
267
261
|
}, cfg);
|
|
268
262
|
norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
|
|
269
263
|
return norm;
|
|
@@ -282,8 +276,11 @@ class Fetcher {
|
|
|
282
276
|
...retry,
|
|
283
277
|
...(0, object_util_1._filterUndefinedValues)(opt.retry || {}),
|
|
284
278
|
},
|
|
285
|
-
init: (0, object_util_1._merge)({ ...this.cfg.init },
|
|
279
|
+
init: (0, object_util_1._merge)({ ...this.cfg.init },
|
|
280
|
+
// opt.init,
|
|
281
|
+
(0, object_util_1._filterUndefinedValues)({
|
|
286
282
|
method: opt.method,
|
|
283
|
+
credentials: opt.credentials,
|
|
287
284
|
headers: (0, object_util_1._mapKeys)(opt.headers || {}, k => k.toLowerCase()),
|
|
288
285
|
})),
|
|
289
286
|
};
|
package/dist/http/http.model.js
CHANGED
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom"/>
|
|
2
|
+
import { __asyncValues } from "tslib";
|
|
2
3
|
import { _anyToErrorObject } from '../error/error.util';
|
|
3
4
|
import { HttpError } from '../error/http.error';
|
|
4
5
|
import { _clamp } from '../number/number.util';
|
|
@@ -6,6 +7,7 @@ import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit,
|
|
|
6
7
|
import { pDelay } from '../promise/pDelay';
|
|
7
8
|
import { _jsonParseIfPossible } from '../string/json.util';
|
|
8
9
|
import { _since } from '../time/time.util';
|
|
10
|
+
import { HTTP_METHODS } from './http.model';
|
|
9
11
|
const defRetryOptions = {
|
|
10
12
|
count: 2,
|
|
11
13
|
timeout: 500,
|
|
@@ -21,28 +23,45 @@ const defRetryOptions = {
|
|
|
21
23
|
export class Fetcher {
|
|
22
24
|
constructor(cfg = {}) {
|
|
23
25
|
this.cfg = this.normalizeCfg(cfg);
|
|
26
|
+
// Dynamically create all helper methods
|
|
27
|
+
HTTP_METHODS.forEach(method => {
|
|
28
|
+
// mode=void
|
|
29
|
+
this[method] = async (url, opt) => {
|
|
30
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method }));
|
|
31
|
+
};
|
|
32
|
+
this[`${method}Text`] = async (url, opt) => {
|
|
33
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'text' }));
|
|
34
|
+
};
|
|
35
|
+
this[`${method}Json`] = async (url, opt) => {
|
|
36
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'json' }));
|
|
37
|
+
};
|
|
38
|
+
});
|
|
24
39
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
async putJson(url, opt) {
|
|
35
|
-
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'put', mode: 'json' }));
|
|
40
|
+
/**
|
|
41
|
+
* Add BeforeRequest hook at the end of the hooks list.
|
|
42
|
+
*/
|
|
43
|
+
onBeforeRequest(hook) {
|
|
44
|
+
var _a;
|
|
45
|
+
;
|
|
46
|
+
((_a = this.cfg.hooks).beforeRequest || (_a.beforeRequest = [])).push(hook);
|
|
47
|
+
return this;
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
onAfterResponse(hook) {
|
|
50
|
+
var _a;
|
|
51
|
+
;
|
|
52
|
+
((_a = this.cfg.hooks).afterResponse || (_a.afterResponse = [])).push(hook);
|
|
53
|
+
return this;
|
|
39
54
|
}
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
onBeforeRetry(hook) {
|
|
56
|
+
var _a;
|
|
57
|
+
;
|
|
58
|
+
((_a = this.cfg.hooks).beforeRetry || (_a.beforeRetry = [])).push(hook);
|
|
59
|
+
return this;
|
|
42
60
|
}
|
|
43
|
-
|
|
44
|
-
return
|
|
61
|
+
static create(cfg = {}) {
|
|
62
|
+
return new Fetcher(cfg);
|
|
45
63
|
}
|
|
64
|
+
// headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
46
65
|
async fetch(url, opt) {
|
|
47
66
|
const res = await this.rawFetch(url, opt);
|
|
48
67
|
if (res.err) {
|
|
@@ -53,7 +72,7 @@ export class Fetcher {
|
|
|
53
72
|
return res.body;
|
|
54
73
|
}
|
|
55
74
|
async rawFetch(url, rawOpt = {}) {
|
|
56
|
-
var _a, _b, _c, _d;
|
|
75
|
+
var _a, e_1, _b, _c, _d, e_2, _e, _f;
|
|
57
76
|
const { logger } = this.cfg;
|
|
58
77
|
const req = this.normalizeOptions(url, rawOpt);
|
|
59
78
|
const { timeoutSeconds, mode, init: { method }, } = req;
|
|
@@ -66,7 +85,26 @@ export class Fetcher {
|
|
|
66
85
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
67
86
|
}, timeoutSeconds * 1000);
|
|
68
87
|
}
|
|
69
|
-
|
|
88
|
+
try {
|
|
89
|
+
for (var _g = true, _h = __asyncValues(this.cfg.hooks.beforeRequest || []), _j; _j = await _h.next(), _a = _j.done, !_a;) {
|
|
90
|
+
_c = _j.value;
|
|
91
|
+
_g = false;
|
|
92
|
+
try {
|
|
93
|
+
const hook = _c;
|
|
94
|
+
await hook(req);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
_g = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
102
|
+
finally {
|
|
103
|
+
try {
|
|
104
|
+
if (!_g && !_a && (_b = _h.return)) await _b.call(_h);
|
|
105
|
+
}
|
|
106
|
+
finally { if (e_1) throw e_1.error; }
|
|
107
|
+
}
|
|
70
108
|
const res = {
|
|
71
109
|
req,
|
|
72
110
|
retryStatus: {
|
|
@@ -130,38 +168,57 @@ export class Fetcher {
|
|
|
130
168
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
131
169
|
// method: req.method,
|
|
132
170
|
url: req.url })));
|
|
133
|
-
// We don't log errors when they are also thrown,
|
|
134
|
-
// otherwise it gets logged twice: here, and upstream
|
|
135
|
-
// if (this.cfg.logResponse) {
|
|
136
|
-
// const { retryAttempt } = res.retryStatus
|
|
137
|
-
// logger.error(
|
|
138
|
-
// [
|
|
139
|
-
// [
|
|
140
|
-
// ' <<',
|
|
141
|
-
// res.fetchResponse.status,
|
|
142
|
-
// signature,
|
|
143
|
-
// retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
144
|
-
// _since(started),
|
|
145
|
-
// ]
|
|
146
|
-
// .filter(Boolean)
|
|
147
|
-
// .join(' '),
|
|
148
|
-
// _stringifyAny(body),
|
|
149
|
-
// ].join('\n'),
|
|
150
|
-
// )
|
|
151
|
-
// }
|
|
152
171
|
await this.processRetry(res);
|
|
153
172
|
}
|
|
154
173
|
}
|
|
155
|
-
|
|
174
|
+
try {
|
|
175
|
+
for (var _k = true, _l = __asyncValues(this.cfg.hooks.afterResponse || []), _m; _m = await _l.next(), _d = _m.done, !_d;) {
|
|
176
|
+
_f = _m.value;
|
|
177
|
+
_k = false;
|
|
178
|
+
try {
|
|
179
|
+
const hook = _f;
|
|
180
|
+
await hook(res);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
_k = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
188
|
+
finally {
|
|
189
|
+
try {
|
|
190
|
+
if (!_k && !_d && (_e = _l.return)) await _e.call(_l);
|
|
191
|
+
}
|
|
192
|
+
finally { if (e_2) throw e_2.error; }
|
|
193
|
+
}
|
|
156
194
|
return res;
|
|
157
195
|
}
|
|
158
196
|
async processRetry(res) {
|
|
159
|
-
var _a, _b;
|
|
197
|
+
var _a, e_3, _b, _c;
|
|
160
198
|
const { retryStatus } = res;
|
|
161
199
|
if (!this.shouldRetry(res)) {
|
|
162
200
|
retryStatus.retryStopped = true;
|
|
163
201
|
}
|
|
164
|
-
|
|
202
|
+
try {
|
|
203
|
+
for (var _d = true, _e = __asyncValues(this.cfg.hooks.beforeRetry || []), _f; _f = await _e.next(), _a = _f.done, !_a;) {
|
|
204
|
+
_c = _f.value;
|
|
205
|
+
_d = false;
|
|
206
|
+
try {
|
|
207
|
+
const hook = _c;
|
|
208
|
+
await hook(res);
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
_d = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
216
|
+
finally {
|
|
217
|
+
try {
|
|
218
|
+
if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
|
|
219
|
+
}
|
|
220
|
+
finally { if (e_3) throw e_3.error; }
|
|
221
|
+
}
|
|
165
222
|
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
166
223
|
if (retryStatus.retryAttempt >= count) {
|
|
167
224
|
retryStatus.retryStopped = true;
|
|
@@ -182,9 +239,9 @@ export class Fetcher {
|
|
|
182
239
|
if (method === 'post' && !retryPost)
|
|
183
240
|
return false;
|
|
184
241
|
const { statusFamily } = res;
|
|
185
|
-
if (statusFamily ===
|
|
242
|
+
if (statusFamily === 5 && !retry5xx)
|
|
186
243
|
return false;
|
|
187
|
-
if (statusFamily ===
|
|
244
|
+
if (statusFamily === 4 && !retry4xx)
|
|
188
245
|
return false;
|
|
189
246
|
return true; // default is true
|
|
190
247
|
}
|
|
@@ -194,15 +251,15 @@ export class Fetcher {
|
|
|
194
251
|
if (!status)
|
|
195
252
|
return;
|
|
196
253
|
if (status >= 500)
|
|
197
|
-
return
|
|
254
|
+
return 5;
|
|
198
255
|
if (status >= 400)
|
|
199
|
-
return
|
|
256
|
+
return 4;
|
|
200
257
|
if (status >= 300)
|
|
201
|
-
return
|
|
258
|
+
return 3;
|
|
202
259
|
if (status >= 200)
|
|
203
|
-
return
|
|
260
|
+
return 2;
|
|
204
261
|
if (status >= 100)
|
|
205
|
-
return
|
|
262
|
+
return 1;
|
|
206
263
|
}
|
|
207
264
|
/**
|
|
208
265
|
* Returns url without baseUrl and before ?queryString
|
|
@@ -219,8 +276,9 @@ export class Fetcher {
|
|
|
219
276
|
console.warn(`Fetcher: baseUrl should not end with /`);
|
|
220
277
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
221
278
|
}
|
|
222
|
-
const { debug } = cfg;
|
|
279
|
+
const { debug = false } = cfg;
|
|
223
280
|
const norm = _merge({
|
|
281
|
+
baseUrl: '',
|
|
224
282
|
url: '',
|
|
225
283
|
searchParams: {},
|
|
226
284
|
timeoutSeconds: 30,
|
|
@@ -229,6 +287,7 @@ export class Fetcher {
|
|
|
229
287
|
retry4xx: false,
|
|
230
288
|
retry5xx: true,
|
|
231
289
|
logger: console,
|
|
290
|
+
debug,
|
|
232
291
|
logRequest: debug,
|
|
233
292
|
logRequestBody: debug,
|
|
234
293
|
logResponse: debug,
|
|
@@ -238,6 +297,7 @@ export class Fetcher {
|
|
|
238
297
|
method: 'get',
|
|
239
298
|
headers: {},
|
|
240
299
|
},
|
|
300
|
+
hooks: {},
|
|
241
301
|
}, cfg);
|
|
242
302
|
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
|
|
243
303
|
return norm;
|
|
@@ -249,8 +309,11 @@ export class Fetcher {
|
|
|
249
309
|
throwHttpErrors,
|
|
250
310
|
retryPost,
|
|
251
311
|
retry4xx,
|
|
252
|
-
retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init),
|
|
312
|
+
retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init),
|
|
313
|
+
// opt.init,
|
|
314
|
+
_filterUndefinedValues({
|
|
253
315
|
method: opt.method,
|
|
316
|
+
credentials: opt.credentials,
|
|
254
317
|
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
255
318
|
})) });
|
|
256
319
|
// setup url
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
package/package.json
CHANGED
package/src/http/fetcher.ts
CHANGED
|
@@ -15,14 +15,22 @@ import { pDelay } from '../promise/pDelay'
|
|
|
15
15
|
import { _jsonParseIfPossible } from '../string/json.util'
|
|
16
16
|
import { _since } from '../time/time.util'
|
|
17
17
|
import type { Promisable } from '../typeFest'
|
|
18
|
+
import { HTTP_METHODS } from './http.model'
|
|
18
19
|
import type { HttpMethod, HttpStatusFamily } from './http.model'
|
|
19
20
|
|
|
20
|
-
export interface FetcherNormalizedCfg extends FetcherCfg
|
|
21
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
21
22
|
logger: CommonLogger
|
|
22
23
|
searchParams: Record<string, any>
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>
|
|
27
|
+
export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>
|
|
28
|
+
export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>
|
|
29
|
+
|
|
25
30
|
export interface FetcherCfg {
|
|
31
|
+
/**
|
|
32
|
+
* Should **not** contain trailing slash.
|
|
33
|
+
*/
|
|
26
34
|
baseUrl?: string
|
|
27
35
|
|
|
28
36
|
/**
|
|
@@ -34,23 +42,30 @@ export interface FetcherCfg {
|
|
|
34
42
|
/**
|
|
35
43
|
* Allows to mutate req.
|
|
36
44
|
*/
|
|
37
|
-
beforeRequest
|
|
45
|
+
beforeRequest?: FetcherBeforeRequestHook[]
|
|
38
46
|
/**
|
|
39
47
|
* Allows to mutate res.
|
|
40
48
|
* If you set `res.err` - it will be thrown.
|
|
41
49
|
*/
|
|
42
|
-
|
|
50
|
+
afterResponse?: FetcherAfterResponseHook[]
|
|
43
51
|
/**
|
|
44
52
|
* Allows to mutate res.retryStatus to override retry behavior.
|
|
45
53
|
*/
|
|
46
|
-
beforeRetry
|
|
54
|
+
beforeRetry?: FetcherBeforeRetryHook[]
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* If true - enables all possible logging.
|
|
59
|
+
*/
|
|
49
60
|
debug?: boolean
|
|
50
61
|
logRequest?: boolean
|
|
51
62
|
logRequestBody?: boolean
|
|
52
63
|
logResponse?: boolean
|
|
53
64
|
logResponseBody?: boolean
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Defaults to `console`.
|
|
68
|
+
*/
|
|
54
69
|
logger?: CommonLogger
|
|
55
70
|
}
|
|
56
71
|
|
|
@@ -90,7 +105,13 @@ export interface FetcherOptions {
|
|
|
90
105
|
timeoutSeconds?: number
|
|
91
106
|
json?: any
|
|
92
107
|
text?: string
|
|
93
|
-
|
|
108
|
+
|
|
109
|
+
credentials?: RequestCredentials
|
|
110
|
+
|
|
111
|
+
// Removing RequestInit from options to simplify FetcherOptions interface.
|
|
112
|
+
// Will instead only add hand-picked useful options, such as `credentials`.
|
|
113
|
+
// init?: Partial<RequestInitNormalized>
|
|
114
|
+
|
|
94
115
|
headers?: Record<string, any>
|
|
95
116
|
mode?: FetcherMode // default to undefined (void response)
|
|
96
117
|
|
|
@@ -159,56 +180,88 @@ const defRetryOptions: FetcherRetryOptions = {
|
|
|
159
180
|
export class Fetcher {
|
|
160
181
|
private constructor(cfg: FetcherCfg & FetcherOptions = {}) {
|
|
161
182
|
this.cfg = this.normalizeCfg(cfg)
|
|
162
|
-
}
|
|
163
183
|
|
|
164
|
-
|
|
184
|
+
// Dynamically create all helper methods
|
|
185
|
+
HTTP_METHODS.forEach(method => {
|
|
186
|
+
// mode=void
|
|
187
|
+
this[method] = async (url: string, opt?: FetcherOptions): Promise<void> => {
|
|
188
|
+
return await this.fetch<void>(url, {
|
|
189
|
+
...opt,
|
|
190
|
+
method,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
165
193
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
// mode=text
|
|
195
|
+
;(this as any)[`${method}Text`] = async (
|
|
196
|
+
url: string,
|
|
197
|
+
opt?: FetcherOptions,
|
|
198
|
+
): Promise<string> => {
|
|
199
|
+
return await this.fetch<string>(url, {
|
|
200
|
+
...opt,
|
|
201
|
+
method,
|
|
202
|
+
mode: 'text',
|
|
203
|
+
})
|
|
204
|
+
}
|
|
169
205
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
206
|
+
// mode=json
|
|
207
|
+
;(this as any)[`${method}Json`] = async <T = unknown>(
|
|
208
|
+
url: string,
|
|
209
|
+
opt?: FetcherOptions,
|
|
210
|
+
): Promise<T> => {
|
|
211
|
+
return await this.fetch<T>(url, {
|
|
212
|
+
...opt,
|
|
213
|
+
method,
|
|
214
|
+
mode: 'json',
|
|
215
|
+
})
|
|
216
|
+
}
|
|
181
217
|
})
|
|
182
218
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Add BeforeRequest hook at the end of the hooks list.
|
|
222
|
+
*/
|
|
223
|
+
onBeforeRequest(hook: FetcherBeforeRequestHook): this {
|
|
224
|
+
;(this.cfg.hooks.beforeRequest ||= []).push(hook)
|
|
225
|
+
return this
|
|
189
226
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
mode: 'json',
|
|
195
|
-
})
|
|
227
|
+
|
|
228
|
+
onAfterResponse(hook: FetcherAfterResponseHook): this {
|
|
229
|
+
;(this.cfg.hooks.afterResponse ||= []).push(hook)
|
|
230
|
+
return this
|
|
196
231
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
mode: 'json',
|
|
202
|
-
})
|
|
232
|
+
|
|
233
|
+
onBeforeRetry(hook: FetcherBeforeRetryHook): this {
|
|
234
|
+
;(this.cfg.hooks.beforeRetry ||= []).push(hook)
|
|
235
|
+
return this
|
|
203
236
|
}
|
|
204
237
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
})
|
|
238
|
+
public cfg: FetcherNormalizedCfg
|
|
239
|
+
|
|
240
|
+
static create(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
|
|
241
|
+
return new Fetcher(cfg)
|
|
210
242
|
}
|
|
211
243
|
|
|
244
|
+
// These methods are generated dynamically in the constructor
|
|
245
|
+
get!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
246
|
+
post!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
247
|
+
put!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
248
|
+
patch!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
249
|
+
delete!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
250
|
+
head!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
251
|
+
|
|
252
|
+
getText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
253
|
+
postText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
254
|
+
putText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
255
|
+
patchText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
256
|
+
deleteText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
257
|
+
|
|
258
|
+
getJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
259
|
+
postJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
260
|
+
putJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
261
|
+
patchJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
262
|
+
deleteJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
263
|
+
// headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
264
|
+
|
|
212
265
|
async fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
213
266
|
const res = await this.rawFetch<T>(url, opt)
|
|
214
267
|
if (res.err) {
|
|
@@ -241,7 +294,9 @@ export class Fetcher {
|
|
|
241
294
|
}, timeoutSeconds * 1000) as any as number
|
|
242
295
|
}
|
|
243
296
|
|
|
244
|
-
await this.cfg.hooks
|
|
297
|
+
for await (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
298
|
+
await hook(req)
|
|
299
|
+
}
|
|
245
300
|
|
|
246
301
|
const res: FetcherResponse<any> = {
|
|
247
302
|
req,
|
|
@@ -330,31 +385,13 @@ export class Fetcher {
|
|
|
330
385
|
}),
|
|
331
386
|
)
|
|
332
387
|
|
|
333
|
-
// We don't log errors when they are also thrown,
|
|
334
|
-
// otherwise it gets logged twice: here, and upstream
|
|
335
|
-
// if (this.cfg.logResponse) {
|
|
336
|
-
// const { retryAttempt } = res.retryStatus
|
|
337
|
-
// logger.error(
|
|
338
|
-
// [
|
|
339
|
-
// [
|
|
340
|
-
// ' <<',
|
|
341
|
-
// res.fetchResponse.status,
|
|
342
|
-
// signature,
|
|
343
|
-
// retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
344
|
-
// _since(started),
|
|
345
|
-
// ]
|
|
346
|
-
// .filter(Boolean)
|
|
347
|
-
// .join(' '),
|
|
348
|
-
// _stringifyAny(body),
|
|
349
|
-
// ].join('\n'),
|
|
350
|
-
// )
|
|
351
|
-
// }
|
|
352
|
-
|
|
353
388
|
await this.processRetry(res)
|
|
354
389
|
}
|
|
355
390
|
}
|
|
356
391
|
|
|
357
|
-
await this.cfg.hooks
|
|
392
|
+
for await (const hook of this.cfg.hooks.afterResponse || []) {
|
|
393
|
+
await hook(res)
|
|
394
|
+
}
|
|
358
395
|
|
|
359
396
|
return res
|
|
360
397
|
}
|
|
@@ -366,7 +403,9 @@ export class Fetcher {
|
|
|
366
403
|
retryStatus.retryStopped = true
|
|
367
404
|
}
|
|
368
405
|
|
|
369
|
-
await this.cfg.hooks
|
|
406
|
+
for await (const hook of this.cfg.hooks.beforeRetry || []) {
|
|
407
|
+
await hook(res)
|
|
408
|
+
}
|
|
370
409
|
|
|
371
410
|
const { count, timeoutMultiplier, timeoutMax } = res.req.retry
|
|
372
411
|
|
|
@@ -391,19 +430,19 @@ export class Fetcher {
|
|
|
391
430
|
const { method } = res.req.init
|
|
392
431
|
if (method === 'post' && !retryPost) return false
|
|
393
432
|
const { statusFamily } = res
|
|
394
|
-
if (statusFamily ===
|
|
395
|
-
if (statusFamily ===
|
|
433
|
+
if (statusFamily === 5 && !retry5xx) return false
|
|
434
|
+
if (statusFamily === 4 && !retry4xx) return false
|
|
396
435
|
return true // default is true
|
|
397
436
|
}
|
|
398
437
|
|
|
399
438
|
private getStatusFamily(res: FetcherResponse): HttpStatusFamily | undefined {
|
|
400
439
|
const status = res.fetchResponse?.status
|
|
401
440
|
if (!status) return
|
|
402
|
-
if (status >= 500) return
|
|
403
|
-
if (status >= 400) return
|
|
404
|
-
if (status >= 300) return
|
|
405
|
-
if (status >= 200) return
|
|
406
|
-
if (status >= 100) return
|
|
441
|
+
if (status >= 500) return 5
|
|
442
|
+
if (status >= 400) return 4
|
|
443
|
+
if (status >= 300) return 3
|
|
444
|
+
if (status >= 200) return 2
|
|
445
|
+
if (status >= 100) return 1
|
|
407
446
|
}
|
|
408
447
|
|
|
409
448
|
/**
|
|
@@ -421,10 +460,11 @@ export class Fetcher {
|
|
|
421
460
|
console.warn(`Fetcher: baseUrl should not end with /`)
|
|
422
461
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1)
|
|
423
462
|
}
|
|
424
|
-
const { debug } = cfg
|
|
463
|
+
const { debug = false } = cfg
|
|
425
464
|
|
|
426
465
|
const norm: FetcherNormalizedCfg = _merge(
|
|
427
466
|
{
|
|
467
|
+
baseUrl: '',
|
|
428
468
|
url: '',
|
|
429
469
|
searchParams: {},
|
|
430
470
|
timeoutSeconds: 30,
|
|
@@ -433,6 +473,7 @@ export class Fetcher {
|
|
|
433
473
|
retry4xx: false,
|
|
434
474
|
retry5xx: true,
|
|
435
475
|
logger: console,
|
|
476
|
+
debug,
|
|
436
477
|
logRequest: debug,
|
|
437
478
|
logRequestBody: debug,
|
|
438
479
|
logResponse: debug,
|
|
@@ -442,6 +483,7 @@ export class Fetcher {
|
|
|
442
483
|
method: 'get',
|
|
443
484
|
headers: {},
|
|
444
485
|
},
|
|
486
|
+
hooks: {},
|
|
445
487
|
},
|
|
446
488
|
cfg,
|
|
447
489
|
)
|
|
@@ -469,11 +511,12 @@ export class Fetcher {
|
|
|
469
511
|
},
|
|
470
512
|
init: _merge(
|
|
471
513
|
{ ...this.cfg.init },
|
|
472
|
-
opt.init,
|
|
514
|
+
// opt.init,
|
|
473
515
|
_filterUndefinedValues({
|
|
474
516
|
method: opt.method,
|
|
517
|
+
credentials: opt.credentials,
|
|
475
518
|
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
476
|
-
}),
|
|
519
|
+
} as RequestInit),
|
|
477
520
|
),
|
|
478
521
|
}
|
|
479
522
|
|
package/src/http/http.model.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'
|
|
2
2
|
|
|
3
|
-
export type HttpStatusFamily =
|
|
3
|
+
export type HttpStatusFamily = 5 | 4 | 3 | 2 | 1
|
|
4
|
+
|
|
5
|
+
export const HTTP_METHODS: HttpMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head']
|