@naturalcycles/js-lib 14.123.2 → 14.124.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.
@@ -8,14 +8,14 @@ import { AppError } from '..';
8
8
  *
9
9
  * Alternatively, if you're sure it's Error - you can use `_assertIsError(err)`.
10
10
  */
11
- export declare function _anyToError<ERROR_TYPE extends Error = Error>(o: any, errorClass?: Class<ERROR_TYPE>, opt?: StringifyAnyOptions): ERROR_TYPE;
11
+ export declare function _anyToError<ERROR_TYPE extends Error = Error>(o: any, errorClass?: Class<ERROR_TYPE>, errorData?: ErrorData, opt?: StringifyAnyOptions): ERROR_TYPE;
12
12
  /**
13
13
  * Converts "anything" to ErrorObject.
14
14
  * Detects if it's HttpErrorResponse, HttpErrorObject, ErrorObject, Error, etc..
15
15
  * If object is Error - Error.message will be used.
16
16
  * Objects (not Errors) get converted to prettified JSON string (via `_stringifyAny`).
17
17
  */
18
- export declare function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(o: any, opt?: StringifyAnyOptions): ErrorObject<DATA_TYPE>;
18
+ export declare function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(o: any, errorData?: Partial<DATA_TYPE>, opt?: StringifyAnyOptions): ErrorObject<DATA_TYPE>;
19
19
  export declare function _errorToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(e: AppError<DATA_TYPE> | Error, includeErrorStack?: boolean): ErrorObject<DATA_TYPE>;
20
20
  export declare function _errorObjectToAppError<DATA_TYPE extends ErrorData>(o: ErrorObject<DATA_TYPE>): AppError<DATA_TYPE>;
21
21
  export declare function _errorObjectToError<DATA_TYPE extends ErrorData, ERROR_TYPE extends Error>(o: ErrorObject<DATA_TYPE>, errorClass?: Class<ERROR_TYPE>): ERROR_TYPE;
@@ -10,11 +10,12 @@ const __1 = require("..");
10
10
  *
11
11
  * Alternatively, if you're sure it's Error - you can use `_assertIsError(err)`.
12
12
  */
13
- function _anyToError(o, errorClass = Error, opt) {
13
+ function _anyToError(o, errorClass = Error, errorData, opt) {
14
14
  if (o instanceof errorClass)
15
15
  return o;
16
16
  // If it's an instance of Error, but ErrorClass is something else (e.g AppError) - it'll be "repacked" into AppError
17
- const errorObject = _isErrorObject(o) ? o : _anyToErrorObject(o, opt);
17
+ const errorObject = _isErrorObject(o) ? o : _anyToErrorObject(o, {}, opt);
18
+ Object.assign(errorObject.data, errorData);
18
19
  return _errorObjectToError(errorObject, errorClass);
19
20
  }
20
21
  exports._anyToError = _anyToError;
@@ -24,30 +25,37 @@ exports._anyToError = _anyToError;
24
25
  * If object is Error - Error.message will be used.
25
26
  * Objects (not Errors) get converted to prettified JSON string (via `_stringifyAny`).
26
27
  */
27
- function _anyToErrorObject(o, opt) {
28
+ function _anyToErrorObject(o, errorData, opt) {
29
+ let eo;
28
30
  if (o instanceof Error) {
29
- return _errorToErrorObject(o, opt?.includeErrorStack ?? true);
31
+ eo = _errorToErrorObject(o, opt?.includeErrorStack ?? true);
30
32
  }
31
- o = (0, __1._jsonParseIfPossible)(o);
32
- if (_isHttpErrorResponse(o)) {
33
- return o.error;
33
+ else {
34
+ o = (0, __1._jsonParseIfPossible)(o);
35
+ if (_isHttpErrorResponse(o)) {
36
+ eo = o.error;
37
+ }
38
+ else if (_isErrorObject(o)) {
39
+ eo = o;
40
+ }
41
+ else {
42
+ // Here we are sure it has no `data` property,
43
+ // so, fair to return `data: {}` in the end
44
+ // Also we're sure it includes no "error name", e.g no `Error: ...`,
45
+ // so, fair to include `name: 'Error'`
46
+ const message = (0, __1._stringifyAny)(o, {
47
+ includeErrorData: true,
48
+ ...opt,
49
+ });
50
+ eo = {
51
+ name: 'Error',
52
+ message,
53
+ data: {}, // empty
54
+ };
55
+ }
34
56
  }
35
- if (_isErrorObject(o)) {
36
- return o;
37
- }
38
- // Here we are sure it has no `data` property,
39
- // so, fair to return `data: {}` in the end
40
- // Also we're sure it includes no "error name", e.g no `Error: ...`,
41
- // so, fair to include `name: 'Error'`
42
- const message = (0, __1._stringifyAny)(o, {
43
- includeErrorData: true,
44
- ...opt,
45
- });
46
- return {
47
- name: 'Error',
48
- message,
49
- data: {}, // empty
50
- };
57
+ Object.assign(eo.data, errorData);
58
+ return eo;
51
59
  }
52
60
  exports._anyToErrorObject = _anyToErrorObject;
53
61
  function _errorToErrorObject(e, includeErrorStack = true) {
@@ -1,6 +1,7 @@
1
1
  /// <reference lib="dom" />
2
2
  import { CommonLogger } from '../log/commonLogger';
3
3
  import type { Promisable } from '../typeFest';
4
+ import { Reviver } from '../types';
4
5
  import type { HttpMethod, HttpStatusFamily } from './http.model';
5
6
  export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
6
7
  logger: CommonLogger;
@@ -61,6 +62,7 @@ export interface FetcherRetryOptions {
61
62
  export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
62
63
  url: string;
63
64
  init: RequestInitNormalized;
65
+ mode: FetcherMode;
64
66
  throwHttpErrors: boolean;
65
67
  timeoutSeconds: number;
66
68
  retry: FetcherRetryOptions;
@@ -108,6 +110,7 @@ export interface FetcherOptions {
108
110
  * Defaults to true.
109
111
  */
110
112
  retry5xx?: boolean;
113
+ jsonReviver?: Reviver;
111
114
  }
112
115
  export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
113
116
  method: HttpMethod;
@@ -129,7 +132,7 @@ export interface FetcherResponse<BODY = unknown> {
129
132
  body?: BODY;
130
133
  retryStatus: FetcherRetryStatus;
131
134
  }
132
- export type FetcherMode = 'json' | 'text';
135
+ export type FetcherMode = 'json' | 'text' | 'void';
133
136
  /**
134
137
  * Experimental wrapper around Fetch.
135
138
  * Works in both Browser and Node, using `globalThis.fetch`.
@@ -146,22 +149,22 @@ export declare class Fetcher {
146
149
  onBeforeRetry(hook: FetcherBeforeRetryHook): this;
147
150
  cfg: FetcherNormalizedCfg;
148
151
  static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
149
- get: (url: string, opt?: FetcherOptions) => Promise<void>;
150
- post: (url: string, opt?: FetcherOptions) => Promise<void>;
151
- put: (url: string, opt?: FetcherOptions) => Promise<void>;
152
- patch: (url: string, opt?: FetcherOptions) => Promise<void>;
153
- delete: (url: string, opt?: FetcherOptions) => Promise<void>;
154
- head: (url: string, opt?: FetcherOptions) => Promise<void>;
152
+ get: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
153
+ post: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
154
+ put: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
155
+ patch: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
156
+ delete: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
155
157
  getText: (url: string, opt?: FetcherOptions) => Promise<string>;
156
158
  postText: (url: string, opt?: FetcherOptions) => Promise<string>;
157
159
  putText: (url: string, opt?: FetcherOptions) => Promise<string>;
158
160
  patchText: (url: string, opt?: FetcherOptions) => Promise<string>;
159
161
  deleteText: (url: string, opt?: FetcherOptions) => Promise<string>;
160
- getJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
161
- postJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
162
- putJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
163
- patchJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
164
- deleteJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
162
+ getVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
163
+ postVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
164
+ putVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
165
+ patchVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
166
+ deleteVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
167
+ headVoid: (url: string, opt?: FetcherOptions) => Promise<void>;
165
168
  fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
166
169
  rawFetch<T = unknown>(url: string, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
167
170
  private processRetry;
@@ -31,12 +31,16 @@ class Fetcher {
31
31
  // Dynamically create all helper methods
32
32
  http_model_1.HTTP_METHODS.forEach(method => {
33
33
  // mode=void
34
- this[method] = async (url, opt) => {
34
+ this[`${method}Void`] = async (url, opt) => {
35
35
  return await this.fetch(url, {
36
36
  ...opt,
37
37
  method,
38
+ mode: 'void',
38
39
  });
39
40
  };
41
+ if (method === 'head')
42
+ return // mode=text
43
+ ;
40
44
  this[`${method}Text`] = async (url, opt) => {
41
45
  return await this.fetch(url, {
42
46
  ...opt,
@@ -44,7 +48,8 @@ class Fetcher {
44
48
  mode: 'text',
45
49
  });
46
50
  };
47
- this[`${method}Json`] = async (url, opt) => {
51
+ // mode=json
52
+ this[method] = async (url, opt) => {
48
53
  return await this.fetch(url, {
49
54
  ...opt,
50
55
  method,
@@ -74,7 +79,6 @@ class Fetcher {
74
79
  static create(cfg = {}) {
75
80
  return new Fetcher(cfg);
76
81
  }
77
- // headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
78
82
  async fetch(url, opt) {
79
83
  const res = await this.rawFetch(url, opt);
80
84
  if (res.err) {
@@ -115,7 +119,7 @@ class Fetcher {
115
119
  const started = Date.now();
116
120
  if (this.cfg.logRequest) {
117
121
  const { retryAttempt } = res.retryStatus;
118
- logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
122
+ logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
119
123
  .filter(Boolean)
120
124
  .join(' '));
121
125
  if (this.cfg.logRequestBody && req.init.body) {
@@ -132,22 +136,38 @@ class Fetcher {
132
136
  res.statusFamily = this.getStatusFamily(res);
133
137
  if (res.fetchResponse?.ok) {
134
138
  if (mode === 'json') {
135
- // if no body: set responseBody as {}
136
- // do not throw a "cannot parse null as Json" error
137
- res.body = res.fetchResponse.body ? await res.fetchResponse.json() : {};
139
+ if (res.fetchResponse.body) {
140
+ const text = await res.fetchResponse.text();
141
+ res.body = text;
142
+ try {
143
+ res.body = JSON.parse(text, req.jsonReviver);
144
+ }
145
+ catch (err) {
146
+ res.err = (0, error_util_1._anyToError)(err, http_error_1.HttpError, (0, object_util_1._filterNullishValues)({
147
+ httpStatusCode: 0,
148
+ url: req.url,
149
+ }));
150
+ }
151
+ }
152
+ else {
153
+ // if no body: set responseBody as {}
154
+ // do not throw a "cannot parse null as Json" error
155
+ res.body = {};
156
+ }
138
157
  }
139
158
  else if (mode === 'text') {
140
159
  res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
141
160
  }
142
161
  clearTimeout(timeout);
143
162
  res.retryStatus.retryStopped = true;
144
- if (this.cfg.logResponse) {
163
+ // res.err can happen on JSON.parse error
164
+ if (!res.err && this.cfg.logResponse) {
145
165
  const { retryAttempt } = res.retryStatus;
146
166
  logger.log([
147
167
  ' <<',
148
168
  res.fetchResponse.status,
149
169
  signature,
150
- retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
170
+ retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
151
171
  (0, time_util_1._since)(started),
152
172
  ]
153
173
  .filter(Boolean)
@@ -158,6 +178,7 @@ class Fetcher {
158
178
  }
159
179
  }
160
180
  else {
181
+ // !res.ok
161
182
  clearTimeout(timeout);
162
183
  let errObj;
163
184
  if (res.fetchResponse) {
@@ -268,6 +289,7 @@ class Fetcher {
268
289
  const norm = (0, object_util_1._merge)({
269
290
  baseUrl: '',
270
291
  url: '',
292
+ mode: 'void',
271
293
  searchParams: {},
272
294
  timeoutSeconds: 30,
273
295
  throwHttpErrors: true,
@@ -292,8 +314,9 @@ class Fetcher {
292
314
  return norm;
293
315
  }
294
316
  normalizeOptions(url, opt) {
295
- const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } = this.cfg;
317
+ const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode } = this.cfg;
296
318
  const req = {
319
+ mode,
297
320
  url,
298
321
  timeoutSeconds,
299
322
  throwHttpErrors,
@@ -7,11 +7,12 @@ import { AppError, _jsonParseIfPossible, _stringifyAny } from '..';
7
7
  *
8
8
  * Alternatively, if you're sure it's Error - you can use `_assertIsError(err)`.
9
9
  */
10
- export function _anyToError(o, errorClass = Error, opt) {
10
+ export function _anyToError(o, errorClass = Error, errorData, opt) {
11
11
  if (o instanceof errorClass)
12
12
  return o;
13
13
  // If it's an instance of Error, but ErrorClass is something else (e.g AppError) - it'll be "repacked" into AppError
14
- const errorObject = _isErrorObject(o) ? o : _anyToErrorObject(o, opt);
14
+ const errorObject = _isErrorObject(o) ? o : _anyToErrorObject(o, {}, opt);
15
+ Object.assign(errorObject.data, errorData);
15
16
  return _errorObjectToError(errorObject, errorClass);
16
17
  }
17
18
  /**
@@ -20,28 +21,35 @@ export function _anyToError(o, errorClass = Error, opt) {
20
21
  * If object is Error - Error.message will be used.
21
22
  * Objects (not Errors) get converted to prettified JSON string (via `_stringifyAny`).
22
23
  */
23
- export function _anyToErrorObject(o, opt) {
24
+ export function _anyToErrorObject(o, errorData, opt) {
24
25
  var _a;
26
+ let eo;
25
27
  if (o instanceof Error) {
26
- return _errorToErrorObject(o, (_a = opt === null || opt === void 0 ? void 0 : opt.includeErrorStack) !== null && _a !== void 0 ? _a : true);
28
+ eo = _errorToErrorObject(o, (_a = opt === null || opt === void 0 ? void 0 : opt.includeErrorStack) !== null && _a !== void 0 ? _a : true);
27
29
  }
28
- o = _jsonParseIfPossible(o);
29
- if (_isHttpErrorResponse(o)) {
30
- return o.error;
30
+ else {
31
+ o = _jsonParseIfPossible(o);
32
+ if (_isHttpErrorResponse(o)) {
33
+ eo = o.error;
34
+ }
35
+ else if (_isErrorObject(o)) {
36
+ eo = o;
37
+ }
38
+ else {
39
+ // Here we are sure it has no `data` property,
40
+ // so, fair to return `data: {}` in the end
41
+ // Also we're sure it includes no "error name", e.g no `Error: ...`,
42
+ // so, fair to include `name: 'Error'`
43
+ const message = _stringifyAny(o, Object.assign({ includeErrorData: true }, opt));
44
+ eo = {
45
+ name: 'Error',
46
+ message,
47
+ data: {}, // empty
48
+ };
49
+ }
31
50
  }
32
- if (_isErrorObject(o)) {
33
- return o;
34
- }
35
- // Here we are sure it has no `data` property,
36
- // so, fair to return `data: {}` in the end
37
- // Also we're sure it includes no "error name", e.g no `Error: ...`,
38
- // so, fair to include `name: 'Error'`
39
- const message = _stringifyAny(o, Object.assign({ includeErrorData: true }, opt));
40
- return {
41
- name: 'Error',
42
- message,
43
- data: {}, // empty
44
- };
51
+ Object.assign(eo.data, errorData);
52
+ return eo;
45
53
  }
46
54
  export function _errorToErrorObject(e, includeErrorStack = true) {
47
55
  const obj = {
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="dom"/>
2
2
  import { __asyncValues } from "tslib";
3
- import { _anyToErrorObject, _errorToErrorObject } from '../error/error.util';
3
+ import { _anyToError, _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';
@@ -29,13 +29,17 @@ export class Fetcher {
29
29
  // Dynamically create all helper methods
30
30
  HTTP_METHODS.forEach(method => {
31
31
  // mode=void
32
- this[method] = async (url, opt) => {
33
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { method }));
32
+ this[`${method}Void`] = async (url, opt) => {
33
+ return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'void' }));
34
34
  };
35
+ if (method === 'head')
36
+ return // mode=text
37
+ ;
35
38
  this[`${method}Text`] = async (url, opt) => {
36
39
  return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'text' }));
37
40
  };
38
- this[`${method}Json`] = async (url, opt) => {
41
+ // mode=json
42
+ this[method] = async (url, opt) => {
39
43
  return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'json' }));
40
44
  };
41
45
  });
@@ -64,7 +68,6 @@ export class Fetcher {
64
68
  static create(cfg = {}) {
65
69
  return new Fetcher(cfg);
66
70
  }
67
- // headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
68
71
  async fetch(url, opt) {
69
72
  const res = await this.rawFetch(url, opt);
70
73
  if (res.err) {
@@ -124,7 +127,7 @@ export class Fetcher {
124
127
  const started = Date.now();
125
128
  if (this.cfg.logRequest) {
126
129
  const { retryAttempt } = res.retryStatus;
127
- logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
130
+ logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
128
131
  .filter(Boolean)
129
132
  .join(' '));
130
133
  if (this.cfg.logRequestBody && req.init.body) {
@@ -141,22 +144,38 @@ export class Fetcher {
141
144
  res.statusFamily = this.getStatusFamily(res);
142
145
  if ((_g = res.fetchResponse) === null || _g === void 0 ? void 0 : _g.ok) {
143
146
  if (mode === 'json') {
144
- // if no body: set responseBody as {}
145
- // do not throw a "cannot parse null as Json" error
146
- res.body = res.fetchResponse.body ? await res.fetchResponse.json() : {};
147
+ if (res.fetchResponse.body) {
148
+ const text = await res.fetchResponse.text();
149
+ res.body = text;
150
+ try {
151
+ res.body = JSON.parse(text, req.jsonReviver);
152
+ }
153
+ catch (err) {
154
+ res.err = _anyToError(err, HttpError, _filterNullishValues({
155
+ httpStatusCode: 0,
156
+ url: req.url,
157
+ }));
158
+ }
159
+ }
160
+ else {
161
+ // if no body: set responseBody as {}
162
+ // do not throw a "cannot parse null as Json" error
163
+ res.body = {};
164
+ }
147
165
  }
148
166
  else if (mode === 'text') {
149
167
  res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
150
168
  }
151
169
  clearTimeout(timeout);
152
170
  res.retryStatus.retryStopped = true;
153
- if (this.cfg.logResponse) {
171
+ // res.err can happen on JSON.parse error
172
+ if (!res.err && this.cfg.logResponse) {
154
173
  const { retryAttempt } = res.retryStatus;
155
174
  logger.log([
156
175
  ' <<',
157
176
  res.fetchResponse.status,
158
177
  signature,
159
- retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
178
+ retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
160
179
  _since(started),
161
180
  ]
162
181
  .filter(Boolean)
@@ -167,6 +186,7 @@ export class Fetcher {
167
186
  }
168
187
  }
169
188
  else {
189
+ // !res.ok
170
190
  clearTimeout(timeout);
171
191
  let errObj;
172
192
  if (res.fetchResponse) {
@@ -310,6 +330,7 @@ export class Fetcher {
310
330
  const norm = _merge({
311
331
  baseUrl: '',
312
332
  url: '',
333
+ mode: 'void',
313
334
  searchParams: {},
314
335
  timeoutSeconds: 30,
315
336
  throwHttpErrors: true,
@@ -334,8 +355,9 @@ export class Fetcher {
334
355
  return norm;
335
356
  }
336
357
  normalizeOptions(url, opt) {
337
- const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } = this.cfg;
338
- const req = Object.assign(Object.assign({ url,
358
+ const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode } = this.cfg;
359
+ const req = Object.assign(Object.assign({ mode,
360
+ url,
339
361
  timeoutSeconds,
340
362
  throwHttpErrors,
341
363
  retryPost,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.123.2",
3
+ "version": "14.124.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -19,13 +19,15 @@ import { AppError, _jsonParseIfPossible, _stringifyAny } from '..'
19
19
  export function _anyToError<ERROR_TYPE extends Error = Error>(
20
20
  o: any,
21
21
  errorClass: Class<ERROR_TYPE> = Error as any,
22
+ errorData?: ErrorData,
22
23
  opt?: StringifyAnyOptions,
23
24
  ): ERROR_TYPE {
24
25
  if (o instanceof errorClass) return o
25
26
 
26
27
  // If it's an instance of Error, but ErrorClass is something else (e.g AppError) - it'll be "repacked" into AppError
27
28
 
28
- const errorObject = _isErrorObject(o) ? o : _anyToErrorObject(o, opt)
29
+ const errorObject = _isErrorObject(o) ? o : _anyToErrorObject(o, {}, opt)
30
+ Object.assign(errorObject.data, errorData)
29
31
  return _errorObjectToError(errorObject, errorClass)
30
32
  }
31
33
 
@@ -37,37 +39,40 @@ export function _anyToError<ERROR_TYPE extends Error = Error>(
37
39
  */
38
40
  export function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
39
41
  o: any,
42
+ errorData?: Partial<DATA_TYPE>,
40
43
  opt?: StringifyAnyOptions,
41
44
  ): ErrorObject<DATA_TYPE> {
42
- if (o instanceof Error) {
43
- return _errorToErrorObject<DATA_TYPE>(o, opt?.includeErrorStack ?? true)
44
- }
45
-
46
- o = _jsonParseIfPossible(o)
47
-
48
- if (_isHttpErrorResponse(o)) {
49
- return o.error as any
50
- }
45
+ let eo: ErrorObject<DATA_TYPE>
51
46
 
52
- if (_isErrorObject(o)) {
53
- return o as ErrorObject<DATA_TYPE>
47
+ if (o instanceof Error) {
48
+ eo = _errorToErrorObject<DATA_TYPE>(o, opt?.includeErrorStack ?? true)
49
+ } else {
50
+ o = _jsonParseIfPossible(o)
51
+
52
+ if (_isHttpErrorResponse(o)) {
53
+ eo = o.error as any
54
+ } else if (_isErrorObject(o)) {
55
+ eo = o as ErrorObject<DATA_TYPE>
56
+ } else {
57
+ // Here we are sure it has no `data` property,
58
+ // so, fair to return `data: {}` in the end
59
+ // Also we're sure it includes no "error name", e.g no `Error: ...`,
60
+ // so, fair to include `name: 'Error'`
61
+ const message = _stringifyAny(o, {
62
+ includeErrorData: true, // cause we're returning an ErrorObject, not a stringified error (yet)
63
+ ...opt,
64
+ })
65
+
66
+ eo = {
67
+ name: 'Error',
68
+ message,
69
+ data: {} as DATA_TYPE, // empty
70
+ }
71
+ }
54
72
  }
55
73
 
56
- // Here we are sure it has no `data` property,
57
- // so, fair to return `data: {}` in the end
58
- // Also we're sure it includes no "error name", e.g no `Error: ...`,
59
- // so, fair to include `name: 'Error'`
60
-
61
- const message = _stringifyAny(o, {
62
- includeErrorData: true, // cause we're returning an ErrorObject, not a stringified error (yet)
63
- ...opt,
64
- })
65
-
66
- return {
67
- name: 'Error',
68
- message,
69
- data: {} as DATA_TYPE, // empty
70
- }
74
+ Object.assign(eo.data, errorData)
75
+ return eo
71
76
  }
72
77
 
73
78
  export function _errorToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="dom"/>
2
2
 
3
3
  import { ErrorObject } from '../error/error.model'
4
- import { _anyToErrorObject, _errorToErrorObject } from '../error/error.util'
4
+ import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util'
5
5
  import { HttpError } from '../error/http.error'
6
6
  import { CommonLogger } from '../log/commonLogger'
7
7
  import { _clamp } from '../number/number.util'
@@ -16,6 +16,7 @@ import { pDelay } from '../promise/pDelay'
16
16
  import { _jsonParseIfPossible } from '../string/json.util'
17
17
  import { _since } from '../time/time.util'
18
18
  import type { Promisable } from '../typeFest'
19
+ import { Reviver } from '../types'
19
20
  import { HTTP_METHODS } from './http.model'
20
21
  import type { HttpMethod, HttpStatusFamily } from './http.model'
21
22
 
@@ -86,6 +87,7 @@ export interface FetcherRetryOptions {
86
87
  export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers'> {
87
88
  url: string
88
89
  init: RequestInitNormalized
90
+ mode: FetcherMode
89
91
  throwHttpErrors: boolean
90
92
  timeoutSeconds: number
91
93
  retry: FetcherRetryOptions
@@ -121,7 +123,7 @@ export interface FetcherOptions {
121
123
  // init?: Partial<RequestInitNormalized>
122
124
 
123
125
  headers?: Record<string, any>
124
- mode?: FetcherMode // default to undefined (void response)
126
+ mode?: FetcherMode // default to 'void'
125
127
 
126
128
  searchParams?: Record<string, any>
127
129
 
@@ -144,6 +146,8 @@ export interface FetcherOptions {
144
146
  * Defaults to true.
145
147
  */
146
148
  retry5xx?: boolean
149
+
150
+ jsonReviver?: Reviver
147
151
  }
148
152
 
149
153
  export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
@@ -170,7 +174,7 @@ export interface FetcherResponse<BODY = unknown> {
170
174
  retryStatus: FetcherRetryStatus
171
175
  }
172
176
 
173
- export type FetcherMode = 'json' | 'text'
177
+ export type FetcherMode = 'json' | 'text' | 'void'
174
178
 
175
179
  const defRetryOptions: FetcherRetryOptions = {
176
180
  count: 2,
@@ -196,14 +200,15 @@ export class Fetcher {
196
200
  // Dynamically create all helper methods
197
201
  HTTP_METHODS.forEach(method => {
198
202
  // mode=void
199
- this[method] = async (url: string, opt?: FetcherOptions): Promise<void> => {
203
+ this[`${method}Void`] = async (url: string, opt?: FetcherOptions): Promise<void> => {
200
204
  return await this.fetch<void>(url, {
201
205
  ...opt,
202
206
  method,
207
+ mode: 'void',
203
208
  })
204
209
  }
205
210
 
206
- // mode=text
211
+ if (method === 'head') return // mode=text
207
212
  ;(this as any)[`${method}Text`] = async (
208
213
  url: string,
209
214
  opt?: FetcherOptions,
@@ -216,10 +221,7 @@ export class Fetcher {
216
221
  }
217
222
 
218
223
  // mode=json
219
- ;(this as any)[`${method}Json`] = async <T = unknown>(
220
- url: string,
221
- opt?: FetcherOptions,
222
- ): Promise<T> => {
224
+ this[method] = async <T = unknown>(url: string, opt?: FetcherOptions): Promise<T> => {
223
225
  return await this.fetch<T>(url, {
224
226
  ...opt,
225
227
  method,
@@ -254,25 +256,27 @@ export class Fetcher {
254
256
  }
255
257
 
256
258
  // These methods are generated dynamically in the constructor
257
- get!: (url: string, opt?: FetcherOptions) => Promise<void>
258
- post!: (url: string, opt?: FetcherOptions) => Promise<void>
259
- put!: (url: string, opt?: FetcherOptions) => Promise<void>
260
- patch!: (url: string, opt?: FetcherOptions) => Promise<void>
261
- delete!: (url: string, opt?: FetcherOptions) => Promise<void>
262
- head!: (url: string, opt?: FetcherOptions) => Promise<void>
263
-
259
+ // These default methods use mode=json
260
+ get!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
261
+ post!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
262
+ put!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
263
+ patch!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
264
+ delete!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
265
+
266
+ // mode=text
264
267
  getText!: (url: string, opt?: FetcherOptions) => Promise<string>
265
268
  postText!: (url: string, opt?: FetcherOptions) => Promise<string>
266
269
  putText!: (url: string, opt?: FetcherOptions) => Promise<string>
267
270
  patchText!: (url: string, opt?: FetcherOptions) => Promise<string>
268
271
  deleteText!: (url: string, opt?: FetcherOptions) => Promise<string>
269
272
 
270
- getJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
271
- postJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
272
- putJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
273
- patchJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
274
- deleteJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
275
- // headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
273
+ // mode=void (no body fetching/parsing)
274
+ getVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
275
+ postVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
276
+ putVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
277
+ patchVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
278
+ deleteVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
279
+ headVoid!: (url: string, opt?: FetcherOptions) => Promise<void>
276
280
 
277
281
  async fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
278
282
  const res = await this.rawFetch<T>(url, opt)
@@ -329,7 +333,7 @@ export class Fetcher {
329
333
  if (this.cfg.logRequest) {
330
334
  const { retryAttempt } = res.retryStatus
331
335
  logger.log(
332
- [' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`]
336
+ [' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
333
337
  .filter(Boolean)
334
338
  .join(' '),
335
339
  )
@@ -348,9 +352,27 @@ export class Fetcher {
348
352
 
349
353
  if (res.fetchResponse?.ok) {
350
354
  if (mode === 'json') {
351
- // if no body: set responseBody as {}
352
- // do not throw a "cannot parse null as Json" error
353
- res.body = res.fetchResponse.body ? await res.fetchResponse.json() : {}
355
+ if (res.fetchResponse.body) {
356
+ const text = await res.fetchResponse.text()
357
+ res.body = text
358
+
359
+ try {
360
+ res.body = JSON.parse(text, req.jsonReviver)
361
+ } catch (err) {
362
+ res.err = _anyToError(
363
+ err,
364
+ HttpError,
365
+ _filterNullishValues({
366
+ httpStatusCode: 0,
367
+ url: req.url,
368
+ }),
369
+ )
370
+ }
371
+ } else {
372
+ // if no body: set responseBody as {}
373
+ // do not throw a "cannot parse null as Json" error
374
+ res.body = {}
375
+ }
354
376
  } else if (mode === 'text') {
355
377
  res.body = res.fetchResponse.body ? await res.fetchResponse.text() : ''
356
378
  }
@@ -358,14 +380,15 @@ export class Fetcher {
358
380
  clearTimeout(timeout)
359
381
  res.retryStatus.retryStopped = true
360
382
 
361
- if (this.cfg.logResponse) {
383
+ // res.err can happen on JSON.parse error
384
+ if (!res.err && this.cfg.logResponse) {
362
385
  const { retryAttempt } = res.retryStatus
363
386
  logger.log(
364
387
  [
365
388
  ' <<',
366
389
  res.fetchResponse.status,
367
390
  signature,
368
- retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
391
+ retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
369
392
  _since(started),
370
393
  ]
371
394
  .filter(Boolean)
@@ -377,6 +400,7 @@ export class Fetcher {
377
400
  }
378
401
  }
379
402
  } else {
403
+ // !res.ok
380
404
  clearTimeout(timeout)
381
405
 
382
406
  let errObj: ErrorObject
@@ -500,6 +524,7 @@ export class Fetcher {
500
524
  {
501
525
  baseUrl: '',
502
526
  url: '',
527
+ mode: 'void',
503
528
  searchParams: {},
504
529
  timeoutSeconds: 30,
505
530
  throwHttpErrors: true,
@@ -529,10 +554,11 @@ export class Fetcher {
529
554
  }
530
555
 
531
556
  private normalizeOptions(url: string, opt: FetcherOptions): FetcherRequest {
532
- const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry } =
557
+ const { baseUrl, timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode } =
533
558
  this.cfg
534
559
 
535
560
  const req: FetcherRequest = {
561
+ mode,
536
562
  url,
537
563
  timeoutSeconds,
538
564
  throwHttpErrors,