@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.
Files changed (62) hide show
  1. package/dist/array/array.util.js +4 -4
  2. package/dist/datetime/dateInterval.js +1 -0
  3. package/dist/datetime/localDate.js +2 -0
  4. package/dist/datetime/localTime.js +2 -3
  5. package/dist/datetime/timeInterval.js +1 -0
  6. package/dist/decorators/asyncMemo.decorator.d.ts +2 -2
  7. package/dist/decorators/createPromiseDecorator.d.ts +6 -6
  8. package/dist/decorators/logMethod.decorator.js +4 -5
  9. package/dist/decorators/memo.decorator.d.ts +2 -2
  10. package/dist/error/tryCatch.d.ts +1 -1
  11. package/dist/http/fetcher.d.ts +146 -0
  12. package/dist/http/fetcher.js +298 -0
  13. package/dist/http/http.model.d.ts +2 -0
  14. package/dist/http/http.model.js +2 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +2 -0
  17. package/dist/object/deepEquals.js +1 -0
  18. package/dist/object/object.util.js +8 -10
  19. package/dist/promise/pRetry.d.ts +1 -1
  20. package/dist/promise/pTimeout.d.ts +1 -1
  21. package/dist/string/pupa.d.ts +2 -2
  22. package/dist/string/readingTime.d.ts +1 -1
  23. package/dist/string/safeJsonStringify.js +5 -7
  24. package/dist/string/stringifyAny.js +2 -1
  25. package/dist/string/url.util.js +1 -1
  26. package/dist-esm/array/array.util.js +4 -4
  27. package/dist-esm/datetime/dateInterval.js +1 -0
  28. package/dist-esm/datetime/localDate.js +2 -0
  29. package/dist-esm/datetime/localTime.js +2 -3
  30. package/dist-esm/datetime/timeInterval.js +1 -0
  31. package/dist-esm/decorators/logMethod.decorator.js +4 -5
  32. package/dist-esm/http/fetcher.js +251 -0
  33. package/dist-esm/http/http.model.js +1 -0
  34. package/dist-esm/index.js +2 -0
  35. package/dist-esm/object/deepEquals.js +1 -0
  36. package/dist-esm/object/object.util.js +8 -10
  37. package/dist-esm/string/safeJsonStringify.js +5 -7
  38. package/dist-esm/string/stringifyAny.js +2 -1
  39. package/dist-esm/string/url.util.js +1 -1
  40. package/package.json +1 -1
  41. package/src/array/array.util.ts +4 -4
  42. package/src/datetime/dateInterval.ts +1 -0
  43. package/src/datetime/localDate.ts +2 -0
  44. package/src/datetime/localTime.ts +2 -2
  45. package/src/datetime/timeInterval.ts +1 -0
  46. package/src/decorators/asyncMemo.decorator.ts +2 -2
  47. package/src/decorators/createPromiseDecorator.ts +4 -4
  48. package/src/decorators/logMethod.decorator.ts +4 -4
  49. package/src/decorators/memo.decorator.ts +2 -2
  50. package/src/error/tryCatch.ts +1 -1
  51. package/src/http/fetcher.ts +469 -0
  52. package/src/http/http.model.ts +3 -0
  53. package/src/index.ts +2 -0
  54. package/src/object/deepEquals.ts +1 -0
  55. package/src/object/object.util.ts +7 -8
  56. package/src/promise/pRetry.ts +1 -1
  57. package/src/promise/pTimeout.ts +1 -1
  58. package/src/string/pupa.ts +1 -1
  59. package/src/string/readingTime.ts +1 -1
  60. package/src/string/safeJsonStringify.ts +3 -5
  61. package/src/string/stringifyAny.ts +3 -1
  62. 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?: () => AsyncMemoCache
14
+ cacheFactory?(): AsyncMemoCache
15
15
 
16
16
  /**
17
17
  * Provide a custom implementation of CacheKey function.
18
18
  */
19
- cacheKeyFn?: (args: any[]) => any
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?: (r: PromiseDecoratorResp<PARAMS>) => void | Promise<void>
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?: (r: PromiseDecoratorResp<PARAMS> & { res: RES }) => RES
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?: (r: PromiseDecoratorResp<PARAMS> & { err: any }) => RES
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?: (r: PromiseDecoratorResp<PARAMS>) => any
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?: () => MemoCache
14
+ cacheFactory?(): MemoCache
15
15
 
16
16
  /**
17
17
  * Provide a custom implementation of CacheKey function.
18
18
  */
19
- cacheKeyFn?: (args: any[]) => any
19
+ cacheKeyFn?(args: any[]): any
20
20
 
21
21
  /**
22
22
  * Defaults to true.
@@ -7,7 +7,7 @@ export interface TryCatchOptions {
7
7
  * The value returned from the function will be returned from the wrapped method (!).
8
8
  * onError function may be asynchronous.
9
9
  */
10
- onError?: (err: Error) => any
10
+ onError?(err: Error): any
11
11
 
12
12
  /**
13
13
  * @default false
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'
2
+
3
+ export type HttpStatusFamily = '5xx' | '4xx' | '3xx' | '2xx' | '1xx'
package/src/index.ts CHANGED
@@ -74,5 +74,7 @@ export * from './datetime/localDate'
74
74
  export * from './datetime/localTime'
75
75
  export * from './datetime/dateInterval'
76
76
  export * from './datetime/timeInterval'
77
+ export * from './http/http.model'
78
+ export * from './http/fetcher'
77
79
 
78
80
  export { is }
@@ -51,5 +51,6 @@ export function _deepEquals(a: any, b: any): boolean {
51
51
  return true
52
52
  }
53
53
 
54
+ // eslint-disable-next-line no-self-compare
54
55
  return a !== a && b !== b
55
56
  }
@@ -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]!.slice(-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-ignore
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
@@ -43,7 +43,7 @@ export interface PRetryOptions {
43
43
  *
44
44
  * @default () => true
45
45
  */
46
- predicate?: (err: Error, attempt: number, maxAttempts: number) => boolean
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).
@@ -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?: (err: TimeoutError) => any
26
+ onTimeout?(err: TimeoutError): any
27
27
 
28
28
  /**
29
29
  * Defaults to true.
@@ -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?: (data: { value: any; key: string }) => unknown
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?: (char: string) => boolean
19
+ wordBound?(char: string): boolean
20
20
  /**
21
21
  * Default 200
22
22
  */