@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.
@@ -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, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
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.
@@ -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, rawOpt = {}) {
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 await (const hook of this.cfg.hooks.beforeRequest || []) {
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
- const started = Date.now();
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, started, timeout);
163
+ await this.onOkResponse(res, timeout);
161
164
  }
162
165
  else {
163
166
  // !res.ok
164
- await this.onNotOkResponse(res, started, timeout);
167
+ await this.onNotOkResponse(res, timeout);
165
168
  }
166
169
  }
167
- for await (const hook of this.cfg.hooks.afterResponse || []) {
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, started, timeout) {
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, started, timeout);
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, started, timeout);
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, started, timeout) {
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 await (const hook of this.cfg.hooks.beforeRetry || []) {
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, 'method' | 'headers' | 'baseUrl'> {
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;
@@ -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 await (const item of items) {
54
+ for (const item of items) {
55
55
  try {
56
56
  const r = await mapper(item, currentIndex++);
57
57
  if (r === __1.END)
@@ -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, rawOpt = {}) {
94
- var _a, e_1, _b, _c, _d, e_2, _e, _f;
95
- var _g;
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
- try {
109
- for (var _h = true, _j = __asyncValues(this.cfg.hooks.beforeRequest || []), _k; _k = await _j.next(), _a = _k.done, !_a;) {
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
- const started = Date.now();
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 ((_g = res.fetchResponse) === null || _g === void 0 ? void 0 : _g.ok) {
165
- await this.onOkResponse(res, started, timeout);
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, started, timeout);
153
+ await this.onNotOkResponse(res, timeout);
170
154
  }
171
155
  }
172
- try {
173
- for (var _l = true, _m = __asyncValues(this.cfg.hooks.afterResponse || []), _o; _o = await _m.next(), _d = _o.done, !_d;) {
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
- catch (e_2_1) { e_2 = { error: e_2_1 }; }
186
- finally {
187
- try {
188
- if (!_l && !_d && (_e = _m.return)) await _e.call(_m);
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, started, timeout) {
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, started, timeout);
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, started, timeout);
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, started, timeout) {
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, e_3, _b, _c;
279
+ var _a;
307
280
  const { retryStatus } = res;
308
281
  if (!this.shouldRetry(res)) {
309
282
  retryStatus.retryStopped = true;
310
283
  }
311
- try {
312
- for (var _d = true, _e = __asyncValues(this.cfg.hooks.beforeRetry || []), _f; _f = await _e.next(), _a = _f.done, !_a;) {
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,
@@ -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
- try {
53
- // Special case for concurrency == 1
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
- if (!_d && !_a && (_b = items_1.return)) await _b.call(items_1);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.145.0",
3
+ "version": "14.146.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -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 extends Required<FetcherCfg>, FetcherRequest {
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 = (req: FetcherRequest) => Promisable<void>
12
- export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>
13
- export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>
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 extends Omit<FetcherOptions, 'method' | 'headers' | 'baseUrl'> {
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'> & {
@@ -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 (url: string, opt?: FetcherOptions): Promise<void> => {
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 (url: string, opt?: FetcherOptions): Promise<string> => {
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(url: string, opt?: FetcherOptions): Promise<ReadableStream<Uint8Array>> {
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
- rawOpt: FetcherOptions = {},
170
+ opt: FetcherOptions<T> = {},
163
171
  ): Promise<FetcherResponse<T>> {
164
- const { logger } = this.cfg
172
+ const req = this.normalizeOptions(url, opt)
173
+ return await this.doFetchRequest<T>(req)
174
+ }
165
175
 
166
- const req = this.normalizeOptions(url, rawOpt)
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 await (const hook of this.cfg.hooks.beforeRequest || []) {
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
- const started = Date.now()
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, started, timeout)
243
+ await this.onNotOkResponse(res, timeout)
237
244
  }
238
245
  }
239
246
 
240
- for await (const hook of this.cfg.hooks.afterResponse || []) {
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, started, timeout)
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, started, timeout)
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 await (const hook of this.cfg.hooks.beforeRetry || []) {
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,
@@ -77,7 +77,7 @@ export async function pMap<IN, OUT>(
77
77
  if (concurrency === 1) {
78
78
  // Special case for concurrency == 1
79
79
 
80
- for await (const item of items) {
80
+ for (const item of items) {
81
81
  try {
82
82
  const r = await mapper(item, currentIndex++)
83
83
  if (r === END) break