@naturalcycles/js-lib 14.118.0 → 14.119.1
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/error/app.error.js +10 -10
- package/dist/http/fetcher.d.ts +15 -14
- package/dist/http/fetcher.js +95 -71
- package/dist/object/object.util.d.ts +2 -0
- package/dist/object/object.util.js +15 -12
- package/dist-esm/error/app.error.js +10 -10
- package/dist-esm/http/fetcher.js +95 -63
- package/dist-esm/object/object.util.js +16 -12
- package/package.json +1 -1
- package/src/error/app.error.ts +10 -9
- package/src/http/fetcher.ts +143 -98
- package/src/object/object.util.ts +12 -10
package/dist/error/app.error.js
CHANGED
|
@@ -23,16 +23,16 @@ class AppError extends Error {
|
|
|
23
23
|
configurable: true,
|
|
24
24
|
enumerable: false,
|
|
25
25
|
});
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
else {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
26
|
+
// todo: check if it's needed at all!
|
|
27
|
+
// if (Error.captureStackTrace) {
|
|
28
|
+
// Error.captureStackTrace(this, this.constructor)
|
|
29
|
+
// } else {
|
|
30
|
+
// Object.defineProperty(this, 'stack', {
|
|
31
|
+
// value: new Error().stack, // eslint-disable-line unicorn/error-message
|
|
32
|
+
// writable: true,
|
|
33
|
+
// configurable: true,
|
|
34
|
+
// })
|
|
35
|
+
// }
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
exports.AppError = AppError;
|
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
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 FetcherCfg, FetcherRequest {
|
|
6
6
|
logger: CommonLogger;
|
|
7
|
+
searchParams: Record<string, any>;
|
|
7
8
|
}
|
|
8
9
|
export interface FetcherCfg {
|
|
9
10
|
baseUrl?: string;
|
|
@@ -45,8 +46,9 @@ export interface FetcherRetryOptions {
|
|
|
45
46
|
timeoutMax: number;
|
|
46
47
|
timeoutMultiplier: number;
|
|
47
48
|
}
|
|
48
|
-
export interface
|
|
49
|
-
|
|
49
|
+
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
|
|
50
|
+
url: string;
|
|
51
|
+
init: RequestInitNormalized;
|
|
50
52
|
throwHttpErrors: boolean;
|
|
51
53
|
timeoutSeconds: number;
|
|
52
54
|
retry: FetcherRetryOptions;
|
|
@@ -66,10 +68,10 @@ export interface FetcherOptions {
|
|
|
66
68
|
timeoutSeconds?: number;
|
|
67
69
|
json?: any;
|
|
68
70
|
text?: string;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
71
|
+
init?: Partial<RequestInitNormalized>;
|
|
72
|
+
headers?: Record<string, any>;
|
|
72
73
|
mode?: FetcherMode;
|
|
74
|
+
searchParams?: Record<string, any>;
|
|
73
75
|
/**
|
|
74
76
|
* Default is 2 retries (3 tries in total).
|
|
75
77
|
* Pass `retry: { count: 0 }` to disable retries.
|
|
@@ -89,13 +91,10 @@ export interface FetcherOptions {
|
|
|
89
91
|
*/
|
|
90
92
|
retry5xx?: boolean;
|
|
91
93
|
}
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
};
|
|
97
|
-
opt: FetcherNormalizedOptions;
|
|
98
|
-
}
|
|
94
|
+
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
95
|
+
method: HttpMethod;
|
|
96
|
+
headers: Record<string, any>;
|
|
97
|
+
};
|
|
99
98
|
export interface FetcherSuccessResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
100
99
|
err?: undefined;
|
|
101
100
|
fetchResponse: Response;
|
|
@@ -125,8 +124,10 @@ export declare class Fetcher {
|
|
|
125
124
|
static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
|
|
126
125
|
getJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
127
126
|
postJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
127
|
+
putJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
128
|
+
patchJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
129
|
+
deleteJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
128
130
|
getText(url: string, opt?: FetcherOptions): Promise<string>;
|
|
129
|
-
postText(url: string, opt?: FetcherOptions): Promise<string>;
|
|
130
131
|
fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
131
132
|
rawFetch<T = unknown>(url: string, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
|
|
132
133
|
private processRetry;
|
package/dist/http/fetcher.js
CHANGED
|
@@ -8,9 +8,7 @@ const number_util_1 = require("../number/number.util");
|
|
|
8
8
|
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
|
-
const stringifyAny_1 = require("../string/stringifyAny");
|
|
12
11
|
const time_util_1 = require("../time/time.util");
|
|
13
|
-
const types_1 = require("../types");
|
|
14
12
|
const defRetryOptions = {
|
|
15
13
|
count: 2,
|
|
16
14
|
timeout: 500,
|
|
@@ -30,68 +28,59 @@ class Fetcher {
|
|
|
30
28
|
static create(cfg = {}) {
|
|
31
29
|
return new Fetcher(cfg);
|
|
32
30
|
}
|
|
33
|
-
async getJson(url, opt
|
|
31
|
+
async getJson(url, opt) {
|
|
34
32
|
return await this.fetch(url, {
|
|
35
33
|
...opt,
|
|
36
34
|
mode: 'json',
|
|
37
35
|
});
|
|
38
36
|
}
|
|
39
|
-
async postJson(url, opt
|
|
37
|
+
async postJson(url, opt) {
|
|
40
38
|
return await this.fetch(url, {
|
|
41
39
|
...opt,
|
|
42
40
|
method: 'post',
|
|
43
41
|
mode: 'json',
|
|
44
42
|
});
|
|
45
43
|
}
|
|
46
|
-
async
|
|
44
|
+
async putJson(url, opt) {
|
|
47
45
|
return await this.fetch(url, {
|
|
48
46
|
...opt,
|
|
49
|
-
|
|
47
|
+
method: 'put',
|
|
48
|
+
mode: 'json',
|
|
50
49
|
});
|
|
51
50
|
}
|
|
52
|
-
async
|
|
51
|
+
async patchJson(url, opt) {
|
|
52
|
+
return await this.fetch(url, {
|
|
53
|
+
...opt,
|
|
54
|
+
method: 'patch',
|
|
55
|
+
mode: 'json',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async deleteJson(url, opt) {
|
|
59
|
+
return await this.fetch(url, {
|
|
60
|
+
...opt,
|
|
61
|
+
method: 'delete',
|
|
62
|
+
mode: 'json',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async getText(url, opt) {
|
|
53
66
|
return await this.fetch(url, {
|
|
54
67
|
...opt,
|
|
55
|
-
method: 'post',
|
|
56
68
|
mode: 'text',
|
|
57
69
|
});
|
|
58
70
|
}
|
|
59
|
-
async fetch(url, opt
|
|
71
|
+
async fetch(url, opt) {
|
|
60
72
|
const res = await this.rawFetch(url, opt);
|
|
61
73
|
if (res.err) {
|
|
62
|
-
if (res.req.
|
|
74
|
+
if (res.req.throwHttpErrors)
|
|
63
75
|
throw res.err;
|
|
64
76
|
return res;
|
|
65
77
|
}
|
|
66
78
|
return res.body;
|
|
67
79
|
}
|
|
68
80
|
async rawFetch(url, rawOpt = {}) {
|
|
69
|
-
const {
|
|
70
|
-
const
|
|
71
|
-
const {
|
|
72
|
-
const req = {
|
|
73
|
-
url,
|
|
74
|
-
init: {
|
|
75
|
-
...this.cfg.requestInit,
|
|
76
|
-
method,
|
|
77
|
-
},
|
|
78
|
-
opt,
|
|
79
|
-
};
|
|
80
|
-
// setup url
|
|
81
|
-
if (baseUrl) {
|
|
82
|
-
if (url.startsWith('/')) {
|
|
83
|
-
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
84
|
-
url = url.slice(1);
|
|
85
|
-
}
|
|
86
|
-
req.url = `${baseUrl}/${url}`;
|
|
87
|
-
}
|
|
88
|
-
// setup request body
|
|
89
|
-
if (opt.json !== undefined) {
|
|
90
|
-
req.init.body = JSON.stringify(opt.json);
|
|
91
|
-
}
|
|
92
|
-
else if (opt.text !== undefined) {
|
|
93
|
-
req.init.body = opt.text;
|
|
94
|
-
}
|
|
81
|
+
const { logger } = this.cfg;
|
|
82
|
+
const req = this.normalizeOptions(url, rawOpt);
|
|
83
|
+
const { timeoutSeconds, mode, init: { method }, } = req;
|
|
95
84
|
// setup timeout
|
|
96
85
|
let timeout;
|
|
97
86
|
if (timeoutSeconds) {
|
|
@@ -101,16 +90,13 @@ class Fetcher {
|
|
|
101
90
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
102
91
|
}, timeoutSeconds * 1000);
|
|
103
92
|
}
|
|
104
|
-
if (opt.requestInit) {
|
|
105
|
-
(0, types_1._objectAssign)(req.init, opt.requestInit);
|
|
106
|
-
}
|
|
107
93
|
await this.cfg.hooks?.beforeRequest?.(req);
|
|
108
94
|
const res = {
|
|
109
95
|
req,
|
|
110
96
|
retryStatus: {
|
|
111
97
|
retryAttempt: 0,
|
|
112
98
|
retryStopped: false,
|
|
113
|
-
retryTimeout:
|
|
99
|
+
retryTimeout: req.retry.timeout,
|
|
114
100
|
},
|
|
115
101
|
};
|
|
116
102
|
const shortUrl = this.getShortUrl(req.url);
|
|
@@ -120,7 +106,7 @@ class Fetcher {
|
|
|
120
106
|
const started = Date.now();
|
|
121
107
|
if (this.cfg.logRequest) {
|
|
122
108
|
const { retryAttempt } = res.retryStatus;
|
|
123
|
-
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${
|
|
109
|
+
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
124
110
|
.filter(Boolean)
|
|
125
111
|
.join(' '));
|
|
126
112
|
if (this.cfg.logRequestBody && req.init.body) {
|
|
@@ -146,7 +132,7 @@ class Fetcher {
|
|
|
146
132
|
' <<',
|
|
147
133
|
res.fetchResponse.status,
|
|
148
134
|
signature,
|
|
149
|
-
retryAttempt && `try#${retryAttempt + 1}/${
|
|
135
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
150
136
|
(0, time_util_1._since)(started),
|
|
151
137
|
]
|
|
152
138
|
.filter(Boolean)
|
|
@@ -173,21 +159,25 @@ class Fetcher {
|
|
|
173
159
|
url: req.url,
|
|
174
160
|
// tryCount: req.tryCount,
|
|
175
161
|
}));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
// }
|
|
191
181
|
await this.processRetry(res);
|
|
192
182
|
}
|
|
193
183
|
}
|
|
@@ -200,12 +190,13 @@ class Fetcher {
|
|
|
200
190
|
retryStatus.retryStopped = true;
|
|
201
191
|
}
|
|
202
192
|
await this.cfg.hooks?.beforeRetry?.(res);
|
|
203
|
-
const { count, timeoutMultiplier, timeoutMax } = res.req.
|
|
193
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
204
194
|
if (retryStatus.retryAttempt >= count) {
|
|
205
195
|
retryStatus.retryStopped = true;
|
|
206
196
|
}
|
|
207
197
|
if (retryStatus.retryStopped)
|
|
208
198
|
return;
|
|
199
|
+
retryStatus.retryAttempt++;
|
|
209
200
|
retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
210
201
|
await (0, pDelay_1.pDelay)(retryStatus.retryTimeout);
|
|
211
202
|
}
|
|
@@ -214,7 +205,7 @@ class Fetcher {
|
|
|
214
205
|
* unless there's reason not to (e.g method is POST).
|
|
215
206
|
*/
|
|
216
207
|
shouldRetry(res) {
|
|
217
|
-
const { retryPost, retry4xx, retry5xx } = res.req
|
|
208
|
+
const { retryPost, retry4xx, retry5xx } = res.req;
|
|
218
209
|
const { method } = res.req.init;
|
|
219
210
|
if (method === 'post' && !retryPost)
|
|
220
211
|
return false;
|
|
@@ -255,9 +246,10 @@ class Fetcher {
|
|
|
255
246
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
256
247
|
}
|
|
257
248
|
const { debug } = cfg;
|
|
258
|
-
|
|
249
|
+
const norm = (0, object_util_1._merge)({
|
|
250
|
+
url: '',
|
|
251
|
+
searchParams: {},
|
|
259
252
|
timeoutSeconds: 30,
|
|
260
|
-
method: 'get',
|
|
261
253
|
throwHttpErrors: true,
|
|
262
254
|
retryPost: false,
|
|
263
255
|
retry4xx: false,
|
|
@@ -267,28 +259,60 @@ class Fetcher {
|
|
|
267
259
|
logRequestBody: debug,
|
|
268
260
|
logResponse: debug,
|
|
269
261
|
logResponseBody: debug,
|
|
270
|
-
...
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
262
|
+
retry: { ...defRetryOptions },
|
|
263
|
+
init: {
|
|
264
|
+
method: 'get',
|
|
265
|
+
headers: {},
|
|
274
266
|
},
|
|
275
|
-
};
|
|
267
|
+
}, cfg);
|
|
268
|
+
norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
|
|
269
|
+
return norm;
|
|
276
270
|
}
|
|
277
|
-
normalizeOptions(opt) {
|
|
278
|
-
const { timeoutSeconds, throwHttpErrors,
|
|
279
|
-
|
|
271
|
+
normalizeOptions(url, opt) {
|
|
272
|
+
const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } = this.cfg;
|
|
273
|
+
const req = {
|
|
274
|
+
url,
|
|
280
275
|
timeoutSeconds,
|
|
281
276
|
throwHttpErrors,
|
|
282
|
-
method,
|
|
283
277
|
retryPost,
|
|
284
278
|
retry4xx,
|
|
285
279
|
retry5xx,
|
|
286
|
-
...opt,
|
|
280
|
+
...(0, object_util_1._omit)(opt, ['method', 'headers']),
|
|
287
281
|
retry: {
|
|
288
282
|
...retry,
|
|
289
283
|
...(0, object_util_1._filterUndefinedValues)(opt.retry || {}),
|
|
290
284
|
},
|
|
285
|
+
init: (0, object_util_1._merge)({ ...this.cfg.init }, opt.init, (0, object_util_1._filterUndefinedValues)({
|
|
286
|
+
method: opt.method,
|
|
287
|
+
headers: (0, object_util_1._mapKeys)(opt.headers || {}, k => k.toLowerCase()),
|
|
288
|
+
})),
|
|
289
|
+
};
|
|
290
|
+
// setup url
|
|
291
|
+
if (baseUrl) {
|
|
292
|
+
if (url.startsWith('/')) {
|
|
293
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
294
|
+
url = url.slice(1);
|
|
295
|
+
}
|
|
296
|
+
req.url = `${baseUrl}/${url}`;
|
|
297
|
+
}
|
|
298
|
+
const searchParams = {
|
|
299
|
+
...this.cfg.searchParams,
|
|
300
|
+
...opt.searchParams,
|
|
291
301
|
};
|
|
302
|
+
if (Object.keys(searchParams).length) {
|
|
303
|
+
const qs = new URLSearchParams(searchParams).toString();
|
|
304
|
+
req.url += req.url.includes('?') ? '&' : '?' + qs;
|
|
305
|
+
}
|
|
306
|
+
// setup request body
|
|
307
|
+
if (opt.json !== undefined) {
|
|
308
|
+
req.init.body = JSON.stringify(opt.json);
|
|
309
|
+
req.init.headers['content-type'] = 'application/json';
|
|
310
|
+
}
|
|
311
|
+
else if (opt.text !== undefined) {
|
|
312
|
+
req.init.body = opt.text;
|
|
313
|
+
req.init.headers['content-type'] = 'text/plain';
|
|
314
|
+
}
|
|
315
|
+
return req;
|
|
292
316
|
}
|
|
293
317
|
}
|
|
294
318
|
exports.Fetcher = Fetcher;
|
|
@@ -101,6 +101,8 @@ export declare function _filterEmptyValues<T extends AnyObject>(obj: T, mutate?:
|
|
|
101
101
|
* are applied from left to right. Subsequent sources overwrite property
|
|
102
102
|
* assignments of previous sources.
|
|
103
103
|
*
|
|
104
|
+
* Works as "recursive Object.assign".
|
|
105
|
+
*
|
|
104
106
|
* **Note:** This method mutates `object`.
|
|
105
107
|
*
|
|
106
108
|
* @category Object
|
|
@@ -186,6 +186,8 @@ exports._filterEmptyValues = _filterEmptyValues;
|
|
|
186
186
|
* are applied from left to right. Subsequent sources overwrite property
|
|
187
187
|
* assignments of previous sources.
|
|
188
188
|
*
|
|
189
|
+
* Works as "recursive Object.assign".
|
|
190
|
+
*
|
|
189
191
|
* **Note:** This method mutates `object`.
|
|
190
192
|
*
|
|
191
193
|
* @category Object
|
|
@@ -209,18 +211,19 @@ exports._filterEmptyValues = _filterEmptyValues;
|
|
|
209
211
|
*/
|
|
210
212
|
function _merge(target, ...sources) {
|
|
211
213
|
sources.forEach(source => {
|
|
212
|
-
if ((0, is_util_1._isObject)(source))
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
214
|
+
if (!(0, is_util_1._isObject)(source))
|
|
215
|
+
return;
|
|
216
|
+
Object.keys(source).forEach(key => {
|
|
217
|
+
if ((0, is_util_1._isObject)(source[key])) {
|
|
218
|
+
;
|
|
219
|
+
target[key] ||= {};
|
|
220
|
+
_merge(target[key], source[key]);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
;
|
|
224
|
+
target[key] = source[key];
|
|
225
|
+
}
|
|
226
|
+
});
|
|
224
227
|
});
|
|
225
228
|
return target;
|
|
226
229
|
}
|
|
@@ -20,15 +20,15 @@ export class AppError extends Error {
|
|
|
20
20
|
configurable: true,
|
|
21
21
|
enumerable: false,
|
|
22
22
|
});
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
else {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
23
|
+
// todo: check if it's needed at all!
|
|
24
|
+
// if (Error.captureStackTrace) {
|
|
25
|
+
// Error.captureStackTrace(this, this.constructor)
|
|
26
|
+
// } else {
|
|
27
|
+
// Object.defineProperty(this, 'stack', {
|
|
28
|
+
// value: new Error().stack, // eslint-disable-line unicorn/error-message
|
|
29
|
+
// writable: true,
|
|
30
|
+
// configurable: true,
|
|
31
|
+
// })
|
|
32
|
+
// }
|
|
33
33
|
}
|
|
34
34
|
}
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import { _anyToErrorObject } from '../error/error.util';
|
|
3
3
|
import { HttpError } from '../error/http.error';
|
|
4
4
|
import { _clamp } from '../number/number.util';
|
|
5
|
-
import { _filterNullishValues, _filterUndefinedValues } from '../object/object.util';
|
|
5
|
+
import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, } from '../object/object.util';
|
|
6
6
|
import { pDelay } from '../promise/pDelay';
|
|
7
7
|
import { _jsonParseIfPossible } from '../string/json.util';
|
|
8
|
-
import { _stringifyAny } from '../string/stringifyAny';
|
|
9
8
|
import { _since } from '../time/time.util';
|
|
10
|
-
import { _objectAssign } from '../types';
|
|
11
9
|
const defRetryOptions = {
|
|
12
10
|
count: 2,
|
|
13
11
|
timeout: 500,
|
|
@@ -27,22 +25,28 @@ export class Fetcher {
|
|
|
27
25
|
static create(cfg = {}) {
|
|
28
26
|
return new Fetcher(cfg);
|
|
29
27
|
}
|
|
30
|
-
async getJson(url, opt
|
|
28
|
+
async getJson(url, opt) {
|
|
31
29
|
return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: 'json' }));
|
|
32
30
|
}
|
|
33
|
-
async postJson(url, opt
|
|
31
|
+
async postJson(url, opt) {
|
|
34
32
|
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'post', mode: 'json' }));
|
|
35
33
|
}
|
|
36
|
-
async
|
|
37
|
-
return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: '
|
|
34
|
+
async putJson(url, opt) {
|
|
35
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'put', mode: 'json' }));
|
|
36
|
+
}
|
|
37
|
+
async patchJson(url, opt) {
|
|
38
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'patch', mode: 'json' }));
|
|
39
|
+
}
|
|
40
|
+
async deleteJson(url, opt) {
|
|
41
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'delete', mode: 'json' }));
|
|
38
42
|
}
|
|
39
|
-
async
|
|
40
|
-
return await this.fetch(url, Object.assign(Object.assign({}, opt), {
|
|
43
|
+
async getText(url, opt) {
|
|
44
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: 'text' }));
|
|
41
45
|
}
|
|
42
|
-
async fetch(url, opt
|
|
46
|
+
async fetch(url, opt) {
|
|
43
47
|
const res = await this.rawFetch(url, opt);
|
|
44
48
|
if (res.err) {
|
|
45
|
-
if (res.req.
|
|
49
|
+
if (res.req.throwHttpErrors)
|
|
46
50
|
throw res.err;
|
|
47
51
|
return res;
|
|
48
52
|
}
|
|
@@ -50,29 +54,9 @@ export class Fetcher {
|
|
|
50
54
|
}
|
|
51
55
|
async rawFetch(url, rawOpt = {}) {
|
|
52
56
|
var _a, _b, _c, _d;
|
|
53
|
-
const {
|
|
54
|
-
const
|
|
55
|
-
const {
|
|
56
|
-
const req = {
|
|
57
|
-
url,
|
|
58
|
-
init: Object.assign(Object.assign({}, this.cfg.requestInit), { method }),
|
|
59
|
-
opt,
|
|
60
|
-
};
|
|
61
|
-
// setup url
|
|
62
|
-
if (baseUrl) {
|
|
63
|
-
if (url.startsWith('/')) {
|
|
64
|
-
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
65
|
-
url = url.slice(1);
|
|
66
|
-
}
|
|
67
|
-
req.url = `${baseUrl}/${url}`;
|
|
68
|
-
}
|
|
69
|
-
// setup request body
|
|
70
|
-
if (opt.json !== undefined) {
|
|
71
|
-
req.init.body = JSON.stringify(opt.json);
|
|
72
|
-
}
|
|
73
|
-
else if (opt.text !== undefined) {
|
|
74
|
-
req.init.body = opt.text;
|
|
75
|
-
}
|
|
57
|
+
const { logger } = this.cfg;
|
|
58
|
+
const req = this.normalizeOptions(url, rawOpt);
|
|
59
|
+
const { timeoutSeconds, mode, init: { method }, } = req;
|
|
76
60
|
// setup timeout
|
|
77
61
|
let timeout;
|
|
78
62
|
if (timeoutSeconds) {
|
|
@@ -82,16 +66,13 @@ export class Fetcher {
|
|
|
82
66
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
83
67
|
}, timeoutSeconds * 1000);
|
|
84
68
|
}
|
|
85
|
-
if (opt.requestInit) {
|
|
86
|
-
_objectAssign(req.init, opt.requestInit);
|
|
87
|
-
}
|
|
88
69
|
await ((_b = (_a = this.cfg.hooks) === null || _a === void 0 ? void 0 : _a.beforeRequest) === null || _b === void 0 ? void 0 : _b.call(_a, req));
|
|
89
70
|
const res = {
|
|
90
71
|
req,
|
|
91
72
|
retryStatus: {
|
|
92
73
|
retryAttempt: 0,
|
|
93
74
|
retryStopped: false,
|
|
94
|
-
retryTimeout:
|
|
75
|
+
retryTimeout: req.retry.timeout,
|
|
95
76
|
},
|
|
96
77
|
};
|
|
97
78
|
const shortUrl = this.getShortUrl(req.url);
|
|
@@ -101,7 +82,7 @@ export class Fetcher {
|
|
|
101
82
|
const started = Date.now();
|
|
102
83
|
if (this.cfg.logRequest) {
|
|
103
84
|
const { retryAttempt } = res.retryStatus;
|
|
104
|
-
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${
|
|
85
|
+
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
105
86
|
.filter(Boolean)
|
|
106
87
|
.join(' '));
|
|
107
88
|
if (this.cfg.logRequestBody && req.init.body) {
|
|
@@ -127,7 +108,7 @@ export class Fetcher {
|
|
|
127
108
|
' <<',
|
|
128
109
|
res.fetchResponse.status,
|
|
129
110
|
signature,
|
|
130
|
-
retryAttempt && `try#${retryAttempt + 1}/${
|
|
111
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
131
112
|
_since(started),
|
|
132
113
|
]
|
|
133
114
|
.filter(Boolean)
|
|
@@ -149,21 +130,25 @@ export class Fetcher {
|
|
|
149
130
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
150
131
|
// method: req.method,
|
|
151
132
|
url: req.url })));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
// }
|
|
167
152
|
await this.processRetry(res);
|
|
168
153
|
}
|
|
169
154
|
}
|
|
@@ -177,12 +162,13 @@ export class Fetcher {
|
|
|
177
162
|
retryStatus.retryStopped = true;
|
|
178
163
|
}
|
|
179
164
|
await ((_b = (_a = this.cfg.hooks) === null || _a === void 0 ? void 0 : _a.beforeRetry) === null || _b === void 0 ? void 0 : _b.call(_a, res));
|
|
180
|
-
const { count, timeoutMultiplier, timeoutMax } = res.req.
|
|
165
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
181
166
|
if (retryStatus.retryAttempt >= count) {
|
|
182
167
|
retryStatus.retryStopped = true;
|
|
183
168
|
}
|
|
184
169
|
if (retryStatus.retryStopped)
|
|
185
170
|
return;
|
|
171
|
+
retryStatus.retryAttempt++;
|
|
186
172
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
187
173
|
await pDelay(retryStatus.retryTimeout);
|
|
188
174
|
}
|
|
@@ -191,7 +177,7 @@ export class Fetcher {
|
|
|
191
177
|
* unless there's reason not to (e.g method is POST).
|
|
192
178
|
*/
|
|
193
179
|
shouldRetry(res) {
|
|
194
|
-
const { retryPost, retry4xx, retry5xx } = res.req
|
|
180
|
+
const { retryPost, retry4xx, retry5xx } = res.req;
|
|
195
181
|
const { method } = res.req.init;
|
|
196
182
|
if (method === 'post' && !retryPost)
|
|
197
183
|
return false;
|
|
@@ -234,16 +220,62 @@ export class Fetcher {
|
|
|
234
220
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
235
221
|
}
|
|
236
222
|
const { debug } = cfg;
|
|
237
|
-
|
|
223
|
+
const norm = _merge({
|
|
224
|
+
url: '',
|
|
225
|
+
searchParams: {},
|
|
226
|
+
timeoutSeconds: 30,
|
|
227
|
+
throwHttpErrors: true,
|
|
228
|
+
retryPost: false,
|
|
229
|
+
retry4xx: false,
|
|
230
|
+
retry5xx: true,
|
|
231
|
+
logger: console,
|
|
232
|
+
logRequest: debug,
|
|
233
|
+
logRequestBody: debug,
|
|
234
|
+
logResponse: debug,
|
|
235
|
+
logResponseBody: debug,
|
|
236
|
+
retry: Object.assign({}, defRetryOptions),
|
|
237
|
+
init: {
|
|
238
|
+
method: 'get',
|
|
239
|
+
headers: {},
|
|
240
|
+
},
|
|
241
|
+
}, cfg);
|
|
242
|
+
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
|
|
243
|
+
return norm;
|
|
238
244
|
}
|
|
239
|
-
normalizeOptions(opt) {
|
|
240
|
-
const { timeoutSeconds, throwHttpErrors,
|
|
241
|
-
|
|
245
|
+
normalizeOptions(url, opt) {
|
|
246
|
+
const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } = this.cfg;
|
|
247
|
+
const req = Object.assign(Object.assign({ url,
|
|
248
|
+
timeoutSeconds,
|
|
242
249
|
throwHttpErrors,
|
|
243
|
-
method,
|
|
244
250
|
retryPost,
|
|
245
251
|
retry4xx,
|
|
246
|
-
retry5xx }, opt), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})) })
|
|
252
|
+
retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init), opt.init, _filterUndefinedValues({
|
|
253
|
+
method: opt.method,
|
|
254
|
+
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
255
|
+
})) });
|
|
256
|
+
// setup url
|
|
257
|
+
if (baseUrl) {
|
|
258
|
+
if (url.startsWith('/')) {
|
|
259
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
260
|
+
url = url.slice(1);
|
|
261
|
+
}
|
|
262
|
+
req.url = `${baseUrl}/${url}`;
|
|
263
|
+
}
|
|
264
|
+
const searchParams = Object.assign(Object.assign({}, this.cfg.searchParams), opt.searchParams);
|
|
265
|
+
if (Object.keys(searchParams).length) {
|
|
266
|
+
const qs = new URLSearchParams(searchParams).toString();
|
|
267
|
+
req.url += req.url.includes('?') ? '&' : '?' + qs;
|
|
268
|
+
}
|
|
269
|
+
// setup request body
|
|
270
|
+
if (opt.json !== undefined) {
|
|
271
|
+
req.init.body = JSON.stringify(opt.json);
|
|
272
|
+
req.init.headers['content-type'] = 'application/json';
|
|
273
|
+
}
|
|
274
|
+
else if (opt.text !== undefined) {
|
|
275
|
+
req.init.body = opt.text;
|
|
276
|
+
req.init.headers['content-type'] = 'text/plain';
|
|
277
|
+
}
|
|
278
|
+
return req;
|
|
247
279
|
}
|
|
248
280
|
}
|
|
249
281
|
export function getFetcher(cfg = {}) {
|
|
@@ -168,6 +168,8 @@ export function _filterEmptyValues(obj, mutate = false) {
|
|
|
168
168
|
* are applied from left to right. Subsequent sources overwrite property
|
|
169
169
|
* assignments of previous sources.
|
|
170
170
|
*
|
|
171
|
+
* Works as "recursive Object.assign".
|
|
172
|
+
*
|
|
171
173
|
* **Note:** This method mutates `object`.
|
|
172
174
|
*
|
|
173
175
|
* @category Object
|
|
@@ -191,18 +193,20 @@ export function _filterEmptyValues(obj, mutate = false) {
|
|
|
191
193
|
*/
|
|
192
194
|
export function _merge(target, ...sources) {
|
|
193
195
|
sources.forEach(source => {
|
|
194
|
-
if (_isObject(source))
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
196
|
+
if (!_isObject(source))
|
|
197
|
+
return;
|
|
198
|
+
Object.keys(source).forEach(key => {
|
|
199
|
+
var _a;
|
|
200
|
+
if (_isObject(source[key])) {
|
|
201
|
+
;
|
|
202
|
+
(_a = target)[key] || (_a[key] = {});
|
|
203
|
+
_merge(target[key], source[key]);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
;
|
|
207
|
+
target[key] = source[key];
|
|
208
|
+
}
|
|
209
|
+
});
|
|
206
210
|
});
|
|
207
211
|
return target;
|
|
208
212
|
}
|
package/package.json
CHANGED
package/src/error/app.error.ts
CHANGED
|
@@ -27,14 +27,15 @@ export class AppError<DATA_TYPE extends ErrorData = ErrorData> extends Error {
|
|
|
27
27
|
enumerable: false,
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
if
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
30
|
+
// todo: check if it's needed at all!
|
|
31
|
+
// if (Error.captureStackTrace) {
|
|
32
|
+
// Error.captureStackTrace(this, this.constructor)
|
|
33
|
+
// } else {
|
|
34
|
+
// Object.defineProperty(this, 'stack', {
|
|
35
|
+
// value: new Error().stack, // eslint-disable-line unicorn/error-message
|
|
36
|
+
// writable: true,
|
|
37
|
+
// configurable: true,
|
|
38
|
+
// })
|
|
39
|
+
// }
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/http/fetcher.ts
CHANGED
|
@@ -4,17 +4,22 @@ import { _anyToErrorObject } from '../error/error.util'
|
|
|
4
4
|
import { HttpError } from '../error/http.error'
|
|
5
5
|
import { CommonLogger } from '../log/commonLogger'
|
|
6
6
|
import { _clamp } from '../number/number.util'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
_filterNullishValues,
|
|
9
|
+
_filterUndefinedValues,
|
|
10
|
+
_mapKeys,
|
|
11
|
+
_merge,
|
|
12
|
+
_omit,
|
|
13
|
+
} from '../object/object.util'
|
|
8
14
|
import { pDelay } from '../promise/pDelay'
|
|
9
15
|
import { _jsonParseIfPossible } from '../string/json.util'
|
|
10
|
-
import { _stringifyAny } from '../string/stringifyAny'
|
|
11
16
|
import { _since } from '../time/time.util'
|
|
12
17
|
import type { Promisable } from '../typeFest'
|
|
13
|
-
import { _objectAssign } from '../types'
|
|
14
18
|
import type { HttpMethod, HttpStatusFamily } from './http.model'
|
|
15
19
|
|
|
16
|
-
export interface FetcherNormalizedCfg extends FetcherCfg,
|
|
20
|
+
export interface FetcherNormalizedCfg extends FetcherCfg, FetcherRequest {
|
|
17
21
|
logger: CommonLogger
|
|
22
|
+
searchParams: Record<string, any>
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export interface FetcherCfg {
|
|
@@ -62,8 +67,9 @@ export interface FetcherRetryOptions {
|
|
|
62
67
|
timeoutMultiplier: number
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
export interface
|
|
66
|
-
|
|
70
|
+
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
|
|
71
|
+
url: string
|
|
72
|
+
init: RequestInitNormalized
|
|
67
73
|
throwHttpErrors: boolean
|
|
68
74
|
timeoutSeconds: number
|
|
69
75
|
retry: FetcherRetryOptions
|
|
@@ -84,9 +90,12 @@ export interface FetcherOptions {
|
|
|
84
90
|
timeoutSeconds?: number
|
|
85
91
|
json?: any
|
|
86
92
|
text?: string
|
|
87
|
-
|
|
93
|
+
init?: Partial<RequestInitNormalized>
|
|
94
|
+
headers?: Record<string, any>
|
|
88
95
|
mode?: FetcherMode // default to undefined (void response)
|
|
89
96
|
|
|
97
|
+
searchParams?: Record<string, any>
|
|
98
|
+
|
|
90
99
|
/**
|
|
91
100
|
* Default is 2 retries (3 tries in total).
|
|
92
101
|
* Pass `retry: { count: 0 }` to disable retries.
|
|
@@ -108,10 +117,9 @@ export interface FetcherOptions {
|
|
|
108
117
|
retry5xx?: boolean
|
|
109
118
|
}
|
|
110
119
|
|
|
111
|
-
export
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
opt: FetcherNormalizedOptions
|
|
120
|
+
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
121
|
+
method: HttpMethod
|
|
122
|
+
headers: Record<string, any>
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
export interface FetcherSuccessResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
@@ -159,40 +167,52 @@ export class Fetcher {
|
|
|
159
167
|
return new Fetcher(cfg)
|
|
160
168
|
}
|
|
161
169
|
|
|
162
|
-
async getJson<T = unknown>(url: string, opt
|
|
170
|
+
async getJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
163
171
|
return await this.fetch<T>(url, {
|
|
164
172
|
...opt,
|
|
165
173
|
mode: 'json',
|
|
166
174
|
})
|
|
167
175
|
}
|
|
168
|
-
|
|
169
|
-
async postJson<T = unknown>(url: string, opt: FetcherOptions = {}): Promise<T> {
|
|
176
|
+
async postJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
170
177
|
return await this.fetch<T>(url, {
|
|
171
178
|
...opt,
|
|
172
179
|
method: 'post',
|
|
173
180
|
mode: 'json',
|
|
174
181
|
})
|
|
175
182
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return await this.fetch<string>(url, {
|
|
183
|
+
async putJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
184
|
+
return await this.fetch<T>(url, {
|
|
179
185
|
...opt,
|
|
180
|
-
|
|
186
|
+
method: 'put',
|
|
187
|
+
mode: 'json',
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
async patchJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
191
|
+
return await this.fetch<T>(url, {
|
|
192
|
+
...opt,
|
|
193
|
+
method: 'patch',
|
|
194
|
+
mode: 'json',
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
async deleteJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
198
|
+
return await this.fetch<T>(url, {
|
|
199
|
+
...opt,
|
|
200
|
+
method: 'delete',
|
|
201
|
+
mode: 'json',
|
|
181
202
|
})
|
|
182
203
|
}
|
|
183
204
|
|
|
184
|
-
async
|
|
205
|
+
async getText(url: string, opt?: FetcherOptions): Promise<string> {
|
|
185
206
|
return await this.fetch<string>(url, {
|
|
186
207
|
...opt,
|
|
187
|
-
method: 'post',
|
|
188
208
|
mode: 'text',
|
|
189
209
|
})
|
|
190
210
|
}
|
|
191
211
|
|
|
192
|
-
async fetch<T = unknown>(url: string, opt
|
|
212
|
+
async fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
193
213
|
const res = await this.rawFetch<T>(url, opt)
|
|
194
214
|
if (res.err) {
|
|
195
|
-
if (res.req.
|
|
215
|
+
if (res.req.throwHttpErrors) throw res.err
|
|
196
216
|
return res as any
|
|
197
217
|
}
|
|
198
218
|
return res.body!
|
|
@@ -202,35 +222,14 @@ export class Fetcher {
|
|
|
202
222
|
url: string,
|
|
203
223
|
rawOpt: FetcherOptions = {},
|
|
204
224
|
): Promise<FetcherResponse<T>> {
|
|
205
|
-
const {
|
|
206
|
-
|
|
207
|
-
const opt = this.normalizeOptions(rawOpt)
|
|
208
|
-
const { method, timeoutSeconds, mode } = opt
|
|
209
|
-
|
|
210
|
-
const req: FetcherRequest = {
|
|
211
|
-
url,
|
|
212
|
-
init: {
|
|
213
|
-
...this.cfg.requestInit,
|
|
214
|
-
method,
|
|
215
|
-
},
|
|
216
|
-
opt,
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// setup url
|
|
220
|
-
if (baseUrl) {
|
|
221
|
-
if (url.startsWith('/')) {
|
|
222
|
-
console.warn(`Fetcher: url should not start with / when baseUrl is specified`)
|
|
223
|
-
url = url.slice(1)
|
|
224
|
-
}
|
|
225
|
-
req.url = `${baseUrl}/${url}`
|
|
226
|
-
}
|
|
225
|
+
const { logger } = this.cfg
|
|
227
226
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
227
|
+
const req = this.normalizeOptions(url, rawOpt)
|
|
228
|
+
const {
|
|
229
|
+
timeoutSeconds,
|
|
230
|
+
mode,
|
|
231
|
+
init: { method },
|
|
232
|
+
} = req
|
|
234
233
|
|
|
235
234
|
// setup timeout
|
|
236
235
|
let timeout: number | undefined
|
|
@@ -242,10 +241,6 @@ export class Fetcher {
|
|
|
242
241
|
}, timeoutSeconds * 1000) as any as number
|
|
243
242
|
}
|
|
244
243
|
|
|
245
|
-
if (opt.requestInit) {
|
|
246
|
-
_objectAssign(req.init, opt.requestInit)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
244
|
await this.cfg.hooks?.beforeRequest?.(req)
|
|
250
245
|
|
|
251
246
|
const res: FetcherResponse<any> = {
|
|
@@ -253,7 +248,7 @@ export class Fetcher {
|
|
|
253
248
|
retryStatus: {
|
|
254
249
|
retryAttempt: 0,
|
|
255
250
|
retryStopped: false,
|
|
256
|
-
retryTimeout:
|
|
251
|
+
retryTimeout: req.retry.timeout,
|
|
257
252
|
},
|
|
258
253
|
}
|
|
259
254
|
|
|
@@ -267,7 +262,7 @@ export class Fetcher {
|
|
|
267
262
|
if (this.cfg.logRequest) {
|
|
268
263
|
const { retryAttempt } = res.retryStatus
|
|
269
264
|
logger.log(
|
|
270
|
-
[' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${
|
|
265
|
+
[' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
271
266
|
.filter(Boolean)
|
|
272
267
|
.join(' '),
|
|
273
268
|
)
|
|
@@ -298,7 +293,7 @@ export class Fetcher {
|
|
|
298
293
|
' <<',
|
|
299
294
|
res.fetchResponse.status,
|
|
300
295
|
signature,
|
|
301
|
-
retryAttempt && `try#${retryAttempt + 1}/${
|
|
296
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
302
297
|
_since(started),
|
|
303
298
|
]
|
|
304
299
|
.filter(Boolean)
|
|
@@ -335,23 +330,25 @@ export class Fetcher {
|
|
|
335
330
|
}),
|
|
336
331
|
)
|
|
337
332
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
// }
|
|
355
352
|
|
|
356
353
|
await this.processRetry(res)
|
|
357
354
|
}
|
|
@@ -371,7 +368,7 @@ export class Fetcher {
|
|
|
371
368
|
|
|
372
369
|
await this.cfg.hooks?.beforeRetry?.(res)
|
|
373
370
|
|
|
374
|
-
const { count, timeoutMultiplier, timeoutMax } = res.req.
|
|
371
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry
|
|
375
372
|
|
|
376
373
|
if (retryStatus.retryAttempt >= count) {
|
|
377
374
|
retryStatus.retryStopped = true
|
|
@@ -379,6 +376,7 @@ export class Fetcher {
|
|
|
379
376
|
|
|
380
377
|
if (retryStatus.retryStopped) return
|
|
381
378
|
|
|
379
|
+
retryStatus.retryAttempt++
|
|
382
380
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax)
|
|
383
381
|
|
|
384
382
|
await pDelay(retryStatus.retryTimeout)
|
|
@@ -389,7 +387,7 @@ export class Fetcher {
|
|
|
389
387
|
* unless there's reason not to (e.g method is POST).
|
|
390
388
|
*/
|
|
391
389
|
private shouldRetry(res: FetcherResponse): boolean {
|
|
392
|
-
const { retryPost, retry4xx, retry5xx } = res.req
|
|
390
|
+
const { retryPost, retry4xx, retry5xx } = res.req
|
|
393
391
|
const { method } = res.req.init
|
|
394
392
|
if (method === 'post' && !retryPost) return false
|
|
395
393
|
const { statusFamily } = res
|
|
@@ -425,42 +423,89 @@ export class Fetcher {
|
|
|
425
423
|
}
|
|
426
424
|
const { debug } = cfg
|
|
427
425
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
...defRetryOptions,
|
|
443
|
-
|
|
426
|
+
const norm: FetcherNormalizedCfg = _merge(
|
|
427
|
+
{
|
|
428
|
+
url: '',
|
|
429
|
+
searchParams: {},
|
|
430
|
+
timeoutSeconds: 30,
|
|
431
|
+
throwHttpErrors: true,
|
|
432
|
+
retryPost: false,
|
|
433
|
+
retry4xx: false,
|
|
434
|
+
retry5xx: true,
|
|
435
|
+
logger: console,
|
|
436
|
+
logRequest: debug,
|
|
437
|
+
logRequestBody: debug,
|
|
438
|
+
logResponse: debug,
|
|
439
|
+
logResponseBody: debug,
|
|
440
|
+
retry: { ...defRetryOptions },
|
|
441
|
+
init: {
|
|
442
|
+
method: 'get',
|
|
443
|
+
headers: {},
|
|
444
|
+
},
|
|
444
445
|
},
|
|
445
|
-
|
|
446
|
+
cfg,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase())
|
|
450
|
+
|
|
451
|
+
return norm
|
|
446
452
|
}
|
|
447
453
|
|
|
448
|
-
private normalizeOptions(opt: FetcherOptions):
|
|
449
|
-
const { timeoutSeconds, throwHttpErrors,
|
|
454
|
+
private normalizeOptions(url: string, opt: FetcherOptions): FetcherRequest {
|
|
455
|
+
const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } =
|
|
450
456
|
this.cfg
|
|
451
|
-
|
|
457
|
+
|
|
458
|
+
const req: FetcherRequest = {
|
|
459
|
+
url,
|
|
452
460
|
timeoutSeconds,
|
|
453
461
|
throwHttpErrors,
|
|
454
|
-
method,
|
|
455
462
|
retryPost,
|
|
456
463
|
retry4xx,
|
|
457
464
|
retry5xx,
|
|
458
|
-
...opt,
|
|
465
|
+
..._omit(opt, ['method', 'headers']),
|
|
459
466
|
retry: {
|
|
460
467
|
...retry,
|
|
461
468
|
..._filterUndefinedValues(opt.retry || {}),
|
|
462
469
|
},
|
|
470
|
+
init: _merge(
|
|
471
|
+
{ ...this.cfg.init },
|
|
472
|
+
opt.init,
|
|
473
|
+
_filterUndefinedValues({
|
|
474
|
+
method: opt.method,
|
|
475
|
+
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
476
|
+
}),
|
|
477
|
+
),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// setup url
|
|
481
|
+
if (baseUrl) {
|
|
482
|
+
if (url.startsWith('/')) {
|
|
483
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`)
|
|
484
|
+
url = url.slice(1)
|
|
485
|
+
}
|
|
486
|
+
req.url = `${baseUrl}/${url}`
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const searchParams = {
|
|
490
|
+
...this.cfg.searchParams,
|
|
491
|
+
...opt.searchParams,
|
|
463
492
|
}
|
|
493
|
+
|
|
494
|
+
if (Object.keys(searchParams).length) {
|
|
495
|
+
const qs = new URLSearchParams(searchParams).toString()
|
|
496
|
+
req.url += req.url.includes('?') ? '&' : '?' + qs
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// setup request body
|
|
500
|
+
if (opt.json !== undefined) {
|
|
501
|
+
req.init.body = JSON.stringify(opt.json)
|
|
502
|
+
req.init.headers['content-type'] = 'application/json'
|
|
503
|
+
} else if (opt.text !== undefined) {
|
|
504
|
+
req.init.body = opt.text
|
|
505
|
+
req.init.headers['content-type'] = 'text/plain'
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return req
|
|
464
509
|
}
|
|
465
510
|
}
|
|
466
511
|
|
|
@@ -213,6 +213,8 @@ export function _filterEmptyValues<T extends AnyObject>(obj: T, mutate = false):
|
|
|
213
213
|
* are applied from left to right. Subsequent sources overwrite property
|
|
214
214
|
* assignments of previous sources.
|
|
215
215
|
*
|
|
216
|
+
* Works as "recursive Object.assign".
|
|
217
|
+
*
|
|
216
218
|
* **Note:** This method mutates `object`.
|
|
217
219
|
*
|
|
218
220
|
* @category Object
|
|
@@ -236,16 +238,16 @@ export function _filterEmptyValues<T extends AnyObject>(obj: T, mutate = false):
|
|
|
236
238
|
*/
|
|
237
239
|
export function _merge<T extends AnyObject>(target: T, ...sources: any[]): T {
|
|
238
240
|
sources.forEach(source => {
|
|
239
|
-
if (_isObject(source))
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
}
|
|
241
|
+
if (!_isObject(source)) return
|
|
242
|
+
|
|
243
|
+
Object.keys(source).forEach(key => {
|
|
244
|
+
if (_isObject(source[key])) {
|
|
245
|
+
;(target as any)[key] ||= {}
|
|
246
|
+
_merge(target[key], source[key])
|
|
247
|
+
} else {
|
|
248
|
+
;(target as any)[key] = source[key]
|
|
249
|
+
}
|
|
250
|
+
})
|
|
249
251
|
})
|
|
250
252
|
|
|
251
253
|
return target
|