@naturalcycles/js-lib 14.157.0 → 14.158.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.
- package/dist/error/assert.d.ts +6 -0
- package/dist/error/assert.js +13 -1
- package/dist/error/httpRequestError.d.ts +7 -1
- package/dist/error/try.d.ts +7 -9
- package/dist/error/try.js +22 -12
- package/dist/http/fetcher.d.ts +23 -2
- package/dist/http/fetcher.js +89 -45
- package/dist/http/fetcher.model.d.ts +15 -5
- package/dist/promise/pTimeout.d.ts +2 -2
- package/dist/types.d.ts +11 -0
- package/dist-esm/error/assert.js +11 -0
- package/dist-esm/error/try.js +20 -11
- package/dist-esm/http/fetcher.js +89 -46
- package/package.json +1 -1
- package/src/error/assert.ts +15 -0
- package/src/error/httpRequestError.ts +8 -1
- package/src/error/try.ts +32 -16
- package/src/http/fetcher.model.ts +17 -5
- package/src/http/fetcher.ts +101 -51
- package/src/promise/pTimeout.ts +2 -2
- package/src/types.ts +12 -0
package/dist/error/assert.d.ts
CHANGED
|
@@ -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;
|
package/dist/error/assert.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/error/try.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
2
|
/// <reference lib="dom.iterable" />
|
|
3
|
-
import
|
|
3
|
+
import { HttpRequestError } from '../error/httpRequestError';
|
|
4
|
+
import { ErrorDataTuple } from '../types';
|
|
5
|
+
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse, FetcherMockFunction, RequestInitNormalized } from './fetcher.model';
|
|
6
|
+
/**
|
|
7
|
+
* Allows to mock Fetcher.callNativeFetch globally (for all Fetcher instances out there).
|
|
8
|
+
*
|
|
9
|
+
* Call it with `null` to reset/unmock.
|
|
10
|
+
*
|
|
11
|
+
* Please note that jest.spyOn(someFetcher, 'callNativeFetch') has higher priority than
|
|
12
|
+
* a global mock, so it'll run instead of a global mock.
|
|
13
|
+
*/
|
|
14
|
+
export declare function setGlobalFetcherMock(fn: FetcherMockFunction | null): void;
|
|
4
15
|
/**
|
|
5
16
|
* Experimental wrapper around Fetch.
|
|
6
17
|
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
@@ -39,17 +50,27 @@ export declare class Fetcher {
|
|
|
39
50
|
*/
|
|
40
51
|
getReadableStream(url: string, opt?: FetcherOptions): Promise<ReadableStream<Uint8Array>>;
|
|
41
52
|
fetch<T = unknown>(opt: FetcherOptions): Promise<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
|
|
55
|
+
* err, if defined, is strictly HttpRequestError.
|
|
56
|
+
* UPD: actually not, err is typed as Error, as it feels unsafe to guarantee error type.
|
|
57
|
+
* UPD: actually yes - it will return HttpRequestError, and throw if there's an error
|
|
58
|
+
* of any other type.
|
|
59
|
+
*/
|
|
60
|
+
tryFetch<T = unknown>(opt: FetcherOptions): Promise<ErrorDataTuple<T, HttpRequestError>>;
|
|
42
61
|
/**
|
|
43
62
|
* Returns FetcherResponse.
|
|
44
63
|
* Never throws, returns `err` property in the response instead.
|
|
45
64
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
65
|
+
*
|
|
66
|
+
* Note: responseType defaults to `void`, so, override it if you expect different.
|
|
46
67
|
*/
|
|
47
68
|
doFetch<T = unknown>(opt: FetcherOptions): Promise<FetcherResponse<T>>;
|
|
48
69
|
private onOkResponse;
|
|
49
70
|
/**
|
|
50
71
|
* This method exists to be able to easily mock it.
|
|
51
72
|
*/
|
|
52
|
-
callNativeFetch(url: string, init:
|
|
73
|
+
callNativeFetch(url: string, init: RequestInitNormalized): Promise<Response>;
|
|
53
74
|
private onNotOkResponse;
|
|
54
75
|
private processRetry;
|
|
55
76
|
private getRetryTimeout;
|
package/dist/http/fetcher.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/// <reference lib="dom"/>
|
|
3
3
|
/// <reference lib="dom.iterable"/>
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.getFetcher = exports.Fetcher = void 0;
|
|
5
|
+
exports.getFetcher = exports.Fetcher = exports.setGlobalFetcherMock = 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");
|
|
@@ -28,6 +29,19 @@ const defRetryOptions = {
|
|
|
28
29
|
timeoutMax: 30000,
|
|
29
30
|
timeoutMultiplier: 2,
|
|
30
31
|
};
|
|
32
|
+
let globalFetcherMock = null;
|
|
33
|
+
/**
|
|
34
|
+
* Allows to mock Fetcher.callNativeFetch globally (for all Fetcher instances out there).
|
|
35
|
+
*
|
|
36
|
+
* Call it with `null` to reset/unmock.
|
|
37
|
+
*
|
|
38
|
+
* Please note that jest.spyOn(someFetcher, 'callNativeFetch') has higher priority than
|
|
39
|
+
* a global mock, so it'll run instead of a global mock.
|
|
40
|
+
*/
|
|
41
|
+
function setGlobalFetcherMock(fn) {
|
|
42
|
+
globalFetcherMock = fn;
|
|
43
|
+
}
|
|
44
|
+
exports.setGlobalFetcherMock = setGlobalFetcherMock;
|
|
31
45
|
/**
|
|
32
46
|
* Experimental wrapper around Fetch.
|
|
33
47
|
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
@@ -112,28 +126,32 @@ class Fetcher {
|
|
|
112
126
|
}
|
|
113
127
|
return res.body;
|
|
114
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
|
|
131
|
+
* err, if defined, is strictly HttpRequestError.
|
|
132
|
+
* UPD: actually not, err is typed as Error, as it feels unsafe to guarantee error type.
|
|
133
|
+
* UPD: actually yes - it will return HttpRequestError, and throw if there's an error
|
|
134
|
+
* of any other type.
|
|
135
|
+
*/
|
|
136
|
+
async tryFetch(opt) {
|
|
137
|
+
const res = await this.doFetch(opt);
|
|
138
|
+
if (res.err) {
|
|
139
|
+
(0, assert_1._assertErrorClassOrRethrow)(res.err, httpRequestError_1.HttpRequestError);
|
|
140
|
+
return [res.err, null];
|
|
141
|
+
}
|
|
142
|
+
return [null, res.body];
|
|
143
|
+
}
|
|
115
144
|
/**
|
|
116
145
|
* Returns FetcherResponse.
|
|
117
146
|
* Never throws, returns `err` property in the response instead.
|
|
118
147
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
148
|
+
*
|
|
149
|
+
* Note: responseType defaults to `void`, so, override it if you expect different.
|
|
119
150
|
*/
|
|
120
151
|
async doFetch(opt) {
|
|
121
152
|
const req = this.normalizeOptions(opt);
|
|
122
153
|
const { logger } = this.cfg;
|
|
123
154
|
const { timeoutSeconds, init: { method }, } = req;
|
|
124
|
-
// setup timeout
|
|
125
|
-
let timeout;
|
|
126
|
-
if (timeoutSeconds) {
|
|
127
|
-
const abortController = new AbortController();
|
|
128
|
-
req.init.signal = abortController.signal;
|
|
129
|
-
timeout = setTimeout(() => {
|
|
130
|
-
// Apparently, providing a `string` reason to abort() causes Undici to throw `invalid_argument` error,
|
|
131
|
-
// so, we're wrapping it in a TimeoutError instance
|
|
132
|
-
abortController.abort(new pTimeout_1.TimeoutError(`request timed out after ${timeoutSeconds} sec`));
|
|
133
|
-
// abortController.abort(`timeout of ${timeoutSeconds} sec`)
|
|
134
|
-
// abortController.abort()
|
|
135
|
-
}, timeoutSeconds * 1000);
|
|
136
|
-
}
|
|
137
155
|
for (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
138
156
|
await hook(req);
|
|
139
157
|
}
|
|
@@ -152,6 +170,18 @@ class Fetcher {
|
|
|
152
170
|
};
|
|
153
171
|
while (!res.retryStatus.retryStopped) {
|
|
154
172
|
req.started = Date.now();
|
|
173
|
+
// setup timeout
|
|
174
|
+
let timeoutId;
|
|
175
|
+
if (timeoutSeconds) {
|
|
176
|
+
const abortController = new AbortController();
|
|
177
|
+
req.init.signal = abortController.signal;
|
|
178
|
+
timeoutId = setTimeout(() => {
|
|
179
|
+
// console.log(`actual request timed out in ${_since(req.started)}`)
|
|
180
|
+
// Apparently, providing a `string` reason to abort() causes Undici to throw `invalid_argument` error,
|
|
181
|
+
// so, we're wrapping it in a TimeoutError instance
|
|
182
|
+
abortController.abort(new pTimeout_1.TimeoutError(`request timed out after ${timeoutSeconds} sec`));
|
|
183
|
+
}, timeoutSeconds * 1000);
|
|
184
|
+
}
|
|
155
185
|
if (this.cfg.logRequest) {
|
|
156
186
|
const { retryAttempt } = res.retryStatus;
|
|
157
187
|
logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
|
|
@@ -175,14 +205,30 @@ class Fetcher {
|
|
|
175
205
|
// important to set it to undefined, otherwise it can keep the previous value (from previous try)
|
|
176
206
|
res.fetchResponse = undefined;
|
|
177
207
|
}
|
|
208
|
+
finally {
|
|
209
|
+
clearTimeout(timeoutId);
|
|
210
|
+
// Separate Timeout will be introduced to "download and parse the body"
|
|
211
|
+
}
|
|
178
212
|
res.statusFamily = this.getStatusFamily(res);
|
|
179
213
|
res.statusCode = res.fetchResponse?.status;
|
|
180
214
|
if (res.fetchResponse?.ok) {
|
|
181
|
-
|
|
215
|
+
try {
|
|
216
|
+
// We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
|
|
217
|
+
await (0, pTimeout_1.pTimeout)(async () => await this.onOkResponse(res), {
|
|
218
|
+
timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
|
|
219
|
+
name: 'Fetcher.onOkResponse',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
// onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
|
|
224
|
+
res.err = (0, error_util_1._anyToError)(err);
|
|
225
|
+
res.ok = false;
|
|
226
|
+
await this.onNotOkResponse(res);
|
|
227
|
+
}
|
|
182
228
|
}
|
|
183
229
|
else {
|
|
184
230
|
// !res.ok
|
|
185
|
-
await this.onNotOkResponse(res
|
|
231
|
+
await this.onNotOkResponse(res);
|
|
186
232
|
}
|
|
187
233
|
}
|
|
188
234
|
for (const hook of this.cfg.hooks.afterResponse || []) {
|
|
@@ -190,31 +236,17 @@ class Fetcher {
|
|
|
190
236
|
}
|
|
191
237
|
return res;
|
|
192
238
|
}
|
|
193
|
-
async onOkResponse(res
|
|
239
|
+
async onOkResponse(res) {
|
|
194
240
|
const { req } = res;
|
|
195
241
|
const { responseType } = res.req;
|
|
242
|
+
// This function is subject to a separate timeout to "download and parse the data"
|
|
196
243
|
if (responseType === 'json') {
|
|
197
244
|
if (res.fetchResponse.body) {
|
|
198
245
|
const text = await res.fetchResponse.text();
|
|
199
246
|
if (text) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
catch (err) {
|
|
205
|
-
// Error while parsing json
|
|
206
|
-
// res.err = _anyToError(err, HttpRequestError, {
|
|
207
|
-
// requestUrl: res.req.url,
|
|
208
|
-
// requestBaseUrl: this.cfg.baseUrl,
|
|
209
|
-
// requestMethod: res.req.init.method,
|
|
210
|
-
// requestSignature: res.signature,
|
|
211
|
-
// requestDuration: Date.now() - started,
|
|
212
|
-
// responseStatusCode: res.fetchResponse.status,
|
|
213
|
-
// } satisfies HttpRequestErrorData)
|
|
214
|
-
res.err = (0, error_util_1._anyToError)(err);
|
|
215
|
-
res.ok = false;
|
|
216
|
-
return await this.onNotOkResponse(res, timeout);
|
|
217
|
-
}
|
|
247
|
+
res.body = text;
|
|
248
|
+
res.body = (0, json_util_1._jsonParse)(text, req.jsonReviver);
|
|
249
|
+
// Error while parsing json can happen - it'll be handled upstream
|
|
218
250
|
}
|
|
219
251
|
else {
|
|
220
252
|
// Body had a '' (empty string)
|
|
@@ -239,12 +271,10 @@ class Fetcher {
|
|
|
239
271
|
else if (responseType === 'readableStream') {
|
|
240
272
|
res.body = res.fetchResponse.body;
|
|
241
273
|
if (res.body === null) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return await this.onNotOkResponse(res, timeout);
|
|
274
|
+
// Error is to be handled upstream
|
|
275
|
+
throw new Error(`fetchResponse.body is null`);
|
|
245
276
|
}
|
|
246
277
|
}
|
|
247
|
-
clearTimeout(timeout);
|
|
248
278
|
res.retryStatus.retryStopped = true;
|
|
249
279
|
// res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
|
|
250
280
|
if (!res.err && this.cfg.logResponse) {
|
|
@@ -268,10 +298,13 @@ class Fetcher {
|
|
|
268
298
|
* This method exists to be able to easily mock it.
|
|
269
299
|
*/
|
|
270
300
|
async callNativeFetch(url, init) {
|
|
301
|
+
// If global mock is defined - call it instead of the native Fetch
|
|
302
|
+
if (globalFetcherMock) {
|
|
303
|
+
return await globalFetcherMock(url, init);
|
|
304
|
+
}
|
|
271
305
|
return await globalThis.fetch(url, init);
|
|
272
306
|
}
|
|
273
|
-
async onNotOkResponse(res
|
|
274
|
-
clearTimeout(timeout);
|
|
307
|
+
async onNotOkResponse(res) {
|
|
275
308
|
let cause;
|
|
276
309
|
if (res.err) {
|
|
277
310
|
// This is only possible on JSON.parse error, or CORS error,
|
|
@@ -285,6 +318,11 @@ class Fetcher {
|
|
|
285
318
|
cause = (0, error_util_1._anyToErrorObject)(body);
|
|
286
319
|
}
|
|
287
320
|
}
|
|
321
|
+
cause ||= {
|
|
322
|
+
name: 'Error',
|
|
323
|
+
message: 'Fetch failed',
|
|
324
|
+
data: {},
|
|
325
|
+
};
|
|
288
326
|
const message = [res.fetchResponse?.status, res.signature].filter(Boolean).join(' ');
|
|
289
327
|
res.err = new httpRequestError_1.HttpRequestError(message, (0, object_util_1._filterNullishValues)({
|
|
290
328
|
responseStatusCode: res.fetchResponse?.status || 0,
|
|
@@ -337,7 +375,11 @@ class Fetcher {
|
|
|
337
375
|
return;
|
|
338
376
|
retryStatus.retryAttempt++;
|
|
339
377
|
retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
|
|
340
|
-
|
|
378
|
+
const timeout = this.getRetryTimeout(res);
|
|
379
|
+
if (res.req.debug) {
|
|
380
|
+
this.cfg.logger.log(` .. ${res.signature} waiting ${(0, time_util_1._ms)(timeout)}`);
|
|
381
|
+
}
|
|
382
|
+
await (0, pDelay_1.pDelay)(timeout);
|
|
341
383
|
}
|
|
342
384
|
getRetryTimeout(res) {
|
|
343
385
|
let timeout = 0;
|
|
@@ -392,8 +434,9 @@ class Fetcher {
|
|
|
392
434
|
if (statusFamily === 3 && !retry3xx)
|
|
393
435
|
return false;
|
|
394
436
|
// should not retry on `unexpected redirect` in error.cause.cause
|
|
395
|
-
if (res.err?.cause?.cause?.message?.includes('unexpected redirect'))
|
|
437
|
+
if (res.err?.cause?.cause?.message?.includes('unexpected redirect')) {
|
|
396
438
|
return false;
|
|
439
|
+
}
|
|
397
440
|
return true; // default is true
|
|
398
441
|
}
|
|
399
442
|
getStatusFamily(res) {
|
|
@@ -431,14 +474,14 @@ class Fetcher {
|
|
|
431
474
|
}
|
|
432
475
|
normalizeCfg(cfg) {
|
|
433
476
|
if (cfg.baseUrl?.endsWith('/')) {
|
|
434
|
-
console.warn(`Fetcher: baseUrl should not end with
|
|
477
|
+
console.warn(`Fetcher: baseUrl should not end with slash: ${cfg.baseUrl}`);
|
|
435
478
|
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
|
|
436
479
|
}
|
|
437
480
|
const { debug = false } = cfg;
|
|
438
481
|
const norm = (0, object_util_1._merge)({
|
|
439
482
|
baseUrl: '',
|
|
440
483
|
inputUrl: '',
|
|
441
|
-
responseType: '
|
|
484
|
+
responseType: 'json',
|
|
442
485
|
searchParams: {},
|
|
443
486
|
timeoutSeconds: 30,
|
|
444
487
|
retryPost: false,
|
|
@@ -482,6 +525,7 @@ class Fetcher {
|
|
|
482
525
|
'logRequestBody',
|
|
483
526
|
'logResponse',
|
|
484
527
|
'logResponseBody',
|
|
528
|
+
'debug',
|
|
485
529
|
]),
|
|
486
530
|
started: Date.now(),
|
|
487
531
|
...(0, object_util_1._omit)(opt, ['method', 'headers', 'credentials']),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { CommonLogger } from '../log/commonLogger';
|
|
2
2
|
import type { Promisable } from '../typeFest';
|
|
3
|
-
import type { AnyObject, Reviver, UnixTimestampMillisNumber } from '../types';
|
|
3
|
+
import type { AnyObject, NumberOfMilliseconds, Reviver, UnixTimestampMillisNumber } from '../types';
|
|
4
4
|
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
5
|
-
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'redirect' | 'credentials'> {
|
|
5
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'debug' | 'redirect' | 'credentials'> {
|
|
6
6
|
logger: CommonLogger;
|
|
7
7
|
searchParams: Record<string, any>;
|
|
8
8
|
}
|
|
@@ -68,13 +68,13 @@ export interface FetcherCfg {
|
|
|
68
68
|
}
|
|
69
69
|
export interface FetcherRetryStatus {
|
|
70
70
|
retryAttempt: number;
|
|
71
|
-
retryTimeout:
|
|
71
|
+
retryTimeout: NumberOfMilliseconds;
|
|
72
72
|
retryStopped: boolean;
|
|
73
73
|
}
|
|
74
74
|
export interface FetcherRetryOptions {
|
|
75
75
|
count: number;
|
|
76
|
-
timeout:
|
|
77
|
-
timeoutMax:
|
|
76
|
+
timeout: NumberOfMilliseconds;
|
|
77
|
+
timeoutMax: NumberOfMilliseconds;
|
|
78
78
|
timeoutMultiplier: number;
|
|
79
79
|
}
|
|
80
80
|
export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers' | 'baseUrl' | 'url'> {
|
|
@@ -172,6 +172,10 @@ export interface FetcherOptions {
|
|
|
172
172
|
logRequestBody?: boolean;
|
|
173
173
|
logResponse?: boolean;
|
|
174
174
|
logResponseBody?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* If true - enables all possible logging.
|
|
177
|
+
*/
|
|
178
|
+
debug?: boolean;
|
|
175
179
|
}
|
|
176
180
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
177
181
|
method: HttpMethod;
|
|
@@ -201,3 +205,9 @@ export interface FetcherErrorResponse<BODY = unknown> {
|
|
|
201
205
|
}
|
|
202
206
|
export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
|
|
203
207
|
export type FetcherResponseType = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob' | 'readableStream';
|
|
208
|
+
/**
|
|
209
|
+
* Function that mocks `Fetcher.callNativeFetch` globally.
|
|
210
|
+
*
|
|
211
|
+
* url is `fullUrl` (includes baseUrl and all).
|
|
212
|
+
*/
|
|
213
|
+
export type FetcherMockFunction = (url: string, init: RequestInitNormalized) => Promise<Response>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AppError } from '../error/app.error';
|
|
2
2
|
import type { ErrorData, ErrorObject } from '../error/error.model';
|
|
3
|
-
import type { AnyAsyncFunction } from '../types';
|
|
3
|
+
import type { AnyAsyncFunction, NumberOfMilliseconds } from '../types';
|
|
4
4
|
export declare class TimeoutError extends AppError {
|
|
5
5
|
constructor(message: string, data?: {}, cause?: ErrorObject);
|
|
6
6
|
}
|
|
@@ -8,7 +8,7 @@ export interface PTimeoutOptions {
|
|
|
8
8
|
/**
|
|
9
9
|
* Timeout in milliseconds.
|
|
10
10
|
*/
|
|
11
|
-
timeout:
|
|
11
|
+
timeout: NumberOfMilliseconds;
|
|
12
12
|
/**
|
|
13
13
|
* If set - will be included in the error message.
|
|
14
14
|
* Can be used to identify the place in the code that failed.
|
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];
|
package/dist-esm/error/assert.js
CHANGED
|
@@ -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');
|
package/dist-esm/error/try.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|