@naturalcycles/js-lib 14.117.0 → 14.118.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/array/array.util.js +4 -4
- package/dist/datetime/dateInterval.js +1 -0
- package/dist/datetime/localDate.js +2 -0
- package/dist/datetime/localTime.js +2 -3
- package/dist/datetime/timeInterval.js +1 -0
- package/dist/decorators/asyncMemo.decorator.d.ts +2 -2
- package/dist/decorators/createPromiseDecorator.d.ts +6 -6
- package/dist/decorators/logMethod.decorator.js +4 -5
- package/dist/decorators/memo.decorator.d.ts +2 -2
- package/dist/error/tryCatch.d.ts +1 -1
- package/dist/http/fetcher.d.ts +146 -0
- package/dist/http/fetcher.js +298 -0
- package/dist/http/http.model.d.ts +2 -0
- package/dist/http/http.model.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/object/deepEquals.js +1 -0
- package/dist/object/object.util.js +8 -10
- package/dist/promise/pRetry.d.ts +1 -1
- package/dist/promise/pTimeout.d.ts +1 -1
- package/dist/string/pupa.d.ts +2 -2
- package/dist/string/readingTime.d.ts +1 -1
- package/dist/string/safeJsonStringify.js +5 -7
- package/dist/string/stringifyAny.js +2 -1
- package/dist/string/url.util.js +1 -1
- package/dist-esm/array/array.util.js +4 -4
- package/dist-esm/datetime/dateInterval.js +1 -0
- package/dist-esm/datetime/localDate.js +2 -0
- package/dist-esm/datetime/localTime.js +2 -3
- package/dist-esm/datetime/timeInterval.js +1 -0
- package/dist-esm/decorators/logMethod.decorator.js +4 -5
- package/dist-esm/http/fetcher.js +251 -0
- package/dist-esm/http/http.model.js +1 -0
- package/dist-esm/index.js +2 -0
- package/dist-esm/object/deepEquals.js +1 -0
- package/dist-esm/object/object.util.js +8 -10
- package/dist-esm/string/safeJsonStringify.js +5 -7
- package/dist-esm/string/stringifyAny.js +2 -1
- package/dist-esm/string/url.util.js +1 -1
- package/package.json +1 -1
- package/src/array/array.util.ts +4 -4
- package/src/datetime/dateInterval.ts +1 -0
- package/src/datetime/localDate.ts +2 -0
- package/src/datetime/localTime.ts +2 -2
- package/src/datetime/timeInterval.ts +1 -0
- package/src/decorators/asyncMemo.decorator.ts +2 -2
- package/src/decorators/createPromiseDecorator.ts +4 -4
- package/src/decorators/logMethod.decorator.ts +4 -4
- package/src/decorators/memo.decorator.ts +2 -2
- package/src/error/tryCatch.ts +1 -1
- package/src/http/fetcher.ts +469 -0
- package/src/http/http.model.ts +3 -0
- package/src/index.ts +2 -0
- package/src/object/deepEquals.ts +1 -0
- package/src/object/object.util.ts +7 -8
- package/src/promise/pRetry.ts +1 -1
- package/src/promise/pTimeout.ts +1 -1
- package/src/string/pupa.ts +1 -1
- package/src/string/readingTime.ts +1 -1
- package/src/string/safeJsonStringify.ts +3 -5
- package/src/string/stringifyAny.ts +3 -1
- package/src/string/url.util.ts +1 -1
|
@@ -431,6 +431,7 @@ export class LocalTime {
|
|
|
431
431
|
|
|
432
432
|
isBetween(min: LocalTimeConfig, max: LocalTimeConfig, incl: Inclusiveness = '[)'): boolean {
|
|
433
433
|
let r = this.cmp(min)
|
|
434
|
+
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
434
435
|
if (r < 0 || (r === 0 && incl[0] === '(')) return false
|
|
435
436
|
r = this.cmp(max)
|
|
436
437
|
if (r > 0 || (r === 0 && incl[1] === ')')) return false
|
|
@@ -641,9 +642,8 @@ function getWeekYear(date: Date): number {
|
|
|
641
642
|
return year + 1
|
|
642
643
|
} else if (date.getTime() >= startOfThisYear.getTime()) {
|
|
643
644
|
return year
|
|
644
|
-
} else {
|
|
645
|
-
return year - 1
|
|
646
645
|
}
|
|
646
|
+
return year - 1
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
// based on: https://github.com/date-fns/date-fns/blob/fd6bb1a0bab143f2da068c05a9c562b9bee1357d/src/startOfWeek/index.ts
|
|
@@ -77,6 +77,7 @@ export class TimeInterval {
|
|
|
77
77
|
|
|
78
78
|
includes(d: LocalTimeConfig, incl: Inclusiveness = '[)'): boolean {
|
|
79
79
|
d = LocalTime.parseToUnixTimestamp(d)
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
|
|
80
81
|
if (d < this.$start || (d === this.$start && incl[0] === '(')) return false
|
|
81
82
|
if (d > this.$end || (d === this.$end && incl[1] === ')')) return false
|
|
82
83
|
return true
|
|
@@ -11,12 +11,12 @@ export interface AsyncMemoOptions {
|
|
|
11
11
|
* Function that creates an instance of `MemoCache`.
|
|
12
12
|
* e.g LRUMemoCache from `@naturalcycles/nodejs-lib`.
|
|
13
13
|
*/
|
|
14
|
-
cacheFactory
|
|
14
|
+
cacheFactory?(): AsyncMemoCache
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Provide a custom implementation of CacheKey function.
|
|
18
18
|
*/
|
|
19
|
-
cacheKeyFn
|
|
19
|
+
cacheKeyFn?(args: any[]): any
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Default true.
|
|
@@ -7,14 +7,14 @@ export interface PromiseDecoratorCfg<RES = any, PARAMS = any> {
|
|
|
7
7
|
* Called BEFORE the original function.
|
|
8
8
|
* If Promise is returned - it will be awaited.
|
|
9
9
|
*/
|
|
10
|
-
beforeFn
|
|
10
|
+
beforeFn?(r: PromiseDecoratorResp<PARAMS>): void | Promise<void>
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Called just AFTER the original function.
|
|
14
14
|
* The output of this hook will be passed further,
|
|
15
15
|
* so, pay attention to pass through (or modify) the result.
|
|
16
16
|
*/
|
|
17
|
-
thenFn
|
|
17
|
+
thenFn?(r: PromiseDecoratorResp<PARAMS> & { res: RES }): RES
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Called on Promise.reject.
|
|
@@ -23,13 +23,13 @@ export interface PromiseDecoratorCfg<RES = any, PARAMS = any> {
|
|
|
23
23
|
* If `catchFn` is present - it's responsible for handling or re-throwing the error.
|
|
24
24
|
* Whatever `catchFn` returns - passed to the original output.
|
|
25
25
|
*/
|
|
26
|
-
catchFn
|
|
26
|
+
catchFn?(r: PromiseDecoratorResp<PARAMS> & { err: any }): RES
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Fires AFTER thenFn / catchFn, like a usual Promise.finally().
|
|
30
30
|
* Doesn't have access to neither res nor err (same as Promise.finally).
|
|
31
31
|
*/
|
|
32
|
-
finallyFn
|
|
32
|
+
finallyFn?(r: PromiseDecoratorResp<PARAMS>): any
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface PromiseDecoratorResp<PARAMS> {
|
|
@@ -122,11 +122,10 @@ export function _LogMethod(opt: LogMethodOptions = {}): MethodDecorator {
|
|
|
122
122
|
logFinished(logger, callSignature, started, sma, logResultFn, undefined, err)
|
|
123
123
|
throw err
|
|
124
124
|
})
|
|
125
|
-
} else {
|
|
126
|
-
// not a Promise
|
|
127
|
-
logFinished(logger, callSignature, started, sma, logResultFn, res)
|
|
128
|
-
return res
|
|
129
125
|
}
|
|
126
|
+
// not a Promise
|
|
127
|
+
logFinished(logger, callSignature, started, sma, logResultFn, res)
|
|
128
|
+
return res
|
|
130
129
|
} catch (err) {
|
|
131
130
|
logFinished(logger, callSignature, started, sma, logResultFn, undefined, err)
|
|
132
131
|
throw err // rethrow
|
|
@@ -137,6 +136,7 @@ export function _LogMethod(opt: LogMethodOptions = {}): MethodDecorator {
|
|
|
137
136
|
}
|
|
138
137
|
}
|
|
139
138
|
|
|
139
|
+
// eslint-disable-next-line max-params
|
|
140
140
|
function logFinished(
|
|
141
141
|
logger: CommonLogger,
|
|
142
142
|
callSignature: string,
|
|
@@ -11,12 +11,12 @@ export interface MemoOptions {
|
|
|
11
11
|
* Function that creates an instance of `MemoCache`.
|
|
12
12
|
* e.g LRUMemoCache from `@naturalcycles/nodejs-lib`
|
|
13
13
|
*/
|
|
14
|
-
cacheFactory
|
|
14
|
+
cacheFactory?(): MemoCache
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Provide a custom implementation of CacheKey function.
|
|
18
18
|
*/
|
|
19
|
-
cacheKeyFn
|
|
19
|
+
cacheKeyFn?(args: any[]): any
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Defaults to true.
|
package/src/error/tryCatch.ts
CHANGED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/// <reference lib="dom"/>
|
|
2
|
+
|
|
3
|
+
import { _anyToErrorObject } from '../error/error.util'
|
|
4
|
+
import { HttpError } from '../error/http.error'
|
|
5
|
+
import { CommonLogger } from '../log/commonLogger'
|
|
6
|
+
import { _clamp } from '../number/number.util'
|
|
7
|
+
import { _filterNullishValues, _filterUndefinedValues } from '../object/object.util'
|
|
8
|
+
import { pDelay } from '../promise/pDelay'
|
|
9
|
+
import { _jsonParseIfPossible } from '../string/json.util'
|
|
10
|
+
import { _stringifyAny } from '../string/stringifyAny'
|
|
11
|
+
import { _since } from '../time/time.util'
|
|
12
|
+
import type { Promisable } from '../typeFest'
|
|
13
|
+
import { _objectAssign } from '../types'
|
|
14
|
+
import type { HttpMethod, HttpStatusFamily } from './http.model'
|
|
15
|
+
|
|
16
|
+
export interface FetcherNormalizedCfg extends FetcherCfg, FetcherNormalizedOptions {
|
|
17
|
+
logger: CommonLogger
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FetcherCfg {
|
|
21
|
+
baseUrl?: string
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Default rule is that you **are allowed** to mutate req, res, res.retryStatus
|
|
25
|
+
* properties of hook function arguments.
|
|
26
|
+
* If you throw an error from the hook - it will be re-thrown as-is.
|
|
27
|
+
*/
|
|
28
|
+
hooks?: {
|
|
29
|
+
/**
|
|
30
|
+
* Allows to mutate req.
|
|
31
|
+
*/
|
|
32
|
+
beforeRequest?(req: FetcherRequest): Promisable<void>
|
|
33
|
+
/**
|
|
34
|
+
* Allows to mutate res.
|
|
35
|
+
* If you set `res.err` - it will be thrown.
|
|
36
|
+
*/
|
|
37
|
+
beforeResponse?(res: FetcherResponse): Promisable<void>
|
|
38
|
+
/**
|
|
39
|
+
* Allows to mutate res.retryStatus to override retry behavior.
|
|
40
|
+
*/
|
|
41
|
+
beforeRetry?(res: FetcherResponse): Promisable<void>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
debug?: boolean
|
|
45
|
+
logRequest?: boolean
|
|
46
|
+
logRequestBody?: boolean
|
|
47
|
+
logResponse?: boolean
|
|
48
|
+
logResponseBody?: boolean
|
|
49
|
+
logger?: CommonLogger
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FetcherRetryStatus {
|
|
53
|
+
retryAttempt: number
|
|
54
|
+
retryTimeout: number
|
|
55
|
+
retryStopped: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FetcherRetryOptions {
|
|
59
|
+
count: number
|
|
60
|
+
timeout: number
|
|
61
|
+
timeoutMax: number
|
|
62
|
+
timeoutMultiplier: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FetcherNormalizedOptions extends FetcherOptions {
|
|
66
|
+
method: HttpMethod
|
|
67
|
+
throwHttpErrors: boolean
|
|
68
|
+
timeoutSeconds: number
|
|
69
|
+
retry: FetcherRetryOptions
|
|
70
|
+
retryPost: boolean
|
|
71
|
+
retry4xx: boolean
|
|
72
|
+
retry5xx: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FetcherOptions {
|
|
76
|
+
method?: HttpMethod
|
|
77
|
+
throwHttpErrors?: boolean
|
|
78
|
+
/**
|
|
79
|
+
* Default: 30.
|
|
80
|
+
*
|
|
81
|
+
* Timeout applies to both get the response and retrieve the body (e.g `await res.json()`),
|
|
82
|
+
* so both should finish within this single timeout (not each).
|
|
83
|
+
*/
|
|
84
|
+
timeoutSeconds?: number
|
|
85
|
+
json?: any
|
|
86
|
+
text?: string
|
|
87
|
+
requestInit?: RequestInit & { method?: HttpMethod }
|
|
88
|
+
mode?: FetcherMode // default to undefined (void response)
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Default is 2 retries (3 tries in total).
|
|
92
|
+
* Pass `retry: { count: 0 }` to disable retries.
|
|
93
|
+
*/
|
|
94
|
+
retry?: Partial<FetcherRetryOptions>
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Defaults to false.
|
|
98
|
+
* Set to true to allow retrying `post` requests.
|
|
99
|
+
*/
|
|
100
|
+
retryPost?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* Defaults to false.
|
|
103
|
+
*/
|
|
104
|
+
retry4xx?: boolean
|
|
105
|
+
/**
|
|
106
|
+
* Defaults to true.
|
|
107
|
+
*/
|
|
108
|
+
retry5xx?: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface FetcherRequest {
|
|
112
|
+
url: string
|
|
113
|
+
init: RequestInit & { method: HttpMethod }
|
|
114
|
+
opt: FetcherNormalizedOptions
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface FetcherSuccessResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
118
|
+
err?: undefined
|
|
119
|
+
fetchResponse: Response
|
|
120
|
+
body: BODY
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface FetcherErrorResponse<BODY = unknown> extends FetcherResponse<BODY> {
|
|
124
|
+
err: Error
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface FetcherResponse<BODY = unknown> {
|
|
128
|
+
err?: Error
|
|
129
|
+
req: FetcherRequest
|
|
130
|
+
fetchResponse?: Response
|
|
131
|
+
statusFamily?: HttpStatusFamily
|
|
132
|
+
body?: BODY
|
|
133
|
+
retryStatus: FetcherRetryStatus
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type FetcherMode = 'json' | 'text'
|
|
137
|
+
|
|
138
|
+
const defRetryOptions: FetcherRetryOptions = {
|
|
139
|
+
count: 2,
|
|
140
|
+
timeout: 500,
|
|
141
|
+
timeoutMax: 30_000,
|
|
142
|
+
timeoutMultiplier: 2,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Experimental wrapper around Fetch.
|
|
147
|
+
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
148
|
+
*
|
|
149
|
+
* @experimental
|
|
150
|
+
*/
|
|
151
|
+
export class Fetcher {
|
|
152
|
+
private constructor(cfg: FetcherCfg & FetcherOptions = {}) {
|
|
153
|
+
this.cfg = this.normalizeCfg(cfg)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public cfg: FetcherNormalizedCfg
|
|
157
|
+
|
|
158
|
+
static create(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
|
|
159
|
+
return new Fetcher(cfg)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getJson<T = unknown>(url: string, opt: FetcherOptions = {}): Promise<T> {
|
|
163
|
+
return await this.fetch<T>(url, {
|
|
164
|
+
...opt,
|
|
165
|
+
mode: 'json',
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async postJson<T = unknown>(url: string, opt: FetcherOptions = {}): Promise<T> {
|
|
170
|
+
return await this.fetch<T>(url, {
|
|
171
|
+
...opt,
|
|
172
|
+
method: 'post',
|
|
173
|
+
mode: 'json',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async getText(url: string, opt: FetcherOptions = {}): Promise<string> {
|
|
178
|
+
return await this.fetch<string>(url, {
|
|
179
|
+
...opt,
|
|
180
|
+
mode: 'text',
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async postText(url: string, opt: FetcherOptions = {}): Promise<string> {
|
|
185
|
+
return await this.fetch<string>(url, {
|
|
186
|
+
...opt,
|
|
187
|
+
method: 'post',
|
|
188
|
+
mode: 'text',
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async fetch<T = unknown>(url: string, opt: FetcherOptions = {}): Promise<T> {
|
|
193
|
+
const res = await this.rawFetch<T>(url, opt)
|
|
194
|
+
if (res.err) {
|
|
195
|
+
if (res.req.opt.throwHttpErrors) throw res.err
|
|
196
|
+
return res as any
|
|
197
|
+
}
|
|
198
|
+
return res.body!
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async rawFetch<T = unknown>(
|
|
202
|
+
url: string,
|
|
203
|
+
rawOpt: FetcherOptions = {},
|
|
204
|
+
): Promise<FetcherResponse<T>> {
|
|
205
|
+
const { baseUrl, logger } = this.cfg
|
|
206
|
+
|
|
207
|
+
const opt = this.normalizeOptions(rawOpt)
|
|
208
|
+
const { method, timeoutSeconds, mode } = opt
|
|
209
|
+
|
|
210
|
+
const req: FetcherRequest = {
|
|
211
|
+
url,
|
|
212
|
+
init: {
|
|
213
|
+
...this.cfg.requestInit,
|
|
214
|
+
method,
|
|
215
|
+
},
|
|
216
|
+
opt,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// setup url
|
|
220
|
+
if (baseUrl) {
|
|
221
|
+
if (url.startsWith('/')) {
|
|
222
|
+
console.warn(`Fetcher: url should not start with / when baseUrl is specified`)
|
|
223
|
+
url = url.slice(1)
|
|
224
|
+
}
|
|
225
|
+
req.url = `${baseUrl}/${url}`
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// setup request body
|
|
229
|
+
if (opt.json !== undefined) {
|
|
230
|
+
req.init.body = JSON.stringify(opt.json)
|
|
231
|
+
} else if (opt.text !== undefined) {
|
|
232
|
+
req.init.body = opt.text
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// setup timeout
|
|
236
|
+
let timeout: number | undefined
|
|
237
|
+
if (timeoutSeconds) {
|
|
238
|
+
const abortController = new AbortController()
|
|
239
|
+
req.init.signal = abortController.signal
|
|
240
|
+
timeout = setTimeout(() => {
|
|
241
|
+
abortController.abort(`timeout of ${timeoutSeconds} sec`)
|
|
242
|
+
}, timeoutSeconds * 1000) as any as number
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (opt.requestInit) {
|
|
246
|
+
_objectAssign(req.init, opt.requestInit)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
await this.cfg.hooks?.beforeRequest?.(req)
|
|
250
|
+
|
|
251
|
+
const res: FetcherResponse<any> = {
|
|
252
|
+
req,
|
|
253
|
+
retryStatus: {
|
|
254
|
+
retryAttempt: 0,
|
|
255
|
+
retryStopped: false,
|
|
256
|
+
retryTimeout: opt.retry.timeout,
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const shortUrl = this.getShortUrl(req.url)
|
|
261
|
+
const signature = [method.toUpperCase(), shortUrl].join(' ')
|
|
262
|
+
|
|
263
|
+
/* eslint-disable no-await-in-loop */
|
|
264
|
+
while (!res.retryStatus.retryStopped) {
|
|
265
|
+
const started = Date.now()
|
|
266
|
+
|
|
267
|
+
if (this.cfg.logRequest) {
|
|
268
|
+
const { retryAttempt } = res.retryStatus
|
|
269
|
+
logger.log(
|
|
270
|
+
[' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${opt.retry.count}`]
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.join(' '),
|
|
273
|
+
)
|
|
274
|
+
if (this.cfg.logRequestBody && req.init.body) {
|
|
275
|
+
logger.log(req.init.body) // todo: check if we can _inspect it
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
res.fetchResponse = await globalThis.fetch(req.url, req.init)
|
|
280
|
+
res.statusFamily = this.getStatusFamily(res)
|
|
281
|
+
|
|
282
|
+
if (res.fetchResponse.ok) {
|
|
283
|
+
if (mode === 'json') {
|
|
284
|
+
// if no body: set responseBody as {}
|
|
285
|
+
// do not throw a "cannot parse null as Json" error
|
|
286
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.json() : {}
|
|
287
|
+
} else if (mode === 'text') {
|
|
288
|
+
res.body = res.fetchResponse.body ? await res.fetchResponse.text() : ''
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
clearTimeout(timeout)
|
|
292
|
+
res.retryStatus.retryStopped = true
|
|
293
|
+
|
|
294
|
+
if (this.cfg.logResponse) {
|
|
295
|
+
const { retryAttempt } = res.retryStatus
|
|
296
|
+
logger.log(
|
|
297
|
+
[
|
|
298
|
+
' <<',
|
|
299
|
+
res.fetchResponse.status,
|
|
300
|
+
signature,
|
|
301
|
+
retryAttempt && `try#${retryAttempt + 1}/${opt.retry.count}`,
|
|
302
|
+
_since(started),
|
|
303
|
+
]
|
|
304
|
+
.filter(Boolean)
|
|
305
|
+
.join(' '),
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if (this.cfg.logResponseBody) {
|
|
309
|
+
logger.log(res.body)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} else {
|
|
313
|
+
clearTimeout(timeout)
|
|
314
|
+
|
|
315
|
+
const body = _jsonParseIfPossible(await res.fetchResponse.text())
|
|
316
|
+
const errObj = _anyToErrorObject(body)
|
|
317
|
+
const originalMessage = errObj.message
|
|
318
|
+
errObj.message = [[res.fetchResponse.status, signature].join(' '), originalMessage].join(
|
|
319
|
+
'\n',
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
res.err = new HttpError(
|
|
323
|
+
errObj.message,
|
|
324
|
+
|
|
325
|
+
_filterNullishValues({
|
|
326
|
+
...errObj.data,
|
|
327
|
+
originalMessage,
|
|
328
|
+
httpStatusCode: res.fetchResponse.status,
|
|
329
|
+
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
330
|
+
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
331
|
+
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
332
|
+
// method: req.method,
|
|
333
|
+
url: req.url,
|
|
334
|
+
// tryCount: req.tryCount,
|
|
335
|
+
}),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if (this.cfg.logResponse) {
|
|
339
|
+
const { retryAttempt } = res.retryStatus
|
|
340
|
+
logger.error(
|
|
341
|
+
[
|
|
342
|
+
[
|
|
343
|
+
' <<',
|
|
344
|
+
res.fetchResponse.status,
|
|
345
|
+
signature,
|
|
346
|
+
retryAttempt && `try#${retryAttempt + 1}/${opt.retry.count}`,
|
|
347
|
+
_since(started),
|
|
348
|
+
]
|
|
349
|
+
.filter(Boolean)
|
|
350
|
+
.join(' '),
|
|
351
|
+
_stringifyAny(body),
|
|
352
|
+
].join('\n'),
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await this.processRetry(res)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await this.cfg.hooks?.beforeResponse?.(res)
|
|
361
|
+
|
|
362
|
+
return res
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private async processRetry(res: FetcherResponse): Promise<void> {
|
|
366
|
+
const { retryStatus } = res
|
|
367
|
+
|
|
368
|
+
if (!this.shouldRetry(res)) {
|
|
369
|
+
retryStatus.retryStopped = true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
await this.cfg.hooks?.beforeRetry?.(res)
|
|
373
|
+
|
|
374
|
+
const { count, timeoutMultiplier, timeoutMax } = res.req.opt.retry
|
|
375
|
+
|
|
376
|
+
if (retryStatus.retryAttempt >= count) {
|
|
377
|
+
retryStatus.retryStopped = true
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (retryStatus.retryStopped) return
|
|
381
|
+
|
|
382
|
+
retryStatus.retryTimeout = _clamp(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax)
|
|
383
|
+
|
|
384
|
+
await pDelay(retryStatus.retryTimeout)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Default is yes,
|
|
389
|
+
* unless there's reason not to (e.g method is POST).
|
|
390
|
+
*/
|
|
391
|
+
private shouldRetry(res: FetcherResponse): boolean {
|
|
392
|
+
const { retryPost, retry4xx, retry5xx } = res.req.opt
|
|
393
|
+
const { method } = res.req.init
|
|
394
|
+
if (method === 'post' && !retryPost) return false
|
|
395
|
+
const { statusFamily } = res
|
|
396
|
+
if (statusFamily === '5xx' && !retry5xx) return false
|
|
397
|
+
if (statusFamily === '4xx' && !retry4xx) return false
|
|
398
|
+
return true // default is true
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private getStatusFamily(res: FetcherResponse): HttpStatusFamily | undefined {
|
|
402
|
+
const status = res.fetchResponse?.status
|
|
403
|
+
if (!status) return
|
|
404
|
+
if (status >= 500) return '5xx'
|
|
405
|
+
if (status >= 400) return '4xx'
|
|
406
|
+
if (status >= 300) return '3xx'
|
|
407
|
+
if (status >= 200) return '2xx'
|
|
408
|
+
if (status >= 100) return '1xx'
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Returns url without baseUrl and before ?queryString
|
|
413
|
+
*/
|
|
414
|
+
private getShortUrl(url: string): string {
|
|
415
|
+
const { baseUrl } = this.cfg
|
|
416
|
+
if (!baseUrl) return url
|
|
417
|
+
|
|
418
|
+
return url.split('?')[0]!.slice(baseUrl.length)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private normalizeCfg(cfg: FetcherCfg & FetcherOptions): FetcherNormalizedCfg {
|
|
422
|
+
if (cfg.baseUrl?.endsWith('/')) {
|
|
423
|
+
console.warn(`Fetcher: baseUrl should not end with /`)
|
|
424
|
+
cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1)
|
|
425
|
+
}
|
|
426
|
+
const { debug } = cfg
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
timeoutSeconds: 30,
|
|
430
|
+
method: 'get',
|
|
431
|
+
throwHttpErrors: true,
|
|
432
|
+
retryPost: false,
|
|
433
|
+
retry4xx: false,
|
|
434
|
+
retry5xx: true,
|
|
435
|
+
logger: console,
|
|
436
|
+
logRequest: debug,
|
|
437
|
+
logRequestBody: debug,
|
|
438
|
+
logResponse: debug,
|
|
439
|
+
logResponseBody: debug,
|
|
440
|
+
...cfg,
|
|
441
|
+
retry: {
|
|
442
|
+
...defRetryOptions,
|
|
443
|
+
...cfg.retry,
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private normalizeOptions(opt: FetcherOptions): FetcherNormalizedOptions {
|
|
449
|
+
const { timeoutSeconds, throwHttpErrors, method, retryPost, retry4xx, retry5xx, retry } =
|
|
450
|
+
this.cfg
|
|
451
|
+
return {
|
|
452
|
+
timeoutSeconds,
|
|
453
|
+
throwHttpErrors,
|
|
454
|
+
method,
|
|
455
|
+
retryPost,
|
|
456
|
+
retry4xx,
|
|
457
|
+
retry5xx,
|
|
458
|
+
...opt,
|
|
459
|
+
retry: {
|
|
460
|
+
...retry,
|
|
461
|
+
..._filterUndefinedValues(opt.retry || {}),
|
|
462
|
+
},
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function getFetcher(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
|
|
468
|
+
return Fetcher.create(cfg)
|
|
469
|
+
}
|
package/src/index.ts
CHANGED
package/src/object/deepEquals.ts
CHANGED
|
@@ -17,13 +17,12 @@ export function _pick<T extends AnyObject, K extends keyof T>(
|
|
|
17
17
|
if (!props.includes(prop as K)) delete r[prop]
|
|
18
18
|
return r
|
|
19
19
|
}, obj)
|
|
20
|
-
} else {
|
|
21
|
-
// Start as empty object, pick/add needed properties
|
|
22
|
-
return props.reduce((r, prop) => {
|
|
23
|
-
if (prop in obj) r[prop] = obj[prop]
|
|
24
|
-
return r
|
|
25
|
-
}, {} as T)
|
|
26
20
|
}
|
|
21
|
+
// Start as empty object, pick/add needed properties
|
|
22
|
+
return props.reduce((r, prop) => {
|
|
23
|
+
if (prop in obj) r[prop] = obj[prop]
|
|
24
|
+
return r
|
|
25
|
+
}, {} as T)
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
/**
|
|
@@ -285,7 +284,7 @@ export function _unset<T extends AnyObject>(obj: T, prop: string): void {
|
|
|
285
284
|
|
|
286
285
|
const segs = prop.split('.')
|
|
287
286
|
let last = segs.pop()
|
|
288
|
-
while (segs.length && segs[segs.length - 1]!.
|
|
287
|
+
while (segs.length && segs[segs.length - 1]!.endsWith('\\')) {
|
|
289
288
|
last = segs.pop()!.slice(0, -1) + '.' + last
|
|
290
289
|
}
|
|
291
290
|
while (segs.length && _isObject(obj)) {
|
|
@@ -359,7 +358,7 @@ export function _set<T extends AnyObject>(obj: T, path: PropertyPath, value: any
|
|
|
359
358
|
a[c]
|
|
360
359
|
: // No: create the key. Is the next key a potential array-index?
|
|
361
360
|
(a[c] =
|
|
362
|
-
// @ts-
|
|
361
|
+
// @ts-expect-error
|
|
363
362
|
// eslint-disable-next-line
|
|
364
363
|
Math.abs(path[i + 1]) >> 0 === +path[i + 1]
|
|
365
364
|
? [] // Yes: assign a new array object
|
package/src/promise/pRetry.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface PRetryOptions {
|
|
|
43
43
|
*
|
|
44
44
|
* @default () => true
|
|
45
45
|
*/
|
|
46
|
-
predicate
|
|
46
|
+
predicate?(err: Error, attempt: number, maxAttempts: number): boolean
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Log the first attempt (which is not a "retry" yet).
|
package/src/promise/pTimeout.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface PTimeoutOptions {
|
|
|
23
23
|
* err (which is TimeoutError) is passed as an argument for convenience, so it can
|
|
24
24
|
* be logged or such. You don't have to consume it in any way though.
|
|
25
25
|
*/
|
|
26
|
-
onTimeout
|
|
26
|
+
onTimeout?(err: TimeoutError): any
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Defaults to true.
|
package/src/string/pupa.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface PupaOptions {
|
|
|
31
31
|
/**
|
|
32
32
|
* Performs arbitrary operation for each interpolation. If the returned value was `undefined`, it behaves differently depending on the `ignoreMissing` option. Otherwise, the returned value will be interpolated into a string (and escaped when double-braced) and embedded into the template.
|
|
33
33
|
*/
|
|
34
|
-
transform
|
|
34
|
+
transform?(data: { value: any; key: string }): unknown
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -16,7 +16,7 @@ export interface ReadingTimeOptions {
|
|
|
16
16
|
* A function that returns a boolean value depending on if a character is considered as a word bound.
|
|
17
17
|
* Default: spaces, new lines and tabs
|
|
18
18
|
*/
|
|
19
|
-
wordBound
|
|
19
|
+
wordBound?(char: string): boolean
|
|
20
20
|
/**
|
|
21
21
|
* Default 200
|
|
22
22
|
*/
|