@naturalcycles/js-lib 14.151.0 → 14.153.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.
@@ -5,7 +5,7 @@ const string_util_1 = require("../string/string.util");
5
5
  const app_error_1 = require("./app.error");
6
6
  class JsonParseError extends app_error_1.AppError {
7
7
  constructor(data, cause) {
8
- super(['Failed to parse', data?.text && (0, string_util_1._truncateMiddle)(data.text, 200)].filter(Boolean).join(': '), data, cause, 'JsonParseError');
8
+ super(['Failed to parse', data.text && (0, string_util_1._truncateMiddle)(data.text, 200)].filter(Boolean).join(': '), data, cause, 'JsonParseError');
9
9
  }
10
10
  }
11
11
  exports.JsonParseError = JsonParseError;
@@ -232,7 +232,7 @@ class Fetcher {
232
232
  }
233
233
  clearTimeout(timeout);
234
234
  res.retryStatus.retryStopped = true;
235
- // res.err can happen on JSON.parse error
235
+ // res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
236
236
  if (!res.err && this.cfg.logResponse) {
237
237
  const { retryAttempt } = res.retryStatus;
238
238
  const { logger } = this.cfg;
@@ -309,7 +309,9 @@ class Fetcher {
309
309
  ' <<',
310
310
  res.fetchResponse?.status || 0,
311
311
  res.signature,
312
- count && `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
312
+ count &&
313
+ (retryStatus.retryAttempt || !retryStatus.retryStopped) &&
314
+ `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
313
315
  (0, time_util_1._since)(res.req.started),
314
316
  ]
315
317
  .filter(Boolean)
@@ -357,7 +359,7 @@ class Fetcher {
357
359
  * statusCode of 0 (or absense of it) will BE retried.
358
360
  */
359
361
  shouldRetry(res) {
360
- const { retryPost, retry4xx, retry5xx } = res.req;
362
+ const { retryPost, retry3xx, retry4xx, retry5xx } = res.req;
361
363
  const { method } = res.req.init;
362
364
  if (method === 'POST' && !retryPost)
363
365
  return false;
@@ -371,6 +373,11 @@ class Fetcher {
371
373
  }
372
374
  if (statusFamily === 4 && !retry4xx)
373
375
  return false;
376
+ if (statusFamily === 3 && !retry3xx)
377
+ return false;
378
+ // should not retry on `unexpected redirect` in error.cause.cause
379
+ if (res.err?.cause?.cause?.message?.includes('unexpected redirect'))
380
+ return false;
374
381
  return true; // default is true
375
382
  }
376
383
  getStatusFamily(res) {
@@ -420,6 +427,7 @@ class Fetcher {
420
427
  timeoutSeconds: 30,
421
428
  throwHttpErrors: true,
422
429
  retryPost: false,
430
+ retry3xx: false,
423
431
  retry4xx: false,
424
432
  retry5xx: true,
425
433
  // logger: console, Danger! doing this mutates console!
@@ -93,6 +93,7 @@ export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers
93
93
  timeoutSeconds: number;
94
94
  retry: FetcherRetryOptions;
95
95
  retryPost: boolean;
96
+ retry3xx: boolean;
96
97
  retry4xx: boolean;
97
98
  retry5xx: boolean;
98
99
  started: UnixTimestampMillisNumber;
@@ -141,6 +142,10 @@ export interface FetcherOptions {
141
142
  * Set to true to allow retrying `post` requests.
142
143
  */
143
144
  retryPost?: boolean;
145
+ /**
146
+ * Defaults to false.
147
+ */
148
+ retry3xx?: boolean;
144
149
  /**
145
150
  * Defaults to false.
146
151
  */
@@ -3,11 +3,16 @@ import { Reviver } from '../types';
3
3
  * Attempts to parse object as JSON.
4
4
  * Returns original object if JSON parse failed (silently).
5
5
  */
6
- export declare function _jsonParseIfPossible(obj: any, reviver?: Reviver): any;
6
+ export declare function _jsonParseIfPossible(obj: any, reviver?: Reviver): unknown;
7
+ /**
8
+ * Convenience function that does JSON.parse, but doesn't throw on error,
9
+ * instead - safely returns `undefined`.
10
+ */
11
+ export declare function _jsonParseOrUndefined<T = unknown>(obj: any, reviver?: Reviver): T | undefined;
7
12
  /**
8
13
  * Same as JSON.parse, but throws JsonParseError:
9
14
  *
10
15
  * 1. It's message includes a piece of source text (truncated)
11
16
  * 2. It's data.text contains full source text
12
17
  */
13
- export declare function _jsonParse(s: string, reviver?: Reviver): unknown;
18
+ export declare function _jsonParse<T = unknown>(s: string, reviver?: Reviver): T;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._jsonParse = exports._jsonParseIfPossible = void 0;
3
+ exports._jsonParse = exports._jsonParseOrUndefined = exports._jsonParseIfPossible = void 0;
4
4
  const jsonParseError_1 = require("../error/jsonParseError");
5
5
  // const possibleJsonStartTokens = ['{', '[', '"']
6
6
  const DETECT_JSON = /^\s*[{["\-\d]/;
@@ -19,6 +19,20 @@ function _jsonParseIfPossible(obj, reviver) {
19
19
  return obj;
20
20
  }
21
21
  exports._jsonParseIfPossible = _jsonParseIfPossible;
22
+ /**
23
+ * Convenience function that does JSON.parse, but doesn't throw on error,
24
+ * instead - safely returns `undefined`.
25
+ */
26
+ function _jsonParseOrUndefined(obj, reviver) {
27
+ // Optimization: only try to parse if it looks like JSON: starts with a json possible character
28
+ if (typeof obj === 'string' && obj && DETECT_JSON.test(obj)) {
29
+ try {
30
+ return JSON.parse(obj, reviver);
31
+ }
32
+ catch { }
33
+ }
34
+ }
35
+ exports._jsonParseOrUndefined = _jsonParseOrUndefined;
22
36
  /**
23
37
  * Same as JSON.parse, but throws JsonParseError:
24
38
  *
@@ -2,6 +2,6 @@ import { _truncateMiddle } from '../string/string.util';
2
2
  import { AppError } from './app.error';
3
3
  export class JsonParseError extends AppError {
4
4
  constructor(data, cause) {
5
- super(['Failed to parse', (data === null || data === void 0 ? void 0 : data.text) && _truncateMiddle(data.text, 200)].filter(Boolean).join(': '), data, cause, 'JsonParseError');
5
+ super(['Failed to parse', data.text && _truncateMiddle(data.text, 200)].filter(Boolean).join(': '), data, cause, 'JsonParseError');
6
6
  }
7
7
  }
@@ -217,7 +217,7 @@ export class Fetcher {
217
217
  }
218
218
  clearTimeout(timeout);
219
219
  res.retryStatus.retryStopped = true;
220
- // res.err can happen on JSON.parse error
220
+ // res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
221
221
  if (!res.err && this.cfg.logResponse) {
222
222
  const { retryAttempt } = res.retryStatus;
223
223
  const { logger } = this.cfg;
@@ -296,7 +296,9 @@ export class Fetcher {
296
296
  ' <<',
297
297
  ((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status) || 0,
298
298
  res.signature,
299
- count && `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
299
+ count &&
300
+ (retryStatus.retryAttempt || !retryStatus.retryStopped) &&
301
+ `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
300
302
  _since(res.req.started),
301
303
  ]
302
304
  .filter(Boolean)
@@ -344,8 +346,8 @@ export class Fetcher {
344
346
  * statusCode of 0 (or absense of it) will BE retried.
345
347
  */
346
348
  shouldRetry(res) {
347
- var _a;
348
- const { retryPost, retry4xx, retry5xx } = res.req;
349
+ var _a, _b, _c, _d, _e;
350
+ const { retryPost, retry3xx, retry4xx, retry5xx } = res.req;
349
351
  const { method } = res.req.init;
350
352
  if (method === 'POST' && !retryPost)
351
353
  return false;
@@ -359,6 +361,11 @@ export class Fetcher {
359
361
  }
360
362
  if (statusFamily === 4 && !retry4xx)
361
363
  return false;
364
+ if (statusFamily === 3 && !retry3xx)
365
+ return false;
366
+ // should not retry on `unexpected redirect` in error.cause.cause
367
+ if ((_e = (_d = (_c = (_b = res.err) === null || _b === void 0 ? void 0 : _b.cause) === null || _c === void 0 ? void 0 : _c.cause) === null || _d === void 0 ? void 0 : _d.message) === null || _e === void 0 ? void 0 : _e.includes('unexpected redirect'))
368
+ return false;
362
369
  return true; // default is true
363
370
  }
364
371
  getStatusFamily(res) {
@@ -410,6 +417,7 @@ export class Fetcher {
410
417
  timeoutSeconds: 30,
411
418
  throwHttpErrors: true,
412
419
  retryPost: false,
420
+ retry3xx: false,
413
421
  retry4xx: false,
414
422
  retry5xx: true,
415
423
  // logger: console, Danger! doing this mutates console!
@@ -15,6 +15,19 @@ export function _jsonParseIfPossible(obj, reviver) {
15
15
  }
16
16
  return obj;
17
17
  }
18
+ /**
19
+ * Convenience function that does JSON.parse, but doesn't throw on error,
20
+ * instead - safely returns `undefined`.
21
+ */
22
+ export function _jsonParseOrUndefined(obj, reviver) {
23
+ // Optimization: only try to parse if it looks like JSON: starts with a json possible character
24
+ if (typeof obj === 'string' && obj && DETECT_JSON.test(obj)) {
25
+ try {
26
+ return JSON.parse(obj, reviver);
27
+ }
28
+ catch (_a) { }
29
+ }
30
+ }
18
31
  /**
19
32
  * Same as JSON.parse, but throws JsonParseError:
20
33
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.151.0",
3
+ "version": "14.153.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -12,7 +12,7 @@ export interface JsonParseErrorData extends ErrorData {
12
12
  export class JsonParseError extends AppError<JsonParseErrorData> {
13
13
  constructor(data: JsonParseErrorData, cause?: ErrorObject) {
14
14
  super(
15
- ['Failed to parse', data?.text && _truncateMiddle(data.text, 200)].filter(Boolean).join(': '),
15
+ ['Failed to parse', data.text && _truncateMiddle(data.text, 200)].filter(Boolean).join(': '),
16
16
  data,
17
17
  cause,
18
18
  'JsonParseError',
@@ -114,6 +114,7 @@ export interface FetcherRequest
114
114
  timeoutSeconds: number
115
115
  retry: FetcherRetryOptions
116
116
  retryPost: boolean
117
+ retry3xx: boolean
117
118
  retry4xx: boolean
118
119
  retry5xx: boolean
119
120
  started: UnixTimestampMillisNumber
@@ -176,6 +177,10 @@ export interface FetcherOptions {
176
177
  * Set to true to allow retrying `post` requests.
177
178
  */
178
179
  retryPost?: boolean
180
+ /**
181
+ * Defaults to false.
182
+ */
183
+ retry3xx?: boolean
179
184
  /**
180
185
  * Defaults to false.
181
186
  */
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom"/>
2
2
 
3
3
  import { isServerSide } from '../env'
4
- import { ErrorObject } from '../error/error.model'
4
+ import { ErrorLike, ErrorObject } from '../error/error.model'
5
5
  import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject } from '../error/error.util'
6
6
  import { HttpRequestError } from '../error/httpRequestError'
7
7
  import { _clamp } from '../number/number.util'
@@ -302,7 +302,7 @@ export class Fetcher {
302
302
  clearTimeout(timeout)
303
303
  res.retryStatus.retryStopped = true
304
304
 
305
- // res.err can happen on JSON.parse error
305
+ // res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
306
306
  if (!res.err && this.cfg.logResponse) {
307
307
  const { retryAttempt } = res.retryStatus
308
308
  const { logger } = this.cfg
@@ -399,7 +399,9 @@ export class Fetcher {
399
399
  ' <<',
400
400
  res.fetchResponse?.status || 0,
401
401
  res.signature,
402
- count && `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
402
+ count &&
403
+ (retryStatus.retryAttempt || !retryStatus.retryStopped) &&
404
+ `try#${retryStatus.retryAttempt + 1}/${count + 1}`,
403
405
  _since(res.req.started),
404
406
  ]
405
407
  .filter(Boolean)
@@ -457,7 +459,7 @@ export class Fetcher {
457
459
  * statusCode of 0 (or absense of it) will BE retried.
458
460
  */
459
461
  private shouldRetry(res: FetcherResponse): boolean {
460
- const { retryPost, retry4xx, retry5xx } = res.req
462
+ const { retryPost, retry3xx, retry4xx, retry5xx } = res.req
461
463
  const { method } = res.req.init
462
464
  if (method === 'POST' && !retryPost) return false
463
465
  const { statusFamily } = res
@@ -468,6 +470,12 @@ export class Fetcher {
468
470
  return true
469
471
  }
470
472
  if (statusFamily === 4 && !retry4xx) return false
473
+ if (statusFamily === 3 && !retry3xx) return false
474
+
475
+ // should not retry on `unexpected redirect` in error.cause.cause
476
+ if ((res.err?.cause as ErrorLike | void)?.cause?.message?.includes('unexpected redirect'))
477
+ return false
478
+
471
479
  return true // default is true
472
480
  }
473
481
 
@@ -521,6 +529,7 @@ export class Fetcher {
521
529
  timeoutSeconds: 30,
522
530
  throwHttpErrors: true,
523
531
  retryPost: false,
532
+ retry3xx: false,
524
533
  retry4xx: false,
525
534
  retry5xx: true,
526
535
  // logger: console, Danger! doing this mutates console!
@@ -8,7 +8,7 @@ const DETECT_JSON = /^\s*[{["\-\d]/
8
8
  * Attempts to parse object as JSON.
9
9
  * Returns original object if JSON parse failed (silently).
10
10
  */
11
- export function _jsonParseIfPossible(obj: any, reviver?: Reviver): any {
11
+ export function _jsonParseIfPossible(obj: any, reviver?: Reviver): unknown {
12
12
  // Optimization: only try to parse if it looks like JSON: starts with a json possible character
13
13
  if (typeof obj === 'string' && obj && DETECT_JSON.test(obj)) {
14
14
  try {
@@ -19,13 +19,26 @@ export function _jsonParseIfPossible(obj: any, reviver?: Reviver): any {
19
19
  return obj
20
20
  }
21
21
 
22
+ /**
23
+ * Convenience function that does JSON.parse, but doesn't throw on error,
24
+ * instead - safely returns `undefined`.
25
+ */
26
+ export function _jsonParseOrUndefined<T = unknown>(obj: any, reviver?: Reviver): T | undefined {
27
+ // Optimization: only try to parse if it looks like JSON: starts with a json possible character
28
+ if (typeof obj === 'string' && obj && DETECT_JSON.test(obj)) {
29
+ try {
30
+ return JSON.parse(obj, reviver)
31
+ } catch {}
32
+ }
33
+ }
34
+
22
35
  /**
23
36
  * Same as JSON.parse, but throws JsonParseError:
24
37
  *
25
38
  * 1. It's message includes a piece of source text (truncated)
26
39
  * 2. It's data.text contains full source text
27
40
  */
28
- export function _jsonParse(s: string, reviver?: Reviver): unknown {
41
+ export function _jsonParse<T = unknown>(s: string, reviver?: Reviver): T {
29
42
  try {
30
43
  return JSON.parse(s, reviver)
31
44
  } catch {