@naturalcycles/js-lib 14.125.0 → 14.127.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http/fetcher.d.ts +1 -138
- package/dist/http/fetcher.js +19 -4
- package/dist/http/fetcher.model.d.ts +151 -0
- package/dist/http/fetcher.model.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist-esm/http/fetcher.js +19 -4
- package/dist-esm/http/fetcher.model.js +1 -0
- package/dist-esm/index.js +1 -0
- package/package.json +1 -1
- package/src/http/fetcher.model.ts +181 -0
- package/src/http/fetcher.ts +39 -171
- package/src/index.ts +1 -0
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -1,142 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import {
|
|
3
|
-
import type { Promisable } from '../typeFest';
|
|
4
|
-
import { Reviver } from '../types';
|
|
5
|
-
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
6
|
-
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
7
|
-
logger: CommonLogger;
|
|
8
|
-
searchParams: Record<string, any>;
|
|
9
|
-
}
|
|
10
|
-
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>;
|
|
11
|
-
export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>;
|
|
12
|
-
export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>;
|
|
13
|
-
export interface FetcherCfg {
|
|
14
|
-
/**
|
|
15
|
-
* Should **not** contain trailing slash.
|
|
16
|
-
*/
|
|
17
|
-
baseUrl?: string;
|
|
18
|
-
/**
|
|
19
|
-
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
|
|
20
|
-
* properties of hook function arguments.
|
|
21
|
-
* If you throw an error from the hook - it will be re-thrown as-is.
|
|
22
|
-
*/
|
|
23
|
-
hooks?: {
|
|
24
|
-
/**
|
|
25
|
-
* Allows to mutate req.
|
|
26
|
-
*/
|
|
27
|
-
beforeRequest?: FetcherBeforeRequestHook[];
|
|
28
|
-
/**
|
|
29
|
-
* Allows to mutate res.
|
|
30
|
-
* If you set `res.err` - it will be thrown.
|
|
31
|
-
*/
|
|
32
|
-
afterResponse?: FetcherAfterResponseHook[];
|
|
33
|
-
/**
|
|
34
|
-
* Allows to mutate res.retryStatus to override retry behavior.
|
|
35
|
-
*/
|
|
36
|
-
beforeRetry?: FetcherBeforeRetryHook[];
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* If true - enables all possible logging.
|
|
40
|
-
*/
|
|
41
|
-
debug?: boolean;
|
|
42
|
-
logRequest?: boolean;
|
|
43
|
-
logRequestBody?: boolean;
|
|
44
|
-
logResponse?: boolean;
|
|
45
|
-
logResponseBody?: boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Defaults to `console`.
|
|
48
|
-
*/
|
|
49
|
-
logger?: CommonLogger;
|
|
50
|
-
}
|
|
51
|
-
export interface FetcherRetryStatus {
|
|
52
|
-
retryAttempt: number;
|
|
53
|
-
retryTimeout: number;
|
|
54
|
-
retryStopped: boolean;
|
|
55
|
-
}
|
|
56
|
-
export interface FetcherRetryOptions {
|
|
57
|
-
count: number;
|
|
58
|
-
timeout: number;
|
|
59
|
-
timeoutMax: number;
|
|
60
|
-
timeoutMultiplier: number;
|
|
61
|
-
}
|
|
62
|
-
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
|
|
63
|
-
url: string;
|
|
64
|
-
init: RequestInitNormalized;
|
|
65
|
-
mode: FetcherMode;
|
|
66
|
-
throwHttpErrors: boolean;
|
|
67
|
-
timeoutSeconds: number;
|
|
68
|
-
retry: FetcherRetryOptions;
|
|
69
|
-
retryPost: boolean;
|
|
70
|
-
retry4xx: boolean;
|
|
71
|
-
retry5xx: boolean;
|
|
72
|
-
}
|
|
73
|
-
export interface FetcherOptions {
|
|
74
|
-
method?: HttpMethod;
|
|
75
|
-
throwHttpErrors?: boolean;
|
|
76
|
-
/**
|
|
77
|
-
* Default: 30.
|
|
78
|
-
*
|
|
79
|
-
* Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
|
|
80
|
-
* so both should finish within this single timeout (not each).
|
|
81
|
-
*/
|
|
82
|
-
timeoutSeconds?: number;
|
|
83
|
-
json?: any;
|
|
84
|
-
text?: string;
|
|
85
|
-
/**
|
|
86
|
-
* Supports all the types that RequestInit.body supports.
|
|
87
|
-
*
|
|
88
|
-
* Useful when you want to e.g pass FormData.
|
|
89
|
-
*/
|
|
90
|
-
body?: Blob | BufferSource | FormData | URLSearchParams | string;
|
|
91
|
-
credentials?: RequestCredentials;
|
|
92
|
-
/**
|
|
93
|
-
* Default to true.
|
|
94
|
-
*/
|
|
95
|
-
followRedirects?: boolean;
|
|
96
|
-
headers?: Record<string, any>;
|
|
97
|
-
mode?: FetcherMode;
|
|
98
|
-
searchParams?: Record<string, any>;
|
|
99
|
-
/**
|
|
100
|
-
* Default is 2 retries (3 tries in total).
|
|
101
|
-
* Pass `retry: { count: 0 }` to disable retries.
|
|
102
|
-
*/
|
|
103
|
-
retry?: Partial<FetcherRetryOptions>;
|
|
104
|
-
/**
|
|
105
|
-
* Defaults to false.
|
|
106
|
-
* Set to true to allow retrying `post` requests.
|
|
107
|
-
*/
|
|
108
|
-
retryPost?: boolean;
|
|
109
|
-
/**
|
|
110
|
-
* Defaults to false.
|
|
111
|
-
*/
|
|
112
|
-
retry4xx?: boolean;
|
|
113
|
-
/**
|
|
114
|
-
* Defaults to true.
|
|
115
|
-
*/
|
|
116
|
-
retry5xx?: boolean;
|
|
117
|
-
jsonReviver?: Reviver;
|
|
118
|
-
}
|
|
119
|
-
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
120
|
-
method: HttpMethod;
|
|
121
|
-
headers: Record<string, any>;
|
|
122
|
-
};
|
|
123
|
-
export interface FetcherSuccessResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
124
|
-
err?: undefined;
|
|
125
|
-
fetchResponse: Response;
|
|
126
|
-
body: BODY;
|
|
127
|
-
}
|
|
128
|
-
export interface FetcherErrorResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
129
|
-
err: Error;
|
|
130
|
-
}
|
|
131
|
-
export interface FetcherResponse<BODY = unknown> {
|
|
132
|
-
err?: Error;
|
|
133
|
-
req: FetcherRequest;
|
|
134
|
-
fetchResponse?: Response;
|
|
135
|
-
statusFamily?: HttpStatusFamily;
|
|
136
|
-
body?: BODY;
|
|
137
|
-
retryStatus: FetcherRetryStatus;
|
|
138
|
-
}
|
|
139
|
-
export type FetcherMode = 'json' | 'text' | 'void';
|
|
2
|
+
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse } from './fetcher.model';
|
|
140
3
|
/**
|
|
141
4
|
* Experimental wrapper around Fetch.
|
|
142
5
|
* Works in both Browser and Node, using `globalThis.fetch`.
|
package/dist/http/fetcher.js
CHANGED
|
@@ -116,7 +116,8 @@ class Fetcher {
|
|
|
116
116
|
retryTimeout: req.retry.timeout,
|
|
117
117
|
},
|
|
118
118
|
};
|
|
119
|
-
const
|
|
119
|
+
const fullUrl = new URL(req.url);
|
|
120
|
+
const shortUrl = this.getShortUrl(fullUrl);
|
|
120
121
|
const signature = [method.toUpperCase(), shortUrl].join(' ');
|
|
121
122
|
/* eslint-disable no-await-in-loop */
|
|
122
123
|
while (!res.retryStatus.retryStopped) {
|
|
@@ -132,10 +133,12 @@ class Fetcher {
|
|
|
132
133
|
}
|
|
133
134
|
try {
|
|
134
135
|
res.fetchResponse = await globalThis.fetch(req.url, req.init);
|
|
136
|
+
res.ok = res.fetchResponse.ok;
|
|
135
137
|
}
|
|
136
138
|
catch (err) {
|
|
137
139
|
// For example, CORS error would result in "TypeError: failed to fetch" here
|
|
138
140
|
res.err = err;
|
|
141
|
+
res.ok = false;
|
|
139
142
|
}
|
|
140
143
|
res.statusFamily = this.getStatusFamily(res);
|
|
141
144
|
if (res.fetchResponse?.ok) {
|
|
@@ -147,6 +150,7 @@ class Fetcher {
|
|
|
147
150
|
res.body = JSON.parse(text, req.jsonReviver);
|
|
148
151
|
}
|
|
149
152
|
catch (err) {
|
|
153
|
+
res.ok = false;
|
|
150
154
|
res.err = (0, error_util_1._anyToError)(err, http_error_1.HttpError, (0, object_util_1._filterNullishValues)({
|
|
151
155
|
httpStatusCode: 0,
|
|
152
156
|
url: req.url,
|
|
@@ -280,9 +284,18 @@ class Fetcher {
|
|
|
280
284
|
*/
|
|
281
285
|
getShortUrl(url) {
|
|
282
286
|
const { baseUrl } = this.cfg;
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
if (url.password) {
|
|
288
|
+
url = new URL(url.toString()); // prevent original url mutation
|
|
289
|
+
url.password = '[redacted]';
|
|
290
|
+
}
|
|
291
|
+
let shortUrl = url.toString();
|
|
292
|
+
if (!this.cfg.logWithSearchParams) {
|
|
293
|
+
shortUrl = shortUrl.split('?')[0];
|
|
294
|
+
}
|
|
295
|
+
if (!this.cfg.logWithPrefixUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
296
|
+
shortUrl = shortUrl.slice(baseUrl.length);
|
|
297
|
+
}
|
|
298
|
+
return shortUrl;
|
|
286
299
|
}
|
|
287
300
|
normalizeCfg(cfg) {
|
|
288
301
|
if (cfg.baseUrl?.endsWith('/')) {
|
|
@@ -306,6 +319,8 @@ class Fetcher {
|
|
|
306
319
|
logRequestBody: debug,
|
|
307
320
|
logResponse: debug,
|
|
308
321
|
logResponseBody: debug,
|
|
322
|
+
logWithPrefixUrl: true,
|
|
323
|
+
logWithSearchParams: true,
|
|
309
324
|
retry: { ...defRetryOptions },
|
|
310
325
|
init: {
|
|
311
326
|
method: cfg.method || 'get',
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { CommonLogger } from '../log/commonLogger';
|
|
2
|
+
import type { Promisable } from '../typeFest';
|
|
3
|
+
import type { Reviver } from '../types';
|
|
4
|
+
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
5
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
6
|
+
logger: CommonLogger;
|
|
7
|
+
searchParams: Record<string, any>;
|
|
8
|
+
}
|
|
9
|
+
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>;
|
|
10
|
+
export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>;
|
|
11
|
+
export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>;
|
|
12
|
+
export interface FetcherCfg {
|
|
13
|
+
/**
|
|
14
|
+
* Should **not** contain trailing slash.
|
|
15
|
+
*/
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
|
|
19
|
+
* properties of hook function arguments.
|
|
20
|
+
* If you throw an error from the hook - it will be re-thrown as-is.
|
|
21
|
+
*/
|
|
22
|
+
hooks?: {
|
|
23
|
+
/**
|
|
24
|
+
* Allows to mutate req.
|
|
25
|
+
*/
|
|
26
|
+
beforeRequest?: FetcherBeforeRequestHook[];
|
|
27
|
+
/**
|
|
28
|
+
* Allows to mutate res.
|
|
29
|
+
* If you set `res.err` - it will be thrown.
|
|
30
|
+
*/
|
|
31
|
+
afterResponse?: FetcherAfterResponseHook[];
|
|
32
|
+
/**
|
|
33
|
+
* Allows to mutate res.retryStatus to override retry behavior.
|
|
34
|
+
*/
|
|
35
|
+
beforeRetry?: FetcherBeforeRetryHook[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* If true - enables all possible logging.
|
|
39
|
+
*/
|
|
40
|
+
debug?: boolean;
|
|
41
|
+
logRequest?: boolean;
|
|
42
|
+
logRequestBody?: boolean;
|
|
43
|
+
logResponse?: boolean;
|
|
44
|
+
logResponseBody?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Default to true.
|
|
47
|
+
* Set to false to exclude `prefixUrl` from logs (both success and error)
|
|
48
|
+
*/
|
|
49
|
+
logWithPrefixUrl?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Default to true.
|
|
52
|
+
* Set to false to strip searchParams from url when logging (both success and error)
|
|
53
|
+
*/
|
|
54
|
+
logWithSearchParams?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Defaults to `console`.
|
|
57
|
+
*/
|
|
58
|
+
logger?: CommonLogger;
|
|
59
|
+
}
|
|
60
|
+
export interface FetcherRetryStatus {
|
|
61
|
+
retryAttempt: number;
|
|
62
|
+
retryTimeout: number;
|
|
63
|
+
retryStopped: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface FetcherRetryOptions {
|
|
66
|
+
count: number;
|
|
67
|
+
timeout: number;
|
|
68
|
+
timeoutMax: number;
|
|
69
|
+
timeoutMultiplier: number;
|
|
70
|
+
}
|
|
71
|
+
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
|
|
72
|
+
url: string;
|
|
73
|
+
init: RequestInitNormalized;
|
|
74
|
+
mode: FetcherMode;
|
|
75
|
+
throwHttpErrors: boolean;
|
|
76
|
+
timeoutSeconds: number;
|
|
77
|
+
retry: FetcherRetryOptions;
|
|
78
|
+
retryPost: boolean;
|
|
79
|
+
retry4xx: boolean;
|
|
80
|
+
retry5xx: boolean;
|
|
81
|
+
}
|
|
82
|
+
export interface FetcherOptions {
|
|
83
|
+
method?: HttpMethod;
|
|
84
|
+
throwHttpErrors?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Default: 30.
|
|
87
|
+
*
|
|
88
|
+
* Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
|
|
89
|
+
* so both should finish within this single timeout (not each).
|
|
90
|
+
*/
|
|
91
|
+
timeoutSeconds?: number;
|
|
92
|
+
json?: any;
|
|
93
|
+
text?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Supports all the types that RequestInit.body supports.
|
|
96
|
+
*
|
|
97
|
+
* Useful when you want to e.g pass FormData.
|
|
98
|
+
*/
|
|
99
|
+
body?: Blob | BufferSource | FormData | URLSearchParams | string;
|
|
100
|
+
credentials?: RequestCredentials;
|
|
101
|
+
/**
|
|
102
|
+
* Default to true.
|
|
103
|
+
*/
|
|
104
|
+
followRedirects?: boolean;
|
|
105
|
+
headers?: Record<string, any>;
|
|
106
|
+
mode?: FetcherMode;
|
|
107
|
+
searchParams?: Record<string, any>;
|
|
108
|
+
/**
|
|
109
|
+
* Default is 2 retries (3 tries in total).
|
|
110
|
+
* Pass `retry: { count: 0 }` to disable retries.
|
|
111
|
+
*/
|
|
112
|
+
retry?: Partial<FetcherRetryOptions>;
|
|
113
|
+
/**
|
|
114
|
+
* Defaults to false.
|
|
115
|
+
* Set to true to allow retrying `post` requests.
|
|
116
|
+
*/
|
|
117
|
+
retryPost?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Defaults to false.
|
|
120
|
+
*/
|
|
121
|
+
retry4xx?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Defaults to true.
|
|
124
|
+
*/
|
|
125
|
+
retry5xx?: boolean;
|
|
126
|
+
jsonReviver?: Reviver;
|
|
127
|
+
}
|
|
128
|
+
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
129
|
+
method: HttpMethod;
|
|
130
|
+
headers: Record<string, any>;
|
|
131
|
+
};
|
|
132
|
+
export interface FetcherSuccessResponse<BODY = unknown> {
|
|
133
|
+
ok: true;
|
|
134
|
+
err: undefined;
|
|
135
|
+
fetchResponse: Response;
|
|
136
|
+
body: BODY;
|
|
137
|
+
req: FetcherRequest;
|
|
138
|
+
statusFamily?: HttpStatusFamily;
|
|
139
|
+
retryStatus: FetcherRetryStatus;
|
|
140
|
+
}
|
|
141
|
+
export interface FetcherErrorResponse<BODY = unknown> {
|
|
142
|
+
ok: false;
|
|
143
|
+
err: Error;
|
|
144
|
+
fetchResponse?: Response;
|
|
145
|
+
body?: BODY;
|
|
146
|
+
req: FetcherRequest;
|
|
147
|
+
statusFamily?: HttpStatusFamily;
|
|
148
|
+
retryStatus: FetcherRetryStatus;
|
|
149
|
+
}
|
|
150
|
+
export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
|
|
151
|
+
export type FetcherMode = 'json' | 'text' | 'void';
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -81,3 +81,4 @@ tslib_1.__exportStar(require("./datetime/dateInterval"), exports);
|
|
|
81
81
|
tslib_1.__exportStar(require("./datetime/timeInterval"), exports);
|
|
82
82
|
tslib_1.__exportStar(require("./http/http.model"), exports);
|
|
83
83
|
tslib_1.__exportStar(require("./http/fetcher"), exports);
|
|
84
|
+
tslib_1.__exportStar(require("./http/fetcher.model"), exports);
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -124,7 +124,8 @@ export class Fetcher {
|
|
|
124
124
|
retryTimeout: req.retry.timeout,
|
|
125
125
|
},
|
|
126
126
|
};
|
|
127
|
-
const
|
|
127
|
+
const fullUrl = new URL(req.url);
|
|
128
|
+
const shortUrl = this.getShortUrl(fullUrl);
|
|
128
129
|
const signature = [method.toUpperCase(), shortUrl].join(' ');
|
|
129
130
|
/* eslint-disable no-await-in-loop */
|
|
130
131
|
while (!res.retryStatus.retryStopped) {
|
|
@@ -140,10 +141,12 @@ export class Fetcher {
|
|
|
140
141
|
}
|
|
141
142
|
try {
|
|
142
143
|
res.fetchResponse = await globalThis.fetch(req.url, req.init);
|
|
144
|
+
res.ok = res.fetchResponse.ok;
|
|
143
145
|
}
|
|
144
146
|
catch (err) {
|
|
145
147
|
// For example, CORS error would result in "TypeError: failed to fetch" here
|
|
146
148
|
res.err = err;
|
|
149
|
+
res.ok = false;
|
|
147
150
|
}
|
|
148
151
|
res.statusFamily = this.getStatusFamily(res);
|
|
149
152
|
if ((_g = res.fetchResponse) === null || _g === void 0 ? void 0 : _g.ok) {
|
|
@@ -155,6 +158,7 @@ export class Fetcher {
|
|
|
155
158
|
res.body = JSON.parse(text, req.jsonReviver);
|
|
156
159
|
}
|
|
157
160
|
catch (err) {
|
|
161
|
+
res.ok = false;
|
|
158
162
|
res.err = _anyToError(err, HttpError, _filterNullishValues({
|
|
159
163
|
httpStatusCode: 0,
|
|
160
164
|
url: req.url,
|
|
@@ -320,9 +324,18 @@ export class Fetcher {
|
|
|
320
324
|
*/
|
|
321
325
|
getShortUrl(url) {
|
|
322
326
|
const { baseUrl } = this.cfg;
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
327
|
+
if (url.password) {
|
|
328
|
+
url = new URL(url.toString()); // prevent original url mutation
|
|
329
|
+
url.password = '[redacted]';
|
|
330
|
+
}
|
|
331
|
+
let shortUrl = url.toString();
|
|
332
|
+
if (!this.cfg.logWithSearchParams) {
|
|
333
|
+
shortUrl = shortUrl.split('?')[0];
|
|
334
|
+
}
|
|
335
|
+
if (!this.cfg.logWithPrefixUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
336
|
+
shortUrl = shortUrl.slice(baseUrl.length);
|
|
337
|
+
}
|
|
338
|
+
return shortUrl;
|
|
326
339
|
}
|
|
327
340
|
normalizeCfg(cfg) {
|
|
328
341
|
var _a;
|
|
@@ -347,6 +360,8 @@ export class Fetcher {
|
|
|
347
360
|
logRequestBody: debug,
|
|
348
361
|
logResponse: debug,
|
|
349
362
|
logResponseBody: debug,
|
|
363
|
+
logWithPrefixUrl: true,
|
|
364
|
+
logWithSearchParams: true,
|
|
350
365
|
retry: Object.assign({}, defRetryOptions),
|
|
351
366
|
init: {
|
|
352
367
|
method: cfg.method || 'get',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist-esm/index.js
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { CommonLogger } from '../log/commonLogger'
|
|
2
|
+
import type { Promisable } from '../typeFest'
|
|
3
|
+
import type { Reviver } from '../types'
|
|
4
|
+
import type { HttpMethod, HttpStatusFamily } from './http.model'
|
|
5
|
+
|
|
6
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
7
|
+
logger: CommonLogger
|
|
8
|
+
searchParams: Record<string, any>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>
|
|
12
|
+
export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>
|
|
13
|
+
export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>
|
|
14
|
+
|
|
15
|
+
export interface FetcherCfg {
|
|
16
|
+
/**
|
|
17
|
+
* Should **not** contain trailing slash.
|
|
18
|
+
*/
|
|
19
|
+
baseUrl?: string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
|
|
23
|
+
* properties of hook function arguments.
|
|
24
|
+
* If you throw an error from the hook - it will be re-thrown as-is.
|
|
25
|
+
*/
|
|
26
|
+
hooks?: {
|
|
27
|
+
/**
|
|
28
|
+
* Allows to mutate req.
|
|
29
|
+
*/
|
|
30
|
+
beforeRequest?: FetcherBeforeRequestHook[]
|
|
31
|
+
/**
|
|
32
|
+
* Allows to mutate res.
|
|
33
|
+
* If you set `res.err` - it will be thrown.
|
|
34
|
+
*/
|
|
35
|
+
afterResponse?: FetcherAfterResponseHook[]
|
|
36
|
+
/**
|
|
37
|
+
* Allows to mutate res.retryStatus to override retry behavior.
|
|
38
|
+
*/
|
|
39
|
+
beforeRetry?: FetcherBeforeRetryHook[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* If true - enables all possible logging.
|
|
44
|
+
*/
|
|
45
|
+
debug?: boolean
|
|
46
|
+
logRequest?: boolean
|
|
47
|
+
logRequestBody?: boolean
|
|
48
|
+
logResponse?: boolean
|
|
49
|
+
logResponseBody?: boolean
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Default to true.
|
|
53
|
+
* Set to false to exclude `prefixUrl` from logs (both success and error)
|
|
54
|
+
*/
|
|
55
|
+
logWithPrefixUrl?: boolean
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Default to true.
|
|
59
|
+
* Set to false to strip searchParams from url when logging (both success and error)
|
|
60
|
+
*/
|
|
61
|
+
logWithSearchParams?: boolean
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Defaults to `console`.
|
|
65
|
+
*/
|
|
66
|
+
logger?: CommonLogger
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FetcherRetryStatus {
|
|
70
|
+
retryAttempt: number
|
|
71
|
+
retryTimeout: number
|
|
72
|
+
retryStopped: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FetcherRetryOptions {
|
|
76
|
+
count: number
|
|
77
|
+
timeout: number
|
|
78
|
+
timeoutMax: number
|
|
79
|
+
timeoutMultiplier: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
|
|
83
|
+
url: string
|
|
84
|
+
init: RequestInitNormalized
|
|
85
|
+
mode: FetcherMode
|
|
86
|
+
throwHttpErrors: boolean
|
|
87
|
+
timeoutSeconds: number
|
|
88
|
+
retry: FetcherRetryOptions
|
|
89
|
+
retryPost: boolean
|
|
90
|
+
retry4xx: boolean
|
|
91
|
+
retry5xx: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface FetcherOptions {
|
|
95
|
+
method?: HttpMethod
|
|
96
|
+
throwHttpErrors?: boolean
|
|
97
|
+
/**
|
|
98
|
+
* Default: 30.
|
|
99
|
+
*
|
|
100
|
+
* Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
|
|
101
|
+
* so both should finish within this single timeout (not each).
|
|
102
|
+
*/
|
|
103
|
+
timeoutSeconds?: number
|
|
104
|
+
|
|
105
|
+
json?: any
|
|
106
|
+
text?: string
|
|
107
|
+
/**
|
|
108
|
+
* Supports all the types that RequestInit.body supports.
|
|
109
|
+
*
|
|
110
|
+
* Useful when you want to e.g pass FormData.
|
|
111
|
+
*/
|
|
112
|
+
body?: Blob | BufferSource | FormData | URLSearchParams | string
|
|
113
|
+
|
|
114
|
+
credentials?: RequestCredentials
|
|
115
|
+
/**
|
|
116
|
+
* Default to true.
|
|
117
|
+
*/
|
|
118
|
+
followRedirects?: boolean
|
|
119
|
+
|
|
120
|
+
// Removing RequestInit from options to simplify FetcherOptions interface.
|
|
121
|
+
// Will instead only add hand-picked useful options, such as `credentials`.
|
|
122
|
+
// init?: Partial<RequestInitNormalized>
|
|
123
|
+
|
|
124
|
+
headers?: Record<string, any>
|
|
125
|
+
mode?: FetcherMode // default to 'void'
|
|
126
|
+
|
|
127
|
+
searchParams?: Record<string, any>
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Default is 2 retries (3 tries in total).
|
|
131
|
+
* Pass `retry: { count: 0 }` to disable retries.
|
|
132
|
+
*/
|
|
133
|
+
retry?: Partial<FetcherRetryOptions>
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Defaults to false.
|
|
137
|
+
* Set to true to allow retrying `post` requests.
|
|
138
|
+
*/
|
|
139
|
+
retryPost?: boolean
|
|
140
|
+
/**
|
|
141
|
+
* Defaults to false.
|
|
142
|
+
*/
|
|
143
|
+
retry4xx?: boolean
|
|
144
|
+
/**
|
|
145
|
+
* Defaults to true.
|
|
146
|
+
*/
|
|
147
|
+
retry5xx?: boolean
|
|
148
|
+
|
|
149
|
+
jsonReviver?: Reviver
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
153
|
+
method: HttpMethod
|
|
154
|
+
headers: Record<string, any>
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface FetcherSuccessResponse<BODY = unknown> {
|
|
158
|
+
ok: true
|
|
159
|
+
err: undefined
|
|
160
|
+
fetchResponse: Response
|
|
161
|
+
body: BODY
|
|
162
|
+
req: FetcherRequest
|
|
163
|
+
statusFamily?: HttpStatusFamily
|
|
164
|
+
retryStatus: FetcherRetryStatus
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface FetcherErrorResponse<BODY = unknown> {
|
|
168
|
+
ok: false
|
|
169
|
+
err: Error
|
|
170
|
+
fetchResponse?: Response
|
|
171
|
+
body?: BODY
|
|
172
|
+
req: FetcherRequest
|
|
173
|
+
statusFamily?: HttpStatusFamily
|
|
174
|
+
retryStatus: FetcherRetryStatus
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export type FetcherResponse<BODY = unknown> =
|
|
178
|
+
| FetcherSuccessResponse<BODY>
|
|
179
|
+
| FetcherErrorResponse<BODY>
|
|
180
|
+
|
|
181
|
+
export type FetcherMode = 'json' | 'text' | 'void'
|
package/src/http/fetcher.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import { ErrorObject } from '../error/error.model'
|
|
4
4
|
import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util'
|
|
5
5
|
import { HttpError } from '../error/http.error'
|
|
6
|
-
import { CommonLogger } from '../log/commonLogger'
|
|
7
6
|
import { _clamp } from '../number/number.util'
|
|
8
7
|
import {
|
|
9
8
|
_filterNullishValues,
|
|
@@ -15,170 +14,19 @@ import {
|
|
|
15
14
|
import { pDelay } from '../promise/pDelay'
|
|
16
15
|
import { _jsonParseIfPossible } from '../string/json.util'
|
|
17
16
|
import { _since } from '../time/time.util'
|
|
18
|
-
import type {
|
|
19
|
-
|
|
17
|
+
import type {
|
|
18
|
+
FetcherAfterResponseHook,
|
|
19
|
+
FetcherBeforeRequestHook,
|
|
20
|
+
FetcherBeforeRetryHook,
|
|
21
|
+
FetcherCfg,
|
|
22
|
+
FetcherNormalizedCfg,
|
|
23
|
+
FetcherOptions,
|
|
24
|
+
FetcherRequest,
|
|
25
|
+
FetcherResponse,
|
|
26
|
+
FetcherRetryOptions,
|
|
27
|
+
} from './fetcher.model'
|
|
20
28
|
import { HTTP_METHODS } from './http.model'
|
|
21
|
-
import type {
|
|
22
|
-
|
|
23
|
-
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
24
|
-
logger: CommonLogger
|
|
25
|
-
searchParams: Record<string, any>
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>
|
|
29
|
-
export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>
|
|
30
|
-
export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>
|
|
31
|
-
|
|
32
|
-
export interface FetcherCfg {
|
|
33
|
-
/**
|
|
34
|
-
* Should **not** contain trailing slash.
|
|
35
|
-
*/
|
|
36
|
-
baseUrl?: string
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
|
|
40
|
-
* properties of hook function arguments.
|
|
41
|
-
* If you throw an error from the hook - it will be re-thrown as-is.
|
|
42
|
-
*/
|
|
43
|
-
hooks?: {
|
|
44
|
-
/**
|
|
45
|
-
* Allows to mutate req.
|
|
46
|
-
*/
|
|
47
|
-
beforeRequest?: FetcherBeforeRequestHook[]
|
|
48
|
-
/**
|
|
49
|
-
* Allows to mutate res.
|
|
50
|
-
* If you set `res.err` - it will be thrown.
|
|
51
|
-
*/
|
|
52
|
-
afterResponse?: FetcherAfterResponseHook[]
|
|
53
|
-
/**
|
|
54
|
-
* Allows to mutate res.retryStatus to override retry behavior.
|
|
55
|
-
*/
|
|
56
|
-
beforeRetry?: FetcherBeforeRetryHook[]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* If true - enables all possible logging.
|
|
61
|
-
*/
|
|
62
|
-
debug?: boolean
|
|
63
|
-
logRequest?: boolean
|
|
64
|
-
logRequestBody?: boolean
|
|
65
|
-
logResponse?: boolean
|
|
66
|
-
logResponseBody?: boolean
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Defaults to `console`.
|
|
70
|
-
*/
|
|
71
|
-
logger?: CommonLogger
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface FetcherRetryStatus {
|
|
75
|
-
retryAttempt: number
|
|
76
|
-
retryTimeout: number
|
|
77
|
-
retryStopped: boolean
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface FetcherRetryOptions {
|
|
81
|
-
count: number
|
|
82
|
-
timeout: number
|
|
83
|
-
timeoutMax: number
|
|
84
|
-
timeoutMultiplier: number
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
|
|
88
|
-
url: string
|
|
89
|
-
init: RequestInitNormalized
|
|
90
|
-
mode: FetcherMode
|
|
91
|
-
throwHttpErrors: boolean
|
|
92
|
-
timeoutSeconds: number
|
|
93
|
-
retry: FetcherRetryOptions
|
|
94
|
-
retryPost: boolean
|
|
95
|
-
retry4xx: boolean
|
|
96
|
-
retry5xx: boolean
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface FetcherOptions {
|
|
100
|
-
method?: HttpMethod
|
|
101
|
-
throwHttpErrors?: boolean
|
|
102
|
-
/**
|
|
103
|
-
* Default: 30.
|
|
104
|
-
*
|
|
105
|
-
* Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
|
|
106
|
-
* so both should finish within this single timeout (not each).
|
|
107
|
-
*/
|
|
108
|
-
timeoutSeconds?: number
|
|
109
|
-
|
|
110
|
-
json?: any
|
|
111
|
-
text?: string
|
|
112
|
-
/**
|
|
113
|
-
* Supports all the types that RequestInit.body supports.
|
|
114
|
-
*
|
|
115
|
-
* Useful when you want to e.g pass FormData.
|
|
116
|
-
*/
|
|
117
|
-
body?: Blob | BufferSource | FormData | URLSearchParams | string
|
|
118
|
-
|
|
119
|
-
credentials?: RequestCredentials
|
|
120
|
-
/**
|
|
121
|
-
* Default to true.
|
|
122
|
-
*/
|
|
123
|
-
followRedirects?: boolean
|
|
124
|
-
|
|
125
|
-
// Removing RequestInit from options to simplify FetcherOptions interface.
|
|
126
|
-
// Will instead only add hand-picked useful options, such as `credentials`.
|
|
127
|
-
// init?: Partial<RequestInitNormalized>
|
|
128
|
-
|
|
129
|
-
headers?: Record<string, any>
|
|
130
|
-
mode?: FetcherMode // default to 'void'
|
|
131
|
-
|
|
132
|
-
searchParams?: Record<string, any>
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Default is 2 retries (3 tries in total).
|
|
136
|
-
* Pass `retry: { count: 0 }` to disable retries.
|
|
137
|
-
*/
|
|
138
|
-
retry?: Partial<FetcherRetryOptions>
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Defaults to false.
|
|
142
|
-
* Set to true to allow retrying `post` requests.
|
|
143
|
-
*/
|
|
144
|
-
retryPost?: boolean
|
|
145
|
-
/**
|
|
146
|
-
* Defaults to false.
|
|
147
|
-
*/
|
|
148
|
-
retry4xx?: boolean
|
|
149
|
-
/**
|
|
150
|
-
* Defaults to true.
|
|
151
|
-
*/
|
|
152
|
-
retry5xx?: boolean
|
|
153
|
-
|
|
154
|
-
jsonReviver?: Reviver
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
158
|
-
method: HttpMethod
|
|
159
|
-
headers: Record<string, any>
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface FetcherSuccessResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
163
|
-
err?: undefined
|
|
164
|
-
fetchResponse: Response
|
|
165
|
-
body: BODY
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface FetcherErrorResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
169
|
-
err: Error
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export interface FetcherResponse<BODY = unknown> {
|
|
173
|
-
err?: Error
|
|
174
|
-
req: FetcherRequest
|
|
175
|
-
fetchResponse?: Response
|
|
176
|
-
statusFamily?: HttpStatusFamily
|
|
177
|
-
body?: BODY
|
|
178
|
-
retryStatus: FetcherRetryStatus
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export type FetcherMode = 'json' | 'text' | 'void'
|
|
29
|
+
import type { HttpStatusFamily } from './http.model'
|
|
182
30
|
|
|
183
31
|
const defRetryOptions: FetcherRetryOptions = {
|
|
184
32
|
count: 2,
|
|
@@ -288,7 +136,7 @@ export class Fetcher {
|
|
|
288
136
|
if (res.req.throwHttpErrors) throw res.err
|
|
289
137
|
return res as any
|
|
290
138
|
}
|
|
291
|
-
return res.body
|
|
139
|
+
return res.body
|
|
292
140
|
}
|
|
293
141
|
|
|
294
142
|
/**
|
|
@@ -322,16 +170,17 @@ export class Fetcher {
|
|
|
322
170
|
await hook(req)
|
|
323
171
|
}
|
|
324
172
|
|
|
325
|
-
const res
|
|
173
|
+
const res = {
|
|
326
174
|
req,
|
|
327
175
|
retryStatus: {
|
|
328
176
|
retryAttempt: 0,
|
|
329
177
|
retryStopped: false,
|
|
330
178
|
retryTimeout: req.retry.timeout,
|
|
331
179
|
},
|
|
332
|
-
}
|
|
180
|
+
} as FetcherResponse<any>
|
|
333
181
|
|
|
334
|
-
const
|
|
182
|
+
const fullUrl = new URL(req.url)
|
|
183
|
+
const shortUrl = this.getShortUrl(fullUrl)
|
|
335
184
|
const signature = [method.toUpperCase(), shortUrl].join(' ')
|
|
336
185
|
|
|
337
186
|
/* eslint-disable no-await-in-loop */
|
|
@@ -352,9 +201,11 @@ export class Fetcher {
|
|
|
352
201
|
|
|
353
202
|
try {
|
|
354
203
|
res.fetchResponse = await globalThis.fetch(req.url, req.init)
|
|
204
|
+
res.ok = res.fetchResponse.ok
|
|
355
205
|
} catch (err) {
|
|
356
206
|
// For example, CORS error would result in "TypeError: failed to fetch" here
|
|
357
207
|
res.err = err as Error
|
|
208
|
+
res.ok = false
|
|
358
209
|
}
|
|
359
210
|
res.statusFamily = this.getStatusFamily(res)
|
|
360
211
|
|
|
@@ -367,6 +218,7 @@ export class Fetcher {
|
|
|
367
218
|
try {
|
|
368
219
|
res.body = JSON.parse(text, req.jsonReviver)
|
|
369
220
|
} catch (err) {
|
|
221
|
+
res.ok = false
|
|
370
222
|
res.err = _anyToError(
|
|
371
223
|
err,
|
|
372
224
|
HttpError,
|
|
@@ -514,11 +366,25 @@ export class Fetcher {
|
|
|
514
366
|
/**
|
|
515
367
|
* Returns url without baseUrl and before ?queryString
|
|
516
368
|
*/
|
|
517
|
-
private getShortUrl(url:
|
|
369
|
+
private getShortUrl(url: URL): string {
|
|
518
370
|
const { baseUrl } = this.cfg
|
|
519
|
-
if (!baseUrl) return url
|
|
520
371
|
|
|
521
|
-
|
|
372
|
+
if (url.password) {
|
|
373
|
+
url = new URL(url.toString()) // prevent original url mutation
|
|
374
|
+
url.password = '[redacted]'
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
let shortUrl = url.toString()
|
|
378
|
+
|
|
379
|
+
if (!this.cfg.logWithSearchParams) {
|
|
380
|
+
shortUrl = shortUrl.split('?')[0]!
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!this.cfg.logWithPrefixUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
384
|
+
shortUrl = shortUrl.slice(baseUrl.length)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return shortUrl
|
|
522
388
|
}
|
|
523
389
|
|
|
524
390
|
private normalizeCfg(cfg: FetcherCfg & FetcherOptions): FetcherNormalizedCfg {
|
|
@@ -545,6 +411,8 @@ export class Fetcher {
|
|
|
545
411
|
logRequestBody: debug,
|
|
546
412
|
logResponse: debug,
|
|
547
413
|
logResponseBody: debug,
|
|
414
|
+
logWithPrefixUrl: true,
|
|
415
|
+
logWithSearchParams: true,
|
|
548
416
|
retry: { ...defRetryOptions },
|
|
549
417
|
init: {
|
|
550
418
|
method: cfg.method || 'get',
|