@naturalcycles/js-lib 14.133.1 → 14.135.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/env.d.ts +14 -0
- package/dist/env.js +23 -0
- package/dist/http/fetcher.d.ts +4 -0
- package/dist/http/fetcher.js +109 -94
- package/dist/http/fetcher.model.d.ts +14 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/string/stringifyAny.js +2 -0
- package/dist/vendor/is.d.ts +2 -2
- package/dist-esm/env.js +18 -0
- package/dist-esm/http/fetcher.js +116 -100
- package/dist-esm/index.js +1 -0
- package/dist-esm/string/stringifyAny.js +2 -0
- package/package.json +1 -1
- package/src/env.ts +19 -0
- package/src/http/fetcher.model.ts +14 -3
- package/src/http/fetcher.ts +122 -97
- package/src/index.ts +1 -0
- package/src/string/stringifyAny.ts +2 -0
- package/src/vendor/is.ts +3 -3
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use it to detect SSR/Node.js environment.
|
|
3
|
+
*
|
|
4
|
+
* Will return `true` in Node.js.
|
|
5
|
+
* Will return `false` in the Browser.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isServerSide(): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Use it to detect Browser (not SSR/Node) environment.
|
|
10
|
+
*
|
|
11
|
+
* Will return `true` in the Browser.
|
|
12
|
+
* Will return `false` in Node.js.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isClientSide(): boolean;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isClientSide = exports.isServerSide = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Use it to detect SSR/Node.js environment.
|
|
6
|
+
*
|
|
7
|
+
* Will return `true` in Node.js.
|
|
8
|
+
* Will return `false` in the Browser.
|
|
9
|
+
*/
|
|
10
|
+
function isServerSide() {
|
|
11
|
+
return typeof window === 'undefined';
|
|
12
|
+
}
|
|
13
|
+
exports.isServerSide = isServerSide;
|
|
14
|
+
/**
|
|
15
|
+
* Use it to detect Browser (not SSR/Node) environment.
|
|
16
|
+
*
|
|
17
|
+
* Will return `true` in the Browser.
|
|
18
|
+
* Will return `false` in Node.js.
|
|
19
|
+
*/
|
|
20
|
+
function isClientSide() {
|
|
21
|
+
return typeof window !== 'undefined';
|
|
22
|
+
}
|
|
23
|
+
exports.isClientSide = isClientSide;
|
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -38,10 +38,14 @@ export declare class Fetcher {
|
|
|
38
38
|
* Never throws, returns `err` property in the response instead.
|
|
39
39
|
*/
|
|
40
40
|
rawFetch<T = unknown>(url: string, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
|
|
41
|
+
private onOkResponse;
|
|
42
|
+
private onNotOkResponse;
|
|
41
43
|
private processRetry;
|
|
42
44
|
/**
|
|
43
45
|
* Default is yes,
|
|
44
46
|
* unless there's reason not to (e.g method is POST).
|
|
47
|
+
*
|
|
48
|
+
* statusCode of 0 (or absense of it) will BE retried.
|
|
45
49
|
*/
|
|
46
50
|
private shouldRetry;
|
|
47
51
|
private getStatusFamily;
|
package/dist/http/fetcher.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/// <reference lib="dom"/>
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.getFetcher = exports.Fetcher = void 0;
|
|
5
|
+
const env_1 = require("../env");
|
|
5
6
|
const error_util_1 = require("../error/error.util");
|
|
6
7
|
const http_error_1 = require("../error/http.error");
|
|
7
8
|
const number_util_1 = require("../number/number.util");
|
|
@@ -12,7 +13,7 @@ const time_util_1 = require("../time/time.util");
|
|
|
12
13
|
const http_model_1 = require("./http.model");
|
|
13
14
|
const defRetryOptions = {
|
|
14
15
|
count: 2,
|
|
15
|
-
timeout:
|
|
16
|
+
timeout: 1000,
|
|
16
17
|
timeoutMax: 30000,
|
|
17
18
|
timeoutMultiplier: 2,
|
|
18
19
|
};
|
|
@@ -94,7 +95,7 @@ class Fetcher {
|
|
|
94
95
|
async rawFetch(url, rawOpt = {}) {
|
|
95
96
|
const { logger } = this.cfg;
|
|
96
97
|
const req = this.normalizeOptions(url, rawOpt);
|
|
97
|
-
const { timeoutSeconds,
|
|
98
|
+
const { timeoutSeconds, init: { method }, } = req;
|
|
98
99
|
// setup timeout
|
|
99
100
|
let timeout;
|
|
100
101
|
if (timeoutSeconds) {
|
|
@@ -107,6 +108,10 @@ class Fetcher {
|
|
|
107
108
|
for await (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
108
109
|
await hook(req);
|
|
109
110
|
}
|
|
111
|
+
const isFullUrl = req.url.includes('://');
|
|
112
|
+
const fullUrl = isFullUrl ? new URL(req.url) : undefined;
|
|
113
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.url;
|
|
114
|
+
const signature = [method, shortUrl].join(' ');
|
|
110
115
|
const res = {
|
|
111
116
|
req,
|
|
112
117
|
retryStatus: {
|
|
@@ -114,10 +119,8 @@ class Fetcher {
|
|
|
114
119
|
retryStopped: false,
|
|
115
120
|
retryTimeout: req.retry.timeout,
|
|
116
121
|
},
|
|
122
|
+
signature,
|
|
117
123
|
};
|
|
118
|
-
const fullUrl = new URL(req.url);
|
|
119
|
-
const shortUrl = this.getShortUrl(fullUrl);
|
|
120
|
-
const signature = [method, shortUrl].join(' ');
|
|
121
124
|
/* eslint-disable no-await-in-loop */
|
|
122
125
|
while (!res.retryStatus.retryStopped) {
|
|
123
126
|
const started = Date.now();
|
|
@@ -141,95 +144,11 @@ class Fetcher {
|
|
|
141
144
|
}
|
|
142
145
|
res.statusFamily = this.getStatusFamily(res);
|
|
143
146
|
if (res.fetchResponse?.ok) {
|
|
144
|
-
|
|
145
|
-
if (res.fetchResponse.body) {
|
|
146
|
-
const text = await res.fetchResponse.text();
|
|
147
|
-
if (text) {
|
|
148
|
-
try {
|
|
149
|
-
res.body = text;
|
|
150
|
-
res.body = JSON.parse(text, req.jsonReviver);
|
|
151
|
-
}
|
|
152
|
-
catch (err) {
|
|
153
|
-
const { message } = (0, error_util_1._anyToError)(err);
|
|
154
|
-
res.err = new http_error_1.HttpError([signature, message].join('\n'), {
|
|
155
|
-
httpStatusCode: 0,
|
|
156
|
-
url: req.url,
|
|
157
|
-
});
|
|
158
|
-
res.ok = false;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
// Body had a '' (empty string)
|
|
163
|
-
res.body = {};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
// if no body: set responseBody as {}
|
|
168
|
-
// do not throw a "cannot parse null as Json" error
|
|
169
|
-
res.body = {};
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
else if (mode === 'text') {
|
|
173
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
|
|
174
|
-
}
|
|
175
|
-
else if (mode === 'arrayBuffer') {
|
|
176
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
|
|
177
|
-
}
|
|
178
|
-
else if (mode === 'blob') {
|
|
179
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
|
|
180
|
-
}
|
|
181
|
-
clearTimeout(timeout);
|
|
182
|
-
res.retryStatus.retryStopped = true;
|
|
183
|
-
// res.err can happen on JSON.parse error
|
|
184
|
-
if (!res.err && this.cfg.logResponse) {
|
|
185
|
-
const { retryAttempt } = res.retryStatus;
|
|
186
|
-
logger.log([
|
|
187
|
-
' <<',
|
|
188
|
-
res.fetchResponse.status,
|
|
189
|
-
signature,
|
|
190
|
-
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
191
|
-
(0, time_util_1._since)(started),
|
|
192
|
-
]
|
|
193
|
-
.filter(Boolean)
|
|
194
|
-
.join(' '));
|
|
195
|
-
if (this.cfg.logResponseBody) {
|
|
196
|
-
logger.log(res.body);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
147
|
+
await this.onOkResponse(res, started, timeout);
|
|
199
148
|
}
|
|
200
149
|
else {
|
|
201
150
|
// !res.ok
|
|
202
|
-
|
|
203
|
-
let errObj;
|
|
204
|
-
if (res.fetchResponse) {
|
|
205
|
-
const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
|
|
206
|
-
errObj = (0, error_util_1._anyToErrorObject)(body);
|
|
207
|
-
}
|
|
208
|
-
else if (res.err) {
|
|
209
|
-
errObj = (0, error_util_1._errorToErrorObject)(res.err);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
errObj = {};
|
|
213
|
-
}
|
|
214
|
-
const originalMessage = errObj.message;
|
|
215
|
-
errObj.message = [
|
|
216
|
-
[res.fetchResponse?.status, signature].filter(Boolean).join(' '),
|
|
217
|
-
originalMessage,
|
|
218
|
-
]
|
|
219
|
-
.filter(Boolean)
|
|
220
|
-
.join('\n');
|
|
221
|
-
res.err = new http_error_1.HttpError(errObj.message, (0, object_util_1._filterNullishValues)({
|
|
222
|
-
...errObj.data,
|
|
223
|
-
originalMessage,
|
|
224
|
-
httpStatusCode: res.fetchResponse?.status || 0,
|
|
225
|
-
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
226
|
-
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
227
|
-
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
228
|
-
// method: req.method,
|
|
229
|
-
url: req.url,
|
|
230
|
-
// tryCount: req.tryCount,
|
|
231
|
-
}));
|
|
232
|
-
await this.processRetry(res);
|
|
151
|
+
await this.onNotOkResponse(res, timeout);
|
|
233
152
|
}
|
|
234
153
|
}
|
|
235
154
|
for await (const hook of this.cfg.hooks.afterResponse || []) {
|
|
@@ -237,6 +156,99 @@ class Fetcher {
|
|
|
237
156
|
}
|
|
238
157
|
return res;
|
|
239
158
|
}
|
|
159
|
+
async onOkResponse(res, started, timeout) {
|
|
160
|
+
const { req } = res;
|
|
161
|
+
const { mode } = res.req;
|
|
162
|
+
if (mode === 'json') {
|
|
163
|
+
if (res.fetchResponse.body) {
|
|
164
|
+
const text = await res.fetchResponse.text();
|
|
165
|
+
if (text) {
|
|
166
|
+
try {
|
|
167
|
+
res.body = text;
|
|
168
|
+
res.body = JSON.parse(text, req.jsonReviver);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
const { message } = (0, error_util_1._anyToError)(err);
|
|
172
|
+
res.err = new http_error_1.HttpError([res.signature, message].join('\n'), {
|
|
173
|
+
httpStatusCode: 0,
|
|
174
|
+
url: req.url,
|
|
175
|
+
});
|
|
176
|
+
res.ok = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
// Body had a '' (empty string)
|
|
181
|
+
res.body = {};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// if no body: set responseBody as {}
|
|
186
|
+
// do not throw a "cannot parse null as Json" error
|
|
187
|
+
res.body = {};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (mode === 'text') {
|
|
191
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
|
|
192
|
+
}
|
|
193
|
+
else if (mode === 'arrayBuffer') {
|
|
194
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
|
|
195
|
+
}
|
|
196
|
+
else if (mode === 'blob') {
|
|
197
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
|
|
198
|
+
}
|
|
199
|
+
clearTimeout(timeout);
|
|
200
|
+
res.retryStatus.retryStopped = true;
|
|
201
|
+
// res.err can happen on JSON.parse error
|
|
202
|
+
if (!res.err && this.cfg.logResponse) {
|
|
203
|
+
const { retryAttempt } = res.retryStatus;
|
|
204
|
+
const { logger } = this.cfg;
|
|
205
|
+
logger.log([
|
|
206
|
+
' <<',
|
|
207
|
+
res.fetchResponse.status,
|
|
208
|
+
res.signature,
|
|
209
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
210
|
+
(0, time_util_1._since)(started),
|
|
211
|
+
]
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join(' '));
|
|
214
|
+
if (this.cfg.logResponseBody) {
|
|
215
|
+
logger.log(res.body);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async onNotOkResponse(res, timeout) {
|
|
220
|
+
clearTimeout(timeout);
|
|
221
|
+
let errObj;
|
|
222
|
+
if (res.fetchResponse) {
|
|
223
|
+
const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
|
|
224
|
+
errObj = (0, error_util_1._anyToErrorObject)(body);
|
|
225
|
+
}
|
|
226
|
+
else if (res.err) {
|
|
227
|
+
errObj = (0, error_util_1._errorToErrorObject)(res.err);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
errObj = {};
|
|
231
|
+
}
|
|
232
|
+
const originalMessage = errObj.message;
|
|
233
|
+
errObj.message = [
|
|
234
|
+
[res.fetchResponse?.status, res.signature].filter(Boolean).join(' '),
|
|
235
|
+
originalMessage,
|
|
236
|
+
]
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join('\n');
|
|
239
|
+
res.err = new http_error_1.HttpError(errObj.message, (0, object_util_1._filterNullishValues)({
|
|
240
|
+
...errObj.data,
|
|
241
|
+
originalMessage,
|
|
242
|
+
httpStatusCode: res.fetchResponse?.status || 0,
|
|
243
|
+
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
244
|
+
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
245
|
+
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
246
|
+
// method: req.method,
|
|
247
|
+
url: res.req.url,
|
|
248
|
+
// tryCount: req.tryCount,
|
|
249
|
+
}));
|
|
250
|
+
await this.processRetry(res);
|
|
251
|
+
}
|
|
240
252
|
async processRetry(res) {
|
|
241
253
|
const { retryStatus } = res;
|
|
242
254
|
if (!this.shouldRetry(res)) {
|
|
@@ -253,11 +265,14 @@ class Fetcher {
|
|
|
253
265
|
return;
|
|
254
266
|
retryStatus.retryAttempt++;
|
|
255
267
|
retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
256
|
-
|
|
268
|
+
const noise = Math.random() * 500;
|
|
269
|
+
await (0, pDelay_1.pDelay)(retryStatus.retryTimeout + noise);
|
|
257
270
|
}
|
|
258
271
|
/**
|
|
259
272
|
* Default is yes,
|
|
260
273
|
* unless there's reason not to (e.g method is POST).
|
|
274
|
+
*
|
|
275
|
+
* statusCode of 0 (or absense of it) will BE retried.
|
|
261
276
|
*/
|
|
262
277
|
shouldRetry(res) {
|
|
263
278
|
const { retryPost, retry4xx, retry5xx } = res.req;
|
|
@@ -304,7 +319,7 @@ class Fetcher {
|
|
|
304
319
|
if (!this.cfg.logWithSearchParams) {
|
|
305
320
|
shortUrl = shortUrl.split('?')[0];
|
|
306
321
|
}
|
|
307
|
-
if (!this.cfg.
|
|
322
|
+
if (!this.cfg.logWithBaseUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
308
323
|
shortUrl = shortUrl.slice(baseUrl.length);
|
|
309
324
|
}
|
|
310
325
|
return shortUrl;
|
|
@@ -331,7 +346,7 @@ class Fetcher {
|
|
|
331
346
|
logRequestBody: debug,
|
|
332
347
|
logResponse: debug,
|
|
333
348
|
logResponseBody: debug,
|
|
334
|
-
|
|
349
|
+
logWithBaseUrl: (0, env_1.isServerSide)(),
|
|
335
350
|
logWithSearchParams: true,
|
|
336
351
|
retry: { ...defRetryOptions },
|
|
337
352
|
init: {
|
|
@@ -43,10 +43,19 @@ export interface FetcherCfg {
|
|
|
43
43
|
logResponse?: boolean;
|
|
44
44
|
logResponseBody?: boolean;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Controls if `baseUrl` should be included in logs (both success and error).
|
|
47
|
+
*
|
|
48
|
+
* Defaults to `true` on ServerSide and `false` on ClientSide.
|
|
49
|
+
*
|
|
50
|
+
* Reasoning.
|
|
51
|
+
*
|
|
52
|
+
* ClientSide often uses one main "backend host".
|
|
53
|
+
* Not including baseUrl improves Sentry error grouping.
|
|
54
|
+
*
|
|
55
|
+
* ServerSide often uses one Fetcher instance per 3rd-party API.
|
|
56
|
+
* Not including baseUrl can introduce confusion of "which API is it?".
|
|
48
57
|
*/
|
|
49
|
-
|
|
58
|
+
logWithBaseUrl?: boolean;
|
|
50
59
|
/**
|
|
51
60
|
* Default to true.
|
|
52
61
|
* Set to false to strip searchParams from url when logging (both success and error)
|
|
@@ -138,6 +147,7 @@ export interface FetcherSuccessResponse<BODY = unknown> {
|
|
|
138
147
|
req: FetcherRequest;
|
|
139
148
|
statusFamily?: HttpStatusFamily;
|
|
140
149
|
retryStatus: FetcherRetryStatus;
|
|
150
|
+
signature: string;
|
|
141
151
|
}
|
|
142
152
|
export interface FetcherErrorResponse<BODY = unknown> {
|
|
143
153
|
ok: false;
|
|
@@ -147,6 +157,7 @@ export interface FetcherErrorResponse<BODY = unknown> {
|
|
|
147
157
|
req: FetcherRequest;
|
|
148
158
|
statusFamily?: HttpStatusFamily;
|
|
149
159
|
retryStatus: FetcherRetryStatus;
|
|
160
|
+
signature: string;
|
|
150
161
|
}
|
|
151
162
|
export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
|
|
152
163
|
export type FetcherMode = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob';
|
package/dist/index.d.ts
CHANGED
|
@@ -72,6 +72,7 @@ export * from './datetime/localDate';
|
|
|
72
72
|
export * from './datetime/localTime';
|
|
73
73
|
export * from './datetime/dateInterval';
|
|
74
74
|
export * from './datetime/timeInterval';
|
|
75
|
+
export * from './env';
|
|
75
76
|
export * from './http/http.model';
|
|
76
77
|
export * from './http/fetcher';
|
|
77
78
|
export * from './http/fetcher.model';
|
package/dist/index.js
CHANGED
|
@@ -76,6 +76,7 @@ tslib_1.__exportStar(require("./datetime/localDate"), exports);
|
|
|
76
76
|
tslib_1.__exportStar(require("./datetime/localTime"), exports);
|
|
77
77
|
tslib_1.__exportStar(require("./datetime/dateInterval"), exports);
|
|
78
78
|
tslib_1.__exportStar(require("./datetime/timeInterval"), exports);
|
|
79
|
+
tslib_1.__exportStar(require("./env"), exports);
|
|
79
80
|
tslib_1.__exportStar(require("./http/http.model"), exports);
|
|
80
81
|
tslib_1.__exportStar(require("./http/fetcher"), exports);
|
|
81
82
|
tslib_1.__exportStar(require("./http/fetcher.model"), exports);
|
|
@@ -81,6 +81,8 @@ function _stringifyAny(obj, opt = {}) {
|
|
|
81
81
|
}
|
|
82
82
|
if ((0, error_util_1._isErrorObject)(obj)) {
|
|
83
83
|
if ((0, error_util_1._isHttpErrorObject)(obj)) {
|
|
84
|
+
// Only include (statusCode) if it's non-zero
|
|
85
|
+
// No: print (0), as it removes ambiguity
|
|
84
86
|
// `replace` here works ONCE, exactly as we need it
|
|
85
87
|
s = s.replace('HttpError', `HttpError(${obj.data.httpStatusCode})`);
|
|
86
88
|
}
|
package/dist/vendor/is.d.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
/// <reference lib="dom" />
|
|
6
6
|
import { Class, ObservableLike, Primitive, TypedArray } from '../typeFest';
|
|
7
7
|
declare const objectTypeNames: readonly ["Function", "Generator", "AsyncGenerator", "GeneratorFunction", "AsyncGeneratorFunction", "AsyncFunction", "Observable", "Array", "Buffer", "Object", "RegExp", "Date", "Error", "Map", "Set", "WeakMap", "WeakSet", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Promise", "URL", "FormData", "URLSearchParams", "HTMLElement", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array"];
|
|
8
|
-
type ObjectTypeName = typeof objectTypeNames[number];
|
|
8
|
+
type ObjectTypeName = (typeof objectTypeNames)[number];
|
|
9
9
|
declare const primitiveTypeNames: readonly ["null", "undefined", "string", "number", "bigint", "boolean", "symbol"];
|
|
10
|
-
type PrimitiveTypeName = typeof primitiveTypeNames[number];
|
|
10
|
+
type PrimitiveTypeName = (typeof primitiveTypeNames)[number];
|
|
11
11
|
export type TypeName = ObjectTypeName | PrimitiveTypeName;
|
|
12
12
|
export declare function is(value: unknown): TypeName;
|
|
13
13
|
export declare namespace is {
|
package/dist-esm/env.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use it to detect SSR/Node.js environment.
|
|
3
|
+
*
|
|
4
|
+
* Will return `true` in Node.js.
|
|
5
|
+
* Will return `false` in the Browser.
|
|
6
|
+
*/
|
|
7
|
+
export function isServerSide() {
|
|
8
|
+
return typeof window === 'undefined';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Use it to detect Browser (not SSR/Node) environment.
|
|
12
|
+
*
|
|
13
|
+
* Will return `true` in the Browser.
|
|
14
|
+
* Will return `false` in Node.js.
|
|
15
|
+
*/
|
|
16
|
+
export function isClientSide() {
|
|
17
|
+
return typeof window !== 'undefined';
|
|
18
|
+
}
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference lib="dom"/>
|
|
2
2
|
import { __asyncValues } from "tslib";
|
|
3
|
+
import { isServerSide } from '../env';
|
|
3
4
|
import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util';
|
|
4
5
|
import { HttpError } from '../error/http.error';
|
|
5
6
|
import { _clamp } from '../number/number.util';
|
|
@@ -10,7 +11,7 @@ import { _since } from '../time/time.util';
|
|
|
10
11
|
import { HTTP_METHODS } from './http.model';
|
|
11
12
|
const defRetryOptions = {
|
|
12
13
|
count: 2,
|
|
13
|
-
timeout:
|
|
14
|
+
timeout: 1000,
|
|
14
15
|
timeoutMax: 30000,
|
|
15
16
|
timeoutMultiplier: 2,
|
|
16
17
|
};
|
|
@@ -82,10 +83,10 @@ export class Fetcher {
|
|
|
82
83
|
*/
|
|
83
84
|
async rawFetch(url, rawOpt = {}) {
|
|
84
85
|
var _a, e_1, _b, _c, _d, e_2, _e, _f;
|
|
85
|
-
var _g
|
|
86
|
+
var _g;
|
|
86
87
|
const { logger } = this.cfg;
|
|
87
88
|
const req = this.normalizeOptions(url, rawOpt);
|
|
88
|
-
const { timeoutSeconds,
|
|
89
|
+
const { timeoutSeconds, init: { method }, } = req;
|
|
89
90
|
// setup timeout
|
|
90
91
|
let timeout;
|
|
91
92
|
if (timeoutSeconds) {
|
|
@@ -96,25 +97,29 @@ export class Fetcher {
|
|
|
96
97
|
}, timeoutSeconds * 1000);
|
|
97
98
|
}
|
|
98
99
|
try {
|
|
99
|
-
for (var
|
|
100
|
-
_c =
|
|
101
|
-
|
|
100
|
+
for (var _h = true, _j = __asyncValues(this.cfg.hooks.beforeRequest || []), _k; _k = await _j.next(), _a = _k.done, !_a;) {
|
|
101
|
+
_c = _k.value;
|
|
102
|
+
_h = false;
|
|
102
103
|
try {
|
|
103
104
|
const hook = _c;
|
|
104
105
|
await hook(req);
|
|
105
106
|
}
|
|
106
107
|
finally {
|
|
107
|
-
|
|
108
|
+
_h = true;
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
112
113
|
finally {
|
|
113
114
|
try {
|
|
114
|
-
if (!
|
|
115
|
+
if (!_h && !_a && (_b = _j.return)) await _b.call(_j);
|
|
115
116
|
}
|
|
116
117
|
finally { if (e_1) throw e_1.error; }
|
|
117
118
|
}
|
|
119
|
+
const isFullUrl = req.url.includes('://');
|
|
120
|
+
const fullUrl = isFullUrl ? new URL(req.url) : undefined;
|
|
121
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.url;
|
|
122
|
+
const signature = [method, shortUrl].join(' ');
|
|
118
123
|
const res = {
|
|
119
124
|
req,
|
|
120
125
|
retryStatus: {
|
|
@@ -122,10 +127,8 @@ export class Fetcher {
|
|
|
122
127
|
retryStopped: false,
|
|
123
128
|
retryTimeout: req.retry.timeout,
|
|
124
129
|
},
|
|
130
|
+
signature,
|
|
125
131
|
};
|
|
126
|
-
const fullUrl = new URL(req.url);
|
|
127
|
-
const shortUrl = this.getShortUrl(fullUrl);
|
|
128
|
-
const signature = [method, shortUrl].join(' ');
|
|
129
132
|
/* eslint-disable no-await-in-loop */
|
|
130
133
|
while (!res.retryStatus.retryStopped) {
|
|
131
134
|
const started = Date.now();
|
|
@@ -149,114 +152,124 @@ export class Fetcher {
|
|
|
149
152
|
}
|
|
150
153
|
res.statusFamily = this.getStatusFamily(res);
|
|
151
154
|
if ((_g = res.fetchResponse) === null || _g === void 0 ? void 0 : _g.ok) {
|
|
152
|
-
|
|
153
|
-
if (res.fetchResponse.body) {
|
|
154
|
-
const text = await res.fetchResponse.text();
|
|
155
|
-
if (text) {
|
|
156
|
-
try {
|
|
157
|
-
res.body = text;
|
|
158
|
-
res.body = JSON.parse(text, req.jsonReviver);
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
const { message } = _anyToError(err);
|
|
162
|
-
res.err = new HttpError([signature, message].join('\n'), {
|
|
163
|
-
httpStatusCode: 0,
|
|
164
|
-
url: req.url,
|
|
165
|
-
});
|
|
166
|
-
res.ok = false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
// Body had a '' (empty string)
|
|
171
|
-
res.body = {};
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
// if no body: set responseBody as {}
|
|
176
|
-
// do not throw a "cannot parse null as Json" error
|
|
177
|
-
res.body = {};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
else if (mode === 'text') {
|
|
181
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
|
|
182
|
-
}
|
|
183
|
-
else if (mode === 'arrayBuffer') {
|
|
184
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
|
|
185
|
-
}
|
|
186
|
-
else if (mode === 'blob') {
|
|
187
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
|
|
188
|
-
}
|
|
189
|
-
clearTimeout(timeout);
|
|
190
|
-
res.retryStatus.retryStopped = true;
|
|
191
|
-
// res.err can happen on JSON.parse error
|
|
192
|
-
if (!res.err && this.cfg.logResponse) {
|
|
193
|
-
const { retryAttempt } = res.retryStatus;
|
|
194
|
-
logger.log([
|
|
195
|
-
' <<',
|
|
196
|
-
res.fetchResponse.status,
|
|
197
|
-
signature,
|
|
198
|
-
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
199
|
-
_since(started),
|
|
200
|
-
]
|
|
201
|
-
.filter(Boolean)
|
|
202
|
-
.join(' '));
|
|
203
|
-
if (this.cfg.logResponseBody) {
|
|
204
|
-
logger.log(res.body);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
155
|
+
await this.onOkResponse(res, started, timeout);
|
|
207
156
|
}
|
|
208
157
|
else {
|
|
209
158
|
// !res.ok
|
|
210
|
-
|
|
211
|
-
let errObj;
|
|
212
|
-
if (res.fetchResponse) {
|
|
213
|
-
const body = _jsonParseIfPossible(await res.fetchResponse.text());
|
|
214
|
-
errObj = _anyToErrorObject(body);
|
|
215
|
-
}
|
|
216
|
-
else if (res.err) {
|
|
217
|
-
errObj = _errorToErrorObject(res.err);
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
errObj = {};
|
|
221
|
-
}
|
|
222
|
-
const originalMessage = errObj.message;
|
|
223
|
-
errObj.message = [
|
|
224
|
-
[(_h = res.fetchResponse) === null || _h === void 0 ? void 0 : _h.status, signature].filter(Boolean).join(' '),
|
|
225
|
-
originalMessage,
|
|
226
|
-
]
|
|
227
|
-
.filter(Boolean)
|
|
228
|
-
.join('\n');
|
|
229
|
-
res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: ((_j = res.fetchResponse) === null || _j === void 0 ? void 0 : _j.status) || 0,
|
|
230
|
-
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
231
|
-
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
232
|
-
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
233
|
-
// method: req.method,
|
|
234
|
-
url: req.url })));
|
|
235
|
-
await this.processRetry(res);
|
|
159
|
+
await this.onNotOkResponse(res, timeout);
|
|
236
160
|
}
|
|
237
161
|
}
|
|
238
162
|
try {
|
|
239
|
-
for (var
|
|
240
|
-
_f =
|
|
241
|
-
|
|
163
|
+
for (var _l = true, _m = __asyncValues(this.cfg.hooks.afterResponse || []), _o; _o = await _m.next(), _d = _o.done, !_d;) {
|
|
164
|
+
_f = _o.value;
|
|
165
|
+
_l = false;
|
|
242
166
|
try {
|
|
243
167
|
const hook = _f;
|
|
244
168
|
await hook(res);
|
|
245
169
|
}
|
|
246
170
|
finally {
|
|
247
|
-
|
|
171
|
+
_l = true;
|
|
248
172
|
}
|
|
249
173
|
}
|
|
250
174
|
}
|
|
251
175
|
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
252
176
|
finally {
|
|
253
177
|
try {
|
|
254
|
-
if (!
|
|
178
|
+
if (!_l && !_d && (_e = _m.return)) await _e.call(_m);
|
|
255
179
|
}
|
|
256
180
|
finally { if (e_2) throw e_2.error; }
|
|
257
181
|
}
|
|
258
182
|
return res;
|
|
259
183
|
}
|
|
184
|
+
async onOkResponse(res, started, timeout) {
|
|
185
|
+
const { req } = res;
|
|
186
|
+
const { mode } = res.req;
|
|
187
|
+
if (mode === 'json') {
|
|
188
|
+
if (res.fetchResponse.body) {
|
|
189
|
+
const text = await res.fetchResponse.text();
|
|
190
|
+
if (text) {
|
|
191
|
+
try {
|
|
192
|
+
res.body = text;
|
|
193
|
+
res.body = JSON.parse(text, req.jsonReviver);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const { message } = _anyToError(err);
|
|
197
|
+
res.err = new HttpError([res.signature, message].join('\n'), {
|
|
198
|
+
httpStatusCode: 0,
|
|
199
|
+
url: req.url,
|
|
200
|
+
});
|
|
201
|
+
res.ok = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Body had a '' (empty string)
|
|
206
|
+
res.body = {};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// if no body: set responseBody as {}
|
|
211
|
+
// do not throw a "cannot parse null as Json" error
|
|
212
|
+
res.body = {};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (mode === 'text') {
|
|
216
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
|
|
217
|
+
}
|
|
218
|
+
else if (mode === 'arrayBuffer') {
|
|
219
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
|
|
220
|
+
}
|
|
221
|
+
else if (mode === 'blob') {
|
|
222
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
|
|
223
|
+
}
|
|
224
|
+
clearTimeout(timeout);
|
|
225
|
+
res.retryStatus.retryStopped = true;
|
|
226
|
+
// res.err can happen on JSON.parse error
|
|
227
|
+
if (!res.err && this.cfg.logResponse) {
|
|
228
|
+
const { retryAttempt } = res.retryStatus;
|
|
229
|
+
const { logger } = this.cfg;
|
|
230
|
+
logger.log([
|
|
231
|
+
' <<',
|
|
232
|
+
res.fetchResponse.status,
|
|
233
|
+
res.signature,
|
|
234
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
235
|
+
_since(started),
|
|
236
|
+
]
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join(' '));
|
|
239
|
+
if (this.cfg.logResponseBody) {
|
|
240
|
+
logger.log(res.body);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async onNotOkResponse(res, timeout) {
|
|
245
|
+
var _a, _b;
|
|
246
|
+
clearTimeout(timeout);
|
|
247
|
+
let errObj;
|
|
248
|
+
if (res.fetchResponse) {
|
|
249
|
+
const body = _jsonParseIfPossible(await res.fetchResponse.text());
|
|
250
|
+
errObj = _anyToErrorObject(body);
|
|
251
|
+
}
|
|
252
|
+
else if (res.err) {
|
|
253
|
+
errObj = _errorToErrorObject(res.err);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
errObj = {};
|
|
257
|
+
}
|
|
258
|
+
const originalMessage = errObj.message;
|
|
259
|
+
errObj.message = [
|
|
260
|
+
[(_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status, res.signature].filter(Boolean).join(' '),
|
|
261
|
+
originalMessage,
|
|
262
|
+
]
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.join('\n');
|
|
265
|
+
res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
|
|
266
|
+
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
267
|
+
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
268
|
+
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
269
|
+
// method: req.method,
|
|
270
|
+
url: res.req.url })));
|
|
271
|
+
await this.processRetry(res);
|
|
272
|
+
}
|
|
260
273
|
async processRetry(res) {
|
|
261
274
|
var _a, e_3, _b, _c;
|
|
262
275
|
const { retryStatus } = res;
|
|
@@ -291,11 +304,14 @@ export class Fetcher {
|
|
|
291
304
|
return;
|
|
292
305
|
retryStatus.retryAttempt++;
|
|
293
306
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
294
|
-
|
|
307
|
+
const noise = Math.random() * 500;
|
|
308
|
+
await pDelay(retryStatus.retryTimeout + noise);
|
|
295
309
|
}
|
|
296
310
|
/**
|
|
297
311
|
* Default is yes,
|
|
298
312
|
* unless there's reason not to (e.g method is POST).
|
|
313
|
+
*
|
|
314
|
+
* statusCode of 0 (or absense of it) will BE retried.
|
|
299
315
|
*/
|
|
300
316
|
shouldRetry(res) {
|
|
301
317
|
var _a;
|
|
@@ -344,7 +360,7 @@ export class Fetcher {
|
|
|
344
360
|
if (!this.cfg.logWithSearchParams) {
|
|
345
361
|
shortUrl = shortUrl.split('?')[0];
|
|
346
362
|
}
|
|
347
|
-
if (!this.cfg.
|
|
363
|
+
if (!this.cfg.logWithBaseUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
348
364
|
shortUrl = shortUrl.slice(baseUrl.length);
|
|
349
365
|
}
|
|
350
366
|
return shortUrl;
|
|
@@ -372,7 +388,7 @@ export class Fetcher {
|
|
|
372
388
|
logRequestBody: debug,
|
|
373
389
|
logResponse: debug,
|
|
374
390
|
logResponseBody: debug,
|
|
375
|
-
|
|
391
|
+
logWithBaseUrl: isServerSide(),
|
|
376
392
|
logWithSearchParams: true,
|
|
377
393
|
retry: Object.assign({}, defRetryOptions),
|
|
378
394
|
init: {
|
package/dist-esm/index.js
CHANGED
|
@@ -72,6 +72,7 @@ export * from './datetime/localDate';
|
|
|
72
72
|
export * from './datetime/localTime';
|
|
73
73
|
export * from './datetime/dateInterval';
|
|
74
74
|
export * from './datetime/timeInterval';
|
|
75
|
+
export * from './env';
|
|
75
76
|
export * from './http/http.model';
|
|
76
77
|
export * from './http/fetcher';
|
|
77
78
|
export * from './http/fetcher.model';
|
|
@@ -77,6 +77,8 @@ export function _stringifyAny(obj, opt = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
if (_isErrorObject(obj)) {
|
|
79
79
|
if (_isHttpErrorObject(obj)) {
|
|
80
|
+
// Only include (statusCode) if it's non-zero
|
|
81
|
+
// No: print (0), as it removes ambiguity
|
|
80
82
|
// `replace` here works ONCE, exactly as we need it
|
|
81
83
|
s = s.replace('HttpError', `HttpError(${obj.data.httpStatusCode})`);
|
|
82
84
|
}
|
package/package.json
CHANGED
package/src/env.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use it to detect SSR/Node.js environment.
|
|
3
|
+
*
|
|
4
|
+
* Will return `true` in Node.js.
|
|
5
|
+
* Will return `false` in the Browser.
|
|
6
|
+
*/
|
|
7
|
+
export function isServerSide(): boolean {
|
|
8
|
+
return typeof window === 'undefined'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Use it to detect Browser (not SSR/Node) environment.
|
|
13
|
+
*
|
|
14
|
+
* Will return `true` in the Browser.
|
|
15
|
+
* Will return `false` in Node.js.
|
|
16
|
+
*/
|
|
17
|
+
export function isClientSide(): boolean {
|
|
18
|
+
return typeof window !== 'undefined'
|
|
19
|
+
}
|
|
@@ -49,10 +49,19 @@ export interface FetcherCfg {
|
|
|
49
49
|
logResponseBody?: boolean
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Controls if `baseUrl` should be included in logs (both success and error).
|
|
53
|
+
*
|
|
54
|
+
* Defaults to `true` on ServerSide and `false` on ClientSide.
|
|
55
|
+
*
|
|
56
|
+
* Reasoning.
|
|
57
|
+
*
|
|
58
|
+
* ClientSide often uses one main "backend host".
|
|
59
|
+
* Not including baseUrl improves Sentry error grouping.
|
|
60
|
+
*
|
|
61
|
+
* ServerSide often uses one Fetcher instance per 3rd-party API.
|
|
62
|
+
* Not including baseUrl can introduce confusion of "which API is it?".
|
|
54
63
|
*/
|
|
55
|
-
|
|
64
|
+
logWithBaseUrl?: boolean
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Default to true.
|
|
@@ -165,6 +174,7 @@ export interface FetcherSuccessResponse<BODY = unknown> {
|
|
|
165
174
|
req: FetcherRequest
|
|
166
175
|
statusFamily?: HttpStatusFamily
|
|
167
176
|
retryStatus: FetcherRetryStatus
|
|
177
|
+
signature: string
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
export interface FetcherErrorResponse<BODY = unknown> {
|
|
@@ -175,6 +185,7 @@ export interface FetcherErrorResponse<BODY = unknown> {
|
|
|
175
185
|
req: FetcherRequest
|
|
176
186
|
statusFamily?: HttpStatusFamily
|
|
177
187
|
retryStatus: FetcherRetryStatus
|
|
188
|
+
signature: string
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
export type FetcherResponse<BODY = unknown> =
|
package/src/http/fetcher.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference lib="dom"/>
|
|
2
2
|
|
|
3
|
+
import { isServerSide } from '../env'
|
|
3
4
|
import { ErrorObject } from '../error/error.model'
|
|
4
5
|
import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util'
|
|
5
6
|
import { HttpError } from '../error/http.error'
|
|
@@ -30,7 +31,7 @@ import type { HttpStatusFamily } from './http.model'
|
|
|
30
31
|
|
|
31
32
|
const defRetryOptions: FetcherRetryOptions = {
|
|
32
33
|
count: 2,
|
|
33
|
-
timeout:
|
|
34
|
+
timeout: 1000,
|
|
34
35
|
timeoutMax: 30_000,
|
|
35
36
|
timeoutMultiplier: 2,
|
|
36
37
|
}
|
|
@@ -151,7 +152,6 @@ export class Fetcher {
|
|
|
151
152
|
const req = this.normalizeOptions(url, rawOpt)
|
|
152
153
|
const {
|
|
153
154
|
timeoutSeconds,
|
|
154
|
-
mode,
|
|
155
155
|
init: { method },
|
|
156
156
|
} = req
|
|
157
157
|
|
|
@@ -169,6 +169,11 @@ export class Fetcher {
|
|
|
169
169
|
await hook(req)
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
const isFullUrl = req.url.includes('://')
|
|
173
|
+
const fullUrl = isFullUrl ? new URL(req.url) : undefined
|
|
174
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.url
|
|
175
|
+
const signature = [method, shortUrl].join(' ')
|
|
176
|
+
|
|
172
177
|
const res = {
|
|
173
178
|
req,
|
|
174
179
|
retryStatus: {
|
|
@@ -176,12 +181,9 @@ export class Fetcher {
|
|
|
176
181
|
retryStopped: false,
|
|
177
182
|
retryTimeout: req.retry.timeout,
|
|
178
183
|
},
|
|
184
|
+
signature,
|
|
179
185
|
} as FetcherResponse<any>
|
|
180
186
|
|
|
181
|
-
const fullUrl = new URL(req.url)
|
|
182
|
-
const shortUrl = this.getShortUrl(fullUrl)
|
|
183
|
-
const signature = [method, shortUrl].join(' ')
|
|
184
|
-
|
|
185
187
|
/* eslint-disable no-await-in-loop */
|
|
186
188
|
while (!res.retryStatus.retryStopped) {
|
|
187
189
|
const started = Date.now()
|
|
@@ -209,109 +211,129 @@ export class Fetcher {
|
|
|
209
211
|
res.statusFamily = this.getStatusFamily(res)
|
|
210
212
|
|
|
211
213
|
if (res.fetchResponse?.ok) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
res.body = text
|
|
219
|
-
res.body = JSON.parse(text, req.jsonReviver)
|
|
220
|
-
} catch (err) {
|
|
221
|
-
const { message } = _anyToError(err)
|
|
222
|
-
res.err = new HttpError([signature, message].join('\n'), {
|
|
223
|
-
httpStatusCode: 0,
|
|
224
|
-
url: req.url,
|
|
225
|
-
})
|
|
226
|
-
res.ok = false
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
// Body had a '' (empty string)
|
|
230
|
-
res.body = {}
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
// if no body: set responseBody as {}
|
|
234
|
-
// do not throw a "cannot parse null as Json" error
|
|
235
|
-
res.body = {}
|
|
236
|
-
}
|
|
237
|
-
} else if (mode === 'text') {
|
|
238
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : ''
|
|
239
|
-
} else if (mode === 'arrayBuffer') {
|
|
240
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {}
|
|
241
|
-
} else if (mode === 'blob') {
|
|
242
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
clearTimeout(timeout)
|
|
246
|
-
res.retryStatus.retryStopped = true
|
|
247
|
-
|
|
248
|
-
// res.err can happen on JSON.parse error
|
|
249
|
-
if (!res.err && this.cfg.logResponse) {
|
|
250
|
-
const { retryAttempt } = res.retryStatus
|
|
251
|
-
logger.log(
|
|
252
|
-
[
|
|
253
|
-
' <<',
|
|
254
|
-
res.fetchResponse.status,
|
|
255
|
-
signature,
|
|
256
|
-
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
257
|
-
_since(started),
|
|
258
|
-
]
|
|
259
|
-
.filter(Boolean)
|
|
260
|
-
.join(' '),
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
if (this.cfg.logResponseBody) {
|
|
264
|
-
logger.log(res.body)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
214
|
+
await this.onOkResponse(
|
|
215
|
+
res as FetcherResponse<T> & { fetchResponse: Response },
|
|
216
|
+
started,
|
|
217
|
+
timeout,
|
|
218
|
+
)
|
|
267
219
|
} else {
|
|
268
220
|
// !res.ok
|
|
269
|
-
|
|
221
|
+
await this.onNotOkResponse(res, timeout)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for await (const hook of this.cfg.hooks.afterResponse || []) {
|
|
226
|
+
await hook(res)
|
|
227
|
+
}
|
|
270
228
|
|
|
271
|
-
|
|
229
|
+
return res
|
|
230
|
+
}
|
|
272
231
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
232
|
+
private async onOkResponse(
|
|
233
|
+
res: FetcherResponse<any> & { fetchResponse: Response },
|
|
234
|
+
started: number,
|
|
235
|
+
timeout?: number,
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
const { req } = res
|
|
238
|
+
const { mode } = res.req
|
|
239
|
+
|
|
240
|
+
if (mode === 'json') {
|
|
241
|
+
if (res.fetchResponse.body) {
|
|
242
|
+
const text = await res.fetchResponse.text()
|
|
243
|
+
|
|
244
|
+
if (text) {
|
|
245
|
+
try {
|
|
246
|
+
res.body = text
|
|
247
|
+
res.body = JSON.parse(text, req.jsonReviver)
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const { message } = _anyToError(err)
|
|
250
|
+
res.err = new HttpError([res.signature, message].join('\n'), {
|
|
251
|
+
httpStatusCode: 0,
|
|
252
|
+
url: req.url,
|
|
253
|
+
})
|
|
254
|
+
res.ok = false
|
|
255
|
+
}
|
|
278
256
|
} else {
|
|
279
|
-
|
|
257
|
+
// Body had a '' (empty string)
|
|
258
|
+
res.body = {}
|
|
280
259
|
}
|
|
260
|
+
} else {
|
|
261
|
+
// if no body: set responseBody as {}
|
|
262
|
+
// do not throw a "cannot parse null as Json" error
|
|
263
|
+
res.body = {}
|
|
264
|
+
}
|
|
265
|
+
} else if (mode === 'text') {
|
|
266
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : ''
|
|
267
|
+
} else if (mode === 'arrayBuffer') {
|
|
268
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {}
|
|
269
|
+
} else if (mode === 'blob') {
|
|
270
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {}
|
|
271
|
+
}
|
|
281
272
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
273
|
+
clearTimeout(timeout)
|
|
274
|
+
res.retryStatus.retryStopped = true
|
|
275
|
+
|
|
276
|
+
// res.err can happen on JSON.parse error
|
|
277
|
+
if (!res.err && this.cfg.logResponse) {
|
|
278
|
+
const { retryAttempt } = res.retryStatus
|
|
279
|
+
const { logger } = this.cfg
|
|
280
|
+
logger.log(
|
|
281
|
+
[
|
|
282
|
+
' <<',
|
|
283
|
+
res.fetchResponse.status,
|
|
284
|
+
res.signature,
|
|
285
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
286
|
+
_since(started),
|
|
286
287
|
]
|
|
287
288
|
.filter(Boolean)
|
|
288
|
-
.join('
|
|
289
|
-
|
|
290
|
-
res.err = new HttpError(
|
|
291
|
-
errObj.message,
|
|
292
|
-
|
|
293
|
-
_filterNullishValues({
|
|
294
|
-
...errObj.data,
|
|
295
|
-
originalMessage,
|
|
296
|
-
httpStatusCode: res.fetchResponse?.status || 0,
|
|
297
|
-
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
298
|
-
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
299
|
-
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
300
|
-
// method: req.method,
|
|
301
|
-
url: req.url,
|
|
302
|
-
// tryCount: req.tryCount,
|
|
303
|
-
}),
|
|
304
|
-
)
|
|
289
|
+
.join(' '),
|
|
290
|
+
)
|
|
305
291
|
|
|
306
|
-
|
|
292
|
+
if (this.cfg.logResponseBody) {
|
|
293
|
+
logger.log(res.body)
|
|
307
294
|
}
|
|
308
295
|
}
|
|
296
|
+
}
|
|
309
297
|
|
|
310
|
-
|
|
311
|
-
|
|
298
|
+
private async onNotOkResponse(res: FetcherResponse, timeout?: number): Promise<void> {
|
|
299
|
+
clearTimeout(timeout)
|
|
300
|
+
|
|
301
|
+
let errObj: ErrorObject
|
|
302
|
+
|
|
303
|
+
if (res.fetchResponse) {
|
|
304
|
+
const body = _jsonParseIfPossible(await res.fetchResponse.text())
|
|
305
|
+
errObj = _anyToErrorObject(body)
|
|
306
|
+
} else if (res.err) {
|
|
307
|
+
errObj = _errorToErrorObject(res.err)
|
|
308
|
+
} else {
|
|
309
|
+
errObj = {} as ErrorObject
|
|
312
310
|
}
|
|
313
311
|
|
|
314
|
-
|
|
312
|
+
const originalMessage = errObj.message
|
|
313
|
+
errObj.message = [
|
|
314
|
+
[res.fetchResponse?.status, res.signature].filter(Boolean).join(' '),
|
|
315
|
+
originalMessage,
|
|
316
|
+
]
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.join('\n')
|
|
319
|
+
|
|
320
|
+
res.err = new HttpError(
|
|
321
|
+
errObj.message,
|
|
322
|
+
|
|
323
|
+
_filterNullishValues({
|
|
324
|
+
...errObj.data,
|
|
325
|
+
originalMessage,
|
|
326
|
+
httpStatusCode: res.fetchResponse?.status || 0,
|
|
327
|
+
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
328
|
+
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
329
|
+
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
330
|
+
// method: req.method,
|
|
331
|
+
url: res.req.url,
|
|
332
|
+
// tryCount: req.tryCount,
|
|
333
|
+
}),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
await this.processRetry(res)
|
|
315
337
|
}
|
|
316
338
|
|
|
317
339
|
private async processRetry(res: FetcherResponse): Promise<void> {
|
|
@@ -336,12 +358,15 @@ export class Fetcher {
|
|
|
336
358
|
retryStatus.retryAttempt++
|
|
337
359
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax)
|
|
338
360
|
|
|
339
|
-
|
|
361
|
+
const noise = Math.random() * 500
|
|
362
|
+
await pDelay(retryStatus.retryTimeout + noise)
|
|
340
363
|
}
|
|
341
364
|
|
|
342
365
|
/**
|
|
343
366
|
* Default is yes,
|
|
344
367
|
* unless there's reason not to (e.g method is POST).
|
|
368
|
+
*
|
|
369
|
+
* statusCode of 0 (or absense of it) will BE retried.
|
|
345
370
|
*/
|
|
346
371
|
private shouldRetry(res: FetcherResponse): boolean {
|
|
347
372
|
const { retryPost, retry4xx, retry5xx } = res.req
|
|
@@ -385,7 +410,7 @@ export class Fetcher {
|
|
|
385
410
|
shortUrl = shortUrl.split('?')[0]!
|
|
386
411
|
}
|
|
387
412
|
|
|
388
|
-
if (!this.cfg.
|
|
413
|
+
if (!this.cfg.logWithBaseUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
389
414
|
shortUrl = shortUrl.slice(baseUrl.length)
|
|
390
415
|
}
|
|
391
416
|
|
|
@@ -416,7 +441,7 @@ export class Fetcher {
|
|
|
416
441
|
logRequestBody: debug,
|
|
417
442
|
logResponse: debug,
|
|
418
443
|
logResponseBody: debug,
|
|
419
|
-
|
|
444
|
+
logWithBaseUrl: isServerSide(),
|
|
420
445
|
logWithSearchParams: true,
|
|
421
446
|
retry: { ...defRetryOptions },
|
|
422
447
|
init: {
|
package/src/index.ts
CHANGED
|
@@ -72,6 +72,7 @@ export * from './datetime/localDate'
|
|
|
72
72
|
export * from './datetime/localTime'
|
|
73
73
|
export * from './datetime/dateInterval'
|
|
74
74
|
export * from './datetime/timeInterval'
|
|
75
|
+
export * from './env'
|
|
75
76
|
export * from './http/http.model'
|
|
76
77
|
export * from './http/fetcher'
|
|
77
78
|
export * from './http/fetcher.model'
|
|
@@ -125,6 +125,8 @@ export function _stringifyAny(obj: any, opt: StringifyAnyOptions = {}): string {
|
|
|
125
125
|
|
|
126
126
|
if (_isErrorObject(obj)) {
|
|
127
127
|
if (_isHttpErrorObject(obj)) {
|
|
128
|
+
// Only include (statusCode) if it's non-zero
|
|
129
|
+
// No: print (0), as it removes ambiguity
|
|
128
130
|
// `replace` here works ONCE, exactly as we need it
|
|
129
131
|
s = s.replace('HttpError', `HttpError(${obj.data.httpStatusCode})`)
|
|
130
132
|
}
|
package/src/vendor/is.ts
CHANGED
|
@@ -25,7 +25,7 @@ const typedArrayTypeNames = [
|
|
|
25
25
|
'BigUint64Array',
|
|
26
26
|
] as const
|
|
27
27
|
|
|
28
|
-
type TypedArrayTypeName = typeof typedArrayTypeNames[number]
|
|
28
|
+
type TypedArrayTypeName = (typeof typedArrayTypeNames)[number]
|
|
29
29
|
|
|
30
30
|
function isTypedArrayName(name: unknown): name is TypedArrayTypeName {
|
|
31
31
|
return typedArrayTypeNames.includes(name as TypedArrayTypeName)
|
|
@@ -60,7 +60,7 @@ const objectTypeNames = [
|
|
|
60
60
|
...typedArrayTypeNames,
|
|
61
61
|
] as const
|
|
62
62
|
|
|
63
|
-
type ObjectTypeName = typeof objectTypeNames[number]
|
|
63
|
+
type ObjectTypeName = (typeof objectTypeNames)[number]
|
|
64
64
|
|
|
65
65
|
function isObjectTypeName(name: unknown): name is ObjectTypeName {
|
|
66
66
|
return objectTypeNames.includes(name as ObjectTypeName)
|
|
@@ -76,7 +76,7 @@ const primitiveTypeNames = [
|
|
|
76
76
|
'symbol',
|
|
77
77
|
] as const
|
|
78
78
|
|
|
79
|
-
type PrimitiveTypeName = typeof primitiveTypeNames[number]
|
|
79
|
+
type PrimitiveTypeName = (typeof primitiveTypeNames)[number]
|
|
80
80
|
|
|
81
81
|
function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName {
|
|
82
82
|
return primitiveTypeNames.includes(name as PrimitiveTypeName)
|