@naturalcycles/js-lib 14.170.0 → 14.172.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.
@@ -17,6 +17,14 @@ export interface ErrorData {
17
17
  * Error id in some error tracking system (e.g Sentry).
18
18
  */
19
19
  errorId?: string;
20
+ /**
21
+ * If set - provides a short semi-user-friendly error message snippet,
22
+ * that would allow to give a hint to the user what went wrong,
23
+ * also to developers and CS to distinguish between different errors.
24
+ *
25
+ * It's not supposed to have full information about the error, just a small extract from it.
26
+ */
27
+ snippet?: string;
20
28
  /**
21
29
  * Set to true to force reporting this error (e.g to Sentry).
22
30
  * Useful to be able to force-report e.g a 4xx error, which by default wouldn't be reported.
@@ -35,11 +43,6 @@ export interface ErrorData {
35
43
  * E.g 0.1 will report 10% of errors (and ignore the 90%)
36
44
  */
37
45
  reportRate?: number;
38
- /**
39
- * Sometimes error.message gets "decorated" with extra information
40
- * (e.g frontend-lib adds a method, url, etc for all the errors)
41
- * `originalMessage` is used to preserve the original `error.message` as it came from the backend.
42
- */
43
46
  /**
44
47
  * Can be used by error-reporting tools (e.g Sentry).
45
48
  * If fingerprint is defined - it'll be used INSTEAD of default fingerprint of a tool.
@@ -18,6 +18,22 @@ export declare function _anyToError<ERROR_TYPE extends Error = Error>(o: any, er
18
18
  export declare function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(o: any, errorData?: Partial<DATA_TYPE>): ErrorObject<DATA_TYPE>;
19
19
  export declare function _errorLikeToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(e: AppError<DATA_TYPE> | Error | ErrorLike): ErrorObject<DATA_TYPE>;
20
20
  export declare function _errorObjectToError<DATA_TYPE extends ErrorData, ERROR_TYPE extends Error>(o: ErrorObject<DATA_TYPE>, errorClass?: Class<ERROR_TYPE>): ERROR_TYPE;
21
+ export interface ErrorSnippetOptions {
22
+ /**
23
+ * Max length of the error line.
24
+ * Snippet may have multiple lines, one original and one per `cause`.
25
+ */
26
+ maxLineLength?: number;
27
+ maxLines?: number;
28
+ }
29
+ /**
30
+ * Provides a short semi-user-friendly error message snippet,
31
+ * that would allow to give a hint to the user what went wrong,
32
+ * also to developers and CS to distinguish between different errors.
33
+ *
34
+ * It's not supposed to have full information about the error, just a small extract from it.
35
+ */
36
+ export declare function _errorSnippet(err: any, opt?: ErrorSnippetOptions): string;
21
37
  export declare function _isBackendErrorResponseObject(o: any): o is BackendErrorResponseObject;
22
38
  export declare function _isHttpRequestErrorObject(o: any): o is ErrorObject<HttpRequestErrorData>;
23
39
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._errorDataAppend = exports._isErrorLike = exports._isErrorObject = exports._isHttpRequestErrorObject = exports._isBackendErrorResponseObject = exports._errorObjectToError = exports._errorLikeToErrorObject = exports._anyToErrorObject = exports._anyToError = void 0;
3
+ exports._errorDataAppend = exports._isErrorLike = exports._isErrorObject = exports._isHttpRequestErrorObject = exports._isBackendErrorResponseObject = exports._errorSnippet = exports._errorObjectToError = exports._errorLikeToErrorObject = exports._anyToErrorObject = exports._anyToError = void 0;
4
4
  const __1 = require("..");
5
5
  /**
6
6
  * Useful to ensure that error in `catch (err) { ... }`
@@ -38,7 +38,6 @@ exports._anyToError = _anyToError;
38
38
  */
39
39
  function _anyToErrorObject(o, errorData) {
40
40
  let eo;
41
- // if (o instanceof Error) {
42
41
  if (_isErrorLike(o)) {
43
42
  eo = _errorLikeToErrorObject(o);
44
43
  }
@@ -71,6 +70,14 @@ function _anyToErrorObject(o, errorData) {
71
70
  }
72
71
  exports._anyToErrorObject = _anyToErrorObject;
73
72
  function _errorLikeToErrorObject(e) {
73
+ // If it's already an ErrorObject - just return it
74
+ // AppError satisfies ErrorObject interface
75
+ // Error does not satisfy (lacks `data`)
76
+ // UPD: no, we expect a "plain object" here as an output,
77
+ // because Error classes sometimes have non-enumerable properties (e.g data)
78
+ if (!(e instanceof Error) && _isErrorObject(e)) {
79
+ return e;
80
+ }
74
81
  const obj = {
75
82
  name: e.name,
76
83
  message: e.message,
@@ -129,6 +136,48 @@ function _errorObjectToError(o, errorClass = Error) {
129
136
  return err;
130
137
  }
131
138
  exports._errorObjectToError = _errorObjectToError;
139
+ // These "common" error classes will not be printed as part of the Error snippet
140
+ const commonErrorClasses = new Set([
141
+ 'Error',
142
+ 'AppError',
143
+ 'AssertionError',
144
+ 'HttpRequestError',
145
+ 'JoiValidationError',
146
+ ]);
147
+ /**
148
+ * Provides a short semi-user-friendly error message snippet,
149
+ * that would allow to give a hint to the user what went wrong,
150
+ * also to developers and CS to distinguish between different errors.
151
+ *
152
+ * It's not supposed to have full information about the error, just a small extract from it.
153
+ */
154
+ function _errorSnippet(err, opt = {}) {
155
+ const { maxLineLength = 60, maxLines = 3 } = opt;
156
+ const e = _anyToErrorObject(err);
157
+ const lines = [errorObjectToSnippet(e)];
158
+ let { cause } = e;
159
+ while (cause && lines.length < maxLines) {
160
+ lines.push('Caused by ' + errorObjectToSnippet(cause));
161
+ cause = cause.cause; // insert DiCaprio Inception meme
162
+ }
163
+ return lines.map(line => (0, __1._truncate)(line, maxLineLength)).join('\n');
164
+ function errorObjectToSnippet(e) {
165
+ // Return snippet if it was already prepared
166
+ if (e.data.snippet)
167
+ return e.data.snippet;
168
+ // Code already serves the purpose of the snippet, so we can just return it
169
+ if (e.data.code)
170
+ return e.data.code;
171
+ return [
172
+ !commonErrorClasses.has(e.name) && e.name,
173
+ // replace "1+ white space characters" with a single space
174
+ e.message.replaceAll(/\s+/gm, ' ').trim(),
175
+ ]
176
+ .filter(Boolean)
177
+ .join(': ');
178
+ }
179
+ }
180
+ exports._errorSnippet = _errorSnippet;
132
181
  function _isBackendErrorResponseObject(o) {
133
182
  return _isErrorObject(o?.error);
134
183
  }
@@ -8,6 +8,14 @@ import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeR
8
8
  * Works in both Browser and Node, using `globalThis.fetch`.
9
9
  */
10
10
  export declare class Fetcher {
11
+ /**
12
+ * Included in UserAgent when run in Node.
13
+ * In the browser it's not included, as we want "browser own" UserAgent to be included instead.
14
+ *
15
+ * Version is to be incremented every time a difference in behaviour (or a bugfix) is done.
16
+ */
17
+ static readonly VERSION = 1;
18
+ static readonly userAgent: string | undefined;
11
19
  private constructor();
12
20
  /**
13
21
  * Add BeforeRequest hook at the end of the hooks list.
@@ -34,6 +34,14 @@ const defRetryOptions = {
34
34
  * Works in both Browser and Node, using `globalThis.fetch`.
35
35
  */
36
36
  class Fetcher {
37
+ /**
38
+ * Included in UserAgent when run in Node.
39
+ * In the browser it's not included, as we want "browser own" UserAgent to be included instead.
40
+ *
41
+ * Version is to be incremented every time a difference in behaviour (or a bugfix) is done.
42
+ */
43
+ static { this.VERSION = 1; }
44
+ static { this.userAgent = (0, env_1.isServerSide)() ? `fetcher${this.VERSION}` : undefined; }
37
45
  constructor(cfg = {}) {
38
46
  if (typeof globalThis.fetch !== 'function') {
39
47
  throw new TypeError(`globalThis.fetch is not available`);
@@ -487,10 +495,10 @@ class Fetcher {
487
495
  retry: { ...defRetryOptions },
488
496
  init: {
489
497
  method: cfg.method || 'GET',
490
- headers: {
491
- 'user-agent': 'fetcher',
498
+ headers: (0, object_util_1._filterNullishValues)({
499
+ 'user-agent': Fetcher.userAgent,
492
500
  ...cfg.headers,
493
- },
501
+ }),
494
502
  credentials: cfg.credentials,
495
503
  redirect: cfg.redirect,
496
504
  },
@@ -532,6 +540,8 @@ class Fetcher {
532
540
  headers: (0, object_util_1._mapKeys)(opt.headers || {}, k => k.toLowerCase()),
533
541
  }),
534
542
  };
543
+ // Because all header values are stringified, so `a: undefined` becomes `undefined` as a string
544
+ (0, object_util_1._filterNullishValues)(req.init.headers, true);
535
545
  // setup url
536
546
  const baseUrl = opt.baseUrl || this.cfg.baseUrl;
537
547
  if (baseUrl) {
package/dist/index.d.ts CHANGED
@@ -86,6 +86,5 @@ export * from './web';
86
86
  export * from './zod/zod.util';
87
87
  export * from './zod/zod.shared.schemas';
88
88
  import { z, ZodSchema, ZodError, ZodIssue } from 'zod';
89
- import { is } from './vendor/is';
90
- export { is, z, ZodSchema, ZodError };
89
+ export { z, ZodSchema, ZodError };
91
90
  export type { ZodIssue };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ZodError = exports.ZodSchema = exports.z = exports.is = void 0;
3
+ exports.ZodError = exports.ZodSchema = exports.z = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  tslib_1.__exportStar(require("./array/array.util"), exports);
6
6
  tslib_1.__exportStar(require("./define"), exports);
@@ -93,5 +93,3 @@ const zod_1 = require("zod");
93
93
  Object.defineProperty(exports, "z", { enumerable: true, get: function () { return zod_1.z; } });
94
94
  Object.defineProperty(exports, "ZodSchema", { enumerable: true, get: function () { return zod_1.ZodSchema; } });
95
95
  Object.defineProperty(exports, "ZodError", { enumerable: true, get: function () { return zod_1.ZodError; } });
96
- const is_1 = require("./vendor/is");
97
- Object.defineProperty(exports, "is", { enumerable: true, get: function () { return is_1.is; } });
@@ -1,4 +1,4 @@
1
- import { AppError, _jsonParseIfPossible, _stringifyAny } from '..';
1
+ import { AppError, _jsonParseIfPossible, _stringifyAny, _truncate } from '..';
2
2
  /**
3
3
  * Useful to ensure that error in `catch (err) { ... }`
4
4
  * is indeed an Error (and not e.g `string` or `undefined`).
@@ -31,7 +31,6 @@ export function _anyToError(o, errorClass = Error, errorData) {
31
31
  */
32
32
  export function _anyToErrorObject(o, errorData) {
33
33
  let eo;
34
- // if (o instanceof Error) {
35
34
  if (_isErrorLike(o)) {
36
35
  eo = _errorLikeToErrorObject(o);
37
36
  }
@@ -63,6 +62,14 @@ export function _anyToErrorObject(o, errorData) {
63
62
  return eo;
64
63
  }
65
64
  export function _errorLikeToErrorObject(e) {
65
+ // If it's already an ErrorObject - just return it
66
+ // AppError satisfies ErrorObject interface
67
+ // Error does not satisfy (lacks `data`)
68
+ // UPD: no, we expect a "plain object" here as an output,
69
+ // because Error classes sometimes have non-enumerable properties (e.g data)
70
+ if (!(e instanceof Error) && _isErrorObject(e)) {
71
+ return e;
72
+ }
66
73
  const obj = {
67
74
  name: e.name,
68
75
  message: e.message,
@@ -119,6 +126,47 @@ export function _errorObjectToError(o, errorClass = Error) {
119
126
  }
120
127
  return err;
121
128
  }
129
+ // These "common" error classes will not be printed as part of the Error snippet
130
+ const commonErrorClasses = new Set([
131
+ 'Error',
132
+ 'AppError',
133
+ 'AssertionError',
134
+ 'HttpRequestError',
135
+ 'JoiValidationError',
136
+ ]);
137
+ /**
138
+ * Provides a short semi-user-friendly error message snippet,
139
+ * that would allow to give a hint to the user what went wrong,
140
+ * also to developers and CS to distinguish between different errors.
141
+ *
142
+ * It's not supposed to have full information about the error, just a small extract from it.
143
+ */
144
+ export function _errorSnippet(err, opt = {}) {
145
+ const { maxLineLength = 60, maxLines = 3 } = opt;
146
+ const e = _anyToErrorObject(err);
147
+ const lines = [errorObjectToSnippet(e)];
148
+ let { cause } = e;
149
+ while (cause && lines.length < maxLines) {
150
+ lines.push('Caused by ' + errorObjectToSnippet(cause));
151
+ cause = cause.cause; // insert DiCaprio Inception meme
152
+ }
153
+ return lines.map(line => _truncate(line, maxLineLength)).join('\n');
154
+ function errorObjectToSnippet(e) {
155
+ // Return snippet if it was already prepared
156
+ if (e.data.snippet)
157
+ return e.data.snippet;
158
+ // Code already serves the purpose of the snippet, so we can just return it
159
+ if (e.data.code)
160
+ return e.data.code;
161
+ return [
162
+ !commonErrorClasses.has(e.name) && e.name,
163
+ // replace "1+ white space characters" with a single space
164
+ e.message.replaceAll(/\s+/gm, ' ').trim(),
165
+ ]
166
+ .filter(Boolean)
167
+ .join(': ');
168
+ }
169
+ }
122
170
  export function _isBackendErrorResponseObject(o) {
123
171
  return _isErrorObject(o === null || o === void 0 ? void 0 : o.error);
124
172
  }
@@ -1,5 +1,6 @@
1
1
  /// <reference lib="dom"/>
2
2
  /// <reference lib="dom.iterable"/>
3
+ var _a;
3
4
  import { isServerSide } from '../env';
4
5
  import { _assertErrorClassOrRethrow } from '../error/assert';
5
6
  import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject } from '../error/error.util';
@@ -60,21 +61,21 @@ export class Fetcher {
60
61
  * Add BeforeRequest hook at the end of the hooks list.
61
62
  */
62
63
  onBeforeRequest(hook) {
63
- var _a;
64
+ var _b;
64
65
  ;
65
- ((_a = this.cfg.hooks).beforeRequest || (_a.beforeRequest = [])).push(hook);
66
+ ((_b = this.cfg.hooks).beforeRequest || (_b.beforeRequest = [])).push(hook);
66
67
  return this;
67
68
  }
68
69
  onAfterResponse(hook) {
69
- var _a;
70
+ var _b;
70
71
  ;
71
- ((_a = this.cfg.hooks).afterResponse || (_a.afterResponse = [])).push(hook);
72
+ ((_b = this.cfg.hooks).afterResponse || (_b.afterResponse = [])).push(hook);
72
73
  return this;
73
74
  }
74
75
  onBeforeRetry(hook) {
75
- var _a;
76
+ var _b;
76
77
  ;
77
- ((_a = this.cfg.hooks).beforeRetry || (_a.beforeRetry = [])).push(hook);
78
+ ((_b = this.cfg.hooks).beforeRetry || (_b.beforeRetry = [])).push(hook);
78
79
  return this;
79
80
  }
80
81
  static create(cfg = {}) {
@@ -120,7 +121,7 @@ export class Fetcher {
120
121
  * Note: responseType defaults to `void`, so, override it if you expect different.
121
122
  */
122
123
  async doFetch(opt) {
123
- var _a, _b;
124
+ var _b, _c;
124
125
  const req = this.normalizeOptions(opt);
125
126
  const { logger } = this.cfg;
126
127
  const { timeoutSeconds, init: { method }, } = req;
@@ -182,8 +183,8 @@ export class Fetcher {
182
183
  // Separate Timeout will be introduced to "download and parse the body"
183
184
  }
184
185
  res.statusFamily = this.getStatusFamily(res);
185
- res.statusCode = (_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status;
186
- if ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.ok) {
186
+ res.statusCode = (_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status;
187
+ if ((_c = res.fetchResponse) === null || _c === void 0 ? void 0 : _c.ok) {
187
188
  try {
188
189
  // We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
189
190
  await pTimeout(async () => await this.onOkResponse(res), {
@@ -274,7 +275,7 @@ export class Fetcher {
274
275
  return await globalThis.fetch(url, init);
275
276
  }
276
277
  async onNotOkResponse(res) {
277
- var _a, _b;
278
+ var _b, _c;
278
279
  let cause;
279
280
  if (res.err) {
280
281
  // This is only possible on JSON.parse error, or CORS error,
@@ -293,10 +294,10 @@ export class Fetcher {
293
294
  message: 'Fetch failed',
294
295
  data: {},
295
296
  });
296
- const message = [(_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status, res.signature].filter(Boolean).join(' ');
297
+ const message = [(_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status, res.signature].filter(Boolean).join(' ');
297
298
  res.err = new HttpRequestError(message, _filterNullishValues({
298
299
  response: res.fetchResponse,
299
- responseStatusCode: ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
300
+ responseStatusCode: ((_c = res.fetchResponse) === null || _c === void 0 ? void 0 : _c.status) || 0,
300
301
  // These properties are provided to be used in e.g custom Sentry error grouping
301
302
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
302
303
  // Enabled, cause `data` is not printed by default when error is HttpError
@@ -313,7 +314,7 @@ export class Fetcher {
313
314
  await this.processRetry(res);
314
315
  }
315
316
  async processRetry(res) {
316
- var _a;
317
+ var _b;
317
318
  const { retryStatus } = res;
318
319
  if (!this.shouldRetry(res)) {
319
320
  retryStatus.retryStopped = true;
@@ -333,7 +334,7 @@ export class Fetcher {
333
334
  if (res.err && (!retryStatus.retryStopped || res.req.logResponse)) {
334
335
  this.cfg.logger.error([
335
336
  ' <<',
336
- ((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status) || 0,
337
+ ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
337
338
  res.signature,
338
339
  count &&
339
340
  (retryStatus.retryAttempt || !retryStatus.retryStopped) &&
@@ -356,12 +357,12 @@ export class Fetcher {
356
357
  await pDelay(timeout);
357
358
  }
358
359
  getRetryTimeout(res) {
359
- var _a;
360
+ var _b;
360
361
  let timeout = 0;
361
362
  // Handling http 429 with specific retry headers
362
363
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
363
364
  if (res.fetchResponse && [429, 503].includes(res.fetchResponse.status)) {
364
- const retryAfterStr = (_a = res.fetchResponse.headers.get('retry-after')) !== null && _a !== void 0 ? _a : res.fetchResponse.headers.get('x-ratelimit-reset');
365
+ const retryAfterStr = (_b = res.fetchResponse.headers.get('retry-after')) !== null && _b !== void 0 ? _b : res.fetchResponse.headers.get('x-ratelimit-reset');
365
366
  if (retryAfterStr) {
366
367
  if (Number(retryAfterStr)) {
367
368
  timeout = Number(retryAfterStr) * 1000;
@@ -391,13 +392,13 @@ export class Fetcher {
391
392
  * statusCode of 0 (or absense of it) will BE retried.
392
393
  */
393
394
  shouldRetry(res) {
394
- var _a, _b, _c, _d, _e;
395
+ var _b, _c, _d, _e, _f;
395
396
  const { retryPost, retry3xx, retry4xx, retry5xx } = res.req;
396
397
  const { method } = res.req.init;
397
398
  if (method === 'POST' && !retryPost)
398
399
  return false;
399
400
  const { statusFamily } = res;
400
- const statusCode = ((_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status) || 0;
401
+ const statusCode = ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0;
401
402
  if (statusFamily === 5 && !retry5xx)
402
403
  return false;
403
404
  if ([408, 429].includes(statusCode)) {
@@ -409,14 +410,14 @@ export class Fetcher {
409
410
  if (statusFamily === 3 && !retry3xx)
410
411
  return false;
411
412
  // should not retry on `unexpected redirect` in error.cause.cause
412
- 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')) {
413
+ if ((_f = (_e = (_d = (_c = res.err) === null || _c === void 0 ? void 0 : _c.cause) === null || _d === void 0 ? void 0 : _d.cause) === null || _e === void 0 ? void 0 : _e.message) === null || _f === void 0 ? void 0 : _f.includes('unexpected redirect')) {
413
414
  return false;
414
415
  }
415
416
  return true; // default is true
416
417
  }
417
418
  getStatusFamily(res) {
418
- var _a;
419
- const status = (_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status;
419
+ var _b;
420
+ const status = (_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status;
420
421
  if (!status)
421
422
  return;
422
423
  if (status >= 500)
@@ -449,8 +450,8 @@ export class Fetcher {
449
450
  return shortUrl;
450
451
  }
451
452
  normalizeCfg(cfg) {
452
- var _a;
453
- if ((_a = cfg.baseUrl) === null || _a === void 0 ? void 0 : _a.endsWith('/')) {
453
+ var _b;
454
+ if ((_b = cfg.baseUrl) === null || _b === void 0 ? void 0 : _b.endsWith('/')) {
454
455
  console.warn(`Fetcher: baseUrl should not end with slash: ${cfg.baseUrl}`);
455
456
  cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
456
457
  }
@@ -477,7 +478,7 @@ export class Fetcher {
477
478
  retry: Object.assign({}, defRetryOptions),
478
479
  init: {
479
480
  method: cfg.method || 'GET',
480
- headers: Object.assign({ 'user-agent': 'fetcher' }, cfg.headers),
481
+ headers: _filterNullishValues(Object.assign({ 'user-agent': Fetcher.userAgent }, cfg.headers)),
481
482
  credentials: cfg.credentials,
482
483
  redirect: cfg.redirect,
483
484
  },
@@ -487,7 +488,7 @@ export class Fetcher {
487
488
  return norm;
488
489
  }
489
490
  normalizeOptions(opt) {
490
- var _a;
491
+ var _b;
491
492
  const req = Object.assign(Object.assign(Object.assign(Object.assign({}, _pick(this.cfg, [
492
493
  'timeoutSeconds',
493
494
  'retryPost',
@@ -503,6 +504,8 @@ export class Fetcher {
503
504
  ])), { started: Date.now() }), _omit(opt, ['method', 'headers', 'credentials'])), { inputUrl: opt.url || '', fullUrl: opt.url || '', retry: Object.assign(Object.assign({}, this.cfg.retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign(Object.assign({}, this.cfg.init), { headers: Object.assign({}, this.cfg.init.headers), method: opt.method || this.cfg.init.method, credentials: opt.credentials || this.cfg.init.credentials, redirect: opt.redirect || this.cfg.init.redirect || 'follow' }), {
504
505
  headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
505
506
  }) });
507
+ // Because all header values are stringified, so `a: undefined` becomes `undefined` as a string
508
+ _filterNullishValues(req.init.headers, true);
506
509
  // setup url
507
510
  const baseUrl = opt.baseUrl || this.cfg.baseUrl;
508
511
  if (baseUrl) {
@@ -540,10 +543,19 @@ export class Fetcher {
540
543
  req.init.body = opt.body;
541
544
  }
542
545
  // Unless `accept` header was already set - set it based on responseType
543
- (_a = req.init.headers)['accept'] || (_a['accept'] = acceptByResponseType[req.responseType]);
546
+ (_b = req.init.headers)['accept'] || (_b['accept'] = acceptByResponseType[req.responseType]);
544
547
  return req;
545
548
  }
546
549
  }
550
+ _a = Fetcher;
551
+ /**
552
+ * Included in UserAgent when run in Node.
553
+ * In the browser it's not included, as we want "browser own" UserAgent to be included instead.
554
+ *
555
+ * Version is to be incremented every time a difference in behaviour (or a bugfix) is done.
556
+ */
557
+ Fetcher.VERSION = 1;
558
+ Fetcher.userAgent = isServerSide() ? `fetcher${_a.VERSION}` : undefined;
547
559
  export function getFetcher(cfg = {}) {
548
560
  return Fetcher.create(cfg);
549
561
  }
package/dist-esm/index.js CHANGED
@@ -86,5 +86,4 @@ export * from './web';
86
86
  export * from './zod/zod.util';
87
87
  export * from './zod/zod.shared.schemas';
88
88
  import { z, ZodSchema, ZodError } from 'zod';
89
- import { is } from './vendor/is';
90
- export { is, z, ZodSchema, ZodError };
89
+ export { z, ZodSchema, ZodError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.170.0",
3
+ "version": "14.172.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -14,7 +14,7 @@
14
14
  "devDependencies": {
15
15
  "@naturalcycles/bench-lib": "^1.5.0",
16
16
  "@naturalcycles/dev-lib": "^13.0.1",
17
- "@naturalcycles/nodejs-lib": "^12.33.4",
17
+ "@naturalcycles/nodejs-lib": "^13.0.1",
18
18
  "@naturalcycles/time-lib": "^3.5.1",
19
19
  "@types/crypto-js": "^4.1.1",
20
20
  "@types/node": "^20.1.0",
@@ -21,6 +21,15 @@ export interface ErrorData {
21
21
  */
22
22
  errorId?: string
23
23
 
24
+ /**
25
+ * If set - provides a short semi-user-friendly error message snippet,
26
+ * that would allow to give a hint to the user what went wrong,
27
+ * also to developers and CS to distinguish between different errors.
28
+ *
29
+ * It's not supposed to have full information about the error, just a small extract from it.
30
+ */
31
+ snippet?: string
32
+
24
33
  /**
25
34
  * Set to true to force reporting this error (e.g to Sentry).
26
35
  * Useful to be able to force-report e.g a 4xx error, which by default wouldn't be reported.
@@ -42,14 +51,6 @@ export interface ErrorData {
42
51
  */
43
52
  reportRate?: number
44
53
 
45
- /**
46
- * Sometimes error.message gets "decorated" with extra information
47
- * (e.g frontend-lib adds a method, url, etc for all the errors)
48
- * `originalMessage` is used to preserve the original `error.message` as it came from the backend.
49
- */
50
- // originalMessage?: string
51
- // use .cause.message instead
52
-
53
54
  /**
54
55
  * Can be used by error-reporting tools (e.g Sentry).
55
56
  * If fingerprint is defined - it'll be used INSTEAD of default fingerprint of a tool.
@@ -6,7 +6,7 @@ import type {
6
6
  HttpRequestErrorData,
7
7
  ErrorLike,
8
8
  } from '..'
9
- import { AppError, _jsonParseIfPossible, _stringifyAny } from '..'
9
+ import { AppError, _jsonParseIfPossible, _stringifyAny, _truncate } from '..'
10
10
 
11
11
  /**
12
12
  * Useful to ensure that error in `catch (err) { ... }`
@@ -54,7 +54,6 @@ export function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
54
54
  ): ErrorObject<DATA_TYPE> {
55
55
  let eo: ErrorObject<DATA_TYPE>
56
56
 
57
- // if (o instanceof Error) {
58
57
  if (_isErrorLike(o)) {
59
58
  eo = _errorLikeToErrorObject(o)
60
59
  } else {
@@ -88,6 +87,15 @@ export function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
88
87
  export function _errorLikeToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
89
88
  e: AppError<DATA_TYPE> | Error | ErrorLike,
90
89
  ): ErrorObject<DATA_TYPE> {
90
+ // If it's already an ErrorObject - just return it
91
+ // AppError satisfies ErrorObject interface
92
+ // Error does not satisfy (lacks `data`)
93
+ // UPD: no, we expect a "plain object" here as an output,
94
+ // because Error classes sometimes have non-enumerable properties (e.g data)
95
+ if (!(e instanceof Error) && _isErrorObject(e)) {
96
+ return e as ErrorObject<DATA_TYPE>
97
+ }
98
+
91
99
  const obj: ErrorObject<DATA_TYPE> = {
92
100
  name: e.name,
93
101
  message: e.message,
@@ -156,6 +164,64 @@ export function _errorObjectToError<DATA_TYPE extends ErrorData, ERROR_TYPE exte
156
164
  return err
157
165
  }
158
166
 
167
+ export interface ErrorSnippetOptions {
168
+ /**
169
+ * Max length of the error line.
170
+ * Snippet may have multiple lines, one original and one per `cause`.
171
+ */
172
+ maxLineLength?: number
173
+
174
+ maxLines?: number
175
+ }
176
+
177
+ // These "common" error classes will not be printed as part of the Error snippet
178
+ const commonErrorClasses = new Set([
179
+ 'Error',
180
+ 'AppError',
181
+ 'AssertionError',
182
+ 'HttpRequestError',
183
+ 'JoiValidationError',
184
+ ])
185
+
186
+ /**
187
+ * Provides a short semi-user-friendly error message snippet,
188
+ * that would allow to give a hint to the user what went wrong,
189
+ * also to developers and CS to distinguish between different errors.
190
+ *
191
+ * It's not supposed to have full information about the error, just a small extract from it.
192
+ */
193
+ export function _errorSnippet(err: any, opt: ErrorSnippetOptions = {}): string {
194
+ const { maxLineLength = 60, maxLines = 3 } = opt
195
+ const e = _anyToErrorObject(err)
196
+
197
+ const lines = [errorObjectToSnippet(e)]
198
+
199
+ let { cause } = e
200
+
201
+ while (cause && lines.length < maxLines) {
202
+ lines.push('Caused by ' + errorObjectToSnippet(cause))
203
+ cause = cause.cause // insert DiCaprio Inception meme
204
+ }
205
+
206
+ return lines.map(line => _truncate(line, maxLineLength)).join('\n')
207
+
208
+ function errorObjectToSnippet(e: ErrorObject): string {
209
+ // Return snippet if it was already prepared
210
+ if (e.data.snippet) return e.data.snippet
211
+
212
+ // Code already serves the purpose of the snippet, so we can just return it
213
+ if (e.data.code) return e.data.code
214
+
215
+ return [
216
+ !commonErrorClasses.has(e.name) && e.name,
217
+ // replace "1+ white space characters" with a single space
218
+ e.message.replaceAll(/\s+/gm, ' ').trim(),
219
+ ]
220
+ .filter(Boolean)
221
+ .join(': ')
222
+ }
223
+ }
224
+
159
225
  export function _isBackendErrorResponseObject(o: any): o is BackendErrorResponseObject {
160
226
  return _isErrorObject(o?.error)
161
227
  }