@naturalcycles/js-lib 14.117.1 → 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 +145 -0
- package/dist/http/fetcher.js +308 -0
- package/dist/http/http.model.d.ts +2 -0
- package/dist/http/http.model.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/object/object.util.d.ts +2 -0
- package/dist/object/object.util.js +15 -12
- package/dist/string/stringifyAny.js +2 -1
- package/dist-esm/error/app.error.js +10 -10
- package/dist-esm/http/fetcher.js +277 -0
- package/dist-esm/http/http.model.js +1 -0
- package/dist-esm/index.js +2 -0
- package/dist-esm/object/object.util.js +16 -12
- package/dist-esm/string/stringifyAny.js +2 -1
- package/package.json +1 -1
- package/src/error/app.error.ts +10 -9
- package/src/http/fetcher.ts +502 -0
- package/src/http/http.model.ts +3 -0
- package/src/index.ts +2 -0
- package/src/object/object.util.ts +12 -10
- package/src/string/stringifyAny.ts +3 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/// <reference lib="dom"/>
|
|
2
|
+
import { _anyToErrorObject } from '../error/error.util';
|
|
3
|
+
import { HttpError } from '../error/http.error';
|
|
4
|
+
import { _clamp } from '../number/number.util';
|
|
5
|
+
import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, } from '../object/object.util';
|
|
6
|
+
import { pDelay } from '../promise/pDelay';
|
|
7
|
+
import { _jsonParseIfPossible } from '../string/json.util';
|
|
8
|
+
import { _since } from '../time/time.util';
|
|
9
|
+
const defRetryOptions = {
|
|
10
|
+
count: 2,
|
|
11
|
+
timeout: 500,
|
|
12
|
+
timeoutMax: 30000,
|
|
13
|
+
timeoutMultiplier: 2,
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Experimental wrapper around Fetch.
|
|
17
|
+
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
18
|
+
*
|
|
19
|
+
* @experimental
|
|
20
|
+
*/
|
|
21
|
+
export class Fetcher {
|
|
22
|
+
constructor(cfg = {}) {
|
|
23
|
+
this.cfg = this.normalizeCfg(cfg);
|
|
24
|
+
}
|
|
25
|
+
static create(cfg = {}) {
|
|
26
|
+
return new Fetcher(cfg);
|
|
27
|
+
}
|
|
28
|
+
async getJson(url, opt = {}) {
|
|
29
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: 'json' }));
|
|
30
|
+
}
|
|
31
|
+
async postJson(url, opt = {}) {
|
|
32
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'post', mode: 'json' }));
|
|
33
|
+
}
|
|
34
|
+
async getText(url, opt = {}) {
|
|
35
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: 'text' }));
|
|
36
|
+
}
|
|
37
|
+
async postText(url, opt = {}) {
|
|
38
|
+
return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'post', mode: 'text' }));
|
|
39
|
+
}
|
|
40
|
+
async fetch(url, opt = {}) {
|
|
41
|
+
const res = await this.rawFetch(url, opt);
|
|
42
|
+
if (res.err) {
|
|
43
|
+
if (res.req.throwHttpErrors)
|
|
44
|
+
throw res.err;
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
return res.body;
|
|
48
|
+
}
|
|
49
|
+
async rawFetch(url, rawOpt = {}) {
|
|
50
|
+
var _a, _b, _c, _d;
|
|
51
|
+
const { logger } = this.cfg;
|
|
52
|
+
const req = this.normalizeOptions(url, rawOpt);
|
|
53
|
+
const { timeoutSeconds, mode, init: { method }, } = req;
|
|
54
|
+
// setup timeout
|
|
55
|
+
let timeout;
|
|
56
|
+
if (timeoutSeconds) {
|
|
57
|
+
const abortController = new AbortController();
|
|
58
|
+
req.init.signal = abortController.signal;
|
|
59
|
+
timeout = setTimeout(() => {
|
|
60
|
+
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
61
|
+
}, timeoutSeconds * 1000);
|
|
62
|
+
}
|
|
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));
|
|
64
|
+
const res = {
|
|
65
|
+
req,
|
|
66
|
+
retryStatus: {
|
|
67
|
+
retryAttempt: 0,
|
|
68
|
+
retryStopped: false,
|
|
69
|
+
retryTimeout: req.retry.timeout,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const shortUrl = this.getShortUrl(req.url);
|
|
73
|
+
const signature = [method.toUpperCase(), shortUrl].join(' ');
|
|
74
|
+
/* eslint-disable no-await-in-loop */
|
|
75
|
+
while (!res.retryStatus.retryStopped) {
|
|
76
|
+
const started = Date.now();
|
|
77
|
+
if (this.cfg.logRequest) {
|
|
78
|
+
const { retryAttempt } = res.retryStatus;
|
|
79
|
+
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.join(' '));
|
|
82
|
+
if (this.cfg.logRequestBody && req.init.body) {
|
|
83
|
+
logger.log(req.init.body); // todo: check if we can _inspect it
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
res.fetchResponse = await globalThis.fetch(req.url, req.init);
|
|
87
|
+
res.statusFamily = this.getStatusFamily(res);
|
|
88
|
+
if (res.fetchResponse.ok) {
|
|
89
|
+
if (mode === 'json') {
|
|
90
|
+
// if no body: set responseBody as {}
|
|
91
|
+
// do not throw a "cannot parse null as Json" error
|
|
92
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.json() : {};
|
|
93
|
+
}
|
|
94
|
+
else if (mode === 'text') {
|
|
95
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
|
|
96
|
+
}
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
res.retryStatus.retryStopped = true;
|
|
99
|
+
if (this.cfg.logResponse) {
|
|
100
|
+
const { retryAttempt } = res.retryStatus;
|
|
101
|
+
logger.log([
|
|
102
|
+
' <<',
|
|
103
|
+
res.fetchResponse.status,
|
|
104
|
+
signature,
|
|
105
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
|
|
106
|
+
_since(started),
|
|
107
|
+
]
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.join(' '));
|
|
110
|
+
if (this.cfg.logResponseBody) {
|
|
111
|
+
logger.log(res.body);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
const body = _jsonParseIfPossible(await res.fetchResponse.text());
|
|
118
|
+
const errObj = _anyToErrorObject(body);
|
|
119
|
+
const originalMessage = errObj.message;
|
|
120
|
+
errObj.message = [[res.fetchResponse.status, signature].join(' '), originalMessage].join('\n');
|
|
121
|
+
res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: res.fetchResponse.status,
|
|
122
|
+
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
123
|
+
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
124
|
+
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
125
|
+
// method: req.method,
|
|
126
|
+
url: req.url })));
|
|
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
|
+
// }
|
|
146
|
+
await this.processRetry(res);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
await ((_d = (_c = this.cfg.hooks) === null || _c === void 0 ? void 0 : _c.beforeResponse) === null || _d === void 0 ? void 0 : _d.call(_c, res));
|
|
150
|
+
return res;
|
|
151
|
+
}
|
|
152
|
+
async processRetry(res) {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
const { retryStatus } = res;
|
|
155
|
+
if (!this.shouldRetry(res)) {
|
|
156
|
+
retryStatus.retryStopped = true;
|
|
157
|
+
}
|
|
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));
|
|
159
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
160
|
+
if (retryStatus.retryAttempt >= count) {
|
|
161
|
+
retryStatus.retryStopped = true;
|
|
162
|
+
}
|
|
163
|
+
if (retryStatus.retryStopped)
|
|
164
|
+
return;
|
|
165
|
+
retryStatus.retryAttempt++;
|
|
166
|
+
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
167
|
+
await pDelay(retryStatus.retryTimeout);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Default is yes,
|
|
171
|
+
* unless there's reason not to (e.g method is POST).
|
|
172
|
+
*/
|
|
173
|
+
shouldRetry(res) {
|
|
174
|
+
const { retryPost, retry4xx, retry5xx } = res.req;
|
|
175
|
+
const { method } = res.req.init;
|
|
176
|
+
if (method === 'post' && !retryPost)
|
|
177
|
+
return false;
|
|
178
|
+
const { statusFamily } = res;
|
|
179
|
+
if (statusFamily === '5xx' && !retry5xx)
|
|
180
|
+
return false;
|
|
181
|
+
if (statusFamily === '4xx' && !retry4xx)
|
|
182
|
+
return false;
|
|
183
|
+
return true; // default is true
|
|
184
|
+
}
|
|
185
|
+
getStatusFamily(res) {
|
|
186
|
+
var _a;
|
|
187
|
+
const status = (_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status;
|
|
188
|
+
if (!status)
|
|
189
|
+
return;
|
|
190
|
+
if (status >= 500)
|
|
191
|
+
return '5xx';
|
|
192
|
+
if (status >= 400)
|
|
193
|
+
return '4xx';
|
|
194
|
+
if (status >= 300)
|
|
195
|
+
return '3xx';
|
|
196
|
+
if (status >= 200)
|
|
197
|
+
return '2xx';
|
|
198
|
+
if (status >= 100)
|
|
199
|
+
return '1xx';
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Returns url without baseUrl and before ?queryString
|
|
203
|
+
*/
|
|
204
|
+
getShortUrl(url) {
|
|
205
|
+
const { baseUrl } = this.cfg;
|
|
206
|
+
if (!baseUrl)
|
|
207
|
+
return url;
|
|
208
|
+
return url.split('?')[0].slice(baseUrl.length);
|
|
209
|
+
}
|
|
210
|
+
normalizeCfg(cfg) {
|
|
211
|
+
var _a;
|
|
212
|
+
if ((_a = cfg.baseUrl) === null || _a === void 0 ? void 0 : _a.endsWith('/')) {
|
|
213
|
+
console.warn(`Fetcher: baseUrl should not end with /`);
|
|
214
|
+
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
215
|
+
}
|
|
216
|
+
const { debug } = cfg;
|
|
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
|
+
}
|
|
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,
|
|
243
|
+
throwHttpErrors,
|
|
244
|
+
retryPost,
|
|
245
|
+
retry4xx,
|
|
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;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export function getFetcher(cfg = {}) {
|
|
276
|
+
return Fetcher.create(cfg);
|
|
277
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist-esm/index.js
CHANGED
|
@@ -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
|
}
|
|
@@ -53,7 +53,8 @@ export function _stringifyAny(obj, opt = {}) {
|
|
|
53
53
|
// This is to fix the rare error (happened with Got) where `err.message` was changed,
|
|
54
54
|
// but err.stack had "old" err.message
|
|
55
55
|
// This should "fix" that
|
|
56
|
-
|
|
56
|
+
const sLines = s.split('\n').length;
|
|
57
|
+
s = [s, ...obj.stack.split('\n').slice(sLines)].join('\n');
|
|
57
58
|
}
|
|
58
59
|
if (_isErrorObject(obj)) {
|
|
59
60
|
if (_isHttpErrorObject(obj)) {
|
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
|
}
|