@naturalcycles/js-lib 14.157.1 → 14.159.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.
@@ -33,6 +33,12 @@ export declare function _assertEquals<T>(actual: any, expected: T, message?: str
33
33
  */
34
34
  export declare function _assertDeepEquals<T>(actual: any, expected: T, message?: string, errorData?: ErrorData): asserts actual is T;
35
35
  export declare function _assertIsError<ERR extends Error = Error>(err: any, errorClass?: Class<ERR>): asserts err is ERR;
36
+ /**
37
+ * Asserts that passed object is indeed an Error of defined ErrorClass.
38
+ * If yes - returns peacefully (with TypeScript assertion).
39
+ * In not - throws (re-throws) that error up.
40
+ */
41
+ export declare function _assertErrorClassOrRethrow<ERR extends Error>(err: any, errorClass: Class<ERR>): asserts err is ERR;
36
42
  export declare function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(obj: any): asserts obj is ErrorObject<DATA_TYPE>;
37
43
  export declare function _assertIsString(v: any, message?: string): asserts v is string;
38
44
  export declare function _assertIsNumber(v: any, message?: string): asserts v is number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AssertionError = exports._assertTypeOf = exports._assertIsNumber = exports._assertIsString = exports._assertIsErrorObject = exports._assertIsError = exports._assertDeepEquals = exports._assertEquals = exports._assert = void 0;
3
+ exports.AssertionError = exports._assertTypeOf = exports._assertIsNumber = exports._assertIsString = exports._assertIsErrorObject = exports._assertErrorClassOrRethrow = exports._assertIsError = exports._assertDeepEquals = exports._assertEquals = exports._assert = void 0;
4
4
  const __1 = require("..");
5
5
  const app_error_1 = require("./app.error");
6
6
  /**
@@ -84,6 +84,18 @@ function _assertIsError(err, errorClass = Error) {
84
84
  }
85
85
  }
86
86
  exports._assertIsError = _assertIsError;
87
+ /**
88
+ * Asserts that passed object is indeed an Error of defined ErrorClass.
89
+ * If yes - returns peacefully (with TypeScript assertion).
90
+ * In not - throws (re-throws) that error up.
91
+ */
92
+ function _assertErrorClassOrRethrow(err, errorClass) {
93
+ if (!(err instanceof errorClass)) {
94
+ // re-throw
95
+ throw err;
96
+ }
97
+ }
98
+ exports._assertErrorClassOrRethrow = _assertErrorClassOrRethrow;
87
99
  function _assertIsErrorObject(obj) {
88
100
  if (!(0, __1._isErrorObject)(obj)) {
89
101
  const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n');
@@ -18,5 +18,11 @@ import type { ErrorObject, HttpRequestErrorData } from './error.model';
18
18
  * (by default).
19
19
  */
20
20
  export declare class HttpRequestError extends AppError<HttpRequestErrorData> {
21
- constructor(message: string, data: HttpRequestErrorData, cause?: ErrorObject);
21
+ constructor(message: string, data: HttpRequestErrorData, cause: ErrorObject);
22
+ /**
23
+ * Cause is strictly-defined for HttpRequestError,
24
+ * so it always has a cause.
25
+ * (for dev convenience)
26
+ */
27
+ cause: ErrorObject;
22
28
  }
@@ -1,5 +1,5 @@
1
1
  import type { Class } from '../typeFest';
2
- import type { AnyFunction } from '../types';
2
+ import type { AnyFunction, ErrorDataTuple } from '../types';
3
3
  import { AppError } from './app.error';
4
4
  /**
5
5
  * Calls a function, returns a Tuple of [error, value].
@@ -8,9 +8,6 @@ import { AppError } from './app.error';
8
8
  *
9
9
  * Similar to pTry, but for sync functions.
10
10
  *
11
- * For convenience, second argument type is non-optional,
12
- * so you can use it without `!`. But you SHOULD always check `if (err)` first!
13
- *
14
11
  * ERR is typed as Error, not `unknown`. While unknown would be more correct,
15
12
  * according to recent TypeScript, Error gives more developer convenience.
16
13
  * In our code we NEVER throw non-errors.
@@ -23,14 +20,11 @@ import { AppError } from './app.error';
23
20
  * if (err) ...do something...
24
21
  * v // go ahead and use v
25
22
  */
26
- export declare function _try<ERR = Error, RETURN = void>(fn: () => RETURN): [err: ERR | null, value: RETURN];
23
+ export declare function _try<T, ERR extends Error = Error>(fn: () => T, errorClass?: Class<ERR>): ErrorDataTuple<T, ERR>;
27
24
  /**
28
25
  * Like _try, but for Promises.
29
- *
30
- * Also, intentionally types second return item as non-optional,
31
- * but you should check for `err` presense first!
32
26
  */
33
- export declare function pTry<ERR = Error, RETURN = void>(promise: Promise<RETURN>): Promise<[err: ERR | null, value: Awaited<RETURN>]>;
27
+ export declare function pTry<T, ERR extends Error = Error>(promise: Promise<T>, errorClass?: Class<ERR>): Promise<ErrorDataTuple<Awaited<T>, ERR>>;
34
28
  /**
35
29
  * It is thrown when Error was expected, but didn't happen
36
30
  * ("pass" happened instead).
@@ -61,3 +55,7 @@ export declare function pExpectedError<ERR = Error>(promise: Promise<any>, error
61
55
  * Shortcut function to simplify error snapshot-matching in tests.
62
56
  */
63
57
  export declare function pExpectedErrorString<ERR = Error>(promise: Promise<any>, errorClass?: Class<ERR>): Promise<string>;
58
+ /**
59
+ * Shortcut function to simplify error snapshot-matching in tests.
60
+ */
61
+ export declare function _expectedErrorString<ERR = Error>(fn: AnyFunction, errorClass?: Class<ERR>): string;
package/dist/error/try.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.pExpectedErrorString = exports.pExpectedError = exports._expectedError = exports.UnexpectedPassError = exports.pTry = exports._try = void 0;
3
+ exports._expectedErrorString = exports.pExpectedErrorString = exports.pExpectedError = exports._expectedError = exports.UnexpectedPassError = exports.pTry = exports._try = void 0;
4
4
  const stringifyAny_1 = require("../string/stringifyAny");
5
5
  const app_error_1 = require("./app.error");
6
+ const assert_1 = require("./assert");
6
7
  /**
7
8
  * Calls a function, returns a Tuple of [error, value].
8
9
  * Allows to write shorter code that avoids `try/catch`.
@@ -10,9 +11,6 @@ const app_error_1 = require("./app.error");
10
11
  *
11
12
  * Similar to pTry, but for sync functions.
12
13
  *
13
- * For convenience, second argument type is non-optional,
14
- * so you can use it without `!`. But you SHOULD always check `if (err)` first!
15
- *
16
14
  * ERR is typed as Error, not `unknown`. While unknown would be more correct,
17
15
  * according to recent TypeScript, Error gives more developer convenience.
18
16
  * In our code we NEVER throw non-errors.
@@ -25,27 +23,30 @@ const app_error_1 = require("./app.error");
25
23
  * if (err) ...do something...
26
24
  * v // go ahead and use v
27
25
  */
28
- function _try(fn) {
26
+ function _try(fn, errorClass) {
29
27
  try {
30
28
  return [null, fn()];
31
29
  }
32
30
  catch (err) {
33
- return [err, undefined];
31
+ if (errorClass) {
32
+ (0, assert_1._assertErrorClassOrRethrow)(err, errorClass);
33
+ }
34
+ return [err, null];
34
35
  }
35
36
  }
36
37
  exports._try = _try;
37
38
  /**
38
39
  * Like _try, but for Promises.
39
- *
40
- * Also, intentionally types second return item as non-optional,
41
- * but you should check for `err` presense first!
42
40
  */
43
- async function pTry(promise) {
41
+ async function pTry(promise, errorClass) {
44
42
  try {
45
43
  return [null, await promise];
46
44
  }
47
45
  catch (err) {
48
- return [err, undefined];
46
+ if (errorClass) {
47
+ (0, assert_1._assertErrorClassOrRethrow)(err, errorClass);
48
+ }
49
+ return [err, null];
49
50
  }
50
51
  }
51
52
  exports.pTry = pTry;
@@ -114,6 +115,15 @@ exports.pExpectedError = pExpectedError;
114
115
  * Shortcut function to simplify error snapshot-matching in tests.
115
116
  */
116
117
  async function pExpectedErrorString(promise, errorClass) {
117
- return (0, stringifyAny_1._stringifyAny)(await pExpectedError(promise, errorClass));
118
+ const err = await pExpectedError(promise, errorClass);
119
+ return (0, stringifyAny_1._stringifyAny)(err);
118
120
  }
119
121
  exports.pExpectedErrorString = pExpectedErrorString;
122
+ /**
123
+ * Shortcut function to simplify error snapshot-matching in tests.
124
+ */
125
+ function _expectedErrorString(fn, errorClass) {
126
+ const err = _expectedError(fn, errorClass);
127
+ return (0, stringifyAny_1._stringifyAny)(err);
128
+ }
129
+ exports._expectedErrorString = _expectedErrorString;
@@ -1,6 +1,8 @@
1
1
  /// <reference lib="dom" />
2
2
  /// <reference lib="dom.iterable" />
3
- import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse } from './fetcher.model';
3
+ import { HttpRequestError } from '../error/httpRequestError';
4
+ import { ErrorDataTuple } from '../types';
5
+ import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse, RequestInitNormalized } from './fetcher.model';
4
6
  /**
5
7
  * Experimental wrapper around Fetch.
6
8
  * Works in both Browser and Node, using `globalThis.fetch`.
@@ -39,17 +41,28 @@ export declare class Fetcher {
39
41
  */
40
42
  getReadableStream(url: string, opt?: FetcherOptions): Promise<ReadableStream<Uint8Array>>;
41
43
  fetch<T = unknown>(opt: FetcherOptions): Promise<T>;
44
+ /**
45
+ * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
46
+ * err, if defined, is strictly HttpRequestError.
47
+ * UPD: actually not, err is typed as Error, as it feels unsafe to guarantee error type.
48
+ * UPD: actually yes - it will return HttpRequestError, and throw if there's an error
49
+ * of any other type.
50
+ */
51
+ tryFetch<T = unknown>(opt: FetcherOptions): Promise<ErrorDataTuple<T, HttpRequestError>>;
42
52
  /**
43
53
  * Returns FetcherResponse.
44
54
  * Never throws, returns `err` property in the response instead.
45
55
  * Use this method instead of `throwHttpErrors: false` or try-catching.
56
+ *
57
+ * Note: responseType defaults to `void`, so, override it if you expect different.
46
58
  */
47
59
  doFetch<T = unknown>(opt: FetcherOptions): Promise<FetcherResponse<T>>;
48
60
  private onOkResponse;
49
61
  /**
50
62
  * This method exists to be able to easily mock it.
63
+ * It is static, so mocking applies to ALL instances (even future ones) of Fetcher at once.
51
64
  */
52
- callNativeFetch(url: string, init: RequestInit): Promise<Response>;
65
+ static callNativeFetch(url: string, init: RequestInitNormalized): Promise<Response>;
53
66
  private onNotOkResponse;
54
67
  private processRetry;
55
68
  private getRetryTimeout;
@@ -4,6 +4,7 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.getFetcher = exports.Fetcher = void 0;
6
6
  const env_1 = require("../env");
7
+ const assert_1 = require("../error/assert");
7
8
  const error_util_1 = require("../error/error.util");
8
9
  const httpRequestError_1 = require("../error/httpRequestError");
9
10
  const number_util_1 = require("../number/number.util");
@@ -112,10 +113,27 @@ class Fetcher {
112
113
  }
113
114
  return res.body;
114
115
  }
116
+ /**
117
+ * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
118
+ * err, if defined, is strictly HttpRequestError.
119
+ * UPD: actually not, err is typed as Error, as it feels unsafe to guarantee error type.
120
+ * UPD: actually yes - it will return HttpRequestError, and throw if there's an error
121
+ * of any other type.
122
+ */
123
+ async tryFetch(opt) {
124
+ const res = await this.doFetch(opt);
125
+ if (res.err) {
126
+ (0, assert_1._assertErrorClassOrRethrow)(res.err, httpRequestError_1.HttpRequestError);
127
+ return [res.err, null];
128
+ }
129
+ return [null, res.body];
130
+ }
115
131
  /**
116
132
  * Returns FetcherResponse.
117
133
  * Never throws, returns `err` property in the response instead.
118
134
  * Use this method instead of `throwHttpErrors: false` or try-catching.
135
+ *
136
+ * Note: responseType defaults to `void`, so, override it if you expect different.
119
137
  */
120
138
  async doFetch(opt) {
121
139
  const req = this.normalizeOptions(opt);
@@ -161,7 +179,7 @@ class Fetcher {
161
179
  }
162
180
  }
163
181
  try {
164
- res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init);
182
+ res.fetchResponse = await Fetcher.callNativeFetch(req.fullUrl, req.init);
165
183
  res.ok = res.fetchResponse.ok;
166
184
  // important to set it to undefined, otherwise it can keep the previous value (from previous try)
167
185
  res.err = undefined;
@@ -265,8 +283,9 @@ class Fetcher {
265
283
  }
266
284
  /**
267
285
  * This method exists to be able to easily mock it.
286
+ * It is static, so mocking applies to ALL instances (even future ones) of Fetcher at once.
268
287
  */
269
- async callNativeFetch(url, init) {
288
+ static async callNativeFetch(url, init) {
270
289
  return await globalThis.fetch(url, init);
271
290
  }
272
291
  async onNotOkResponse(res) {
@@ -283,6 +302,11 @@ class Fetcher {
283
302
  cause = (0, error_util_1._anyToErrorObject)(body);
284
303
  }
285
304
  }
305
+ cause ||= {
306
+ name: 'Error',
307
+ message: 'Fetch failed',
308
+ data: {},
309
+ };
286
310
  const message = [res.fetchResponse?.status, res.signature].filter(Boolean).join(' ');
287
311
  res.err = new httpRequestError_1.HttpRequestError(message, (0, object_util_1._filterNullishValues)({
288
312
  responseStatusCode: res.fetchResponse?.status || 0,
@@ -441,7 +465,7 @@ class Fetcher {
441
465
  const norm = (0, object_util_1._merge)({
442
466
  baseUrl: '',
443
467
  inputUrl: '',
444
- responseType: 'void',
468
+ responseType: 'json',
445
469
  searchParams: {},
446
470
  timeoutSeconds: 30,
447
471
  retryPost: false,
package/dist/types.d.ts CHANGED
@@ -207,3 +207,14 @@ export declare function _typeCast<T>(v: any): asserts v is T;
207
207
  * Type-safe Object.assign that checks that part is indeed a Partial<T>
208
208
  */
209
209
  export declare const _objectAssign: <T extends AnyObject>(target: T, part: Partial<T>) => T;
210
+ /**
211
+ * Defines a tuple of [err, data]
212
+ * where only 1 of them exists.
213
+ * Either error exists and data is null
214
+ * Or error is null and data is defined.
215
+ * This forces you to check `if (err)`, which lets
216
+ * TypeScript infer the existence of `data`.
217
+ *
218
+ * Functions like pTry use that.
219
+ */
220
+ export type ErrorDataTuple<T = unknown, ERR = Error> = [err: null, data: T] | [err: ERR, data: null];
@@ -68,6 +68,17 @@ export function _assertIsError(err, errorClass = Error) {
68
68
  });
69
69
  }
70
70
  }
71
+ /**
72
+ * Asserts that passed object is indeed an Error of defined ErrorClass.
73
+ * If yes - returns peacefully (with TypeScript assertion).
74
+ * In not - throws (re-throws) that error up.
75
+ */
76
+ export function _assertErrorClassOrRethrow(err, errorClass) {
77
+ if (!(err instanceof errorClass)) {
78
+ // re-throw
79
+ throw err;
80
+ }
81
+ }
71
82
  export function _assertIsErrorObject(obj) {
72
83
  if (!_isErrorObject(obj)) {
73
84
  const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n');
@@ -1,5 +1,6 @@
1
1
  import { _stringifyAny } from '../string/stringifyAny';
2
2
  import { AppError } from './app.error';
3
+ import { _assertErrorClassOrRethrow } from './assert';
3
4
  /**
4
5
  * Calls a function, returns a Tuple of [error, value].
5
6
  * Allows to write shorter code that avoids `try/catch`.
@@ -7,9 +8,6 @@ import { AppError } from './app.error';
7
8
  *
8
9
  * Similar to pTry, but for sync functions.
9
10
  *
10
- * For convenience, second argument type is non-optional,
11
- * so you can use it without `!`. But you SHOULD always check `if (err)` first!
12
- *
13
11
  * ERR is typed as Error, not `unknown`. While unknown would be more correct,
14
12
  * according to recent TypeScript, Error gives more developer convenience.
15
13
  * In our code we NEVER throw non-errors.
@@ -22,26 +20,29 @@ import { AppError } from './app.error';
22
20
  * if (err) ...do something...
23
21
  * v // go ahead and use v
24
22
  */
25
- export function _try(fn) {
23
+ export function _try(fn, errorClass) {
26
24
  try {
27
25
  return [null, fn()];
28
26
  }
29
27
  catch (err) {
30
- return [err, undefined];
28
+ if (errorClass) {
29
+ _assertErrorClassOrRethrow(err, errorClass);
30
+ }
31
+ return [err, null];
31
32
  }
32
33
  }
33
34
  /**
34
35
  * Like _try, but for Promises.
35
- *
36
- * Also, intentionally types second return item as non-optional,
37
- * but you should check for `err` presense first!
38
36
  */
39
- export async function pTry(promise) {
37
+ export async function pTry(promise, errorClass) {
40
38
  try {
41
39
  return [null, await promise];
42
40
  }
43
41
  catch (err) {
44
- return [err, undefined];
42
+ if (errorClass) {
43
+ _assertErrorClassOrRethrow(err, errorClass);
44
+ }
45
+ return [err, null];
45
46
  }
46
47
  }
47
48
  /**
@@ -106,5 +107,13 @@ export async function pExpectedError(promise, errorClass) {
106
107
  * Shortcut function to simplify error snapshot-matching in tests.
107
108
  */
108
109
  export async function pExpectedErrorString(promise, errorClass) {
109
- return _stringifyAny(await pExpectedError(promise, errorClass));
110
+ const err = await pExpectedError(promise, errorClass);
111
+ return _stringifyAny(err);
112
+ }
113
+ /**
114
+ * Shortcut function to simplify error snapshot-matching in tests.
115
+ */
116
+ export function _expectedErrorString(fn, errorClass) {
117
+ const err = _expectedError(fn, errorClass);
118
+ return _stringifyAny(err);
110
119
  }
@@ -1,6 +1,7 @@
1
1
  /// <reference lib="dom"/>
2
2
  /// <reference lib="dom.iterable"/>
3
3
  import { isServerSide } from '../env';
4
+ import { _assertErrorClassOrRethrow } from '../error/assert';
4
5
  import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject } from '../error/error.util';
5
6
  import { HttpRequestError } from '../error/httpRequestError';
6
7
  import { _clamp } from '../number/number.util';
@@ -96,10 +97,27 @@ export class Fetcher {
96
97
  }
97
98
  return res.body;
98
99
  }
100
+ /**
101
+ * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
102
+ * err, if defined, is strictly HttpRequestError.
103
+ * UPD: actually not, err is typed as Error, as it feels unsafe to guarantee error type.
104
+ * UPD: actually yes - it will return HttpRequestError, and throw if there's an error
105
+ * of any other type.
106
+ */
107
+ async tryFetch(opt) {
108
+ const res = await this.doFetch(opt);
109
+ if (res.err) {
110
+ _assertErrorClassOrRethrow(res.err, HttpRequestError);
111
+ return [res.err, null];
112
+ }
113
+ return [null, res.body];
114
+ }
99
115
  /**
100
116
  * Returns FetcherResponse.
101
117
  * Never throws, returns `err` property in the response instead.
102
118
  * Use this method instead of `throwHttpErrors: false` or try-catching.
119
+ *
120
+ * Note: responseType defaults to `void`, so, override it if you expect different.
103
121
  */
104
122
  async doFetch(opt) {
105
123
  var _a, _b;
@@ -146,7 +164,7 @@ export class Fetcher {
146
164
  }
147
165
  }
148
166
  try {
149
- res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init);
167
+ res.fetchResponse = await Fetcher.callNativeFetch(req.fullUrl, req.init);
150
168
  res.ok = res.fetchResponse.ok;
151
169
  // important to set it to undefined, otherwise it can keep the previous value (from previous try)
152
170
  res.err = undefined;
@@ -250,8 +268,9 @@ export class Fetcher {
250
268
  }
251
269
  /**
252
270
  * This method exists to be able to easily mock it.
271
+ * It is static, so mocking applies to ALL instances (even future ones) of Fetcher at once.
253
272
  */
254
- async callNativeFetch(url, init) {
273
+ static async callNativeFetch(url, init) {
255
274
  return await globalThis.fetch(url, init);
256
275
  }
257
276
  async onNotOkResponse(res) {
@@ -269,6 +288,11 @@ export class Fetcher {
269
288
  cause = _anyToErrorObject(body);
270
289
  }
271
290
  }
291
+ cause || (cause = {
292
+ name: 'Error',
293
+ message: 'Fetch failed',
294
+ data: {},
295
+ });
272
296
  const message = [(_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status, res.signature].filter(Boolean).join(' ');
273
297
  res.err = new HttpRequestError(message, _filterNullishValues({
274
298
  responseStatusCode: ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
@@ -431,7 +455,7 @@ export class Fetcher {
431
455
  const norm = _merge({
432
456
  baseUrl: '',
433
457
  inputUrl: '',
434
- responseType: 'void',
458
+ responseType: 'json',
435
459
  searchParams: {},
436
460
  timeoutSeconds: 30,
437
461
  retryPost: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.157.1",
3
+ "version": "14.159.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
package/readme.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@naturalcycles/js-lib/latest.svg)](https://www.npmjs.com/package/@naturalcycles/js-lib)
6
6
  [![min.gz size](https://badgen.net/bundlephobia/minzip/@naturalcycles/js-lib)](https://bundlephobia.com/result?p=@naturalcycles/js-lib)
7
- [![Actions](https://github.com/NaturalCycles/js-lib/workflows/default/badge.svg)](https://github.com/NaturalCycles/js-lib/actions)
7
+ [![Actions](https://github.com/NaturalCycles/js-lib/workflows/ci/badge.svg)](https://github.com/NaturalCycles/js-lib/actions)
8
8
  [![loc](https://badgen.net/codeclimate/loc/NaturalCycles/js-lib)](https://github.com/NaturalCycles/js-lib)
9
9
  [![Maintainability](https://api.codeclimate.com/v1/badges/c2dc8d53bd79f79b1d8b/maintainability)](https://codeclimate.com/github/NaturalCycles/js-lib/maintainability)
10
10
  [![Test Coverage](https://api.codeclimate.com/v1/badges/c2dc8d53bd79f79b1d8b/test_coverage)](https://codeclimate.com/github/NaturalCycles/js-lib/test_coverage)
@@ -102,6 +102,21 @@ export function _assertIsError<ERR extends Error = Error>(
102
102
  }
103
103
  }
104
104
 
105
+ /**
106
+ * Asserts that passed object is indeed an Error of defined ErrorClass.
107
+ * If yes - returns peacefully (with TypeScript assertion).
108
+ * In not - throws (re-throws) that error up.
109
+ */
110
+ export function _assertErrorClassOrRethrow<ERR extends Error>(
111
+ err: any,
112
+ errorClass: Class<ERR>,
113
+ ): asserts err is ERR {
114
+ if (!(err instanceof errorClass)) {
115
+ // re-throw
116
+ throw err
117
+ }
118
+ }
119
+
105
120
  export function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
106
121
  obj: any,
107
122
  ): asserts obj is ErrorObject<DATA_TYPE> {
@@ -19,7 +19,14 @@ import type { ErrorObject, HttpRequestErrorData } from './error.model'
19
19
  * (by default).
20
20
  */
21
21
  export class HttpRequestError extends AppError<HttpRequestErrorData> {
22
- constructor(message: string, data: HttpRequestErrorData, cause?: ErrorObject) {
22
+ constructor(message: string, data: HttpRequestErrorData, cause: ErrorObject) {
23
23
  super(message, data, cause, 'HttpRequestError')
24
24
  }
25
+
26
+ /**
27
+ * Cause is strictly-defined for HttpRequestError,
28
+ * so it always has a cause.
29
+ * (for dev convenience)
30
+ */
31
+ override cause!: ErrorObject
25
32
  }
package/src/error/try.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { _stringifyAny } from '../string/stringifyAny'
2
2
  import type { Class } from '../typeFest'
3
- import type { AnyFunction } from '../types'
3
+ import type { AnyFunction, ErrorDataTuple } from '../types'
4
4
  import { AppError } from './app.error'
5
+ import { _assertErrorClassOrRethrow } from './assert'
5
6
 
6
7
  /**
7
8
  * Calls a function, returns a Tuple of [error, value].
@@ -10,9 +11,6 @@ import { AppError } from './app.error'
10
11
  *
11
12
  * Similar to pTry, but for sync functions.
12
13
  *
13
- * For convenience, second argument type is non-optional,
14
- * so you can use it without `!`. But you SHOULD always check `if (err)` first!
15
- *
16
14
  * ERR is typed as Error, not `unknown`. While unknown would be more correct,
17
15
  * according to recent TypeScript, Error gives more developer convenience.
18
16
  * In our code we NEVER throw non-errors.
@@ -25,29 +23,35 @@ import { AppError } from './app.error'
25
23
  * if (err) ...do something...
26
24
  * v // go ahead and use v
27
25
  */
28
- export function _try<ERR = Error, RETURN = void>(
29
- fn: () => RETURN,
30
- ): [err: ERR | null, value: RETURN] {
26
+ export function _try<T, ERR extends Error = Error>(
27
+ fn: () => T,
28
+ errorClass?: Class<ERR>,
29
+ ): ErrorDataTuple<T, ERR> {
31
30
  try {
32
31
  return [null, fn()]
33
32
  } catch (err) {
34
- return [err as ERR, undefined as any]
33
+ if (errorClass) {
34
+ _assertErrorClassOrRethrow(err, errorClass)
35
+ }
36
+
37
+ return [err as ERR, null]
35
38
  }
36
39
  }
37
40
 
38
41
  /**
39
42
  * Like _try, but for Promises.
40
- *
41
- * Also, intentionally types second return item as non-optional,
42
- * but you should check for `err` presense first!
43
43
  */
44
- export async function pTry<ERR = Error, RETURN = void>(
45
- promise: Promise<RETURN>,
46
- ): Promise<[err: ERR | null, value: Awaited<RETURN>]> {
44
+ export async function pTry<T, ERR extends Error = Error>(
45
+ promise: Promise<T>,
46
+ errorClass?: Class<ERR>,
47
+ ): Promise<ErrorDataTuple<Awaited<T>, ERR>> {
47
48
  try {
48
49
  return [null, await promise]
49
50
  } catch (err) {
50
- return [err as ERR, undefined as any]
51
+ if (errorClass) {
52
+ _assertErrorClassOrRethrow(err, errorClass)
53
+ }
54
+ return [err as ERR, null]
51
55
  }
52
56
  }
53
57
 
@@ -122,5 +126,17 @@ export async function pExpectedErrorString<ERR = Error>(
122
126
  promise: Promise<any>,
123
127
  errorClass?: Class<ERR>,
124
128
  ): Promise<string> {
125
- return _stringifyAny(await pExpectedError<ERR>(promise, errorClass))
129
+ const err = await pExpectedError<ERR>(promise, errorClass)
130
+ return _stringifyAny(err)
131
+ }
132
+
133
+ /**
134
+ * Shortcut function to simplify error snapshot-matching in tests.
135
+ */
136
+ export function _expectedErrorString<ERR = Error>(
137
+ fn: AnyFunction,
138
+ errorClass?: Class<ERR>,
139
+ ): string {
140
+ const err = _expectedError<ERR>(fn, errorClass)
141
+ return _stringifyAny(err)
126
142
  }
@@ -186,7 +186,7 @@ export interface FetcherOptions {
186
186
  // init?: Partial<RequestInitNormalized>
187
187
 
188
188
  headers?: Record<string, any>
189
- responseType?: FetcherResponseType // default to 'void'
189
+ responseType?: FetcherResponseType // default to 'json'
190
190
 
191
191
  searchParams?: Record<string, any>
192
192
 
@@ -2,6 +2,7 @@
2
2
  /// <reference lib="dom.iterable"/>
3
3
 
4
4
  import { isServerSide } from '../env'
5
+ import { _assertErrorClassOrRethrow } from '../error/assert'
5
6
  import { ErrorLike, ErrorObject } from '../error/error.model'
6
7
  import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject } from '../error/error.util'
7
8
  import { HttpRequestError } from '../error/httpRequestError'
@@ -19,7 +20,7 @@ import { pTimeout, TimeoutError } from '../promise/pTimeout'
19
20
  import { _jsonParse, _jsonParseIfPossible } from '../string/json.util'
20
21
  import { _stringifyAny } from '../string/stringifyAny'
21
22
  import { _ms, _since } from '../time/time.util'
22
- import { NumberOfMilliseconds } from '../types'
23
+ import { ErrorDataTuple, NumberOfMilliseconds } from '../types'
23
24
  import type {
24
25
  FetcherAfterResponseHook,
25
26
  FetcherBeforeRequestHook,
@@ -31,6 +32,7 @@ import type {
31
32
  FetcherResponse,
32
33
  FetcherResponseType,
33
34
  FetcherRetryOptions,
35
+ RequestInitNormalized,
34
36
  } from './fetcher.model'
35
37
  import { HTTP_METHODS } from './http.model'
36
38
  import type { HttpStatusFamily } from './http.model'
@@ -169,10 +171,28 @@ export class Fetcher {
169
171
  return res.body
170
172
  }
171
173
 
174
+ /**
175
+ * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
176
+ * err, if defined, is strictly HttpRequestError.
177
+ * UPD: actually not, err is typed as Error, as it feels unsafe to guarantee error type.
178
+ * UPD: actually yes - it will return HttpRequestError, and throw if there's an error
179
+ * of any other type.
180
+ */
181
+ async tryFetch<T = unknown>(opt: FetcherOptions): Promise<ErrorDataTuple<T, HttpRequestError>> {
182
+ const res = await this.doFetch<T>(opt)
183
+ if (res.err) {
184
+ _assertErrorClassOrRethrow(res.err, HttpRequestError)
185
+ return [res.err, null]
186
+ }
187
+ return [null, res.body]
188
+ }
189
+
172
190
  /**
173
191
  * Returns FetcherResponse.
174
192
  * Never throws, returns `err` property in the response instead.
175
193
  * Use this method instead of `throwHttpErrors: false` or try-catching.
194
+ *
195
+ * Note: responseType defaults to `void`, so, override it if you expect different.
176
196
  */
177
197
  async doFetch<T = unknown>(opt: FetcherOptions): Promise<FetcherResponse<T>> {
178
198
  const req = this.normalizeOptions(opt)
@@ -230,7 +250,7 @@ export class Fetcher {
230
250
  }
231
251
 
232
252
  try {
233
- res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init)
253
+ res.fetchResponse = await Fetcher.callNativeFetch(req.fullUrl, req.init)
234
254
  res.ok = res.fetchResponse.ok
235
255
  // important to set it to undefined, otherwise it can keep the previous value (from previous try)
236
256
  res.err = undefined
@@ -343,13 +363,14 @@ export class Fetcher {
343
363
 
344
364
  /**
345
365
  * This method exists to be able to easily mock it.
366
+ * It is static, so mocking applies to ALL instances (even future ones) of Fetcher at once.
346
367
  */
347
- async callNativeFetch(url: string, init: RequestInit): Promise<Response> {
368
+ static async callNativeFetch(url: string, init: RequestInitNormalized): Promise<Response> {
348
369
  return await globalThis.fetch(url, init)
349
370
  }
350
371
 
351
372
  private async onNotOkResponse(res: FetcherResponse): Promise<void> {
352
- let cause: ErrorObject | undefined
373
+ let cause: ErrorObject
353
374
 
354
375
  if (res.err) {
355
376
  // This is only possible on JSON.parse error, or CORS error,
@@ -363,6 +384,12 @@ export class Fetcher {
363
384
  }
364
385
  }
365
386
 
387
+ cause ||= {
388
+ name: 'Error',
389
+ message: 'Fetch failed',
390
+ data: {},
391
+ }
392
+
366
393
  const message = [res.fetchResponse?.status, res.signature].filter(Boolean).join(' ')
367
394
 
368
395
  res.err = new HttpRequestError(
@@ -545,7 +572,7 @@ export class Fetcher {
545
572
  {
546
573
  baseUrl: '',
547
574
  inputUrl: '',
548
- responseType: 'void',
575
+ responseType: 'json',
549
576
  searchParams: {},
550
577
  timeoutSeconds: 30,
551
578
  retryPost: false,
package/src/types.ts CHANGED
@@ -278,3 +278,15 @@ export const _objectAssign = Object.assign as <T extends AnyObject>(
278
278
  target: T,
279
279
  part: Partial<T>,
280
280
  ) => T
281
+
282
+ /**
283
+ * Defines a tuple of [err, data]
284
+ * where only 1 of them exists.
285
+ * Either error exists and data is null
286
+ * Or error is null and data is defined.
287
+ * This forces you to check `if (err)`, which lets
288
+ * TypeScript infer the existence of `data`.
289
+ *
290
+ * Functions like pTry use that.
291
+ */
292
+ export type ErrorDataTuple<T = unknown, ERR = Error> = [err: null, data: T] | [err: ERR, data: null]