@naturalcycles/js-lib 14.174.0 → 14.175.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.
@@ -20,7 +20,7 @@ const __1 = require("..");
20
20
  function _assert(condition, // will be evaluated as Boolean
21
21
  message, errorData) {
22
22
  if (!condition) {
23
- throw new __1.AssertionError(message || 'see stacktrace', {
23
+ throw new __1.AssertionError(message || 'condition failed', {
24
24
  userFriendly: true,
25
25
  ...errorData,
26
26
  });
@@ -71,11 +71,7 @@ function _assertDeepEquals(actual, expected, message, errorData) {
71
71
  exports._assertDeepEquals = _assertDeepEquals;
72
72
  function _assertIsError(err, errorClass = Error) {
73
73
  if (!(err instanceof errorClass)) {
74
- const msg = [
75
- `expected to be instanceof ${errorClass.name}`,
76
- `actual typeof: ${typeof err}`,
77
- ].join('\n');
78
- throw new __1.AssertionError(msg, {
74
+ throw new __1.AssertionError(`Expected to be instanceof ${errorClass.name}, actual typeof: ${typeof err}`, {
79
75
  userFriendly: true,
80
76
  });
81
77
  }
@@ -95,8 +91,7 @@ function _assertErrorClassOrRethrow(err, errorClass) {
95
91
  exports._assertErrorClassOrRethrow = _assertErrorClassOrRethrow;
96
92
  function _assertIsErrorObject(obj) {
97
93
  if (!(0, __1._isErrorObject)(obj)) {
98
- const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n');
99
- throw new __1.AssertionError(msg, {
94
+ throw new __1.AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`, {
100
95
  userFriendly: true,
101
96
  });
102
97
  }
@@ -112,11 +107,7 @@ function _assertIsNumber(v, message) {
112
107
  exports._assertIsNumber = _assertIsNumber;
113
108
  function _assertTypeOf(v, expectedType, message) {
114
109
  if (typeof v !== expectedType) {
115
- const msg = [
116
- message || `unexpected type`,
117
- `expected: ${expectedType}`,
118
- `got : ${typeof v}`,
119
- ].join('\n');
110
+ const msg = message || `Expected typeof ${expectedType}, actual typeof: ${typeof v}`;
120
111
  throw new __1.AssertionError(msg, {
121
112
  userFriendly: true,
122
113
  });
@@ -143,5 +143,5 @@ export declare class TimeoutError extends AppError {
143
143
  * "Pass" means "no error".
144
144
  */
145
145
  export declare class UnexpectedPassError extends AppError {
146
- constructor();
146
+ constructor(message?: string);
147
147
  }
@@ -342,8 +342,8 @@ exports.TimeoutError = TimeoutError;
342
342
  * "Pass" means "no error".
343
343
  */
344
344
  class UnexpectedPassError extends AppError {
345
- constructor() {
346
- super('expected error was not thrown', {}, {
345
+ constructor(message) {
346
+ super(message || 'expected error was not thrown', {}, {
347
347
  name: 'UnexpectedPassError',
348
348
  });
349
349
  }
package/dist/error/try.js CHANGED
@@ -61,18 +61,16 @@ exports.pTry = pTry;
61
61
  function _expectedError(fn, errorClass) {
62
62
  try {
63
63
  fn();
64
- // Unexpected!
65
- throw new error_util_1.UnexpectedPassError();
66
64
  }
67
65
  catch (err) {
68
- if (err instanceof error_util_1.UnexpectedPassError)
69
- throw err; // re-throw
70
66
  if (errorClass && !(err instanceof errorClass)) {
71
67
  console.warn(`_expectedError expected ${errorClass.constructor.name} but got different error class`);
72
68
  throw err;
73
69
  }
74
70
  return err; // this is expected!
75
71
  }
72
+ // Unexpected!
73
+ throw new error_util_1.UnexpectedPassError();
76
74
  }
77
75
  exports._expectedError = _expectedError;
78
76
  /**
@@ -86,18 +84,16 @@ exports._expectedError = _expectedError;
86
84
  async function pExpectedError(promise, errorClass) {
87
85
  try {
88
86
  await promise;
89
- // Unexpected!
90
- throw new error_util_1.UnexpectedPassError();
91
87
  }
92
88
  catch (err) {
93
- if (err instanceof error_util_1.UnexpectedPassError)
94
- throw err; // re-throw
95
89
  if (errorClass && !(err instanceof errorClass)) {
96
90
  console.warn(`pExpectedError expected ${errorClass.constructor.name} but got different error class`);
97
91
  throw err;
98
92
  }
99
93
  return err; // this is expected!
100
94
  }
95
+ // Unexpected!
96
+ throw new error_util_1.UnexpectedPassError();
101
97
  }
102
98
  exports.pExpectedError = pExpectedError;
103
99
  /**
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2022" />
1
2
  /// <reference lib="dom" />
2
3
  /// <reference lib="dom.iterable" />
3
4
  import { HttpRequestError } from '../error/error.util';
@@ -49,6 +50,13 @@ export declare class Fetcher {
49
50
  */
50
51
  getReadableStream(url: string, opt?: FetcherOptions): Promise<ReadableStream<Uint8Array>>;
51
52
  fetch<T = unknown>(opt: FetcherOptions): Promise<T>;
53
+ /**
54
+ * Execute fetch and expect/assert it to return an Error (which will be wrapped in
55
+ * HttpRequestError as it normally would).
56
+ * If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
57
+ * Useful in unit testing.
58
+ */
59
+ expectError(opt: FetcherOptions): Promise<HttpRequestError>;
52
60
  /**
53
61
  * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
54
62
  * err, if defined, is strictly HttpRequestError.
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /// <reference lib="es2022"/>
2
3
  /// <reference lib="dom"/>
3
4
  /// <reference lib="dom.iterable"/>
4
5
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -120,6 +121,20 @@ class Fetcher {
120
121
  }
121
122
  return res.body;
122
123
  }
124
+ /**
125
+ * Execute fetch and expect/assert it to return an Error (which will be wrapped in
126
+ * HttpRequestError as it normally would).
127
+ * If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
128
+ * Useful in unit testing.
129
+ */
130
+ async expectError(opt) {
131
+ const res = await this.doFetch(opt);
132
+ if (!res.err) {
133
+ throw new error_util_1.UnexpectedPassError('Fetch was expected to error');
134
+ }
135
+ (0, assert_1._assertIsError)(res.err, error_util_1.HttpRequestError);
136
+ return res.err;
137
+ }
123
138
  /**
124
139
  * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
125
140
  * err, if defined, is strictly HttpRequestError.
@@ -167,6 +182,9 @@ class Fetcher {
167
182
  // setup timeout
168
183
  let timeoutId;
169
184
  if (timeoutSeconds) {
185
+ // Used for Request timeout (when timeoutSeconds is set),
186
+ // but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
187
+ // UPD: no, not using for "downloadBody" currently
170
188
  const abortController = new AbortController();
171
189
  req.init.signal = abortController.signal;
172
190
  timeoutId = setTimeout(() => {
@@ -210,10 +228,13 @@ class Fetcher {
210
228
  // We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
211
229
  await (0, pTimeout_1.pTimeout)(async () => await this.onOkResponse(res), {
212
230
  timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
213
- name: 'Fetcher.onOkResponse',
231
+ name: 'Fetcher.downloadBody',
214
232
  });
215
233
  }
216
234
  catch (err) {
235
+ // Important to cancel the original request to not keep it running (and occupying resources)
236
+ // UPD: no, we probably don't need to, because "request" has already completed, it's just the "body" is pending
237
+ // if (err instanceof TimeoutError) {}
217
238
  // onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
218
239
  res.err = (0, error_util_1._anyToError)(err);
219
240
  res.ok = false;
@@ -314,10 +335,17 @@ class Fetcher {
314
335
  message: 'Fetch failed',
315
336
  data: {},
316
337
  };
317
- const message = [res.fetchResponse?.status, res.signature].filter(Boolean).join(' ');
338
+ let responseStatusCode = res.fetchResponse?.status || 0;
339
+ if (res.statusFamily === 2) {
340
+ // important to reset httpStatusCode to 0 in this case, as status 2xx can be misleading
341
+ res.statusFamily = undefined;
342
+ res.statusCode = undefined;
343
+ responseStatusCode = 0;
344
+ }
345
+ const message = [res.statusCode, res.signature].filter(Boolean).join(' ');
318
346
  res.err = new error_util_1.HttpRequestError(message, (0, object_util_1._filterNullishValues)({
319
347
  response: res.fetchResponse,
320
- responseStatusCode: res.fetchResponse?.status || 0,
348
+ responseStatusCode,
321
349
  // These properties are provided to be used in e.g custom Sentry error grouping
322
350
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
323
351
  // Enabled, cause `data` is not printed by default when error is HttpError
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2022" />
1
2
  /// <reference lib="dom" />
2
3
  import type { CommonLogger } from '../log/commonLogger';
3
4
  import type { Promisable } from '../typeFest';
@@ -1,3 +1,4 @@
1
1
  "use strict";
2
+ /// <reference lib="es2022"/>
2
3
  /// <reference lib="dom"/>
3
4
  Object.defineProperty(exports, "__esModule", { value: true });
package/dist/web.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2022" />
1
2
  /// <reference lib="dom" />
2
3
  import { StringMap } from './types';
3
4
  /**
package/dist/web.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ /// <reference lib="es2022"/>
2
3
  /// <reference lib="dom"/>
3
4
  Object.defineProperty(exports, "__esModule", { value: true });
4
5
  exports.InMemoryWebStorage = void 0;
@@ -17,7 +17,7 @@ import { _deepEquals, _isErrorObject, _stringifyAny, AssertionError } from '..';
17
17
  export function _assert(condition, // will be evaluated as Boolean
18
18
  message, errorData) {
19
19
  if (!condition) {
20
- throw new AssertionError(message || 'see stacktrace', Object.assign({ userFriendly: true }, errorData));
20
+ throw new AssertionError(message || 'condition failed', Object.assign({ userFriendly: true }, errorData));
21
21
  }
22
22
  }
23
23
  /**
@@ -56,11 +56,7 @@ export function _assertDeepEquals(actual, expected, message, errorData) {
56
56
  }
57
57
  export function _assertIsError(err, errorClass = Error) {
58
58
  if (!(err instanceof errorClass)) {
59
- const msg = [
60
- `expected to be instanceof ${errorClass.name}`,
61
- `actual typeof: ${typeof err}`,
62
- ].join('\n');
63
- throw new AssertionError(msg, {
59
+ throw new AssertionError(`Expected to be instanceof ${errorClass.name}, actual typeof: ${typeof err}`, {
64
60
  userFriendly: true,
65
61
  });
66
62
  }
@@ -78,8 +74,7 @@ export function _assertErrorClassOrRethrow(err, errorClass) {
78
74
  }
79
75
  export function _assertIsErrorObject(obj) {
80
76
  if (!_isErrorObject(obj)) {
81
- const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n');
82
- throw new AssertionError(msg, {
77
+ throw new AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`, {
83
78
  userFriendly: true,
84
79
  });
85
80
  }
@@ -92,11 +87,7 @@ export function _assertIsNumber(v, message) {
92
87
  }
93
88
  export function _assertTypeOf(v, expectedType, message) {
94
89
  if (typeof v !== expectedType) {
95
- const msg = [
96
- message || `unexpected type`,
97
- `expected: ${expectedType}`,
98
- `got : ${typeof v}`,
99
- ].join('\n');
90
+ const msg = message || `Expected typeof ${expectedType}, actual typeof: ${typeof v}`;
100
91
  throw new AssertionError(msg, {
101
92
  userFriendly: true,
102
93
  });
@@ -319,8 +319,8 @@ export class TimeoutError extends AppError {
319
319
  * "Pass" means "no error".
320
320
  */
321
321
  export class UnexpectedPassError extends AppError {
322
- constructor() {
323
- super('expected error was not thrown', {}, {
322
+ constructor(message) {
323
+ super(message || 'expected error was not thrown', {}, {
324
324
  name: 'UnexpectedPassError',
325
325
  });
326
326
  }
@@ -56,18 +56,16 @@ export async function pTry(promise, errorClass) {
56
56
  export function _expectedError(fn, errorClass) {
57
57
  try {
58
58
  fn();
59
- // Unexpected!
60
- throw new UnexpectedPassError();
61
59
  }
62
60
  catch (err) {
63
- if (err instanceof UnexpectedPassError)
64
- throw err; // re-throw
65
61
  if (errorClass && !(err instanceof errorClass)) {
66
62
  console.warn(`_expectedError expected ${errorClass.constructor.name} but got different error class`);
67
63
  throw err;
68
64
  }
69
65
  return err; // this is expected!
70
66
  }
67
+ // Unexpected!
68
+ throw new UnexpectedPassError();
71
69
  }
72
70
  /**
73
71
  * Awaits passed `promise`, expects is to throw (reject), catches the expected error and returns.
@@ -80,18 +78,16 @@ export function _expectedError(fn, errorClass) {
80
78
  export async function pExpectedError(promise, errorClass) {
81
79
  try {
82
80
  await promise;
83
- // Unexpected!
84
- throw new UnexpectedPassError();
85
81
  }
86
82
  catch (err) {
87
- if (err instanceof UnexpectedPassError)
88
- throw err; // re-throw
89
83
  if (errorClass && !(err instanceof errorClass)) {
90
84
  console.warn(`pExpectedError expected ${errorClass.constructor.name} but got different error class`);
91
85
  throw err;
92
86
  }
93
87
  return err; // this is expected!
94
88
  }
89
+ // Unexpected!
90
+ throw new UnexpectedPassError();
95
91
  }
96
92
  /**
97
93
  * Shortcut function to simplify error snapshot-matching in tests.
@@ -1,9 +1,10 @@
1
+ /// <reference lib="es2022"/>
1
2
  /// <reference lib="dom"/>
2
3
  /// <reference lib="dom.iterable"/>
3
4
  var _a;
4
5
  import { isServerSide } from '../env';
5
- import { _assertErrorClassOrRethrow } from '../error/assert';
6
- import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject, HttpRequestError, TimeoutError, } from '../error/error.util';
6
+ import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert';
7
+ import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject, HttpRequestError, TimeoutError, UnexpectedPassError, } from '../error/error.util';
7
8
  import { _clamp } from '../number/number.util';
8
9
  import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, _pick, } from '../object/object.util';
9
10
  import { pDelay } from '../promise/pDelay';
@@ -97,6 +98,20 @@ export class Fetcher {
97
98
  }
98
99
  return res.body;
99
100
  }
101
+ /**
102
+ * Execute fetch and expect/assert it to return an Error (which will be wrapped in
103
+ * HttpRequestError as it normally would).
104
+ * If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
105
+ * Useful in unit testing.
106
+ */
107
+ async expectError(opt) {
108
+ const res = await this.doFetch(opt);
109
+ if (!res.err) {
110
+ throw new UnexpectedPassError('Fetch was expected to error');
111
+ }
112
+ _assertIsError(res.err, HttpRequestError);
113
+ return res.err;
114
+ }
100
115
  /**
101
116
  * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
102
117
  * err, if defined, is strictly HttpRequestError.
@@ -145,6 +160,9 @@ export class Fetcher {
145
160
  // setup timeout
146
161
  let timeoutId;
147
162
  if (timeoutSeconds) {
163
+ // Used for Request timeout (when timeoutSeconds is set),
164
+ // but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
165
+ // UPD: no, not using for "downloadBody" currently
148
166
  const abortController = new AbortController();
149
167
  req.init.signal = abortController.signal;
150
168
  timeoutId = setTimeout(() => {
@@ -188,10 +206,13 @@ export class Fetcher {
188
206
  // We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
189
207
  await pTimeout(async () => await this.onOkResponse(res), {
190
208
  timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
191
- name: 'Fetcher.onOkResponse',
209
+ name: 'Fetcher.downloadBody',
192
210
  });
193
211
  }
194
212
  catch (err) {
213
+ // Important to cancel the original request to not keep it running (and occupying resources)
214
+ // UPD: no, we probably don't need to, because "request" has already completed, it's just the "body" is pending
215
+ // if (err instanceof TimeoutError) {}
195
216
  // onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
196
217
  res.err = _anyToError(err);
197
218
  res.ok = false;
@@ -274,7 +295,7 @@ export class Fetcher {
274
295
  return await globalThis.fetch(url, init);
275
296
  }
276
297
  async onNotOkResponse(res) {
277
- var _b, _c;
298
+ var _b;
278
299
  let cause;
279
300
  if (res.err) {
280
301
  // This is only possible on JSON.parse error, or CORS error,
@@ -293,10 +314,17 @@ export class Fetcher {
293
314
  message: 'Fetch failed',
294
315
  data: {},
295
316
  });
296
- const message = [(_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status, res.signature].filter(Boolean).join(' ');
317
+ let responseStatusCode = ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0;
318
+ if (res.statusFamily === 2) {
319
+ // important to reset httpStatusCode to 0 in this case, as status 2xx can be misleading
320
+ res.statusFamily = undefined;
321
+ res.statusCode = undefined;
322
+ responseStatusCode = 0;
323
+ }
324
+ const message = [res.statusCode, res.signature].filter(Boolean).join(' ');
297
325
  res.err = new HttpRequestError(message, _filterNullishValues({
298
326
  response: res.fetchResponse,
299
- responseStatusCode: ((_c = res.fetchResponse) === null || _c === void 0 ? void 0 : _c.status) || 0,
327
+ responseStatusCode,
300
328
  // These properties are provided to be used in e.g custom Sentry error grouping
301
329
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
302
330
  // Enabled, cause `data` is not printed by default when error is HttpError
@@ -1,2 +1,3 @@
1
+ /// <reference lib="es2022"/>
1
2
  /// <reference lib="dom"/>
2
3
  export {};
package/dist-esm/web.js CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2022"/>
1
2
  /// <reference lib="dom"/>
2
3
  /**
3
4
  * Implements WebStorage API by using in-memory storage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.174.0",
3
+ "version": "14.175.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -22,7 +22,7 @@ export function _assert(
22
22
  errorData?: ErrorData,
23
23
  ): asserts condition {
24
24
  if (!condition) {
25
- throw new AssertionError(message || 'see stacktrace', {
25
+ throw new AssertionError(message || 'condition failed', {
26
26
  userFriendly: true,
27
27
  ...errorData,
28
28
  })
@@ -90,14 +90,12 @@ export function _assertIsError<ERR extends Error = Error>(
90
90
  errorClass: Class<ERR> = Error as any,
91
91
  ): asserts err is ERR {
92
92
  if (!(err instanceof errorClass)) {
93
- const msg = [
94
- `expected to be instanceof ${errorClass.name}`,
95
- `actual typeof: ${typeof err}`,
96
- ].join('\n')
97
-
98
- throw new AssertionError(msg, {
99
- userFriendly: true,
100
- })
93
+ throw new AssertionError(
94
+ `Expected to be instanceof ${errorClass.name}, actual typeof: ${typeof err}`,
95
+ {
96
+ userFriendly: true,
97
+ },
98
+ )
101
99
  }
102
100
  }
103
101
 
@@ -120,9 +118,7 @@ export function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
120
118
  obj: any,
121
119
  ): asserts obj is ErrorObject<DATA_TYPE> {
122
120
  if (!_isErrorObject(obj)) {
123
- const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n')
124
-
125
- throw new AssertionError(msg, {
121
+ throw new AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`, {
126
122
  userFriendly: true,
127
123
  })
128
124
  }
@@ -138,12 +134,7 @@ export function _assertIsNumber(v: any, message?: string): asserts v is number {
138
134
 
139
135
  export function _assertTypeOf<T>(v: any, expectedType: string, message?: string): asserts v is T {
140
136
  if (typeof v !== expectedType) {
141
- const msg = [
142
- message || `unexpected type`,
143
- `expected: ${expectedType}`,
144
- `got : ${typeof v}`,
145
- ].join('\n')
146
-
137
+ const msg = message || `Expected typeof ${expectedType}, actual typeof: ${typeof v}`
147
138
  throw new AssertionError(msg, {
148
139
  userFriendly: true,
149
140
  })
@@ -441,9 +441,9 @@ export class TimeoutError extends AppError {
441
441
  * "Pass" means "no error".
442
442
  */
443
443
  export class UnexpectedPassError extends AppError {
444
- constructor() {
444
+ constructor(message?: string) {
445
445
  super(
446
- 'expected error was not thrown',
446
+ message || 'expected error was not thrown',
447
447
  {},
448
448
  {
449
449
  name: 'UnexpectedPassError',
package/src/error/try.ts CHANGED
@@ -66,10 +66,7 @@ export async function pTry<T, ERR extends Error = Error>(
66
66
  export function _expectedError<ERR = Error>(fn: AnyFunction, errorClass?: Class<ERR>): ERR {
67
67
  try {
68
68
  fn()
69
- // Unexpected!
70
- throw new UnexpectedPassError()
71
69
  } catch (err) {
72
- if (err instanceof UnexpectedPassError) throw err // re-throw
73
70
  if (errorClass && !(err instanceof errorClass)) {
74
71
  console.warn(
75
72
  `_expectedError expected ${errorClass.constructor.name} but got different error class`,
@@ -78,6 +75,9 @@ export function _expectedError<ERR = Error>(fn: AnyFunction, errorClass?: Class<
78
75
  }
79
76
  return err as ERR // this is expected!
80
77
  }
78
+
79
+ // Unexpected!
80
+ throw new UnexpectedPassError()
81
81
  }
82
82
 
83
83
  /**
@@ -94,10 +94,7 @@ export async function pExpectedError<ERR = Error>(
94
94
  ): Promise<ERR> {
95
95
  try {
96
96
  await promise
97
- // Unexpected!
98
- throw new UnexpectedPassError()
99
97
  } catch (err) {
100
- if (err instanceof UnexpectedPassError) throw err // re-throw
101
98
  if (errorClass && !(err instanceof errorClass)) {
102
99
  console.warn(
103
100
  `pExpectedError expected ${errorClass.constructor.name} but got different error class`,
@@ -106,6 +103,9 @@ export async function pExpectedError<ERR = Error>(
106
103
  }
107
104
  return err as ERR // this is expected!
108
105
  }
106
+
107
+ // Unexpected!
108
+ throw new UnexpectedPassError()
109
109
  }
110
110
 
111
111
  /**
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2022"/>
1
2
  /// <reference lib="dom"/>
2
3
 
3
4
  import type { CommonLogger } from '../log/commonLogger'
@@ -1,8 +1,9 @@
1
+ /// <reference lib="es2022"/>
1
2
  /// <reference lib="dom"/>
2
3
  /// <reference lib="dom.iterable"/>
3
4
 
4
5
  import { isServerSide } from '../env'
5
- import { _assertErrorClassOrRethrow } from '../error/assert'
6
+ import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert'
6
7
  import { ErrorLike, ErrorObject } from '../error/error.model'
7
8
  import {
8
9
  _anyToError,
@@ -10,6 +11,7 @@ import {
10
11
  _errorLikeToErrorObject,
11
12
  HttpRequestError,
12
13
  TimeoutError,
14
+ UnexpectedPassError,
13
15
  } from '../error/error.util'
14
16
  import { _clamp } from '../number/number.util'
15
17
  import {
@@ -185,6 +187,23 @@ export class Fetcher {
185
187
  return res.body
186
188
  }
187
189
 
190
+ /**
191
+ * Execute fetch and expect/assert it to return an Error (which will be wrapped in
192
+ * HttpRequestError as it normally would).
193
+ * If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
194
+ * Useful in unit testing.
195
+ */
196
+ async expectError(opt: FetcherOptions): Promise<HttpRequestError> {
197
+ const res = await this.doFetch(opt)
198
+
199
+ if (!res.err) {
200
+ throw new UnexpectedPassError('Fetch was expected to error')
201
+ }
202
+
203
+ _assertIsError(res.err, HttpRequestError)
204
+ return res.err
205
+ }
206
+
188
207
  /**
189
208
  * Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
190
209
  * err, if defined, is strictly HttpRequestError.
@@ -241,6 +260,9 @@ export class Fetcher {
241
260
  // setup timeout
242
261
  let timeoutId: number | undefined
243
262
  if (timeoutSeconds) {
263
+ // Used for Request timeout (when timeoutSeconds is set),
264
+ // but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
265
+ // UPD: no, not using for "downloadBody" currently
244
266
  const abortController = new AbortController()
245
267
  req.init.signal = abortController.signal
246
268
  timeoutId = setTimeout(() => {
@@ -290,10 +312,14 @@ export class Fetcher {
290
312
  await this.onOkResponse(res as FetcherResponse<T> & { fetchResponse: Response }),
291
313
  {
292
314
  timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
293
- name: 'Fetcher.onOkResponse',
315
+ name: 'Fetcher.downloadBody',
294
316
  },
295
317
  )
296
318
  } catch (err) {
319
+ // Important to cancel the original request to not keep it running (and occupying resources)
320
+ // UPD: no, we probably don't need to, because "request" has already completed, it's just the "body" is pending
321
+ // if (err instanceof TimeoutError) {}
322
+
297
323
  // onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
298
324
  res.err = _anyToError(err)
299
325
  res.ok = false
@@ -404,13 +430,21 @@ export class Fetcher {
404
430
  data: {},
405
431
  }
406
432
 
407
- const message = [res.fetchResponse?.status, res.signature].filter(Boolean).join(' ')
433
+ let responseStatusCode = res.fetchResponse?.status || 0
434
+ if (res.statusFamily === 2) {
435
+ // important to reset httpStatusCode to 0 in this case, as status 2xx can be misleading
436
+ res.statusFamily = undefined
437
+ res.statusCode = undefined
438
+ responseStatusCode = 0
439
+ }
440
+
441
+ const message = [res.statusCode, res.signature].filter(Boolean).join(' ')
408
442
 
409
443
  res.err = new HttpRequestError(
410
444
  message,
411
445
  _filterNullishValues({
412
446
  response: res.fetchResponse,
413
- responseStatusCode: res.fetchResponse?.status || 0,
447
+ responseStatusCode,
414
448
  // These properties are provided to be used in e.g custom Sentry error grouping
415
449
  // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
416
450
  // Enabled, cause `data` is not printed by default when error is HttpError
package/src/web.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference lib="es2022"/>
1
2
  /// <reference lib="dom"/>
2
3
 
3
4
  import { StringMap } from './types'