@naturalcycles/js-lib 14.134.0 → 14.135.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/env.d.ts ADDED
@@ -0,0 +1,14 @@
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 declare function isServerSide(): boolean;
8
+ /**
9
+ * Use it to detect Browser (not SSR/Node) environment.
10
+ *
11
+ * Will return `true` in the Browser.
12
+ * Will return `false` in Node.js.
13
+ */
14
+ export declare function isClientSide(): boolean;
package/dist/env.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isClientSide = exports.isServerSide = void 0;
4
+ /**
5
+ * Use it to detect SSR/Node.js environment.
6
+ *
7
+ * Will return `true` in Node.js.
8
+ * Will return `false` in the Browser.
9
+ */
10
+ function isServerSide() {
11
+ return typeof window === 'undefined';
12
+ }
13
+ exports.isServerSide = isServerSide;
14
+ /**
15
+ * Use it to detect Browser (not SSR/Node) environment.
16
+ *
17
+ * Will return `true` in the Browser.
18
+ * Will return `false` in Node.js.
19
+ */
20
+ function isClientSide() {
21
+ return typeof window !== 'undefined';
22
+ }
23
+ exports.isClientSide = isClientSide;
@@ -38,6 +38,8 @@ export declare class Fetcher {
38
38
  * Never throws, returns `err` property in the response instead.
39
39
  */
40
40
  rawFetch<T = unknown>(url: string, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
41
+ private onOkResponse;
42
+ private onNotOkResponse;
41
43
  private processRetry;
42
44
  /**
43
45
  * Default is yes,
@@ -2,6 +2,7 @@
2
2
  /// <reference lib="dom"/>
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.getFetcher = exports.Fetcher = void 0;
5
+ const env_1 = require("../env");
5
6
  const error_util_1 = require("../error/error.util");
6
7
  const http_error_1 = require("../error/http.error");
7
8
  const number_util_1 = require("../number/number.util");
@@ -94,7 +95,7 @@ class Fetcher {
94
95
  async rawFetch(url, rawOpt = {}) {
95
96
  const { logger } = this.cfg;
96
97
  const req = this.normalizeOptions(url, rawOpt);
97
- const { timeoutSeconds, mode, init: { method }, } = req;
98
+ const { timeoutSeconds, init: { method }, } = req;
98
99
  // setup timeout
99
100
  let timeout;
100
101
  if (timeoutSeconds) {
@@ -107,6 +108,10 @@ class Fetcher {
107
108
  for await (const hook of this.cfg.hooks.beforeRequest || []) {
108
109
  await hook(req);
109
110
  }
111
+ const isFullUrl = req.url.includes('://');
112
+ const fullUrl = isFullUrl ? new URL(req.url) : undefined;
113
+ const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.url;
114
+ const signature = [method, shortUrl].join(' ');
110
115
  const res = {
111
116
  req,
112
117
  retryStatus: {
@@ -114,10 +119,8 @@ class Fetcher {
114
119
  retryStopped: false,
115
120
  retryTimeout: req.retry.timeout,
116
121
  },
122
+ signature,
117
123
  };
118
- const fullUrl = new URL(req.url);
119
- const shortUrl = this.getShortUrl(fullUrl);
120
- const signature = [method, shortUrl].join(' ');
121
124
  /* eslint-disable no-await-in-loop */
122
125
  while (!res.retryStatus.retryStopped) {
123
126
  const started = Date.now();
@@ -141,95 +144,11 @@ class Fetcher {
141
144
  }
142
145
  res.statusFamily = this.getStatusFamily(res);
143
146
  if (res.fetchResponse?.ok) {
144
- if (mode === 'json') {
145
- if (res.fetchResponse.body) {
146
- const text = await res.fetchResponse.text();
147
- if (text) {
148
- try {
149
- res.body = text;
150
- res.body = JSON.parse(text, req.jsonReviver);
151
- }
152
- catch (err) {
153
- const { message } = (0, error_util_1._anyToError)(err);
154
- res.err = new http_error_1.HttpError([signature, message].join('\n'), {
155
- httpStatusCode: 0,
156
- url: req.url,
157
- });
158
- res.ok = false;
159
- }
160
- }
161
- else {
162
- // Body had a '' (empty string)
163
- res.body = {};
164
- }
165
- }
166
- else {
167
- // if no body: set responseBody as {}
168
- // do not throw a "cannot parse null as Json" error
169
- res.body = {};
170
- }
171
- }
172
- else if (mode === 'text') {
173
- res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
174
- }
175
- else if (mode === 'arrayBuffer') {
176
- res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
177
- }
178
- else if (mode === 'blob') {
179
- res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
180
- }
181
- clearTimeout(timeout);
182
- res.retryStatus.retryStopped = true;
183
- // res.err can happen on JSON.parse error
184
- if (!res.err && this.cfg.logResponse) {
185
- const { retryAttempt } = res.retryStatus;
186
- logger.log([
187
- ' <<',
188
- res.fetchResponse.status,
189
- signature,
190
- retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
191
- (0, time_util_1._since)(started),
192
- ]
193
- .filter(Boolean)
194
- .join(' '));
195
- if (this.cfg.logResponseBody) {
196
- logger.log(res.body);
197
- }
198
- }
147
+ await this.onOkResponse(res, started, timeout);
199
148
  }
200
149
  else {
201
150
  // !res.ok
202
- clearTimeout(timeout);
203
- let errObj;
204
- if (res.fetchResponse) {
205
- const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
206
- errObj = (0, error_util_1._anyToErrorObject)(body);
207
- }
208
- else if (res.err) {
209
- errObj = (0, error_util_1._errorToErrorObject)(res.err);
210
- }
211
- else {
212
- errObj = {};
213
- }
214
- const originalMessage = errObj.message;
215
- errObj.message = [
216
- [res.fetchResponse?.status, signature].filter(Boolean).join(' '),
217
- originalMessage,
218
- ]
219
- .filter(Boolean)
220
- .join('\n');
221
- res.err = new http_error_1.HttpError(errObj.message, (0, object_util_1._filterNullishValues)({
222
- ...errObj.data,
223
- originalMessage,
224
- httpStatusCode: res.fetchResponse?.status || 0,
225
- // These properties are provided to be used in e.g custom Sentry error grouping
226
- // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
227
- // Enabled, cause `data` is not printed by default when error is HttpError
228
- // method: req.method,
229
- url: req.url,
230
- // tryCount: req.tryCount,
231
- }));
232
- await this.processRetry(res);
151
+ await this.onNotOkResponse(res, timeout);
233
152
  }
234
153
  }
235
154
  for await (const hook of this.cfg.hooks.afterResponse || []) {
@@ -237,6 +156,99 @@ class Fetcher {
237
156
  }
238
157
  return res;
239
158
  }
159
+ async onOkResponse(res, started, timeout) {
160
+ const { req } = res;
161
+ const { mode } = res.req;
162
+ if (mode === 'json') {
163
+ if (res.fetchResponse.body) {
164
+ const text = await res.fetchResponse.text();
165
+ if (text) {
166
+ try {
167
+ res.body = text;
168
+ res.body = JSON.parse(text, req.jsonReviver);
169
+ }
170
+ catch (err) {
171
+ const { message } = (0, error_util_1._anyToError)(err);
172
+ res.err = new http_error_1.HttpError([res.signature, message].join('\n'), {
173
+ httpStatusCode: 0,
174
+ url: req.url,
175
+ });
176
+ res.ok = false;
177
+ }
178
+ }
179
+ else {
180
+ // Body had a '' (empty string)
181
+ res.body = {};
182
+ }
183
+ }
184
+ else {
185
+ // if no body: set responseBody as {}
186
+ // do not throw a "cannot parse null as Json" error
187
+ res.body = {};
188
+ }
189
+ }
190
+ else if (mode === 'text') {
191
+ res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
192
+ }
193
+ else if (mode === 'arrayBuffer') {
194
+ res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
195
+ }
196
+ else if (mode === 'blob') {
197
+ res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
198
+ }
199
+ clearTimeout(timeout);
200
+ res.retryStatus.retryStopped = true;
201
+ // res.err can happen on JSON.parse error
202
+ if (!res.err && this.cfg.logResponse) {
203
+ const { retryAttempt } = res.retryStatus;
204
+ const { logger } = this.cfg;
205
+ logger.log([
206
+ ' <<',
207
+ res.fetchResponse.status,
208
+ res.signature,
209
+ retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
210
+ (0, time_util_1._since)(started),
211
+ ]
212
+ .filter(Boolean)
213
+ .join(' '));
214
+ if (this.cfg.logResponseBody) {
215
+ logger.log(res.body);
216
+ }
217
+ }
218
+ }
219
+ async onNotOkResponse(res, timeout) {
220
+ clearTimeout(timeout);
221
+ let errObj;
222
+ if (res.fetchResponse) {
223
+ const body = (0, json_util_1._jsonParseIfPossible)(await res.fetchResponse.text());
224
+ errObj = (0, error_util_1._anyToErrorObject)(body);
225
+ }
226
+ else if (res.err) {
227
+ errObj = (0, error_util_1._errorToErrorObject)(res.err);
228
+ }
229
+ else {
230
+ errObj = {};
231
+ }
232
+ const originalMessage = errObj.message;
233
+ errObj.message = [
234
+ [res.fetchResponse?.status, res.signature].filter(Boolean).join(' '),
235
+ originalMessage,
236
+ ]
237
+ .filter(Boolean)
238
+ .join('\n');
239
+ res.err = new http_error_1.HttpError(errObj.message, (0, object_util_1._filterNullishValues)({
240
+ ...errObj.data,
241
+ originalMessage,
242
+ httpStatusCode: res.fetchResponse?.status || 0,
243
+ // These properties are provided to be used in e.g custom Sentry error grouping
244
+ // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
245
+ // Enabled, cause `data` is not printed by default when error is HttpError
246
+ // method: req.method,
247
+ url: res.req.url,
248
+ // tryCount: req.tryCount,
249
+ }));
250
+ await this.processRetry(res);
251
+ }
240
252
  async processRetry(res) {
241
253
  const { retryStatus } = res;
242
254
  if (!this.shouldRetry(res)) {
@@ -307,7 +319,7 @@ class Fetcher {
307
319
  if (!this.cfg.logWithSearchParams) {
308
320
  shortUrl = shortUrl.split('?')[0];
309
321
  }
310
- if (!this.cfg.logWithPrefixUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
322
+ if (!this.cfg.logWithBaseUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
311
323
  shortUrl = shortUrl.slice(baseUrl.length);
312
324
  }
313
325
  return shortUrl;
@@ -334,7 +346,7 @@ class Fetcher {
334
346
  logRequestBody: debug,
335
347
  logResponse: debug,
336
348
  logResponseBody: debug,
337
- logWithPrefixUrl: true,
349
+ logWithBaseUrl: (0, env_1.isServerSide)(),
338
350
  logWithSearchParams: true,
339
351
  retry: { ...defRetryOptions },
340
352
  init: {
@@ -43,10 +43,19 @@ export interface FetcherCfg {
43
43
  logResponse?: boolean;
44
44
  logResponseBody?: boolean;
45
45
  /**
46
- * Default to true.
47
- * Set to false to exclude `prefixUrl` from logs (both success and error)
46
+ * Controls if `baseUrl` should be included in logs (both success and error).
47
+ *
48
+ * Defaults to `true` on ServerSide and `false` on ClientSide.
49
+ *
50
+ * Reasoning.
51
+ *
52
+ * ClientSide often uses one main "backend host".
53
+ * Not including baseUrl improves Sentry error grouping.
54
+ *
55
+ * ServerSide often uses one Fetcher instance per 3rd-party API.
56
+ * Not including baseUrl can introduce confusion of "which API is it?".
48
57
  */
49
- logWithPrefixUrl?: boolean;
58
+ logWithBaseUrl?: boolean;
50
59
  /**
51
60
  * Default to true.
52
61
  * Set to false to strip searchParams from url when logging (both success and error)
@@ -138,6 +147,7 @@ export interface FetcherSuccessResponse<BODY = unknown> {
138
147
  req: FetcherRequest;
139
148
  statusFamily?: HttpStatusFamily;
140
149
  retryStatus: FetcherRetryStatus;
150
+ signature: string;
141
151
  }
142
152
  export interface FetcherErrorResponse<BODY = unknown> {
143
153
  ok: false;
@@ -147,6 +157,7 @@ export interface FetcherErrorResponse<BODY = unknown> {
147
157
  req: FetcherRequest;
148
158
  statusFamily?: HttpStatusFamily;
149
159
  retryStatus: FetcherRetryStatus;
160
+ signature: string;
150
161
  }
151
162
  export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
152
163
  export type FetcherMode = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob';
package/dist/index.d.ts CHANGED
@@ -72,6 +72,7 @@ export * from './datetime/localDate';
72
72
  export * from './datetime/localTime';
73
73
  export * from './datetime/dateInterval';
74
74
  export * from './datetime/timeInterval';
75
+ export * from './env';
75
76
  export * from './http/http.model';
76
77
  export * from './http/fetcher';
77
78
  export * from './http/fetcher.model';
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ tslib_1.__exportStar(require("./datetime/localDate"), exports);
76
76
  tslib_1.__exportStar(require("./datetime/localTime"), exports);
77
77
  tslib_1.__exportStar(require("./datetime/dateInterval"), exports);
78
78
  tslib_1.__exportStar(require("./datetime/timeInterval"), exports);
79
+ tslib_1.__exportStar(require("./env"), exports);
79
80
  tslib_1.__exportStar(require("./http/http.model"), exports);
80
81
  tslib_1.__exportStar(require("./http/fetcher"), exports);
81
82
  tslib_1.__exportStar(require("./http/fetcher.model"), exports);
@@ -5,9 +5,9 @@
5
5
  /// <reference lib="dom" />
6
6
  import { Class, ObservableLike, Primitive, TypedArray } from '../typeFest';
7
7
  declare const objectTypeNames: readonly ["Function", "Generator", "AsyncGenerator", "GeneratorFunction", "AsyncGeneratorFunction", "AsyncFunction", "Observable", "Array", "Buffer", "Object", "RegExp", "Date", "Error", "Map", "Set", "WeakMap", "WeakSet", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Promise", "URL", "FormData", "URLSearchParams", "HTMLElement", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array"];
8
- type ObjectTypeName = typeof objectTypeNames[number];
8
+ type ObjectTypeName = (typeof objectTypeNames)[number];
9
9
  declare const primitiveTypeNames: readonly ["null", "undefined", "string", "number", "bigint", "boolean", "symbol"];
10
- type PrimitiveTypeName = typeof primitiveTypeNames[number];
10
+ type PrimitiveTypeName = (typeof primitiveTypeNames)[number];
11
11
  export type TypeName = ObjectTypeName | PrimitiveTypeName;
12
12
  export declare function is(value: unknown): TypeName;
13
13
  export declare namespace is {
@@ -0,0 +1,18 @@
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() {
8
+ return typeof window === 'undefined';
9
+ }
10
+ /**
11
+ * Use it to detect Browser (not SSR/Node) environment.
12
+ *
13
+ * Will return `true` in the Browser.
14
+ * Will return `false` in Node.js.
15
+ */
16
+ export function isClientSide() {
17
+ return typeof window !== 'undefined';
18
+ }
@@ -1,5 +1,6 @@
1
1
  /// <reference lib="dom"/>
2
2
  import { __asyncValues } from "tslib";
3
+ import { isServerSide } from '../env';
3
4
  import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util';
4
5
  import { HttpError } from '../error/http.error';
5
6
  import { _clamp } from '../number/number.util';
@@ -82,10 +83,10 @@ export class Fetcher {
82
83
  */
83
84
  async rawFetch(url, rawOpt = {}) {
84
85
  var _a, e_1, _b, _c, _d, e_2, _e, _f;
85
- var _g, _h, _j;
86
+ var _g;
86
87
  const { logger } = this.cfg;
87
88
  const req = this.normalizeOptions(url, rawOpt);
88
- const { timeoutSeconds, mode, init: { method }, } = req;
89
+ const { timeoutSeconds, init: { method }, } = req;
89
90
  // setup timeout
90
91
  let timeout;
91
92
  if (timeoutSeconds) {
@@ -96,25 +97,29 @@ export class Fetcher {
96
97
  }, timeoutSeconds * 1000);
97
98
  }
98
99
  try {
99
- for (var _k = true, _l = __asyncValues(this.cfg.hooks.beforeRequest || []), _m; _m = await _l.next(), _a = _m.done, !_a;) {
100
- _c = _m.value;
101
- _k = false;
100
+ for (var _h = true, _j = __asyncValues(this.cfg.hooks.beforeRequest || []), _k; _k = await _j.next(), _a = _k.done, !_a;) {
101
+ _c = _k.value;
102
+ _h = false;
102
103
  try {
103
104
  const hook = _c;
104
105
  await hook(req);
105
106
  }
106
107
  finally {
107
- _k = true;
108
+ _h = true;
108
109
  }
109
110
  }
110
111
  }
111
112
  catch (e_1_1) { e_1 = { error: e_1_1 }; }
112
113
  finally {
113
114
  try {
114
- if (!_k && !_a && (_b = _l.return)) await _b.call(_l);
115
+ if (!_h && !_a && (_b = _j.return)) await _b.call(_j);
115
116
  }
116
117
  finally { if (e_1) throw e_1.error; }
117
118
  }
119
+ const isFullUrl = req.url.includes('://');
120
+ const fullUrl = isFullUrl ? new URL(req.url) : undefined;
121
+ const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.url;
122
+ const signature = [method, shortUrl].join(' ');
118
123
  const res = {
119
124
  req,
120
125
  retryStatus: {
@@ -122,10 +127,8 @@ export class Fetcher {
122
127
  retryStopped: false,
123
128
  retryTimeout: req.retry.timeout,
124
129
  },
130
+ signature,
125
131
  };
126
- const fullUrl = new URL(req.url);
127
- const shortUrl = this.getShortUrl(fullUrl);
128
- const signature = [method, shortUrl].join(' ');
129
132
  /* eslint-disable no-await-in-loop */
130
133
  while (!res.retryStatus.retryStopped) {
131
134
  const started = Date.now();
@@ -149,114 +152,124 @@ export class Fetcher {
149
152
  }
150
153
  res.statusFamily = this.getStatusFamily(res);
151
154
  if ((_g = res.fetchResponse) === null || _g === void 0 ? void 0 : _g.ok) {
152
- if (mode === 'json') {
153
- if (res.fetchResponse.body) {
154
- const text = await res.fetchResponse.text();
155
- if (text) {
156
- try {
157
- res.body = text;
158
- res.body = JSON.parse(text, req.jsonReviver);
159
- }
160
- catch (err) {
161
- const { message } = _anyToError(err);
162
- res.err = new HttpError([signature, message].join('\n'), {
163
- httpStatusCode: 0,
164
- url: req.url,
165
- });
166
- res.ok = false;
167
- }
168
- }
169
- else {
170
- // Body had a '' (empty string)
171
- res.body = {};
172
- }
173
- }
174
- else {
175
- // if no body: set responseBody as {}
176
- // do not throw a "cannot parse null as Json" error
177
- res.body = {};
178
- }
179
- }
180
- else if (mode === 'text') {
181
- res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
182
- }
183
- else if (mode === 'arrayBuffer') {
184
- res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
185
- }
186
- else if (mode === 'blob') {
187
- res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
188
- }
189
- clearTimeout(timeout);
190
- res.retryStatus.retryStopped = true;
191
- // res.err can happen on JSON.parse error
192
- if (!res.err && this.cfg.logResponse) {
193
- const { retryAttempt } = res.retryStatus;
194
- logger.log([
195
- ' <<',
196
- res.fetchResponse.status,
197
- signature,
198
- retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
199
- _since(started),
200
- ]
201
- .filter(Boolean)
202
- .join(' '));
203
- if (this.cfg.logResponseBody) {
204
- logger.log(res.body);
205
- }
206
- }
155
+ await this.onOkResponse(res, started, timeout);
207
156
  }
208
157
  else {
209
158
  // !res.ok
210
- clearTimeout(timeout);
211
- let errObj;
212
- if (res.fetchResponse) {
213
- const body = _jsonParseIfPossible(await res.fetchResponse.text());
214
- errObj = _anyToErrorObject(body);
215
- }
216
- else if (res.err) {
217
- errObj = _errorToErrorObject(res.err);
218
- }
219
- else {
220
- errObj = {};
221
- }
222
- const originalMessage = errObj.message;
223
- errObj.message = [
224
- [(_h = res.fetchResponse) === null || _h === void 0 ? void 0 : _h.status, signature].filter(Boolean).join(' '),
225
- originalMessage,
226
- ]
227
- .filter(Boolean)
228
- .join('\n');
229
- res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: ((_j = res.fetchResponse) === null || _j === void 0 ? void 0 : _j.status) || 0,
230
- // These properties are provided to be used in e.g custom Sentry error grouping
231
- // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
232
- // Enabled, cause `data` is not printed by default when error is HttpError
233
- // method: req.method,
234
- url: req.url })));
235
- await this.processRetry(res);
159
+ await this.onNotOkResponse(res, timeout);
236
160
  }
237
161
  }
238
162
  try {
239
- for (var _o = true, _p = __asyncValues(this.cfg.hooks.afterResponse || []), _q; _q = await _p.next(), _d = _q.done, !_d;) {
240
- _f = _q.value;
241
- _o = false;
163
+ for (var _l = true, _m = __asyncValues(this.cfg.hooks.afterResponse || []), _o; _o = await _m.next(), _d = _o.done, !_d;) {
164
+ _f = _o.value;
165
+ _l = false;
242
166
  try {
243
167
  const hook = _f;
244
168
  await hook(res);
245
169
  }
246
170
  finally {
247
- _o = true;
171
+ _l = true;
248
172
  }
249
173
  }
250
174
  }
251
175
  catch (e_2_1) { e_2 = { error: e_2_1 }; }
252
176
  finally {
253
177
  try {
254
- if (!_o && !_d && (_e = _p.return)) await _e.call(_p);
178
+ if (!_l && !_d && (_e = _m.return)) await _e.call(_m);
255
179
  }
256
180
  finally { if (e_2) throw e_2.error; }
257
181
  }
258
182
  return res;
259
183
  }
184
+ async onOkResponse(res, started, timeout) {
185
+ const { req } = res;
186
+ const { mode } = res.req;
187
+ if (mode === 'json') {
188
+ if (res.fetchResponse.body) {
189
+ const text = await res.fetchResponse.text();
190
+ if (text) {
191
+ try {
192
+ res.body = text;
193
+ res.body = JSON.parse(text, req.jsonReviver);
194
+ }
195
+ catch (err) {
196
+ const { message } = _anyToError(err);
197
+ res.err = new HttpError([res.signature, message].join('\n'), {
198
+ httpStatusCode: 0,
199
+ url: req.url,
200
+ });
201
+ res.ok = false;
202
+ }
203
+ }
204
+ else {
205
+ // Body had a '' (empty string)
206
+ res.body = {};
207
+ }
208
+ }
209
+ else {
210
+ // if no body: set responseBody as {}
211
+ // do not throw a "cannot parse null as Json" error
212
+ res.body = {};
213
+ }
214
+ }
215
+ else if (mode === 'text') {
216
+ res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
217
+ }
218
+ else if (mode === 'arrayBuffer') {
219
+ res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
220
+ }
221
+ else if (mode === 'blob') {
222
+ res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
223
+ }
224
+ clearTimeout(timeout);
225
+ res.retryStatus.retryStopped = true;
226
+ // res.err can happen on JSON.parse error
227
+ if (!res.err && this.cfg.logResponse) {
228
+ const { retryAttempt } = res.retryStatus;
229
+ const { logger } = this.cfg;
230
+ logger.log([
231
+ ' <<',
232
+ res.fetchResponse.status,
233
+ res.signature,
234
+ retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`,
235
+ _since(started),
236
+ ]
237
+ .filter(Boolean)
238
+ .join(' '));
239
+ if (this.cfg.logResponseBody) {
240
+ logger.log(res.body);
241
+ }
242
+ }
243
+ }
244
+ async onNotOkResponse(res, timeout) {
245
+ var _a, _b;
246
+ clearTimeout(timeout);
247
+ let errObj;
248
+ if (res.fetchResponse) {
249
+ const body = _jsonParseIfPossible(await res.fetchResponse.text());
250
+ errObj = _anyToErrorObject(body);
251
+ }
252
+ else if (res.err) {
253
+ errObj = _errorToErrorObject(res.err);
254
+ }
255
+ else {
256
+ errObj = {};
257
+ }
258
+ const originalMessage = errObj.message;
259
+ errObj.message = [
260
+ [(_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status, res.signature].filter(Boolean).join(' '),
261
+ originalMessage,
262
+ ]
263
+ .filter(Boolean)
264
+ .join('\n');
265
+ res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
266
+ // These properties are provided to be used in e.g custom Sentry error grouping
267
+ // Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
268
+ // Enabled, cause `data` is not printed by default when error is HttpError
269
+ // method: req.method,
270
+ url: res.req.url })));
271
+ await this.processRetry(res);
272
+ }
260
273
  async processRetry(res) {
261
274
  var _a, e_3, _b, _c;
262
275
  const { retryStatus } = res;
@@ -347,7 +360,7 @@ export class Fetcher {
347
360
  if (!this.cfg.logWithSearchParams) {
348
361
  shortUrl = shortUrl.split('?')[0];
349
362
  }
350
- if (!this.cfg.logWithPrefixUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
363
+ if (!this.cfg.logWithBaseUrl && baseUrl && shortUrl.startsWith(baseUrl)) {
351
364
  shortUrl = shortUrl.slice(baseUrl.length);
352
365
  }
353
366
  return shortUrl;
@@ -375,7 +388,7 @@ export class Fetcher {
375
388
  logRequestBody: debug,
376
389
  logResponse: debug,
377
390
  logResponseBody: debug,
378
- logWithPrefixUrl: true,
391
+ logWithBaseUrl: isServerSide(),
379
392
  logWithSearchParams: true,
380
393
  retry: Object.assign({}, defRetryOptions),
381
394
  init: {
package/dist-esm/index.js CHANGED
@@ -72,6 +72,7 @@ export * from './datetime/localDate';
72
72
  export * from './datetime/localTime';
73
73
  export * from './datetime/dateInterval';
74
74
  export * from './datetime/timeInterval';
75
+ export * from './env';
75
76
  export * from './http/http.model';
76
77
  export * from './http/fetcher';
77
78
  export * from './http/fetcher.model';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.134.0",
3
+ "version": "14.135.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
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
+ }
@@ -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
@@ -72,6 +72,7 @@ export * from './datetime/localDate'
72
72
  export * from './datetime/localTime'
73
73
  export * from './datetime/dateInterval'
74
74
  export * from './datetime/timeInterval'
75
+ export * from './env'
75
76
  export * from './http/http.model'
76
77
  export * from './http/fetcher'
77
78
  export * from './http/fetcher.model'
package/src/vendor/is.ts CHANGED
@@ -25,7 +25,7 @@ const typedArrayTypeNames = [
25
25
  'BigUint64Array',
26
26
  ] as const
27
27
 
28
- type TypedArrayTypeName = typeof typedArrayTypeNames[number]
28
+ type TypedArrayTypeName = (typeof typedArrayTypeNames)[number]
29
29
 
30
30
  function isTypedArrayName(name: unknown): name is TypedArrayTypeName {
31
31
  return typedArrayTypeNames.includes(name as TypedArrayTypeName)
@@ -60,7 +60,7 @@ const objectTypeNames = [
60
60
  ...typedArrayTypeNames,
61
61
  ] as const
62
62
 
63
- type ObjectTypeName = typeof objectTypeNames[number]
63
+ type ObjectTypeName = (typeof objectTypeNames)[number]
64
64
 
65
65
  function isObjectTypeName(name: unknown): name is ObjectTypeName {
66
66
  return objectTypeNames.includes(name as ObjectTypeName)
@@ -76,7 +76,7 @@ const primitiveTypeNames = [
76
76
  'symbol',
77
77
  ] as const
78
78
 
79
- type PrimitiveTypeName = typeof primitiveTypeNames[number]
79
+ type PrimitiveTypeName = (typeof primitiveTypeNames)[number]
80
80
 
81
81
  function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName {
82
82
  return primitiveTypeNames.includes(name as PrimitiveTypeName)