@naturalcycles/js-lib 14.134.0 → 14.136.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/decorators/logMethod.decorator.js +2 -2
- package/dist/env.d.ts +14 -0
- package/dist/env.js +23 -0
- package/dist/error/error.util.d.ts +1 -1
- package/dist/error/error.util.js +2 -0
- package/dist/error/tryCatch.js +1 -3
- package/dist/http/fetcher.d.ts +2 -0
- package/dist/http/fetcher.js +104 -92
- package/dist/http/fetcher.model.d.ts +14 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/promise/abortable.d.ts +20 -0
- package/dist/promise/abortable.js +36 -0
- package/dist/promise/pDefer.d.ts +14 -1
- package/dist/promise/pDefer.js +2 -0
- package/dist/promise/pDelay.d.ts +18 -0
- package/dist/promise/pDelay.js +37 -2
- package/dist/promise/pRetry.d.ts +0 -8
- package/dist/promise/pRetry.js +37 -63
- package/dist/promise/pTimeout.d.ts +4 -6
- package/dist/promise/pTimeout.js +8 -10
- package/dist/string/stringifyAny.d.ts +0 -6
- package/dist/string/stringifyAny.js +0 -5
- package/dist/types.d.ts +3 -0
- package/dist/vendor/is.d.ts +2 -2
- package/dist-esm/decorators/logMethod.decorator.js +2 -2
- package/dist-esm/env.js +18 -0
- package/dist-esm/error/error.util.js +2 -0
- package/dist-esm/error/tryCatch.js +2 -4
- package/dist-esm/http/fetcher.js +111 -98
- package/dist-esm/index.js +2 -0
- package/dist-esm/promise/abortable.js +32 -0
- package/dist-esm/promise/pDefer.js +2 -0
- package/dist-esm/promise/pDelay.js +35 -1
- package/dist-esm/promise/pRetry.js +38 -61
- package/dist-esm/promise/pTimeout.js +8 -7
- package/dist-esm/string/stringifyAny.js +0 -5
- package/package.json +1 -1
- package/src/decorators/logMethod.decorator.ts +2 -2
- package/src/env.ts +19 -0
- package/src/error/error.util.ts +3 -1
- package/src/error/tryCatch.ts +2 -6
- package/src/http/fetcher.model.ts +14 -3
- package/src/http/fetcher.ts +117 -95
- package/src/index.ts +2 -0
- package/src/promise/abortable.ts +34 -0
- package/src/promise/pDefer.ts +19 -1
- package/src/promise/pDelay.ts +44 -2
- package/src/promise/pRetry.ts +41 -89
- package/src/promise/pState.ts +1 -1
- package/src/promise/pTimeout.ts +12 -14
- package/src/string/stringifyAny.ts +0 -13
- package/src/types.ts +3 -0
- package/src/vendor/is.ts +3 -3
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
import { pDefer } from './pDefer';
|
|
2
|
+
/**
|
|
3
|
+
* Promisified version of setTimeout.
|
|
4
|
+
*
|
|
5
|
+
* Can return a value.
|
|
6
|
+
* If value is instanceof Error - rejects the Promise instead of resolving.
|
|
7
|
+
*/
|
|
1
8
|
export async function pDelay(ms = 0, value) {
|
|
2
|
-
return await new Promise(resolve => setTimeout(
|
|
9
|
+
return await new Promise((resolve, reject) => setTimeout(value instanceof Error ? reject : resolve, ms, value));
|
|
10
|
+
}
|
|
11
|
+
/* eslint-disable @typescript-eslint/promise-function-async */
|
|
12
|
+
/**
|
|
13
|
+
* Promisified version of setTimeout.
|
|
14
|
+
*
|
|
15
|
+
* Wraps the passed function with try/catch,
|
|
16
|
+
* catch will propagate to pDelayFn rejection,
|
|
17
|
+
* otherwise pDelayFn will resolve with returned value.
|
|
18
|
+
*
|
|
19
|
+
* On abort() - clears the Timeout and immediately resolves the Promise with void.
|
|
20
|
+
*/
|
|
21
|
+
export function pDelayFn(ms = 0, fn) {
|
|
22
|
+
const p = pDefer();
|
|
23
|
+
const timer = setTimeout(async () => {
|
|
24
|
+
try {
|
|
25
|
+
p.resolve(await fn());
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
p.reject(err);
|
|
29
|
+
}
|
|
30
|
+
}, ms);
|
|
31
|
+
p.abort = () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
// p.rejectAborted(reason) // nope
|
|
34
|
+
p.resolve();
|
|
35
|
+
};
|
|
36
|
+
return p;
|
|
3
37
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { _since,
|
|
2
|
-
import { TimeoutError } from './pTimeout';
|
|
1
|
+
import { _errorDataAppend, _since, pDelay, pTimeout } from '..';
|
|
3
2
|
/**
|
|
4
3
|
* Returns a Function (!), enhanced with retry capabilities.
|
|
5
4
|
* Implements "Exponential back-off strategy" by multiplying the delay by `delayMultiplier` with each try.
|
|
@@ -10,8 +9,8 @@ export function pRetryFn(fn, opt = {}) {
|
|
|
10
9
|
};
|
|
11
10
|
}
|
|
12
11
|
export async function pRetry(fn, opt = {}) {
|
|
13
|
-
const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name,
|
|
14
|
-
const fakeError =
|
|
12
|
+
const { maxAttempts = 4, delay: initialDelay = 1000, delayMultiplier = 2, predicate, logger = console, name, timeout, } = opt;
|
|
13
|
+
const fakeError = timeout ? new Error('TimeoutError') : undefined;
|
|
15
14
|
let { logFirstAttempt = false, logRetries = true, logFailures = false, logSuccess = false } = opt;
|
|
16
15
|
if (opt.logAll) {
|
|
17
16
|
logSuccess = logFirstAttempt = logRetries = logFailures = true;
|
|
@@ -22,66 +21,44 @@ export async function pRetry(fn, opt = {}) {
|
|
|
22
21
|
const fname = name || fn.name || 'pRetry function';
|
|
23
22
|
let delay = initialDelay;
|
|
24
23
|
let attempt = 0;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// keep original stack
|
|
33
|
-
err.stack = fakeError.stack.replace('Error: RetryError', 'TimeoutError');
|
|
24
|
+
/* eslint-disable no-await-in-loop, no-constant-condition */
|
|
25
|
+
while (true) {
|
|
26
|
+
const started = Date.now();
|
|
27
|
+
try {
|
|
28
|
+
attempt++;
|
|
29
|
+
if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
|
|
30
|
+
logger.log(`${fname} attempt #${attempt}...`);
|
|
34
31
|
}
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
const next = async () => {
|
|
38
|
-
if (timedOut)
|
|
39
|
-
return;
|
|
32
|
+
let result;
|
|
40
33
|
if (timeout) {
|
|
41
|
-
|
|
34
|
+
await pTimeout(async () => await fn(attempt), {
|
|
35
|
+
timeout,
|
|
36
|
+
name: fname,
|
|
37
|
+
errorData: opt.errorData,
|
|
38
|
+
fakeError,
|
|
39
|
+
});
|
|
42
40
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
attempt++;
|
|
46
|
-
if ((attempt === 1 && logFirstAttempt) || (attempt > 1 && logRetries)) {
|
|
47
|
-
logger.log(`${fname} attempt #${attempt}...`);
|
|
48
|
-
}
|
|
49
|
-
const r = await fn(attempt);
|
|
50
|
-
clearTimeout(timer);
|
|
51
|
-
if (logSuccess) {
|
|
52
|
-
logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`);
|
|
53
|
-
}
|
|
54
|
-
resolve(r);
|
|
41
|
+
else {
|
|
42
|
+
result = await fn(attempt);
|
|
55
43
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (logFailures) {
|
|
59
|
-
logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, _stringifyAny(err, {
|
|
60
|
-
includeErrorData: true,
|
|
61
|
-
}));
|
|
62
|
-
}
|
|
63
|
-
if (attempt >= maxAttempts ||
|
|
64
|
-
(predicate && !predicate(err, attempt, maxAttempts))) {
|
|
65
|
-
// Give up
|
|
66
|
-
if (fakeError) {
|
|
67
|
-
// Preserve the original call stack
|
|
68
|
-
Object.defineProperty(err, 'stack', {
|
|
69
|
-
value: err.stack +
|
|
70
|
-
'\n --' +
|
|
71
|
-
fakeError.stack.replace('Error: RetryError', ''),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
;
|
|
75
|
-
err.data = Object.assign(Object.assign({}, err.data), opt.errorData);
|
|
76
|
-
reject(err);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
// Retry after delay
|
|
80
|
-
delay *= delayMultiplier;
|
|
81
|
-
setTimeout(next, delay);
|
|
82
|
-
}
|
|
44
|
+
if (logSuccess) {
|
|
45
|
+
logger.log(`${fname} attempt #${attempt} succeeded in ${_since(started)}`);
|
|
83
46
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
if (logFailures) {
|
|
51
|
+
logger.warn(`${fname} attempt #${attempt} error in ${_since(started)}:`, err);
|
|
52
|
+
}
|
|
53
|
+
if (attempt >= maxAttempts || (predicate && !predicate(err, attempt, maxAttempts))) {
|
|
54
|
+
// Give up
|
|
55
|
+
_errorDataAppend(err, opt.errorData);
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
// Retry after delay
|
|
59
|
+
delay *= delayMultiplier;
|
|
60
|
+
await pDelay(delay);
|
|
61
|
+
// back to while(true) loop
|
|
62
|
+
}
|
|
63
|
+
}
|
|
87
64
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AppError } from '../error/app.error';
|
|
2
|
+
import { _errorDataAppend } from '../error/error.util';
|
|
2
3
|
export class TimeoutError extends AppError {
|
|
3
4
|
constructor(message, data = {}, opt) {
|
|
4
5
|
super(message, data, opt, 'TimeoutError');
|
|
@@ -24,23 +25,23 @@ export function pTimeoutFn(fn, opt) {
|
|
|
24
25
|
* If the Function rejects - passes this rejection further.
|
|
25
26
|
*/
|
|
26
27
|
export async function pTimeout(fn, opt) {
|
|
27
|
-
const { timeout, name = fn.name || 'pTimeout function', onTimeout
|
|
28
|
-
const fakeError =
|
|
28
|
+
const { timeout, name = fn.name || 'pTimeout function', onTimeout } = opt;
|
|
29
|
+
const fakeError = opt.fakeError || new Error('TimeoutError');
|
|
29
30
|
// eslint-disable-next-line no-async-promise-executor
|
|
30
31
|
return await new Promise(async (resolve, reject) => {
|
|
31
32
|
// Prepare the timeout timer
|
|
32
33
|
const timer = setTimeout(() => {
|
|
33
34
|
const err = new TimeoutError(`"${name}" timed out after ${timeout} ms`, opt.errorData);
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// keep original stack
|
|
36
|
+
err.stack = fakeError.stack.replace('Error: TimeoutError', 'TimeoutError: ' + err.message);
|
|
36
37
|
if (onTimeout) {
|
|
37
38
|
try {
|
|
38
39
|
resolve(onTimeout(err));
|
|
39
40
|
}
|
|
40
41
|
catch (err) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// keep original stack
|
|
43
|
+
err.stack = fakeError.stack.replace('Error: TimeoutError', err.name + ': ' + err.message);
|
|
44
|
+
_errorDataAppend(err, opt.errorData);
|
|
44
45
|
reject(err);
|
|
45
46
|
}
|
|
46
47
|
return;
|
|
@@ -82,11 +82,6 @@ export function _stringifyAny(obj, opt = {}) {
|
|
|
82
82
|
// `replace` here works ONCE, exactly as we need it
|
|
83
83
|
s = s.replace('HttpError', `HttpError(${obj.data.httpStatusCode})`);
|
|
84
84
|
}
|
|
85
|
-
// Here we ensure it has `data`
|
|
86
|
-
const { data } = obj;
|
|
87
|
-
if (opt.includeErrorData && Object.keys(data).length > 0) {
|
|
88
|
-
s = [s, _stringifyAny(data, opt)].join('\n');
|
|
89
|
-
}
|
|
90
85
|
}
|
|
91
86
|
else if (typeof obj.code === 'string') {
|
|
92
87
|
// Error that has no `data`, but has `code` property
|
package/package.json
CHANGED
|
@@ -155,10 +155,10 @@ function logFinished(
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
if (err !== undefined) {
|
|
158
|
-
t.push('ERROR:',
|
|
158
|
+
t.push('ERROR:', err)
|
|
159
159
|
} else if (logResultFn) {
|
|
160
160
|
t.push(...logResultFn(res))
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
logger.log(t.filter(Boolean)
|
|
163
|
+
logger.log(...t.filter(Boolean))
|
|
164
164
|
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use it to detect SSR/Node.js environment.
|
|
3
|
+
*
|
|
4
|
+
* Will return `true` in Node.js.
|
|
5
|
+
* Will return `false` in the Browser.
|
|
6
|
+
*/
|
|
7
|
+
export function isServerSide(): boolean {
|
|
8
|
+
return typeof window === 'undefined'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Use it to detect Browser (not SSR/Node) environment.
|
|
13
|
+
*
|
|
14
|
+
* Will return `true` in the Browser.
|
|
15
|
+
* Will return `false` in Node.js.
|
|
16
|
+
*/
|
|
17
|
+
export function isClientSide(): boolean {
|
|
18
|
+
return typeof window !== 'undefined'
|
|
19
|
+
}
|
package/src/error/error.util.ts
CHANGED
|
@@ -181,7 +181,9 @@ export function _isErrorObject(o: any): o is ErrorObject {
|
|
|
181
181
|
* })
|
|
182
182
|
* }
|
|
183
183
|
*/
|
|
184
|
-
export function _errorDataAppend(err: any, data
|
|
184
|
+
export function _errorDataAppend(err: any, data?: ErrorData): void {
|
|
185
|
+
if (!data) return
|
|
186
|
+
|
|
185
187
|
err.data = {
|
|
186
188
|
...err.data,
|
|
187
189
|
...data,
|
package/src/error/tryCatch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommonLogger } from '../index'
|
|
2
|
-
import { _anyToError, _since
|
|
2
|
+
import { _anyToError, _since } from '../index'
|
|
3
3
|
import type { AnyFunction } from '../types'
|
|
4
4
|
|
|
5
5
|
export interface TryCatchOptions {
|
|
@@ -51,11 +51,7 @@ export function _tryCatch<T extends AnyFunction>(fn: T, opt: TryCatchOptions = {
|
|
|
51
51
|
return r
|
|
52
52
|
} catch (err) {
|
|
53
53
|
if (logError) {
|
|
54
|
-
logger.warn(
|
|
55
|
-
`tryCatch.${fname} error in ${_since(started)}:\n${_stringifyAny(err, {
|
|
56
|
-
includeErrorData: true,
|
|
57
|
-
})}`,
|
|
58
|
-
)
|
|
54
|
+
logger.warn(`tryCatch.${fname} error in ${_since(started)}:`, err)
|
|
59
55
|
}
|
|
60
56
|
|
|
61
57
|
if (onError) {
|
|
@@ -49,10 +49,19 @@ export interface FetcherCfg {
|
|
|
49
49
|
logResponseBody?: boolean
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Controls if `baseUrl` should be included in logs (both success and error).
|
|
53
|
+
*
|
|
54
|
+
* Defaults to `true` on ServerSide and `false` on ClientSide.
|
|
55
|
+
*
|
|
56
|
+
* Reasoning.
|
|
57
|
+
*
|
|
58
|
+
* ClientSide often uses one main "backend host".
|
|
59
|
+
* Not including baseUrl improves Sentry error grouping.
|
|
60
|
+
*
|
|
61
|
+
* ServerSide often uses one Fetcher instance per 3rd-party API.
|
|
62
|
+
* Not including baseUrl can introduce confusion of "which API is it?".
|
|
54
63
|
*/
|
|
55
|
-
|
|
64
|
+
logWithBaseUrl?: boolean
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Default to true.
|
|
@@ -165,6 +174,7 @@ export interface FetcherSuccessResponse<BODY = unknown> {
|
|
|
165
174
|
req: FetcherRequest
|
|
166
175
|
statusFamily?: HttpStatusFamily
|
|
167
176
|
retryStatus: FetcherRetryStatus
|
|
177
|
+
signature: string
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
export interface FetcherErrorResponse<BODY = unknown> {
|
|
@@ -175,6 +185,7 @@ export interface FetcherErrorResponse<BODY = unknown> {
|
|
|
175
185
|
req: FetcherRequest
|
|
176
186
|
statusFamily?: HttpStatusFamily
|
|
177
187
|
retryStatus: FetcherRetryStatus
|
|
188
|
+
signature: string
|
|
178
189
|
}
|
|
179
190
|
|
|
180
191
|
export type FetcherResponse<BODY = unknown> =
|
package/src/http/fetcher.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference lib="dom"/>
|
|
2
2
|
|
|
3
|
+
import { isServerSide } from '../env'
|
|
3
4
|
import { ErrorObject } from '../error/error.model'
|
|
4
5
|
import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util'
|
|
5
6
|
import { HttpError } from '../error/http.error'
|
|
@@ -151,7 +152,6 @@ export class Fetcher {
|
|
|
151
152
|
const req = this.normalizeOptions(url, rawOpt)
|
|
152
153
|
const {
|
|
153
154
|
timeoutSeconds,
|
|
154
|
-
mode,
|
|
155
155
|
init: { method },
|
|
156
156
|
} = req
|
|
157
157
|
|
|
@@ -169,6 +169,11 @@ export class Fetcher {
|
|
|
169
169
|
await hook(req)
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
const isFullUrl = req.url.includes('://')
|
|
173
|
+
const fullUrl = isFullUrl ? new URL(req.url) : undefined
|
|
174
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.url
|
|
175
|
+
const signature = [method, shortUrl].join(' ')
|
|
176
|
+
|
|
172
177
|
const res = {
|
|
173
178
|
req,
|
|
174
179
|
retryStatus: {
|
|
@@ -176,12 +181,9 @@ export class Fetcher {
|
|
|
176
181
|
retryStopped: false,
|
|
177
182
|
retryTimeout: req.retry.timeout,
|
|
178
183
|
},
|
|
184
|
+
signature,
|
|
179
185
|
} as FetcherResponse<any>
|
|
180
186
|
|
|
181
|
-
const fullUrl = new URL(req.url)
|
|
182
|
-
const shortUrl = this.getShortUrl(fullUrl)
|
|
183
|
-
const signature = [method, shortUrl].join(' ')
|
|
184
|
-
|
|
185
187
|
/* eslint-disable no-await-in-loop */
|
|
186
188
|
while (!res.retryStatus.retryStopped) {
|
|
187
189
|
const started = Date.now()
|
|
@@ -209,109 +211,129 @@ export class Fetcher {
|
|
|
209
211
|
res.statusFamily = this.getStatusFamily(res)
|
|
210
212
|
|
|
211
213
|
if (res.fetchResponse?.ok) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
res.body = text
|
|
219
|
-
res.body = JSON.parse(text, req.jsonReviver)
|
|
220
|
-
} catch (err) {
|
|
221
|
-
const { message } = _anyToError(err)
|
|
222
|
-
res.err = new HttpError([signature, message].join('\n'), {
|
|
223
|
-
httpStatusCode: 0,
|
|
224
|
-
url: req.url,
|
|
225
|
-
})
|
|
226
|
-
res.ok = false
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
// Body had a '' (empty string)
|
|
230
|
-
res.body = {}
|
|
231
|
-
}
|
|
232
|
-
} else {
|
|
233
|
-
// if no body: set responseBody as {}
|
|
234
|
-
// do not throw a "cannot parse null as Json" error
|
|
235
|
-
res.body = {}
|
|
236
|
-
}
|
|
237
|
-
} else if (mode === 'text') {
|
|
238
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : ''
|
|
239
|
-
} else if (mode === 'arrayBuffer') {
|
|
240
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {}
|
|
241
|
-
} else if (mode === 'blob') {
|
|
242
|
-
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
clearTimeout(timeout)
|
|
246
|
-
res.retryStatus.retryStopped = true
|
|
247
|
-
|
|
248
|
-
// res.err can happen on JSON.parse error
|
|
249
|
-
if (!res.err && this.cfg.logResponse) {
|
|
250
|
-
const { retryAttempt } = res.retryStatus
|
|
251
|
-
logger.log(
|
|
252
|
-
[
|
|
253
|
-
' <<',
|
|
254
|
-
res.fetchResponse.status,
|
|
255
|
-
signature,
|
|
256
|
-
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
257
|
-
_since(started),
|
|
258
|
-
]
|
|
259
|
-
.filter(Boolean)
|
|
260
|
-
.join(' '),
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
if (this.cfg.logResponseBody) {
|
|
264
|
-
logger.log(res.body)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
214
|
+
await this.onOkResponse(
|
|
215
|
+
res as FetcherResponse<T> & { fetchResponse: Response },
|
|
216
|
+
started,
|
|
217
|
+
timeout,
|
|
218
|
+
)
|
|
267
219
|
} else {
|
|
268
220
|
// !res.ok
|
|
269
|
-
|
|
221
|
+
await this.onNotOkResponse(res, timeout)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for await (const hook of this.cfg.hooks.afterResponse || []) {
|
|
226
|
+
await hook(res)
|
|
227
|
+
}
|
|
270
228
|
|
|
271
|
-
|
|
229
|
+
return res
|
|
230
|
+
}
|
|
272
231
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
232
|
+
private async onOkResponse(
|
|
233
|
+
res: FetcherResponse<any> & { fetchResponse: Response },
|
|
234
|
+
started: number,
|
|
235
|
+
timeout?: number,
|
|
236
|
+
): Promise<void> {
|
|
237
|
+
const { req } = res
|
|
238
|
+
const { mode } = res.req
|
|
239
|
+
|
|
240
|
+
if (mode === 'json') {
|
|
241
|
+
if (res.fetchResponse.body) {
|
|
242
|
+
const text = await res.fetchResponse.text()
|
|
243
|
+
|
|
244
|
+
if (text) {
|
|
245
|
+
try {
|
|
246
|
+
res.body = text
|
|
247
|
+
res.body = JSON.parse(text, req.jsonReviver)
|
|
248
|
+
} catch (err) {
|
|
249
|
+
const { message } = _anyToError(err)
|
|
250
|
+
res.err = new HttpError([res.signature, message].join('\n'), {
|
|
251
|
+
httpStatusCode: 0,
|
|
252
|
+
url: req.url,
|
|
253
|
+
})
|
|
254
|
+
res.ok = false
|
|
255
|
+
}
|
|
278
256
|
} else {
|
|
279
|
-
|
|
257
|
+
// Body had a '' (empty string)
|
|
258
|
+
res.body = {}
|
|
280
259
|
}
|
|
260
|
+
} else {
|
|
261
|
+
// if no body: set responseBody as {}
|
|
262
|
+
// do not throw a "cannot parse null as Json" error
|
|
263
|
+
res.body = {}
|
|
264
|
+
}
|
|
265
|
+
} else if (mode === 'text') {
|
|
266
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : ''
|
|
267
|
+
} else if (mode === 'arrayBuffer') {
|
|
268
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {}
|
|
269
|
+
} else if (mode === 'blob') {
|
|
270
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {}
|
|
271
|
+
}
|
|
281
272
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
273
|
+
clearTimeout(timeout)
|
|
274
|
+
res.retryStatus.retryStopped = true
|
|
275
|
+
|
|
276
|
+
// res.err can happen on JSON.parse error
|
|
277
|
+
if (!res.err && this.cfg.logResponse) {
|
|
278
|
+
const { retryAttempt } = res.retryStatus
|
|
279
|
+
const { logger } = this.cfg
|
|
280
|
+
logger.log(
|
|
281
|
+
[
|
|
282
|
+
' <<',
|
|
283
|
+
res.fetchResponse.status,
|
|
284
|
+
res.signature,
|
|
285
|
+
retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
|
|
286
|
+
_since(started),
|
|
286
287
|
]
|
|
287
288
|
.filter(Boolean)
|
|
288
|
-
.join('
|
|
289
|
-
|
|
290
|
-
res.err = new HttpError(
|
|
291
|
-
errObj.message,
|
|
292
|
-
|
|
293
|
-
_filterNullishValues({
|
|
294
|
-
...errObj.data,
|
|
295
|
-
originalMessage,
|
|
296
|
-
httpStatusCode: res.fetchResponse?.status || 0,
|
|
297
|
-
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
298
|
-
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
299
|
-
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
300
|
-
// method: req.method,
|
|
301
|
-
url: req.url,
|
|
302
|
-
// tryCount: req.tryCount,
|
|
303
|
-
}),
|
|
304
|
-
)
|
|
289
|
+
.join(' '),
|
|
290
|
+
)
|
|
305
291
|
|
|
306
|
-
|
|
292
|
+
if (this.cfg.logResponseBody) {
|
|
293
|
+
logger.log(res.body)
|
|
307
294
|
}
|
|
308
295
|
}
|
|
296
|
+
}
|
|
309
297
|
|
|
310
|
-
|
|
311
|
-
|
|
298
|
+
private async onNotOkResponse(res: FetcherResponse, timeout?: number): Promise<void> {
|
|
299
|
+
clearTimeout(timeout)
|
|
300
|
+
|
|
301
|
+
let errObj: ErrorObject
|
|
302
|
+
|
|
303
|
+
if (res.fetchResponse) {
|
|
304
|
+
const body = _jsonParseIfPossible(await res.fetchResponse.text())
|
|
305
|
+
errObj = _anyToErrorObject(body)
|
|
306
|
+
} else if (res.err) {
|
|
307
|
+
errObj = _errorToErrorObject(res.err)
|
|
308
|
+
} else {
|
|
309
|
+
errObj = {} as ErrorObject
|
|
312
310
|
}
|
|
313
311
|
|
|
314
|
-
|
|
312
|
+
const originalMessage = errObj.message
|
|
313
|
+
errObj.message = [
|
|
314
|
+
[res.fetchResponse?.status, res.signature].filter(Boolean).join(' '),
|
|
315
|
+
originalMessage,
|
|
316
|
+
]
|
|
317
|
+
.filter(Boolean)
|
|
318
|
+
.join('\n')
|
|
319
|
+
|
|
320
|
+
res.err = new HttpError(
|
|
321
|
+
errObj.message,
|
|
322
|
+
|
|
323
|
+
_filterNullishValues({
|
|
324
|
+
...errObj.data,
|
|
325
|
+
originalMessage,
|
|
326
|
+
httpStatusCode: res.fetchResponse?.status || 0,
|
|
327
|
+
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
328
|
+
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
329
|
+
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
330
|
+
// method: req.method,
|
|
331
|
+
url: res.req.url,
|
|
332
|
+
// tryCount: req.tryCount,
|
|
333
|
+
}),
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
await this.processRetry(res)
|
|
315
337
|
}
|
|
316
338
|
|
|
317
339
|
private async processRetry(res: FetcherResponse): Promise<void> {
|
|
@@ -388,7 +410,7 @@ export class Fetcher {
|
|
|
388
410
|
shortUrl = shortUrl.split('?')[0]!
|
|
389
411
|
}
|
|
390
412
|
|
|
391
|
-
if (!this.cfg.
|
|
413
|
+
if (!this.cfg.logWithBaseUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
|
|
392
414
|
shortUrl = shortUrl.slice(baseUrl.length)
|
|
393
415
|
}
|
|
394
416
|
|
|
@@ -419,7 +441,7 @@ export class Fetcher {
|
|
|
419
441
|
logRequestBody: debug,
|
|
420
442
|
logResponse: debug,
|
|
421
443
|
logResponseBody: debug,
|
|
422
|
-
|
|
444
|
+
logWithBaseUrl: isServerSide(),
|
|
423
445
|
logWithSearchParams: true,
|
|
424
446
|
retry: { ...defRetryOptions },
|
|
425
447
|
init: {
|
package/src/index.ts
CHANGED
|
@@ -61,6 +61,7 @@ export * from './unit/size.util'
|
|
|
61
61
|
export * from './log/commonLogger'
|
|
62
62
|
export * from './string/safeJsonStringify'
|
|
63
63
|
export * from './promise/pQueue'
|
|
64
|
+
export * from './promise/abortable'
|
|
64
65
|
export * from './seq/seq'
|
|
65
66
|
export * from './math/stack.util'
|
|
66
67
|
export * from './string/leven'
|
|
@@ -72,6 +73,7 @@ export * from './datetime/localDate'
|
|
|
72
73
|
export * from './datetime/localTime'
|
|
73
74
|
export * from './datetime/dateInterval'
|
|
74
75
|
export * from './datetime/timeInterval'
|
|
76
|
+
export * from './env'
|
|
75
77
|
export * from './http/http.model'
|
|
76
78
|
export * from './http/fetcher'
|
|
77
79
|
export * from './http/fetcher.model'
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AnyFunction } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Similar to AbortController and AbortSignal.
|
|
5
|
+
* Similar to pDefer and Promise.
|
|
6
|
+
* Similar to Subject and Observable.
|
|
7
|
+
*
|
|
8
|
+
* Minimal interface for something that can be aborted in the future,
|
|
9
|
+
* but not necessary.
|
|
10
|
+
* Allows to listen to `onAbort` event.
|
|
11
|
+
*
|
|
12
|
+
* @experimental
|
|
13
|
+
*/
|
|
14
|
+
export class Abortable {
|
|
15
|
+
constructor(public onAbort?: AnyFunction) {}
|
|
16
|
+
|
|
17
|
+
aborted = false
|
|
18
|
+
|
|
19
|
+
abort(): void {
|
|
20
|
+
if (this.aborted) return
|
|
21
|
+
this.aborted = true
|
|
22
|
+
this.onAbort?.()
|
|
23
|
+
this.onAbort = undefined // cleanup listener
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clear(): void {
|
|
27
|
+
this.onAbort = undefined
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// convenience function
|
|
32
|
+
export function abortable(onAbort?: AnyFunction): Abortable {
|
|
33
|
+
return new Abortable(onAbort)
|
|
34
|
+
}
|