@naturalcycles/js-lib 14.145.0 → 14.146.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 +21 -20
- package/dist/http/fetcher.js +40 -14
- package/dist/http/fetcher.model.d.ts +20 -7
- package/dist/promise/pMap.js +1 -1
- package/dist-esm/http/fetcher.js +44 -72
- package/dist-esm/promise/pMap.js +15 -34
- package/package.json +1 -1
- package/src/http/fetcher.model.ts +30 -7
- package/src/http/fetcher.ts +77 -47
- package/src/promise/pMap.ts +1 -1
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse } from './fetcher.model';
|
|
2
|
+
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherRequest, FetcherResponse } from './fetcher.model';
|
|
3
3
|
/**
|
|
4
4
|
* Experimental wrapper around Fetch.
|
|
5
5
|
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
@@ -14,36 +14,37 @@ export declare class Fetcher {
|
|
|
14
14
|
onBeforeRetry(hook: FetcherBeforeRetryHook): this;
|
|
15
15
|
cfg: FetcherNormalizedCfg;
|
|
16
16
|
static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
|
|
17
|
-
get: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
18
|
-
post: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
19
|
-
put: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
20
|
-
patch: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
21
|
-
delete: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
|
|
22
|
-
getText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
23
|
-
postText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
24
|
-
putText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
25
|
-
patchText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
26
|
-
deleteText: (url: string, opt?: FetcherOptions) => Promise<string>;
|
|
27
|
-
getVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
28
|
-
postVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
29
|
-
putVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
30
|
-
patchVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
31
|
-
deleteVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
32
|
-
headVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
|
|
17
|
+
get: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>;
|
|
18
|
+
post: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>;
|
|
19
|
+
put: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>;
|
|
20
|
+
patch: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>;
|
|
21
|
+
delete: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>;
|
|
22
|
+
getText: (url: string, opt?: FetcherOptions<string>) => Promise<string>;
|
|
23
|
+
postText: (url: string, opt?: FetcherOptions<string>) => Promise<string>;
|
|
24
|
+
putText: (url: string, opt?: FetcherOptions<string>) => Promise<string>;
|
|
25
|
+
patchText: (url: string, opt?: FetcherOptions<string>) => Promise<string>;
|
|
26
|
+
deleteText: (url: string, opt?: FetcherOptions<string>) => Promise<string>;
|
|
27
|
+
getVoid: (url: string, opt?: FetcherOptions<void>) => Promise<void>;
|
|
28
|
+
postVoid: (url: string, opt?: FetcherOptions<void>) => Promise<void>;
|
|
29
|
+
putVoid: (url: string, opt?: FetcherOptions<void>) => Promise<void>;
|
|
30
|
+
patchVoid: (url: string, opt?: FetcherOptions<void>) => Promise<void>;
|
|
31
|
+
deleteVoid: (url: string, opt?: FetcherOptions<void>) => Promise<void>;
|
|
32
|
+
headVoid: (url: string, opt?: FetcherOptions<void>) => Promise<void>;
|
|
33
33
|
/**
|
|
34
34
|
* Returns raw fetchResponse.body, which is a ReadableStream<Uint8Array>
|
|
35
35
|
*
|
|
36
36
|
* More on streams and Node interop:
|
|
37
37
|
* https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/
|
|
38
38
|
*/
|
|
39
|
-
getReadableStream(url: string, opt?: FetcherOptions): Promise<ReadableStream<Uint8Array>>;
|
|
40
|
-
fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
|
|
39
|
+
getReadableStream(url: string, opt?: FetcherOptions<ReadableStream<Uint8Array>>): Promise<ReadableStream<Uint8Array>>;
|
|
40
|
+
fetch<T = unknown>(url: string, opt?: FetcherOptions<T>): Promise<T>;
|
|
41
41
|
/**
|
|
42
42
|
* Returns FetcherResponse.
|
|
43
43
|
* Never throws, returns `err` property in the response instead.
|
|
44
44
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
45
45
|
*/
|
|
46
|
-
doFetch<T = unknown>(url: string,
|
|
46
|
+
doFetch<T = unknown>(url: string, opt?: FetcherOptions<T>): Promise<FetcherResponse<T>>;
|
|
47
|
+
doFetchRequest<T = unknown>(req: FetcherRequest<T>): Promise<FetcherResponse<T>>;
|
|
47
48
|
private onOkResponse;
|
|
48
49
|
/**
|
|
49
50
|
* This method exists to be able to easily mock it.
|
package/dist/http/fetcher.js
CHANGED
|
@@ -104,9 +104,12 @@ class Fetcher {
|
|
|
104
104
|
* Never throws, returns `err` property in the response instead.
|
|
105
105
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
106
106
|
*/
|
|
107
|
-
async doFetch(url,
|
|
107
|
+
async doFetch(url, opt = {}) {
|
|
108
|
+
const req = this.normalizeOptions(url, opt);
|
|
109
|
+
return await this.doFetchRequest(req);
|
|
110
|
+
}
|
|
111
|
+
async doFetchRequest(req) {
|
|
108
112
|
const { logger } = this.cfg;
|
|
109
|
-
const req = this.normalizeOptions(url, rawOpt);
|
|
110
113
|
const { timeoutSeconds, init: { method }, } = req;
|
|
111
114
|
// setup timeout
|
|
112
115
|
let timeout;
|
|
@@ -117,7 +120,7 @@ class Fetcher {
|
|
|
117
120
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
118
121
|
}, timeoutSeconds * 1000);
|
|
119
122
|
}
|
|
120
|
-
for
|
|
123
|
+
for (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
121
124
|
await hook(req);
|
|
122
125
|
}
|
|
123
126
|
const isFullUrl = req.url.includes('://');
|
|
@@ -134,7 +137,7 @@ class Fetcher {
|
|
|
134
137
|
signature,
|
|
135
138
|
};
|
|
136
139
|
while (!res.retryStatus.retryStopped) {
|
|
137
|
-
|
|
140
|
+
req.started = Date.now();
|
|
138
141
|
if (this.cfg.logRequest) {
|
|
139
142
|
const { retryAttempt } = res.retryStatus;
|
|
140
143
|
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
|
|
@@ -157,19 +160,25 @@ class Fetcher {
|
|
|
157
160
|
}
|
|
158
161
|
res.statusFamily = this.getStatusFamily(res);
|
|
159
162
|
if (res.fetchResponse?.ok) {
|
|
160
|
-
await this.onOkResponse(res,
|
|
163
|
+
await this.onOkResponse(res, timeout);
|
|
161
164
|
}
|
|
162
165
|
else {
|
|
163
166
|
// !res.ok
|
|
164
|
-
await this.onNotOkResponse(res,
|
|
167
|
+
await this.onNotOkResponse(res, timeout);
|
|
165
168
|
}
|
|
166
169
|
}
|
|
167
|
-
for
|
|
170
|
+
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
168
171
|
await hook(res);
|
|
169
172
|
}
|
|
173
|
+
if (req.paginate && res.ok) {
|
|
174
|
+
const proceed = await req.paginate(req, res);
|
|
175
|
+
if (proceed) {
|
|
176
|
+
return await this.doFetchRequest(req);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
170
179
|
return res;
|
|
171
180
|
}
|
|
172
|
-
async onOkResponse(res,
|
|
181
|
+
async onOkResponse(res, timeout) {
|
|
173
182
|
const { req } = res;
|
|
174
183
|
const { mode } = res.req;
|
|
175
184
|
if (mode === 'json') {
|
|
@@ -192,7 +201,7 @@ class Fetcher {
|
|
|
192
201
|
// } satisfies HttpRequestErrorData)
|
|
193
202
|
res.err = (0, error_util_1._anyToError)(err);
|
|
194
203
|
res.ok = false;
|
|
195
|
-
return await this.onNotOkResponse(res,
|
|
204
|
+
return await this.onNotOkResponse(res, timeout);
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
else {
|
|
@@ -220,7 +229,7 @@ class Fetcher {
|
|
|
220
229
|
if (res.body === null) {
|
|
221
230
|
res.err = new Error(`fetchResponse.body is null`);
|
|
222
231
|
res.ok = false;
|
|
223
|
-
return await this.onNotOkResponse(res,
|
|
232
|
+
return await this.onNotOkResponse(res, timeout);
|
|
224
233
|
}
|
|
225
234
|
}
|
|
226
235
|
clearTimeout(timeout);
|
|
@@ -234,7 +243,7 @@ class Fetcher {
|
|
|
234
243
|
res.fetchResponse.status,
|
|
235
244
|
res.signature,
|
|
236
245
|
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
237
|
-
(0, time_util_1._since)(started),
|
|
246
|
+
(0, time_util_1._since)(res.req.started),
|
|
238
247
|
]
|
|
239
248
|
.filter(Boolean)
|
|
240
249
|
.join(' '));
|
|
@@ -249,7 +258,7 @@ class Fetcher {
|
|
|
249
258
|
async callNativeFetch(url, init) {
|
|
250
259
|
return await globalThis.fetch(url, init);
|
|
251
260
|
}
|
|
252
|
-
async onNotOkResponse(res,
|
|
261
|
+
async onNotOkResponse(res, timeout) {
|
|
253
262
|
clearTimeout(timeout);
|
|
254
263
|
let cause;
|
|
255
264
|
if (res.err) {
|
|
@@ -275,7 +284,7 @@ class Fetcher {
|
|
|
275
284
|
requestBaseUrl: this.cfg.baseUrl || null,
|
|
276
285
|
requestMethod: res.req.init.method,
|
|
277
286
|
requestSignature: res.signature,
|
|
278
|
-
requestDuration: Date.now() - started,
|
|
287
|
+
requestDuration: Date.now() - res.req.started,
|
|
279
288
|
}), cause);
|
|
280
289
|
await this.processRetry(res);
|
|
281
290
|
}
|
|
@@ -284,7 +293,7 @@ class Fetcher {
|
|
|
284
293
|
if (!this.shouldRetry(res)) {
|
|
285
294
|
retryStatus.retryStopped = true;
|
|
286
295
|
}
|
|
287
|
-
for
|
|
296
|
+
for (const hook of this.cfg.hooks.beforeRetry || []) {
|
|
288
297
|
await hook(res);
|
|
289
298
|
}
|
|
290
299
|
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
@@ -293,6 +302,22 @@ class Fetcher {
|
|
|
293
302
|
}
|
|
294
303
|
if (retryStatus.retryStopped)
|
|
295
304
|
return;
|
|
305
|
+
// Here we know that more retries will be attempted
|
|
306
|
+
// We don't log "last error", because it will be thrown and logged by consumer,
|
|
307
|
+
// but we should log all previous errors, otherwise they are lost.
|
|
308
|
+
// Here is the right place where we know it's not the "last error"
|
|
309
|
+
if (res.err) {
|
|
310
|
+
const { retryAttempt } = retryStatus;
|
|
311
|
+
this.cfg.logger.error([
|
|
312
|
+
' <<',
|
|
313
|
+
res.fetchResponse?.status || 0,
|
|
314
|
+
res.signature,
|
|
315
|
+
`try#${retryAttempt + 1}/${count + 1}`,
|
|
316
|
+
(0, time_util_1._since)(res.req.started),
|
|
317
|
+
]
|
|
318
|
+
.filter(Boolean)
|
|
319
|
+
.join(' '), res.err.cause || res.err);
|
|
320
|
+
}
|
|
296
321
|
retryStatus.retryAttempt++;
|
|
297
322
|
retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
298
323
|
const noise = Math.random() * 500;
|
|
@@ -393,6 +418,7 @@ class Fetcher {
|
|
|
393
418
|
normalizeOptions(url, opt) {
|
|
394
419
|
const { timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode, jsonReviver, } = this.cfg;
|
|
395
420
|
const req = {
|
|
421
|
+
started: Date.now(),
|
|
396
422
|
mode,
|
|
397
423
|
url,
|
|
398
424
|
timeoutSeconds,
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger';
|
|
2
2
|
import type { Promisable } from '../typeFest';
|
|
3
|
-
import type { Reviver } from '../types';
|
|
3
|
+
import type { Reviver, UnixTimestampMillisNumber } from '../types';
|
|
4
4
|
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
5
|
-
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
|
|
5
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started'> {
|
|
6
6
|
logger: CommonLogger;
|
|
7
7
|
searchParams: Record<string, any>;
|
|
8
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>;
|
|
9
|
+
export type FetcherBeforeRequestHook = <BODY = unknown>(req: FetcherRequest<BODY>) => Promisable<void>;
|
|
10
|
+
export type FetcherAfterResponseHook = <BODY = unknown>(res: FetcherResponse<BODY>) => Promisable<void>;
|
|
11
|
+
export type FetcherBeforeRetryHook = <BODY = unknown>(res: FetcherResponse<BODY>) => Promisable<void>;
|
|
12
12
|
export interface FetcherCfg {
|
|
13
13
|
/**
|
|
14
14
|
* Should **not** contain trailing slash.
|
|
@@ -77,7 +77,7 @@ export interface FetcherRetryOptions {
|
|
|
77
77
|
timeoutMax: number;
|
|
78
78
|
timeoutMultiplier: number;
|
|
79
79
|
}
|
|
80
|
-
export interface FetcherRequest extends Omit<FetcherOptions
|
|
80
|
+
export interface FetcherRequest<BODY = unknown> extends Omit<FetcherOptions<BODY>, 'method' | 'headers' | 'baseUrl'> {
|
|
81
81
|
url: string;
|
|
82
82
|
init: RequestInitNormalized;
|
|
83
83
|
mode: FetcherMode;
|
|
@@ -87,8 +87,9 @@ export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers
|
|
|
87
87
|
retryPost: boolean;
|
|
88
88
|
retry4xx: boolean;
|
|
89
89
|
retry5xx: boolean;
|
|
90
|
+
started: UnixTimestampMillisNumber;
|
|
90
91
|
}
|
|
91
|
-
export interface FetcherOptions {
|
|
92
|
+
export interface FetcherOptions<BODY = unknown> {
|
|
92
93
|
method?: HttpMethod;
|
|
93
94
|
baseUrl?: string;
|
|
94
95
|
throwHttpErrors?: boolean;
|
|
@@ -134,6 +135,18 @@ export interface FetcherOptions {
|
|
|
134
135
|
*/
|
|
135
136
|
retry5xx?: boolean;
|
|
136
137
|
jsonReviver?: Reviver;
|
|
138
|
+
/**
|
|
139
|
+
* Allows to walk over multiple pages of results.
|
|
140
|
+
* Paginate take a function.
|
|
141
|
+
* Function has access to FetcherRequest and FetcherResponse
|
|
142
|
+
* and has to make a decision to continue pagination or not.
|
|
143
|
+
* Return true to continue, false otherwise.
|
|
144
|
+
* If continue - it is expected to modify/mutate the FetcherRequest, as it will be used
|
|
145
|
+
* to request the next page.
|
|
146
|
+
*
|
|
147
|
+
* @experimental
|
|
148
|
+
*/
|
|
149
|
+
paginate?: (req: FetcherRequest<BODY>, res: FetcherSuccessResponse<BODY>) => Promisable<boolean>;
|
|
137
150
|
}
|
|
138
151
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
139
152
|
method: HttpMethod;
|
package/dist/promise/pMap.js
CHANGED
|
@@ -51,7 +51,7 @@ async function pMap(iterable, mapper, opt = {}) {
|
|
|
51
51
|
// Special cases that are able to preserve async stack traces
|
|
52
52
|
if (concurrency === 1) {
|
|
53
53
|
// Special case for concurrency == 1
|
|
54
|
-
for
|
|
54
|
+
for (const item of items) {
|
|
55
55
|
try {
|
|
56
56
|
const r = await mapper(item, currentIndex++);
|
|
57
57
|
if (r === __1.END)
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/// <reference lib="dom"/>
|
|
2
|
-
import { __asyncValues } from "tslib";
|
|
3
2
|
import { isServerSide } from '../env';
|
|
4
3
|
import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util';
|
|
5
4
|
import { HttpRequestError } from '../error/httpRequestError';
|
|
@@ -90,11 +89,13 @@ export class Fetcher {
|
|
|
90
89
|
* Never throws, returns `err` property in the response instead.
|
|
91
90
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
92
91
|
*/
|
|
93
|
-
async doFetch(url,
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
async doFetch(url, opt = {}) {
|
|
93
|
+
const req = this.normalizeOptions(url, opt);
|
|
94
|
+
return await this.doFetchRequest(req);
|
|
95
|
+
}
|
|
96
|
+
async doFetchRequest(req) {
|
|
97
|
+
var _a;
|
|
96
98
|
const { logger } = this.cfg;
|
|
97
|
-
const req = this.normalizeOptions(url, rawOpt);
|
|
98
99
|
const { timeoutSeconds, init: { method }, } = req;
|
|
99
100
|
// setup timeout
|
|
100
101
|
let timeout;
|
|
@@ -105,25 +106,8 @@ export class Fetcher {
|
|
|
105
106
|
abortController.abort(`timeout of ${timeoutSeconds} sec`);
|
|
106
107
|
}, timeoutSeconds * 1000);
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
_c = _k.value;
|
|
111
|
-
_h = false;
|
|
112
|
-
try {
|
|
113
|
-
const hook = _c;
|
|
114
|
-
await hook(req);
|
|
115
|
-
}
|
|
116
|
-
finally {
|
|
117
|
-
_h = true;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
122
|
-
finally {
|
|
123
|
-
try {
|
|
124
|
-
if (!_h && !_a && (_b = _j.return)) await _b.call(_j);
|
|
125
|
-
}
|
|
126
|
-
finally { if (e_1) throw e_1.error; }
|
|
109
|
+
for (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
110
|
+
await hook(req);
|
|
127
111
|
}
|
|
128
112
|
const isFullUrl = req.url.includes('://');
|
|
129
113
|
const fullUrl = isFullUrl ? new URL(req.url) : undefined;
|
|
@@ -139,7 +123,7 @@ export class Fetcher {
|
|
|
139
123
|
signature,
|
|
140
124
|
};
|
|
141
125
|
while (!res.retryStatus.retryStopped) {
|
|
142
|
-
|
|
126
|
+
req.started = Date.now();
|
|
143
127
|
if (this.cfg.logRequest) {
|
|
144
128
|
const { retryAttempt } = res.retryStatus;
|
|
145
129
|
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
|
|
@@ -161,37 +145,26 @@ export class Fetcher {
|
|
|
161
145
|
res.fetchResponse = undefined;
|
|
162
146
|
}
|
|
163
147
|
res.statusFamily = this.getStatusFamily(res);
|
|
164
|
-
if ((
|
|
165
|
-
await this.onOkResponse(res,
|
|
148
|
+
if ((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.ok) {
|
|
149
|
+
await this.onOkResponse(res, timeout);
|
|
166
150
|
}
|
|
167
151
|
else {
|
|
168
152
|
// !res.ok
|
|
169
|
-
await this.onNotOkResponse(res,
|
|
153
|
+
await this.onNotOkResponse(res, timeout);
|
|
170
154
|
}
|
|
171
155
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
_f = _o.value;
|
|
175
|
-
_l = false;
|
|
176
|
-
try {
|
|
177
|
-
const hook = _f;
|
|
178
|
-
await hook(res);
|
|
179
|
-
}
|
|
180
|
-
finally {
|
|
181
|
-
_l = true;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
156
|
+
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
157
|
+
await hook(res);
|
|
184
158
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
159
|
+
if (req.paginate && res.ok) {
|
|
160
|
+
const proceed = await req.paginate(req, res);
|
|
161
|
+
if (proceed) {
|
|
162
|
+
return await this.doFetchRequest(req);
|
|
189
163
|
}
|
|
190
|
-
finally { if (e_2) throw e_2.error; }
|
|
191
164
|
}
|
|
192
165
|
return res;
|
|
193
166
|
}
|
|
194
|
-
async onOkResponse(res,
|
|
167
|
+
async onOkResponse(res, timeout) {
|
|
195
168
|
const { req } = res;
|
|
196
169
|
const { mode } = res.req;
|
|
197
170
|
if (mode === 'json') {
|
|
@@ -214,7 +187,7 @@ export class Fetcher {
|
|
|
214
187
|
// } satisfies HttpRequestErrorData)
|
|
215
188
|
res.err = _anyToError(err);
|
|
216
189
|
res.ok = false;
|
|
217
|
-
return await this.onNotOkResponse(res,
|
|
190
|
+
return await this.onNotOkResponse(res, timeout);
|
|
218
191
|
}
|
|
219
192
|
}
|
|
220
193
|
else {
|
|
@@ -242,7 +215,7 @@ export class Fetcher {
|
|
|
242
215
|
if (res.body === null) {
|
|
243
216
|
res.err = new Error(`fetchResponse.body is null`);
|
|
244
217
|
res.ok = false;
|
|
245
|
-
return await this.onNotOkResponse(res,
|
|
218
|
+
return await this.onNotOkResponse(res, timeout);
|
|
246
219
|
}
|
|
247
220
|
}
|
|
248
221
|
clearTimeout(timeout);
|
|
@@ -256,7 +229,7 @@ export class Fetcher {
|
|
|
256
229
|
res.fetchResponse.status,
|
|
257
230
|
res.signature,
|
|
258
231
|
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
259
|
-
_since(started),
|
|
232
|
+
_since(res.req.started),
|
|
260
233
|
]
|
|
261
234
|
.filter(Boolean)
|
|
262
235
|
.join(' '));
|
|
@@ -271,7 +244,7 @@ export class Fetcher {
|
|
|
271
244
|
async callNativeFetch(url, init) {
|
|
272
245
|
return await globalThis.fetch(url, init);
|
|
273
246
|
}
|
|
274
|
-
async onNotOkResponse(res,
|
|
247
|
+
async onNotOkResponse(res, timeout) {
|
|
275
248
|
var _a, _b;
|
|
276
249
|
clearTimeout(timeout);
|
|
277
250
|
let cause;
|
|
@@ -298,35 +271,18 @@ export class Fetcher {
|
|
|
298
271
|
requestBaseUrl: this.cfg.baseUrl || null,
|
|
299
272
|
requestMethod: res.req.init.method,
|
|
300
273
|
requestSignature: res.signature,
|
|
301
|
-
requestDuration: Date.now() - started,
|
|
274
|
+
requestDuration: Date.now() - res.req.started,
|
|
302
275
|
}), cause);
|
|
303
276
|
await this.processRetry(res);
|
|
304
277
|
}
|
|
305
278
|
async processRetry(res) {
|
|
306
|
-
var _a
|
|
279
|
+
var _a;
|
|
307
280
|
const { retryStatus } = res;
|
|
308
281
|
if (!this.shouldRetry(res)) {
|
|
309
282
|
retryStatus.retryStopped = true;
|
|
310
283
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
_c = _f.value;
|
|
314
|
-
_d = false;
|
|
315
|
-
try {
|
|
316
|
-
const hook = _c;
|
|
317
|
-
await hook(res);
|
|
318
|
-
}
|
|
319
|
-
finally {
|
|
320
|
-
_d = true;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
catch (e_3_1) { e_3 = { error: e_3_1 }; }
|
|
325
|
-
finally {
|
|
326
|
-
try {
|
|
327
|
-
if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
|
|
328
|
-
}
|
|
329
|
-
finally { if (e_3) throw e_3.error; }
|
|
284
|
+
for (const hook of this.cfg.hooks.beforeRetry || []) {
|
|
285
|
+
await hook(res);
|
|
330
286
|
}
|
|
331
287
|
const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
|
|
332
288
|
if (retryStatus.retryAttempt >= count) {
|
|
@@ -334,6 +290,22 @@ export class Fetcher {
|
|
|
334
290
|
}
|
|
335
291
|
if (retryStatus.retryStopped)
|
|
336
292
|
return;
|
|
293
|
+
// Here we know that more retries will be attempted
|
|
294
|
+
// We don't log "last error", because it will be thrown and logged by consumer,
|
|
295
|
+
// but we should log all previous errors, otherwise they are lost.
|
|
296
|
+
// Here is the right place where we know it's not the "last error"
|
|
297
|
+
if (res.err) {
|
|
298
|
+
const { retryAttempt } = retryStatus;
|
|
299
|
+
this.cfg.logger.error([
|
|
300
|
+
' <<',
|
|
301
|
+
((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status) || 0,
|
|
302
|
+
res.signature,
|
|
303
|
+
`try#${retryAttempt + 1}/${count + 1}`,
|
|
304
|
+
_since(res.req.started),
|
|
305
|
+
]
|
|
306
|
+
.filter(Boolean)
|
|
307
|
+
.join(' '), res.err.cause || res.err);
|
|
308
|
+
}
|
|
337
309
|
retryStatus.retryAttempt++;
|
|
338
310
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
339
311
|
const noise = Math.random() * 500;
|
|
@@ -437,7 +409,7 @@ export class Fetcher {
|
|
|
437
409
|
normalizeOptions(url, opt) {
|
|
438
410
|
var _a, _b;
|
|
439
411
|
const { timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode, jsonReviver, } = this.cfg;
|
|
440
|
-
const req = Object.assign(Object.assign({ mode,
|
|
412
|
+
const req = Object.assign(Object.assign({ started: Date.now(), mode,
|
|
441
413
|
url,
|
|
442
414
|
timeoutSeconds,
|
|
443
415
|
throwHttpErrors,
|
package/dist-esm/promise/pMap.js
CHANGED
|
@@ -6,7 +6,6 @@ Improvements:
|
|
|
6
6
|
- Included Typescript typings (no need for @types/p-map)
|
|
7
7
|
- Compatible with pProps (that had typings issues)
|
|
8
8
|
*/
|
|
9
|
-
import { __asyncValues } from "tslib";
|
|
10
9
|
import { END, ErrorMode, SKIP } from '..';
|
|
11
10
|
/**
|
|
12
11
|
* Returns a `Promise` that is fulfilled when all promises in `input` and ones returned from `mapper` are fulfilled,
|
|
@@ -35,7 +34,6 @@ import { END, ErrorMode, SKIP } from '..';
|
|
|
35
34
|
* })();
|
|
36
35
|
*/
|
|
37
36
|
export async function pMap(iterable, mapper, opt = {}) {
|
|
38
|
-
var _a, e_1, _b, _c;
|
|
39
37
|
const ret = [];
|
|
40
38
|
// const iterator = iterable[Symbol.iterator]()
|
|
41
39
|
const items = [...iterable];
|
|
@@ -49,40 +47,23 @@ export async function pMap(iterable, mapper, opt = {}) {
|
|
|
49
47
|
let currentIndex = 0;
|
|
50
48
|
// Special cases that are able to preserve async stack traces
|
|
51
49
|
if (concurrency === 1) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
for (var _d = true, items_1 = __asyncValues(items), items_1_1; items_1_1 = await items_1.next(), _a = items_1_1.done, !_a;) {
|
|
55
|
-
_c = items_1_1.value;
|
|
56
|
-
_d = false;
|
|
57
|
-
try {
|
|
58
|
-
const item = _c;
|
|
59
|
-
try {
|
|
60
|
-
const r = await mapper(item, currentIndex++);
|
|
61
|
-
if (r === END)
|
|
62
|
-
break;
|
|
63
|
-
if (r !== SKIP)
|
|
64
|
-
ret.push(r);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
if (errorMode === ErrorMode.THROW_IMMEDIATELY)
|
|
68
|
-
throw err;
|
|
69
|
-
if (errorMode === ErrorMode.THROW_AGGREGATED) {
|
|
70
|
-
errors.push(err);
|
|
71
|
-
}
|
|
72
|
-
// otherwise, suppress completely
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
finally {
|
|
76
|
-
_d = true;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
81
|
-
finally {
|
|
50
|
+
// Special case for concurrency == 1
|
|
51
|
+
for (const item of items) {
|
|
82
52
|
try {
|
|
83
|
-
|
|
53
|
+
const r = await mapper(item, currentIndex++);
|
|
54
|
+
if (r === END)
|
|
55
|
+
break;
|
|
56
|
+
if (r !== SKIP)
|
|
57
|
+
ret.push(r);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (errorMode === ErrorMode.THROW_IMMEDIATELY)
|
|
61
|
+
throw err;
|
|
62
|
+
if (errorMode === ErrorMode.THROW_AGGREGATED) {
|
|
63
|
+
errors.push(err);
|
|
64
|
+
}
|
|
65
|
+
// otherwise, suppress completely
|
|
84
66
|
}
|
|
85
|
-
finally { if (e_1) throw e_1.error; }
|
|
86
67
|
}
|
|
87
68
|
if (errors.length) {
|
|
88
69
|
throw new AggregateError(errors, `pMap resulted in ${errors.length} error(s)`);
|
package/package.json
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger'
|
|
2
2
|
import type { Promisable } from '../typeFest'
|
|
3
|
-
import type { Reviver } from '../types'
|
|
3
|
+
import type { Reviver, UnixTimestampMillisNumber } from '../types'
|
|
4
4
|
import type { HttpMethod, HttpStatusFamily } from './http.model'
|
|
5
5
|
|
|
6
|
-
export interface FetcherNormalizedCfg
|
|
6
|
+
export interface FetcherNormalizedCfg
|
|
7
|
+
extends Required<FetcherCfg>,
|
|
8
|
+
Omit<FetcherRequest, 'started'> {
|
|
7
9
|
logger: CommonLogger
|
|
8
10
|
searchParams: Record<string, any>
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export type FetcherBeforeRequestHook =
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export type FetcherBeforeRequestHook = <BODY = unknown>(
|
|
14
|
+
req: FetcherRequest<BODY>,
|
|
15
|
+
) => Promisable<void>
|
|
16
|
+
export type FetcherAfterResponseHook = <BODY = unknown>(
|
|
17
|
+
res: FetcherResponse<BODY>,
|
|
18
|
+
) => Promisable<void>
|
|
19
|
+
export type FetcherBeforeRetryHook = <BODY = unknown>(
|
|
20
|
+
res: FetcherResponse<BODY>,
|
|
21
|
+
) => Promisable<void>
|
|
14
22
|
|
|
15
23
|
export interface FetcherCfg {
|
|
16
24
|
/**
|
|
@@ -88,7 +96,8 @@ export interface FetcherRetryOptions {
|
|
|
88
96
|
timeoutMultiplier: number
|
|
89
97
|
}
|
|
90
98
|
|
|
91
|
-
export interface FetcherRequest
|
|
99
|
+
export interface FetcherRequest<BODY = unknown>
|
|
100
|
+
extends Omit<FetcherOptions<BODY>, 'method' | 'headers' | 'baseUrl'> {
|
|
92
101
|
url: string
|
|
93
102
|
init: RequestInitNormalized
|
|
94
103
|
mode: FetcherMode
|
|
@@ -98,9 +107,10 @@ export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers
|
|
|
98
107
|
retryPost: boolean
|
|
99
108
|
retry4xx: boolean
|
|
100
109
|
retry5xx: boolean
|
|
110
|
+
started: UnixTimestampMillisNumber
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
export interface FetcherOptions {
|
|
113
|
+
export interface FetcherOptions<BODY = unknown> {
|
|
104
114
|
method?: HttpMethod
|
|
105
115
|
|
|
106
116
|
baseUrl?: string
|
|
@@ -159,6 +169,19 @@ export interface FetcherOptions {
|
|
|
159
169
|
retry5xx?: boolean
|
|
160
170
|
|
|
161
171
|
jsonReviver?: Reviver
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Allows to walk over multiple pages of results.
|
|
175
|
+
* Paginate take a function.
|
|
176
|
+
* Function has access to FetcherRequest and FetcherResponse
|
|
177
|
+
* and has to make a decision to continue pagination or not.
|
|
178
|
+
* Return true to continue, false otherwise.
|
|
179
|
+
* If continue - it is expected to modify/mutate the FetcherRequest, as it will be used
|
|
180
|
+
* to request the next page.
|
|
181
|
+
*
|
|
182
|
+
* @experimental
|
|
183
|
+
*/
|
|
184
|
+
paginate?: (req: FetcherRequest<BODY>, res: FetcherSuccessResponse<BODY>) => Promisable<boolean>
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
package/src/http/fetcher.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
import { pDelay } from '../promise/pDelay'
|
|
16
16
|
import { _jsonParse, _jsonParseIfPossible } from '../string/json.util'
|
|
17
17
|
import { _since } from '../time/time.util'
|
|
18
|
-
import { UnixTimestampNumber } from '../types'
|
|
19
18
|
import type {
|
|
20
19
|
FetcherAfterResponseHook,
|
|
21
20
|
FetcherBeforeRequestHook,
|
|
@@ -54,7 +53,10 @@ export class Fetcher {
|
|
|
54
53
|
const m = method.toLowerCase()
|
|
55
54
|
|
|
56
55
|
// mode=void
|
|
57
|
-
;(this as any)[`${m}Void`] = async (
|
|
56
|
+
;(this as any)[`${m}Void`] = async (
|
|
57
|
+
url: string,
|
|
58
|
+
opt?: FetcherOptions<void>,
|
|
59
|
+
): Promise<void> => {
|
|
58
60
|
return await this.fetch<void>(url, {
|
|
59
61
|
method,
|
|
60
62
|
mode: 'void',
|
|
@@ -63,7 +65,10 @@ export class Fetcher {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
if (method === 'HEAD') return // mode=text
|
|
66
|
-
;(this as any)[`${m}Text`] = async (
|
|
68
|
+
;(this as any)[`${m}Text`] = async (
|
|
69
|
+
url: string,
|
|
70
|
+
opt?: FetcherOptions<string>,
|
|
71
|
+
): Promise<string> => {
|
|
67
72
|
return await this.fetch<string>(url, {
|
|
68
73
|
method,
|
|
69
74
|
mode: 'text',
|
|
@@ -72,7 +77,7 @@ export class Fetcher {
|
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
// Default mode=json, but overridable
|
|
75
|
-
;(this as any)[m] = async <T = unknown>(url: string, opt?: FetcherOptions): Promise<T> => {
|
|
80
|
+
;(this as any)[m] = async <T = unknown>(url: string, opt?: FetcherOptions<T>): Promise<T> => {
|
|
76
81
|
return await this.fetch<T>(url, {
|
|
77
82
|
method,
|
|
78
83
|
mode: 'json',
|
|
@@ -108,26 +113,26 @@ export class Fetcher {
|
|
|
108
113
|
|
|
109
114
|
// These methods are generated dynamically in the constructor
|
|
110
115
|
// These default methods use mode=json
|
|
111
|
-
get!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
112
|
-
post!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
113
|
-
put!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
114
|
-
patch!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
115
|
-
delete!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
|
|
116
|
+
get!: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>
|
|
117
|
+
post!: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>
|
|
118
|
+
put!: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>
|
|
119
|
+
patch!: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>
|
|
120
|
+
delete!: <T = unknown>(url: string, opt?: FetcherOptions<T>) => Promise<T>
|
|
116
121
|
|
|
117
122
|
// mode=text
|
|
118
|
-
getText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
119
|
-
postText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
120
|
-
putText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
121
|
-
patchText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
122
|
-
deleteText!: (url: string, opt?: FetcherOptions) => Promise<string>
|
|
123
|
+
getText!: (url: string, opt?: FetcherOptions<string>) => Promise<string>
|
|
124
|
+
postText!: (url: string, opt?: FetcherOptions<string>) => Promise<string>
|
|
125
|
+
putText!: (url: string, opt?: FetcherOptions<string>) => Promise<string>
|
|
126
|
+
patchText!: (url: string, opt?: FetcherOptions<string>) => Promise<string>
|
|
127
|
+
deleteText!: (url: string, opt?: FetcherOptions<string>) => Promise<string>
|
|
123
128
|
|
|
124
129
|
// mode=void (no body fetching/parsing)
|
|
125
|
-
getVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
126
|
-
postVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
127
|
-
putVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
128
|
-
patchVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
129
|
-
deleteVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
130
|
-
headVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
|
|
130
|
+
getVoid!: (url: string, opt?: FetcherOptions<void>) => Promise<void>
|
|
131
|
+
postVoid!: (url: string, opt?: FetcherOptions<void>) => Promise<void>
|
|
132
|
+
putVoid!: (url: string, opt?: FetcherOptions<void>) => Promise<void>
|
|
133
|
+
patchVoid!: (url: string, opt?: FetcherOptions<void>) => Promise<void>
|
|
134
|
+
deleteVoid!: (url: string, opt?: FetcherOptions<void>) => Promise<void>
|
|
135
|
+
headVoid!: (url: string, opt?: FetcherOptions<void>) => Promise<void>
|
|
131
136
|
|
|
132
137
|
// mode=readableStream
|
|
133
138
|
/**
|
|
@@ -136,14 +141,17 @@ export class Fetcher {
|
|
|
136
141
|
* More on streams and Node interop:
|
|
137
142
|
* https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/
|
|
138
143
|
*/
|
|
139
|
-
async getReadableStream(
|
|
144
|
+
async getReadableStream(
|
|
145
|
+
url: string,
|
|
146
|
+
opt?: FetcherOptions<ReadableStream<Uint8Array>>,
|
|
147
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
140
148
|
return await this.fetch(url, {
|
|
141
149
|
mode: 'readableStream',
|
|
142
150
|
...opt,
|
|
143
151
|
})
|
|
144
152
|
}
|
|
145
153
|
|
|
146
|
-
async fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
|
|
154
|
+
async fetch<T = unknown>(url: string, opt?: FetcherOptions<T>): Promise<T> {
|
|
147
155
|
const res = await this.doFetch<T>(url, opt)
|
|
148
156
|
if (res.err) {
|
|
149
157
|
if (res.req.throwHttpErrors) throw res.err
|
|
@@ -159,11 +167,14 @@ export class Fetcher {
|
|
|
159
167
|
*/
|
|
160
168
|
async doFetch<T = unknown>(
|
|
161
169
|
url: string,
|
|
162
|
-
|
|
170
|
+
opt: FetcherOptions<T> = {},
|
|
163
171
|
): Promise<FetcherResponse<T>> {
|
|
164
|
-
const
|
|
172
|
+
const req = this.normalizeOptions(url, opt)
|
|
173
|
+
return await this.doFetchRequest<T>(req)
|
|
174
|
+
}
|
|
165
175
|
|
|
166
|
-
|
|
176
|
+
async doFetchRequest<T = unknown>(req: FetcherRequest<T>): Promise<FetcherResponse<T>> {
|
|
177
|
+
const { logger } = this.cfg
|
|
167
178
|
const {
|
|
168
179
|
timeoutSeconds,
|
|
169
180
|
init: { method },
|
|
@@ -179,7 +190,7 @@ export class Fetcher {
|
|
|
179
190
|
}, timeoutSeconds * 1000) as any as number
|
|
180
191
|
}
|
|
181
192
|
|
|
182
|
-
for
|
|
193
|
+
for (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
183
194
|
await hook(req)
|
|
184
195
|
}
|
|
185
196
|
|
|
@@ -199,7 +210,7 @@ export class Fetcher {
|
|
|
199
210
|
} as FetcherResponse<any>
|
|
200
211
|
|
|
201
212
|
while (!res.retryStatus.retryStopped) {
|
|
202
|
-
|
|
213
|
+
req.started = Date.now()
|
|
203
214
|
|
|
204
215
|
if (this.cfg.logRequest) {
|
|
205
216
|
const { retryAttempt } = res.retryStatus
|
|
@@ -226,27 +237,29 @@ export class Fetcher {
|
|
|
226
237
|
res.statusFamily = this.getStatusFamily(res)
|
|
227
238
|
|
|
228
239
|
if (res.fetchResponse?.ok) {
|
|
229
|
-
await this.onOkResponse(
|
|
230
|
-
res as FetcherResponse<T> & { fetchResponse: Response },
|
|
231
|
-
started,
|
|
232
|
-
timeout,
|
|
233
|
-
)
|
|
240
|
+
await this.onOkResponse(res as FetcherResponse<T> & { fetchResponse: Response }, timeout)
|
|
234
241
|
} else {
|
|
235
242
|
// !res.ok
|
|
236
|
-
await this.onNotOkResponse(res,
|
|
243
|
+
await this.onNotOkResponse(res, timeout)
|
|
237
244
|
}
|
|
238
245
|
}
|
|
239
246
|
|
|
240
|
-
for
|
|
247
|
+
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
241
248
|
await hook(res)
|
|
242
249
|
}
|
|
243
250
|
|
|
251
|
+
if (req.paginate && res.ok) {
|
|
252
|
+
const proceed = await req.paginate(req, res)
|
|
253
|
+
if (proceed) {
|
|
254
|
+
return await this.doFetchRequest(req)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
244
258
|
return res
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
private async onOkResponse(
|
|
248
262
|
res: FetcherResponse<any> & { fetchResponse: Response },
|
|
249
|
-
started: UnixTimestampNumber,
|
|
250
263
|
timeout?: number,
|
|
251
264
|
): Promise<void> {
|
|
252
265
|
const { req } = res
|
|
@@ -273,7 +286,7 @@ export class Fetcher {
|
|
|
273
286
|
res.err = _anyToError(err)
|
|
274
287
|
res.ok = false
|
|
275
288
|
|
|
276
|
-
return await this.onNotOkResponse(res,
|
|
289
|
+
return await this.onNotOkResponse(res, timeout)
|
|
277
290
|
}
|
|
278
291
|
} else {
|
|
279
292
|
// Body had a '' (empty string)
|
|
@@ -296,7 +309,7 @@ export class Fetcher {
|
|
|
296
309
|
if (res.body === null) {
|
|
297
310
|
res.err = new Error(`fetchResponse.body is null`)
|
|
298
311
|
res.ok = false
|
|
299
|
-
return await this.onNotOkResponse(res,
|
|
312
|
+
return await this.onNotOkResponse(res, timeout)
|
|
300
313
|
}
|
|
301
314
|
}
|
|
302
315
|
|
|
@@ -313,7 +326,7 @@ export class Fetcher {
|
|
|
313
326
|
res.fetchResponse.status,
|
|
314
327
|
res.signature,
|
|
315
328
|
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
316
|
-
_since(started),
|
|
329
|
+
_since(res.req.started),
|
|
317
330
|
]
|
|
318
331
|
.filter(Boolean)
|
|
319
332
|
.join(' '),
|
|
@@ -332,11 +345,7 @@ export class Fetcher {
|
|
|
332
345
|
return await globalThis.fetch(url, init)
|
|
333
346
|
}
|
|
334
347
|
|
|
335
|
-
private async onNotOkResponse(
|
|
336
|
-
res: FetcherResponse,
|
|
337
|
-
started: UnixTimestampNumber,
|
|
338
|
-
timeout?: number,
|
|
339
|
-
): Promise<void> {
|
|
348
|
+
private async onNotOkResponse(res: FetcherResponse, timeout?: number): Promise<void> {
|
|
340
349
|
clearTimeout(timeout)
|
|
341
350
|
|
|
342
351
|
let cause: ErrorObject | undefined
|
|
@@ -367,7 +376,7 @@ export class Fetcher {
|
|
|
367
376
|
requestBaseUrl: this.cfg.baseUrl || (null as any),
|
|
368
377
|
requestMethod: res.req.init.method,
|
|
369
378
|
requestSignature: res.signature,
|
|
370
|
-
requestDuration: Date.now() - started,
|
|
379
|
+
requestDuration: Date.now() - res.req.started,
|
|
371
380
|
}),
|
|
372
381
|
cause,
|
|
373
382
|
)
|
|
@@ -382,7 +391,7 @@ export class Fetcher {
|
|
|
382
391
|
retryStatus.retryStopped = true
|
|
383
392
|
}
|
|
384
393
|
|
|
385
|
-
for
|
|
394
|
+
for (const hook of this.cfg.hooks.beforeRetry || []) {
|
|
386
395
|
await hook(res)
|
|
387
396
|
}
|
|
388
397
|
|
|
@@ -394,6 +403,26 @@ export class Fetcher {
|
|
|
394
403
|
|
|
395
404
|
if (retryStatus.retryStopped) return
|
|
396
405
|
|
|
406
|
+
// Here we know that more retries will be attempted
|
|
407
|
+
// We don't log "last error", because it will be thrown and logged by consumer,
|
|
408
|
+
// but we should log all previous errors, otherwise they are lost.
|
|
409
|
+
// Here is the right place where we know it's not the "last error"
|
|
410
|
+
if (res.err) {
|
|
411
|
+
const { retryAttempt } = retryStatus
|
|
412
|
+
this.cfg.logger.error(
|
|
413
|
+
[
|
|
414
|
+
' <<',
|
|
415
|
+
res.fetchResponse?.status || 0,
|
|
416
|
+
res.signature,
|
|
417
|
+
`try#${retryAttempt + 1}/${count + 1}`,
|
|
418
|
+
_since(res.req.started),
|
|
419
|
+
]
|
|
420
|
+
.filter(Boolean)
|
|
421
|
+
.join(' '),
|
|
422
|
+
res.err.cause || res.err,
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
397
426
|
retryStatus.retryAttempt++
|
|
398
427
|
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax)
|
|
399
428
|
|
|
@@ -499,7 +528,7 @@ export class Fetcher {
|
|
|
499
528
|
return norm
|
|
500
529
|
}
|
|
501
530
|
|
|
502
|
-
private normalizeOptions(url: string, opt: FetcherOptions): FetcherRequest {
|
|
531
|
+
private normalizeOptions<BODY>(url: string, opt: FetcherOptions<BODY>): FetcherRequest<BODY> {
|
|
503
532
|
const {
|
|
504
533
|
timeoutSeconds,
|
|
505
534
|
throwHttpErrors,
|
|
@@ -511,7 +540,8 @@ export class Fetcher {
|
|
|
511
540
|
jsonReviver,
|
|
512
541
|
} = this.cfg
|
|
513
542
|
|
|
514
|
-
const req: FetcherRequest = {
|
|
543
|
+
const req: FetcherRequest<BODY> = {
|
|
544
|
+
started: Date.now(),
|
|
515
545
|
mode,
|
|
516
546
|
url,
|
|
517
547
|
timeoutSeconds,
|
package/src/promise/pMap.ts
CHANGED