@naturalcycles/js-lib 14.118.0 → 14.119.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/error/app.error.js +10 -10
- package/dist/http/fetcher.d.ts +12 -13
- package/dist/http/fetcher.js +74 -64
- 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 +82 -56
- 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 +121 -88
- 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;
|
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,
|
|
@@ -59,39 +57,16 @@ class Fetcher {
|
|
|
59
57
|
async fetch(url, opt = {}) {
|
|
60
58
|
const res = await this.rawFetch(url, opt);
|
|
61
59
|
if (res.err) {
|
|
62
|
-
if (res.req.
|
|
60
|
+
if (res.req.throwHttpErrors)
|
|
63
61
|
throw res.err;
|
|
64
62
|
return res;
|
|
65
63
|
}
|
|
66
64
|
return res.body;
|
|
67
65
|
}
|
|
68
66
|
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
|
-
}
|
|
67
|
+
const { logger } = this.cfg;
|
|
68
|
+
const req = this.normalizeOptions(url, rawOpt);
|
|
69
|
+
const { timeoutSeconds, mode, init: { method }, } = req;
|
|
95
70
|
// setup timeout
|
|
96
71
|
let timeout;
|
|
97
72
|
if (timeoutSeconds) {
|
|
@@ -101,16 +76,13 @@ class Fetcher {
|
|
|
101
76
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
102
77
|
}, timeoutSeconds * 1000);
|
|
103
78
|
}
|
|
104
|
-
if (opt.requestInit) {
|
|
105
|
-
(0, types_1._objectAssign)(req.init, opt.requestInit);
|
|
106
|
-
}
|
|
107
79
|
await this.cfg.hooks?.beforeRequest?.(req);
|
|
108
80
|
const res = {
|
|
109
81
|
req,
|
|
110
82
|
retryStatus: {
|
|
111
83
|
retryAttempt: 0,
|
|
112
84
|
retryStopped: false,
|
|
113
|
-
retryTimeout:
|
|
85
|
+
retryTimeout: req.retry.timeout,
|
|
114
86
|
},
|
|
115
87
|
};
|
|
116
88
|
const shortUrl = this.getShortUrl(req.url);
|
|
@@ -120,7 +92,7 @@ class Fetcher {
|
|
|
120
92
|
const started = Date.now();
|
|
121
93
|
if (this.cfg.logRequest) {
|
|
122
94
|
const { retryAttempt } = res.retryStatus;
|
|
123
|
-
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${
|
|
95
|
+
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
124
96
|
.filter(Boolean)
|
|
125
97
|
.join(' '));
|
|
126
98
|
if (this.cfg.logRequestBody && req.init.body) {
|
|
@@ -146,7 +118,7 @@ class Fetcher {
|
|
|
146
118
|
' <<',
|
|
147
119
|
res.fetchResponse.status,
|
|
148
120
|
signature,
|
|
149
|
-
retryAttempt && `try#${retryAttempt + 1}/${
|
|
121
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
150
122
|
(0, time_util_1._since)(started),
|
|
151
123
|
]
|
|
152
124
|
.filter(Boolean)
|
|
@@ -173,21 +145,25 @@ class Fetcher {
|
|
|
173
145
|
url: req.url,
|
|
174
146
|
// tryCount: req.tryCount,
|
|
175
147
|
}));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
148
|
+
// We don't log errors when they are also thrown,
|
|
149
|
+
// otherwise it gets logged twice: here, and upstream
|
|
150
|
+
// if (this.cfg.logResponse) {
|
|
151
|
+
// const { retryAttempt } = res.retryStatus
|
|
152
|
+
// logger.error(
|
|
153
|
+
// [
|
|
154
|
+
// [
|
|
155
|
+
// ' <<',
|
|
156
|
+
// res.fetchResponse.status,
|
|
157
|
+
// signature,
|
|
158
|
+
// retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
159
|
+
// _since(started),
|
|
160
|
+
// ]
|
|
161
|
+
// .filter(Boolean)
|
|
162
|
+
// .join(' '),
|
|
163
|
+
// _stringifyAny(body),
|
|
164
|
+
// ].join('\n'),
|
|
165
|
+
// )
|
|
166
|
+
// }
|
|
191
167
|
await this.processRetry(res);
|
|
192
168
|
}
|
|
193
169
|
}
|
|
@@ -200,12 +176,13 @@ class Fetcher {
|
|
|
200
176
|
retryStatus.retryStopped = true;
|
|
201
177
|
}
|
|
202
178
|
await this.cfg.hooks?.beforeRetry?.(res);
|
|
203
|
-
const { count, timeoutMultiplier, timeoutMax } = res.req.
|
|
179
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
204
180
|
if (retryStatus.retryAttempt >= count) {
|
|
205
181
|
retryStatus.retryStopped = true;
|
|
206
182
|
}
|
|
207
183
|
if (retryStatus.retryStopped)
|
|
208
184
|
return;
|
|
185
|
+
retryStatus.retryAttempt++;
|
|
209
186
|
retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
210
187
|
await (0, pDelay_1.pDelay)(retryStatus.retryTimeout);
|
|
211
188
|
}
|
|
@@ -214,7 +191,7 @@ class Fetcher {
|
|
|
214
191
|
* unless there's reason not to (e.g method is POST).
|
|
215
192
|
*/
|
|
216
193
|
shouldRetry(res) {
|
|
217
|
-
const { retryPost, retry4xx, retry5xx } = res.req
|
|
194
|
+
const { retryPost, retry4xx, retry5xx } = res.req;
|
|
218
195
|
const { method } = res.req.init;
|
|
219
196
|
if (method === 'post' && !retryPost)
|
|
220
197
|
return false;
|
|
@@ -255,9 +232,10 @@ class Fetcher {
|
|
|
255
232
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
256
233
|
}
|
|
257
234
|
const { debug } = cfg;
|
|
258
|
-
|
|
235
|
+
const norm = (0, object_util_1._merge)({
|
|
236
|
+
url: '',
|
|
237
|
+
searchParams: {},
|
|
259
238
|
timeoutSeconds: 30,
|
|
260
|
-
method: 'get',
|
|
261
239
|
throwHttpErrors: true,
|
|
262
240
|
retryPost: false,
|
|
263
241
|
retry4xx: false,
|
|
@@ -267,28 +245,60 @@ class Fetcher {
|
|
|
267
245
|
logRequestBody: debug,
|
|
268
246
|
logResponse: debug,
|
|
269
247
|
logResponseBody: debug,
|
|
270
|
-
...
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
248
|
+
retry: { ...defRetryOptions },
|
|
249
|
+
init: {
|
|
250
|
+
method: 'get',
|
|
251
|
+
headers: {},
|
|
274
252
|
},
|
|
275
|
-
};
|
|
253
|
+
}, cfg);
|
|
254
|
+
norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
|
|
255
|
+
return norm;
|
|
276
256
|
}
|
|
277
|
-
normalizeOptions(opt) {
|
|
278
|
-
const { timeoutSeconds, throwHttpErrors,
|
|
279
|
-
|
|
257
|
+
normalizeOptions(url, opt) {
|
|
258
|
+
const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } = this.cfg;
|
|
259
|
+
const req = {
|
|
260
|
+
url,
|
|
280
261
|
timeoutSeconds,
|
|
281
262
|
throwHttpErrors,
|
|
282
|
-
method,
|
|
283
263
|
retryPost,
|
|
284
264
|
retry4xx,
|
|
285
265
|
retry5xx,
|
|
286
|
-
...opt,
|
|
266
|
+
...(0, object_util_1._omit)(opt, ['method', 'headers']),
|
|
287
267
|
retry: {
|
|
288
268
|
...retry,
|
|
289
269
|
...(0, object_util_1._filterUndefinedValues)(opt.retry || {}),
|
|
290
270
|
},
|
|
271
|
+
init: (0, object_util_1._merge)({ ...this.cfg.init }, opt.init, (0, object_util_1._filterUndefinedValues)({
|
|
272
|
+
method: opt.method,
|
|
273
|
+
headers: (0, object_util_1._mapKeys)(opt.headers || {}, k => k.toLowerCase()),
|
|
274
|
+
})),
|
|
291
275
|
};
|
|
276
|
+
// setup url
|
|
277
|
+
if (baseUrl) {
|
|
278
|
+
if (url.startsWith('/')) {
|
|
279
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
280
|
+
url = url.slice(1);
|
|
281
|
+
}
|
|
282
|
+
req.url = `${baseUrl}/${url}`;
|
|
283
|
+
}
|
|
284
|
+
const searchParams = {
|
|
285
|
+
...this.cfg.searchParams,
|
|
286
|
+
...opt.searchParams,
|
|
287
|
+
};
|
|
288
|
+
if (Object.keys(searchParams).length) {
|
|
289
|
+
const qs = new URLSearchParams(searchParams).toString();
|
|
290
|
+
req.url += req.url.includes('?') ? '&' : '?' + qs;
|
|
291
|
+
}
|
|
292
|
+
// setup request body
|
|
293
|
+
if (opt.json !== undefined) {
|
|
294
|
+
req.init.body = JSON.stringify(opt.json);
|
|
295
|
+
req.init.headers['content-type'] = 'application/json';
|
|
296
|
+
}
|
|
297
|
+
else if (opt.text !== undefined) {
|
|
298
|
+
req.init.body = opt.text;
|
|
299
|
+
req.init.headers['content-type'] = 'text/plain';
|
|
300
|
+
}
|
|
301
|
+
return req;
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
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,
|
|
@@ -42,7 +40,7 @@ export class Fetcher {
|
|
|
42
40
|
async fetch(url, opt = {}) {
|
|
43
41
|
const res = await this.rawFetch(url, opt);
|
|
44
42
|
if (res.err) {
|
|
45
|
-
if (res.req.
|
|
43
|
+
if (res.req.throwHttpErrors)
|
|
46
44
|
throw res.err;
|
|
47
45
|
return res;
|
|
48
46
|
}
|
|
@@ -50,29 +48,9 @@ export class Fetcher {
|
|
|
50
48
|
}
|
|
51
49
|
async rawFetch(url, rawOpt = {}) {
|
|
52
50
|
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
|
-
}
|
|
51
|
+
const { logger } = this.cfg;
|
|
52
|
+
const req = this.normalizeOptions(url, rawOpt);
|
|
53
|
+
const { timeoutSeconds, mode, init: { method }, } = req;
|
|
76
54
|
// setup timeout
|
|
77
55
|
let timeout;
|
|
78
56
|
if (timeoutSeconds) {
|
|
@@ -82,16 +60,13 @@ export class Fetcher {
|
|
|
82
60
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
83
61
|
}, timeoutSeconds * 1000);
|
|
84
62
|
}
|
|
85
|
-
if (opt.requestInit) {
|
|
86
|
-
_objectAssign(req.init, opt.requestInit);
|
|
87
|
-
}
|
|
88
63
|
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
64
|
const res = {
|
|
90
65
|
req,
|
|
91
66
|
retryStatus: {
|
|
92
67
|
retryAttempt: 0,
|
|
93
68
|
retryStopped: false,
|
|
94
|
-
retryTimeout:
|
|
69
|
+
retryTimeout: req.retry.timeout,
|
|
95
70
|
},
|
|
96
71
|
};
|
|
97
72
|
const shortUrl = this.getShortUrl(req.url);
|
|
@@ -101,7 +76,7 @@ export class Fetcher {
|
|
|
101
76
|
const started = Date.now();
|
|
102
77
|
if (this.cfg.logRequest) {
|
|
103
78
|
const { retryAttempt } = res.retryStatus;
|
|
104
|
-
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${
|
|
79
|
+
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
105
80
|
.filter(Boolean)
|
|
106
81
|
.join(' '));
|
|
107
82
|
if (this.cfg.logRequestBody && req.init.body) {
|
|
@@ -127,7 +102,7 @@ export class Fetcher {
|
|
|
127
102
|
' <<',
|
|
128
103
|
res.fetchResponse.status,
|
|
129
104
|
signature,
|
|
130
|
-
retryAttempt && `try#${retryAttempt + 1}/${
|
|
105
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
131
106
|
_since(started),
|
|
132
107
|
]
|
|
133
108
|
.filter(Boolean)
|
|
@@ -149,21 +124,25 @@ export class Fetcher {
|
|
|
149
124
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
150
125
|
// method: req.method,
|
|
151
126
|
url: req.url })));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
127
|
+
// We don't log errors when they are also thrown,
|
|
128
|
+
// otherwise it gets logged twice: here, and upstream
|
|
129
|
+
// if (this.cfg.logResponse) {
|
|
130
|
+
// const { retryAttempt } = res.retryStatus
|
|
131
|
+
// logger.error(
|
|
132
|
+
// [
|
|
133
|
+
// [
|
|
134
|
+
// ' <<',
|
|
135
|
+
// res.fetchResponse.status,
|
|
136
|
+
// signature,
|
|
137
|
+
// retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
138
|
+
// _since(started),
|
|
139
|
+
// ]
|
|
140
|
+
// .filter(Boolean)
|
|
141
|
+
// .join(' '),
|
|
142
|
+
// _stringifyAny(body),
|
|
143
|
+
// ].join('\n'),
|
|
144
|
+
// )
|
|
145
|
+
// }
|
|
167
146
|
await this.processRetry(res);
|
|
168
147
|
}
|
|
169
148
|
}
|
|
@@ -177,12 +156,13 @@ export class Fetcher {
|
|
|
177
156
|
retryStatus.retryStopped = true;
|
|
178
157
|
}
|
|
179
158
|
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.
|
|
159
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
181
160
|
if (retryStatus.retryAttempt >= count) {
|
|
182
161
|
retryStatus.retryStopped = true;
|
|
183
162
|
}
|
|
184
163
|
if (retryStatus.retryStopped)
|
|
185
164
|
return;
|
|
165
|
+
retryStatus.retryAttempt++;
|
|
186
166
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
187
167
|
await pDelay(retryStatus.retryTimeout);
|
|
188
168
|
}
|
|
@@ -191,7 +171,7 @@ export class Fetcher {
|
|
|
191
171
|
* unless there's reason not to (e.g method is POST).
|
|
192
172
|
*/
|
|
193
173
|
shouldRetry(res) {
|
|
194
|
-
const { retryPost, retry4xx, retry5xx } = res.req
|
|
174
|
+
const { retryPost, retry4xx, retry5xx } = res.req;
|
|
195
175
|
const { method } = res.req.init;
|
|
196
176
|
if (method === 'post' && !retryPost)
|
|
197
177
|
return false;
|
|
@@ -234,16 +214,62 @@ export class Fetcher {
|
|
|
234
214
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
235
215
|
}
|
|
236
216
|
const { debug } = cfg;
|
|
237
|
-
|
|
217
|
+
const norm = _merge({
|
|
218
|
+
url: '',
|
|
219
|
+
searchParams: {},
|
|
220
|
+
timeoutSeconds: 30,
|
|
221
|
+
throwHttpErrors: true,
|
|
222
|
+
retryPost: false,
|
|
223
|
+
retry4xx: false,
|
|
224
|
+
retry5xx: true,
|
|
225
|
+
logger: console,
|
|
226
|
+
logRequest: debug,
|
|
227
|
+
logRequestBody: debug,
|
|
228
|
+
logResponse: debug,
|
|
229
|
+
logResponseBody: debug,
|
|
230
|
+
retry: Object.assign({}, defRetryOptions),
|
|
231
|
+
init: {
|
|
232
|
+
method: 'get',
|
|
233
|
+
headers: {},
|
|
234
|
+
},
|
|
235
|
+
}, cfg);
|
|
236
|
+
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
|
|
237
|
+
return norm;
|
|
238
238
|
}
|
|
239
|
-
normalizeOptions(opt) {
|
|
240
|
-
const { timeoutSeconds, throwHttpErrors,
|
|
241
|
-
|
|
239
|
+
normalizeOptions(url, opt) {
|
|
240
|
+
const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } = this.cfg;
|
|
241
|
+
const req = Object.assign(Object.assign({ url,
|
|
242
|
+
timeoutSeconds,
|
|
242
243
|
throwHttpErrors,
|
|
243
|
-
method,
|
|
244
244
|
retryPost,
|
|
245
245
|
retry4xx,
|
|
246
|
-
retry5xx }, opt), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})) })
|
|
246
|
+
retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init), opt.init, _filterUndefinedValues({
|
|
247
|
+
method: opt.method,
|
|
248
|
+
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
249
|
+
})) });
|
|
250
|
+
// setup url
|
|
251
|
+
if (baseUrl) {
|
|
252
|
+
if (url.startsWith('/')) {
|
|
253
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
254
|
+
url = url.slice(1);
|
|
255
|
+
}
|
|
256
|
+
req.url = `${baseUrl}/${url}`;
|
|
257
|
+
}
|
|
258
|
+
const searchParams = Object.assign(Object.assign({}, this.cfg.searchParams), opt.searchParams);
|
|
259
|
+
if (Object.keys(searchParams).length) {
|
|
260
|
+
const qs = new URLSearchParams(searchParams).toString();
|
|
261
|
+
req.url += req.url.includes('?') ? '&' : '?' + qs;
|
|
262
|
+
}
|
|
263
|
+
// setup request body
|
|
264
|
+
if (opt.json !== undefined) {
|
|
265
|
+
req.init.body = JSON.stringify(opt.json);
|
|
266
|
+
req.init.headers['content-type'] = 'application/json';
|
|
267
|
+
}
|
|
268
|
+
else if (opt.text !== undefined) {
|
|
269
|
+
req.init.body = opt.text;
|
|
270
|
+
req.init.headers['content-type'] = 'text/plain';
|
|
271
|
+
}
|
|
272
|
+
return req;
|
|
247
273
|
}
|
|
248
274
|
}
|
|
249
275
|
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> {
|
|
@@ -192,7 +200,7 @@ export class Fetcher {
|
|
|
192
200
|
async fetch<T = unknown>(url: string, opt: FetcherOptions = {}): Promise<T> {
|
|
193
201
|
const res = await this.rawFetch<T>(url, opt)
|
|
194
202
|
if (res.err) {
|
|
195
|
-
if (res.req.
|
|
203
|
+
if (res.req.throwHttpErrors) throw res.err
|
|
196
204
|
return res as any
|
|
197
205
|
}
|
|
198
206
|
return res.body!
|
|
@@ -202,35 +210,14 @@ export class Fetcher {
|
|
|
202
210
|
url: string,
|
|
203
211
|
rawOpt: FetcherOptions = {},
|
|
204
212
|
): 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
|
-
}
|
|
213
|
+
const { logger } = this.cfg
|
|
227
214
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
215
|
+
const req = this.normalizeOptions(url, rawOpt)
|
|
216
|
+
const {
|
|
217
|
+
timeoutSeconds,
|
|
218
|
+
mode,
|
|
219
|
+
init: { method },
|
|
220
|
+
} = req
|
|
234
221
|
|
|
235
222
|
// setup timeout
|
|
236
223
|
let timeout: number | undefined
|
|
@@ -242,10 +229,6 @@ export class Fetcher {
|
|
|
242
229
|
}, timeoutSeconds * 1000) as any as number
|
|
243
230
|
}
|
|
244
231
|
|
|
245
|
-
if (opt.requestInit) {
|
|
246
|
-
_objectAssign(req.init, opt.requestInit)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
232
|
await this.cfg.hooks?.beforeRequest?.(req)
|
|
250
233
|
|
|
251
234
|
const res: FetcherResponse<any> = {
|
|
@@ -253,7 +236,7 @@ export class Fetcher {
|
|
|
253
236
|
retryStatus: {
|
|
254
237
|
retryAttempt: 0,
|
|
255
238
|
retryStopped: false,
|
|
256
|
-
retryTimeout:
|
|
239
|
+
retryTimeout: req.retry.timeout,
|
|
257
240
|
},
|
|
258
241
|
}
|
|
259
242
|
|
|
@@ -267,7 +250,7 @@ export class Fetcher {
|
|
|
267
250
|
if (this.cfg.logRequest) {
|
|
268
251
|
const { retryAttempt } = res.retryStatus
|
|
269
252
|
logger.log(
|
|
270
|
-
[' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${
|
|
253
|
+
[' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
271
254
|
.filter(Boolean)
|
|
272
255
|
.join(' '),
|
|
273
256
|
)
|
|
@@ -298,7 +281,7 @@ export class Fetcher {
|
|
|
298
281
|
' <<',
|
|
299
282
|
res.fetchResponse.status,
|
|
300
283
|
signature,
|
|
301
|
-
retryAttempt && `try#${retryAttempt + 1}/${
|
|
284
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
302
285
|
_since(started),
|
|
303
286
|
]
|
|
304
287
|
.filter(Boolean)
|
|
@@ -335,23 +318,25 @@ export class Fetcher {
|
|
|
335
318
|
}),
|
|
336
319
|
)
|
|
337
320
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
321
|
+
// We don't log errors when they are also thrown,
|
|
322
|
+
// otherwise it gets logged twice: here, and upstream
|
|
323
|
+
// if (this.cfg.logResponse) {
|
|
324
|
+
// const { retryAttempt } = res.retryStatus
|
|
325
|
+
// logger.error(
|
|
326
|
+
// [
|
|
327
|
+
// [
|
|
328
|
+
// ' <<',
|
|
329
|
+
// res.fetchResponse.status,
|
|
330
|
+
// signature,
|
|
331
|
+
// retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
332
|
+
// _since(started),
|
|
333
|
+
// ]
|
|
334
|
+
// .filter(Boolean)
|
|
335
|
+
// .join(' '),
|
|
336
|
+
// _stringifyAny(body),
|
|
337
|
+
// ].join('\n'),
|
|
338
|
+
// )
|
|
339
|
+
// }
|
|
355
340
|
|
|
356
341
|
await this.processRetry(res)
|
|
357
342
|
}
|
|
@@ -371,7 +356,7 @@ export class Fetcher {
|
|
|
371
356
|
|
|
372
357
|
await this.cfg.hooks?.beforeRetry?.(res)
|
|
373
358
|
|
|
374
|
-
const { count, timeoutMultiplier, timeoutMax } = res.req.
|
|
359
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry
|
|
375
360
|
|
|
376
361
|
if (retryStatus.retryAttempt >= count) {
|
|
377
362
|
retryStatus.retryStopped = true
|
|
@@ -379,6 +364,7 @@ export class Fetcher {
|
|
|
379
364
|
|
|
380
365
|
if (retryStatus.retryStopped) return
|
|
381
366
|
|
|
367
|
+
retryStatus.retryAttempt++
|
|
382
368
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax)
|
|
383
369
|
|
|
384
370
|
await pDelay(retryStatus.retryTimeout)
|
|
@@ -389,7 +375,7 @@ export class Fetcher {
|
|
|
389
375
|
* unless there's reason not to (e.g method is POST).
|
|
390
376
|
*/
|
|
391
377
|
private shouldRetry(res: FetcherResponse): boolean {
|
|
392
|
-
const { retryPost, retry4xx, retry5xx } = res.req
|
|
378
|
+
const { retryPost, retry4xx, retry5xx } = res.req
|
|
393
379
|
const { method } = res.req.init
|
|
394
380
|
if (method === 'post' && !retryPost) return false
|
|
395
381
|
const { statusFamily } = res
|
|
@@ -425,42 +411,89 @@ export class Fetcher {
|
|
|
425
411
|
}
|
|
426
412
|
const { debug } = cfg
|
|
427
413
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
...defRetryOptions,
|
|
443
|
-
|
|
414
|
+
const norm: FetcherNormalizedCfg = _merge(
|
|
415
|
+
{
|
|
416
|
+
url: '',
|
|
417
|
+
searchParams: {},
|
|
418
|
+
timeoutSeconds: 30,
|
|
419
|
+
throwHttpErrors: true,
|
|
420
|
+
retryPost: false,
|
|
421
|
+
retry4xx: false,
|
|
422
|
+
retry5xx: true,
|
|
423
|
+
logger: console,
|
|
424
|
+
logRequest: debug,
|
|
425
|
+
logRequestBody: debug,
|
|
426
|
+
logResponse: debug,
|
|
427
|
+
logResponseBody: debug,
|
|
428
|
+
retry: { ...defRetryOptions },
|
|
429
|
+
init: {
|
|
430
|
+
method: 'get',
|
|
431
|
+
headers: {},
|
|
432
|
+
},
|
|
444
433
|
},
|
|
445
|
-
|
|
434
|
+
cfg,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase())
|
|
438
|
+
|
|
439
|
+
return norm
|
|
446
440
|
}
|
|
447
441
|
|
|
448
|
-
private normalizeOptions(opt: FetcherOptions):
|
|
449
|
-
const { timeoutSeconds, throwHttpErrors,
|
|
442
|
+
private normalizeOptions(url: string, opt: FetcherOptions): FetcherRequest {
|
|
443
|
+
const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } =
|
|
450
444
|
this.cfg
|
|
451
|
-
|
|
445
|
+
|
|
446
|
+
const req: FetcherRequest = {
|
|
447
|
+
url,
|
|
452
448
|
timeoutSeconds,
|
|
453
449
|
throwHttpErrors,
|
|
454
|
-
method,
|
|
455
450
|
retryPost,
|
|
456
451
|
retry4xx,
|
|
457
452
|
retry5xx,
|
|
458
|
-
...opt,
|
|
453
|
+
..._omit(opt, ['method', 'headers']),
|
|
459
454
|
retry: {
|
|
460
455
|
...retry,
|
|
461
456
|
..._filterUndefinedValues(opt.retry || {}),
|
|
462
457
|
},
|
|
458
|
+
init: _merge(
|
|
459
|
+
{ ...this.cfg.init },
|
|
460
|
+
opt.init,
|
|
461
|
+
_filterUndefinedValues({
|
|
462
|
+
method: opt.method,
|
|
463
|
+
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
464
|
+
}),
|
|
465
|
+
),
|
|
463
466
|
}
|
|
467
|
+
|
|
468
|
+
// setup url
|
|
469
|
+
if (baseUrl) {
|
|
470
|
+
if (url.startsWith('/')) {
|
|
471
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`)
|
|
472
|
+
url = url.slice(1)
|
|
473
|
+
}
|
|
474
|
+
req.url = `${baseUrl}/${url}`
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const searchParams = {
|
|
478
|
+
...this.cfg.searchParams,
|
|
479
|
+
...opt.searchParams,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (Object.keys(searchParams).length) {
|
|
483
|
+
const qs = new URLSearchParams(searchParams).toString()
|
|
484
|
+
req.url += req.url.includes('?') ? '&' : '?' + qs
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// setup request body
|
|
488
|
+
if (opt.json !== undefined) {
|
|
489
|
+
req.init.body = JSON.stringify(opt.json)
|
|
490
|
+
req.init.headers['content-type'] = 'application/json'
|
|
491
|
+
} else if (opt.text !== undefined) {
|
|
492
|
+
req.init.body = opt.text
|
|
493
|
+
req.init.headers['content-type'] = 'text/plain'
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return req
|
|
464
497
|
}
|
|
465
498
|
}
|
|
466
499
|
|
|
@@ -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
|