@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.
Files changed (54) hide show
  1. package/dist/decorators/logMethod.decorator.js +2 -2
  2. package/dist/env.d.ts +14 -0
  3. package/dist/env.js +23 -0
  4. package/dist/error/error.util.d.ts +1 -1
  5. package/dist/error/error.util.js +2 -0
  6. package/dist/error/tryCatch.js +1 -3
  7. package/dist/http/fetcher.d.ts +2 -0
  8. package/dist/http/fetcher.js +104 -92
  9. package/dist/http/fetcher.model.d.ts +14 -3
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +2 -0
  12. package/dist/promise/abortable.d.ts +20 -0
  13. package/dist/promise/abortable.js +36 -0
  14. package/dist/promise/pDefer.d.ts +14 -1
  15. package/dist/promise/pDefer.js +2 -0
  16. package/dist/promise/pDelay.d.ts +18 -0
  17. package/dist/promise/pDelay.js +37 -2
  18. package/dist/promise/pRetry.d.ts +0 -8
  19. package/dist/promise/pRetry.js +37 -63
  20. package/dist/promise/pTimeout.d.ts +4 -6
  21. package/dist/promise/pTimeout.js +8 -10
  22. package/dist/string/stringifyAny.d.ts +0 -6
  23. package/dist/string/stringifyAny.js +0 -5
  24. package/dist/types.d.ts +3 -0
  25. package/dist/vendor/is.d.ts +2 -2
  26. package/dist-esm/decorators/logMethod.decorator.js +2 -2
  27. package/dist-esm/env.js +18 -0
  28. package/dist-esm/error/error.util.js +2 -0
  29. package/dist-esm/error/tryCatch.js +2 -4
  30. package/dist-esm/http/fetcher.js +111 -98
  31. package/dist-esm/index.js +2 -0
  32. package/dist-esm/promise/abortable.js +32 -0
  33. package/dist-esm/promise/pDefer.js +2 -0
  34. package/dist-esm/promise/pDelay.js +35 -1
  35. package/dist-esm/promise/pRetry.js +38 -61
  36. package/dist-esm/promise/pTimeout.js +8 -7
  37. package/dist-esm/string/stringifyAny.js +0 -5
  38. package/package.json +1 -1
  39. package/src/decorators/logMethod.decorator.ts +2 -2
  40. package/src/env.ts +19 -0
  41. package/src/error/error.util.ts +3 -1
  42. package/src/error/tryCatch.ts +2 -6
  43. package/src/http/fetcher.model.ts +14 -3
  44. package/src/http/fetcher.ts +117 -95
  45. package/src/index.ts +2 -0
  46. package/src/promise/abortable.ts +34 -0
  47. package/src/promise/pDefer.ts +19 -1
  48. package/src/promise/pDelay.ts +44 -2
  49. package/src/promise/pRetry.ts +41 -89
  50. package/src/promise/pState.ts +1 -1
  51. package/src/promise/pTimeout.ts +12 -14
  52. package/src/string/stringifyAny.ts +0 -13
  53. package/src/types.ts +3 -0
  54. 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(() => resolve(value), ms));
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, _stringifyAny } from '..';
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, keepStackTrace = true, timeout, } = opt;
14
- const fakeError = keepStackTrace ? new Error('RetryError') : undefined;
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
- let timer;
26
- let timedOut = false;
27
- return await new Promise((resolve, reject) => {
28
- const rejectWithTimeout = () => {
29
- timedOut = true; // to prevent more tries
30
- const err = new TimeoutError(`"${fname}" timed out after ${timeout} ms`, opt.errorData);
31
- if (fakeError) {
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
- reject(err);
36
- };
37
- const next = async () => {
38
- if (timedOut)
39
- return;
32
+ let result;
40
33
  if (timeout) {
41
- timer = setTimeout(rejectWithTimeout, timeout);
34
+ await pTimeout(async () => await fn(attempt), {
35
+ timeout,
36
+ name: fname,
37
+ errorData: opt.errorData,
38
+ fakeError,
39
+ });
42
40
  }
43
- const started = Date.now();
44
- try {
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
- catch (err) {
57
- clearTimeout(timer);
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
- void next();
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, keepStackTrace = true } = opt;
28
- const fakeError = keepStackTrace ? new Error('TimeoutError') : undefined;
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
- if (fakeError)
35
- err.stack = fakeError.stack; // keep original stack
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
- if (fakeError)
42
- err.stack = fakeError.stack; // keep original stack
43
- err.data = Object.assign(Object.assign({}, err.data), opt.errorData);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.134.0",
3
+ "version": "14.136.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -155,10 +155,10 @@ function logFinished(
155
155
  }
156
156
 
157
157
  if (err !== undefined) {
158
- t.push('ERROR:', _stringifyAny(err, { includeErrorData: true }))
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).join(' '))
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
+ }
@@ -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: ErrorData): void {
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,
@@ -1,5 +1,5 @@
1
1
  import type { CommonLogger } from '../index'
2
- import { _anyToError, _since, _stringifyAny } from '../index'
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
- * Default to true.
53
- * Set to false to exclude `prefixUrl` from logs (both success and error)
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
- logWithPrefixUrl?: boolean
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> =
@@ -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
- if (mode === 'json') {
213
- if (res.fetchResponse.body) {
214
- const text = await res.fetchResponse.text()
215
-
216
- if (text) {
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
- clearTimeout(timeout)
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
- let errObj: ErrorObject
229
+ return res
230
+ }
272
231
 
273
- if (res.fetchResponse) {
274
- const body = _jsonParseIfPossible(await res.fetchResponse.text())
275
- errObj = _anyToErrorObject(body)
276
- } else if (res.err) {
277
- errObj = _errorToErrorObject(res.err)
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
- errObj = {} as ErrorObject
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
- const originalMessage = errObj.message
283
- errObj.message = [
284
- [res.fetchResponse?.status, signature].filter(Boolean).join(' '),
285
- originalMessage,
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('\n')
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
- await this.processRetry(res)
292
+ if (this.cfg.logResponseBody) {
293
+ logger.log(res.body)
307
294
  }
308
295
  }
296
+ }
309
297
 
310
- for await (const hook of this.cfg.hooks.afterResponse || []) {
311
- await hook(res)
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
- return res
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.logWithPrefixUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
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
- logWithPrefixUrl: true,
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
+ }