@naturalcycles/js-lib 14.121.0 → 14.122.1

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.
@@ -80,6 +80,12 @@ export interface FetcherOptions {
80
80
  timeoutSeconds?: number;
81
81
  json?: any;
82
82
  text?: string;
83
+ /**
84
+ * Supports all the types that RequestInit.body supports.
85
+ *
86
+ * Useful when you want to e.g pass FormData.
87
+ */
88
+ body?: Blob | BufferSource | FormData | URLSearchParams | string;
83
89
  credentials?: RequestCredentials;
84
90
  headers?: Record<string, any>;
85
91
  mode?: FetcherMode;
@@ -119,9 +119,15 @@ class Fetcher {
119
119
  logger.log(req.init.body); // todo: check if we can _inspect it
120
120
  }
121
121
  }
122
- res.fetchResponse = await globalThis.fetch(req.url, req.init);
122
+ try {
123
+ res.fetchResponse = await globalThis.fetch(req.url, req.init);
124
+ }
125
+ catch (err) {
126
+ // For example, CORS error would result in "TypeError: failed to fetch" here
127
+ res.err = err;
128
+ }
123
129
  res.statusFamily = this.getStatusFamily(res);
124
- if (res.fetchResponse.ok) {
130
+ if (res.fetchResponse?.ok) {
125
131
  if (mode === 'json') {
126
132
  // if no body: set responseBody as {}
127
133
  // do not throw a "cannot parse null as Json" error
@@ -150,14 +156,28 @@ class Fetcher {
150
156
  }
151
157
  else {
152
158
  clearTimeout(timeout);
153
- const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
154
- const errObj = (0, error_util_1._anyToErrorObject)(body);
159
+ let errObj;
160
+ if (res.fetchResponse) {
161
+ const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
162
+ errObj = (0, error_util_1._anyToErrorObject)(body);
163
+ }
164
+ else if (res.err) {
165
+ errObj = (0, error_util_1._errorToErrorObject)(res.err);
166
+ }
167
+ else {
168
+ errObj = {};
169
+ }
155
170
  const originalMessage = errObj.message;
156
- errObj.message = [[res.fetchResponse.status, signature].join(' '), originalMessage].join('\n');
171
+ errObj.message = [
172
+ [res.fetchResponse?.status, signature].filter(Boolean).join(' '),
173
+ originalMessage,
174
+ ]
175
+ .filter(Boolean)
176
+ .join('\n');
157
177
  res.err = new http_error_1.HttpError(errObj.message, (0, object_util_1._filterNullishValues)({
158
178
  ...errObj.data,
159
179
  originalMessage,
160
- httpStatusCode: res.fetchResponse.status,
180
+ httpStatusCode: res.fetchResponse?.status || 0,
161
181
  // These properties are provided to be used in e.g custom Sentry error grouping
162
182
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
163
183
  // Enabled, cause `data` is not printed by default when error is HttpError
@@ -201,8 +221,13 @@ class Fetcher {
201
221
  if (method === 'post' && !retryPost)
202
222
  return false;
203
223
  const { statusFamily } = res;
224
+ const statusCode = res.fetchResponse?.status || 0;
204
225
  if (statusFamily === 5 && !retry5xx)
205
226
  return false;
227
+ if ([408, 429].includes(statusCode)) {
228
+ // these codes are always retried
229
+ return true;
230
+ }
206
231
  if (statusFamily === 4 && !retry4xx)
207
232
  return false;
208
233
  return true; // default is true
@@ -254,11 +279,12 @@ class Fetcher {
254
279
  logResponseBody: debug,
255
280
  retry: { ...defRetryOptions },
256
281
  init: {
257
- method: 'get',
258
- headers: {},
282
+ method: cfg.method || 'get',
283
+ headers: cfg.headers || {},
284
+ credentials: cfg.credentials,
259
285
  },
260
286
  hooks: {},
261
- }, cfg);
287
+ }, (0, object_util_1._omit)(cfg, ['method', 'credentials', 'headers']));
262
288
  norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
263
289
  return norm;
264
290
  }
@@ -271,18 +297,18 @@ class Fetcher {
271
297
  retryPost,
272
298
  retry4xx,
273
299
  retry5xx,
274
- ...(0, object_util_1._omit)(opt, ['method', 'headers']),
300
+ ...(0, object_util_1._omit)(opt, ['method', 'headers', 'credentials']),
275
301
  retry: {
276
302
  ...retry,
277
303
  ...(0, object_util_1._filterUndefinedValues)(opt.retry || {}),
278
304
  },
279
- init: (0, object_util_1._merge)({ ...this.cfg.init },
280
- // opt.init,
281
- (0, object_util_1._filterUndefinedValues)({
282
- method: opt.method,
283
- credentials: opt.credentials,
305
+ init: (0, object_util_1._merge)({
306
+ ...this.cfg.init,
307
+ method: opt.method || this.cfg.init.method,
308
+ credentials: opt.credentials || this.cfg.init.credentials,
309
+ }, {
284
310
  headers: (0, object_util_1._mapKeys)(opt.headers || {}, k => k.toLowerCase()),
285
- })),
311
+ }),
286
312
  };
287
313
  // setup url
288
314
  if (baseUrl) {
@@ -309,6 +335,9 @@ class Fetcher {
309
335
  req.init.body = opt.text;
310
336
  req.init.headers['content-type'] = 'text/plain';
311
337
  }
338
+ else if (opt.body !== undefined) {
339
+ req.init.body = opt.body;
340
+ }
312
341
  return req;
313
342
  }
314
343
  }
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom"/>
2
2
  import { __asyncValues } from "tslib";
3
- import { _anyToErrorObject } from '../error/error.util';
3
+ import { _anyToErrorObject, _errorToErrorObject } from '../error/error.util';
4
4
  import { HttpError } from '../error/http.error';
5
5
  import { _clamp } from '../number/number.util';
6
6
  import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, } from '../object/object.util';
@@ -73,6 +73,7 @@ export class Fetcher {
73
73
  }
74
74
  async rawFetch(url, rawOpt = {}) {
75
75
  var _a, e_1, _b, _c, _d, e_2, _e, _f;
76
+ var _g, _h, _j;
76
77
  const { logger } = this.cfg;
77
78
  const req = this.normalizeOptions(url, rawOpt);
78
79
  const { timeoutSeconds, mode, init: { method }, } = req;
@@ -86,22 +87,22 @@ export class Fetcher {
86
87
  }, timeoutSeconds * 1000);
87
88
  }
88
89
  try {
89
- for (var _g = true, _h = __asyncValues(this.cfg.hooks.beforeRequest || []), _j; _j = await _h.next(), _a = _j.done, !_a;) {
90
- _c = _j.value;
91
- _g = false;
90
+ for (var _k = true, _l = __asyncValues(this.cfg.hooks.beforeRequest || []), _m; _m = await _l.next(), _a = _m.done, !_a;) {
91
+ _c = _m.value;
92
+ _k = false;
92
93
  try {
93
94
  const hook = _c;
94
95
  await hook(req);
95
96
  }
96
97
  finally {
97
- _g = true;
98
+ _k = true;
98
99
  }
99
100
  }
100
101
  }
101
102
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
102
103
  finally {
103
104
  try {
104
- if (!_g && !_a && (_b = _h.return)) await _b.call(_h);
105
+ if (!_k && !_a && (_b = _l.return)) await _b.call(_l);
105
106
  }
106
107
  finally { if (e_1) throw e_1.error; }
107
108
  }
@@ -127,9 +128,15 @@ export class Fetcher {
127
128
  logger.log(req.init.body); // todo: check if we can _inspect it
128
129
  }
129
130
  }
130
- res.fetchResponse = await globalThis.fetch(req.url, req.init);
131
+ try {
132
+ res.fetchResponse = await globalThis.fetch(req.url, req.init);
133
+ }
134
+ catch (err) {
135
+ // For example, CORS error would result in "TypeError: failed to fetch" here
136
+ res.err = err;
137
+ }
131
138
  res.statusFamily = this.getStatusFamily(res);
132
- if (res.fetchResponse.ok) {
139
+ if ((_g = res.fetchResponse) === null || _g === void 0 ? void 0 : _g.ok) {
133
140
  if (mode === 'json') {
134
141
  // if no body: set responseBody as {}
135
142
  // do not throw a "cannot parse null as Json" error
@@ -158,11 +165,25 @@ export class Fetcher {
158
165
  }
159
166
  else {
160
167
  clearTimeout(timeout);
161
- const body = _jsonParseIfPossible(await res.fetchResponse.text());
162
- const errObj = _anyToErrorObject(body);
168
+ let errObj;
169
+ if (res.fetchResponse) {
170
+ const body = _jsonParseIfPossible(await res.fetchResponse.text());
171
+ errObj = _anyToErrorObject(body);
172
+ }
173
+ else if (res.err) {
174
+ errObj = _errorToErrorObject(res.err);
175
+ }
176
+ else {
177
+ errObj = {};
178
+ }
163
179
  const originalMessage = errObj.message;
164
- errObj.message = [[res.fetchResponse.status, signature].join(' '), originalMessage].join('\n');
165
- res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: res.fetchResponse.status,
180
+ errObj.message = [
181
+ [(_h = res.fetchResponse) === null || _h === void 0 ? void 0 : _h.status, signature].filter(Boolean).join(' '),
182
+ originalMessage,
183
+ ]
184
+ .filter(Boolean)
185
+ .join('\n');
186
+ res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: ((_j = res.fetchResponse) === null || _j === void 0 ? void 0 : _j.status) || 0,
166
187
  // These properties are provided to be used in e.g custom Sentry error grouping
167
188
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
168
189
  // Enabled, cause `data` is not printed by default when error is HttpError
@@ -172,22 +193,22 @@ export class Fetcher {
172
193
  }
173
194
  }
174
195
  try {
175
- for (var _k = true, _l = __asyncValues(this.cfg.hooks.afterResponse || []), _m; _m = await _l.next(), _d = _m.done, !_d;) {
176
- _f = _m.value;
177
- _k = false;
196
+ for (var _o = true, _p = __asyncValues(this.cfg.hooks.afterResponse || []), _q; _q = await _p.next(), _d = _q.done, !_d;) {
197
+ _f = _q.value;
198
+ _o = false;
178
199
  try {
179
200
  const hook = _f;
180
201
  await hook(res);
181
202
  }
182
203
  finally {
183
- _k = true;
204
+ _o = true;
184
205
  }
185
206
  }
186
207
  }
187
208
  catch (e_2_1) { e_2 = { error: e_2_1 }; }
188
209
  finally {
189
210
  try {
190
- if (!_k && !_d && (_e = _l.return)) await _e.call(_l);
211
+ if (!_o && !_d && (_e = _p.return)) await _e.call(_p);
191
212
  }
192
213
  finally { if (e_2) throw e_2.error; }
193
214
  }
@@ -234,13 +255,19 @@ export class Fetcher {
234
255
  * unless there's reason not to (e.g method is POST).
235
256
  */
236
257
  shouldRetry(res) {
258
+ var _a;
237
259
  const { retryPost, retry4xx, retry5xx } = res.req;
238
260
  const { method } = res.req.init;
239
261
  if (method === 'post' && !retryPost)
240
262
  return false;
241
263
  const { statusFamily } = res;
264
+ const statusCode = ((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status) || 0;
242
265
  if (statusFamily === 5 && !retry5xx)
243
266
  return false;
267
+ if ([408, 429].includes(statusCode)) {
268
+ // these codes are always retried
269
+ return true;
270
+ }
244
271
  if (statusFamily === 4 && !retry4xx)
245
272
  return false;
246
273
  return true; // default is true
@@ -294,11 +321,12 @@ export class Fetcher {
294
321
  logResponseBody: debug,
295
322
  retry: Object.assign({}, defRetryOptions),
296
323
  init: {
297
- method: 'get',
298
- headers: {},
324
+ method: cfg.method || 'get',
325
+ headers: cfg.headers || {},
326
+ credentials: cfg.credentials,
299
327
  },
300
328
  hooks: {},
301
- }, cfg);
329
+ }, _omit(cfg, ['method', 'credentials', 'headers']));
302
330
  norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
303
331
  return norm;
304
332
  }
@@ -309,13 +337,9 @@ export class Fetcher {
309
337
  throwHttpErrors,
310
338
  retryPost,
311
339
  retry4xx,
312
- retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init),
313
- // opt.init,
314
- _filterUndefinedValues({
315
- method: opt.method,
316
- credentials: opt.credentials,
340
+ retry5xx }, _omit(opt, ['method', 'headers', 'credentials'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign(Object.assign({}, this.cfg.init), { method: opt.method || this.cfg.init.method, credentials: opt.credentials || this.cfg.init.credentials }), {
317
341
  headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
318
- })) });
342
+ }) });
319
343
  // setup url
320
344
  if (baseUrl) {
321
345
  if (url.startsWith('/')) {
@@ -338,6 +362,9 @@ export class Fetcher {
338
362
  req.init.body = opt.text;
339
363
  req.init.headers['content-type'] = 'text/plain';
340
364
  }
365
+ else if (opt.body !== undefined) {
366
+ req.init.body = opt.body;
367
+ }
341
368
  return req;
342
369
  }
343
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.121.0",
3
+ "version": "14.122.1",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -1,6 +1,7 @@
1
1
  /// <reference lib="dom"/>
2
2
 
3
- import { _anyToErrorObject } from '../error/error.util'
3
+ import { ErrorObject } from '../error/error.model'
4
+ import { _anyToErrorObject, _errorToErrorObject } from '../error/error.util'
4
5
  import { HttpError } from '../error/http.error'
5
6
  import { CommonLogger } from '../log/commonLogger'
6
7
  import { _clamp } from '../number/number.util'
@@ -103,8 +104,15 @@ export interface FetcherOptions {
103
104
  * so both should finish within this single timeout (not each).
104
105
  */
105
106
  timeoutSeconds?: number
107
+
106
108
  json?: any
107
109
  text?: string
110
+ /**
111
+ * Supports all the types that RequestInit.body supports.
112
+ *
113
+ * Useful when you want to e.g pass FormData.
114
+ */
115
+ body?: Blob | BufferSource | FormData | URLSearchParams | string
108
116
 
109
117
  credentials?: RequestCredentials
110
118
 
@@ -326,10 +334,15 @@ export class Fetcher {
326
334
  }
327
335
  }
328
336
 
329
- res.fetchResponse = await globalThis.fetch(req.url, req.init)
337
+ try {
338
+ res.fetchResponse = await globalThis.fetch(req.url, req.init)
339
+ } catch (err) {
340
+ // For example, CORS error would result in "TypeError: failed to fetch" here
341
+ res.err = err as Error
342
+ }
330
343
  res.statusFamily = this.getStatusFamily(res)
331
344
 
332
- if (res.fetchResponse.ok) {
345
+ if (res.fetchResponse?.ok) {
333
346
  if (mode === 'json') {
334
347
  // if no body: set responseBody as {}
335
348
  // do not throw a "cannot parse null as Json" error
@@ -362,12 +375,24 @@ export class Fetcher {
362
375
  } else {
363
376
  clearTimeout(timeout)
364
377
 
365
- const body = _jsonParseIfPossible(await res.fetchResponse.text())
366
- const errObj = _anyToErrorObject(body)
378
+ let errObj: ErrorObject
379
+
380
+ if (res.fetchResponse) {
381
+ const body = _jsonParseIfPossible(await res.fetchResponse.text())
382
+ errObj = _anyToErrorObject(body)
383
+ } else if (res.err) {
384
+ errObj = _errorToErrorObject(res.err)
385
+ } else {
386
+ errObj = {} as ErrorObject
387
+ }
388
+
367
389
  const originalMessage = errObj.message
368
- errObj.message = [[res.fetchResponse.status, signature].join(' '), originalMessage].join(
369
- '\n',
370
- )
390
+ errObj.message = [
391
+ [res.fetchResponse?.status, signature].filter(Boolean).join(' '),
392
+ originalMessage,
393
+ ]
394
+ .filter(Boolean)
395
+ .join('\n')
371
396
 
372
397
  res.err = new HttpError(
373
398
  errObj.message,
@@ -375,7 +400,7 @@ export class Fetcher {
375
400
  _filterNullishValues({
376
401
  ...errObj.data,
377
402
  originalMessage,
378
- httpStatusCode: res.fetchResponse.status,
403
+ httpStatusCode: res.fetchResponse?.status || 0,
379
404
  // These properties are provided to be used in e.g custom Sentry error grouping
380
405
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
381
406
  // Enabled, cause `data` is not printed by default when error is HttpError
@@ -430,7 +455,12 @@ export class Fetcher {
430
455
  const { method } = res.req.init
431
456
  if (method === 'post' && !retryPost) return false
432
457
  const { statusFamily } = res
458
+ const statusCode = res.fetchResponse?.status || 0
433
459
  if (statusFamily === 5 && !retry5xx) return false
460
+ if ([408, 429].includes(statusCode)) {
461
+ // these codes are always retried
462
+ return true
463
+ }
434
464
  if (statusFamily === 4 && !retry4xx) return false
435
465
  return true // default is true
436
466
  }
@@ -480,12 +510,13 @@ export class Fetcher {
480
510
  logResponseBody: debug,
481
511
  retry: { ...defRetryOptions },
482
512
  init: {
483
- method: 'get',
484
- headers: {},
513
+ method: cfg.method || 'get',
514
+ headers: cfg.headers || {},
515
+ credentials: cfg.credentials,
485
516
  },
486
517
  hooks: {},
487
518
  },
488
- cfg,
519
+ _omit(cfg, ['method', 'credentials', 'headers']),
489
520
  )
490
521
 
491
522
  norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase())
@@ -504,19 +535,20 @@ export class Fetcher {
504
535
  retryPost,
505
536
  retry4xx,
506
537
  retry5xx,
507
- ..._omit(opt, ['method', 'headers']),
538
+ ..._omit(opt, ['method', 'headers', 'credentials']),
508
539
  retry: {
509
540
  ...retry,
510
541
  ..._filterUndefinedValues(opt.retry || {}),
511
542
  },
512
543
  init: _merge(
513
- { ...this.cfg.init },
514
- // opt.init,
515
- _filterUndefinedValues({
516
- method: opt.method,
517
- credentials: opt.credentials,
544
+ {
545
+ ...this.cfg.init,
546
+ method: opt.method || this.cfg.init.method,
547
+ credentials: opt.credentials || this.cfg.init.credentials,
548
+ },
549
+ {
518
550
  headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
519
- } as RequestInit),
551
+ } as RequestInit,
520
552
  ),
521
553
  }
522
554
 
@@ -546,6 +578,8 @@ export class Fetcher {
546
578
  } else if (opt.text !== undefined) {
547
579
  req.init.body = opt.text
548
580
  req.init.headers['content-type'] = 'text/plain'
581
+ } else if (opt.body !== undefined) {
582
+ req.init.body = opt.body
549
583
  }
550
584
 
551
585
  return req