@naturalcycles/js-lib 14.156.0 → 14.157.1

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.
@@ -0,0 +1,9 @@
1
+ import type { AnyObject } from './types';
2
+ /**
3
+ * Convert any object to FormData.
4
+ * Please note that every key and value of FormData is `string`.
5
+ * Even if you pass a number - it'll be converted to string.
6
+ * Think URLSearchParams.
7
+ */
8
+ export declare function objectToFormData(obj?: AnyObject): FormData;
9
+ export declare function formDataToObject<T extends AnyObject>(formData: FormData): T;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formDataToObject = exports.objectToFormData = void 0;
4
+ /**
5
+ * Convert any object to FormData.
6
+ * Please note that every key and value of FormData is `string`.
7
+ * Even if you pass a number - it'll be converted to string.
8
+ * Think URLSearchParams.
9
+ */
10
+ function objectToFormData(obj = {}) {
11
+ const fd = new FormData();
12
+ Object.entries(obj).forEach(([k, v]) => fd.append(k, v));
13
+ return fd;
14
+ }
15
+ exports.objectToFormData = objectToFormData;
16
+ function formDataToObject(formData) {
17
+ return Object.fromEntries(formData);
18
+ }
19
+ exports.formDataToObject = formDataToObject;
@@ -1,4 +1,5 @@
1
1
  /// <reference lib="dom" />
2
+ /// <reference lib="dom.iterable" />
2
3
  import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse } from './fetcher.model';
3
4
  /**
4
5
  * Experimental wrapper around Fetch.
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  /// <reference lib="dom"/>
3
+ /// <reference lib="dom.iterable"/>
3
4
  Object.defineProperty(exports, "__esModule", { value: true });
4
5
  exports.getFetcher = exports.Fetcher = void 0;
5
6
  const env_1 = require("../env");
@@ -13,6 +14,14 @@ const json_util_1 = require("../string/json.util");
13
14
  const stringifyAny_1 = require("../string/stringifyAny");
14
15
  const time_util_1 = require("../time/time.util");
15
16
  const http_model_1 = require("./http.model");
17
+ const acceptByResponseType = {
18
+ text: 'text/plain',
19
+ json: 'application/json',
20
+ void: '*/*',
21
+ readableStream: 'application/octet-stream',
22
+ arrayBuffer: 'application/octet-stream',
23
+ blob: 'application/octet-stream',
24
+ };
16
25
  const defRetryOptions = {
17
26
  count: 2,
18
27
  timeout: 1000,
@@ -36,18 +45,18 @@ class Fetcher {
36
45
  return await this.fetch({
37
46
  url,
38
47
  method,
39
- mode: 'void',
48
+ responseType: 'void',
40
49
  ...opt,
41
50
  });
42
51
  };
43
52
  if (method === 'HEAD')
44
- return // mode=text
53
+ return // responseType=text
45
54
  ;
46
55
  this[`${m}Text`] = async (url, opt) => {
47
56
  return await this.fetch({
48
57
  url,
49
58
  method,
50
- mode: 'text',
59
+ responseType: 'text',
51
60
  ...opt,
52
61
  });
53
62
  };
@@ -55,7 +64,7 @@ class Fetcher {
55
64
  return await this.fetch({
56
65
  url,
57
66
  method,
58
- mode: 'json',
67
+ responseType: 'json',
59
68
  ...opt,
60
69
  });
61
70
  };
@@ -82,7 +91,7 @@ class Fetcher {
82
91
  static create(cfg = {}) {
83
92
  return new Fetcher(cfg);
84
93
  }
85
- // mode=readableStream
94
+ // responseType=readableStream
86
95
  /**
87
96
  * Returns raw fetchResponse.body, which is a ReadableStream<Uint8Array>
88
97
  *
@@ -92,7 +101,7 @@ class Fetcher {
92
101
  async getReadableStream(url, opt) {
93
102
  return await this.fetch({
94
103
  url,
95
- mode: 'readableStream',
104
+ responseType: 'readableStream',
96
105
  ...opt,
97
106
  });
98
107
  }
@@ -112,19 +121,6 @@ class Fetcher {
112
121
  const req = this.normalizeOptions(opt);
113
122
  const { logger } = this.cfg;
114
123
  const { timeoutSeconds, init: { method }, } = req;
115
- // setup timeout
116
- let timeout;
117
- if (timeoutSeconds) {
118
- const abortController = new AbortController();
119
- req.init.signal = abortController.signal;
120
- timeout = setTimeout(() => {
121
- // Apparently, providing a `string` reason to abort() causes Undici to throw `invalid_argument` error,
122
- // so, we're wrapping it in a TimeoutError instance
123
- abortController.abort(new pTimeout_1.TimeoutError(`request timed out after ${timeoutSeconds} sec`));
124
- // abortController.abort(`timeout of ${timeoutSeconds} sec`)
125
- // abortController.abort()
126
- }, timeoutSeconds * 1000);
127
- }
128
124
  for (const hook of this.cfg.hooks.beforeRequest || []) {
129
125
  await hook(req);
130
126
  }
@@ -143,6 +139,18 @@ class Fetcher {
143
139
  };
144
140
  while (!res.retryStatus.retryStopped) {
145
141
  req.started = Date.now();
142
+ // setup timeout
143
+ let timeoutId;
144
+ if (timeoutSeconds) {
145
+ const abortController = new AbortController();
146
+ req.init.signal = abortController.signal;
147
+ timeoutId = setTimeout(() => {
148
+ // console.log(`actual request timed out in ${_since(req.started)}`)
149
+ // Apparently, providing a `string` reason to abort() causes Undici to throw `invalid_argument` error,
150
+ // so, we're wrapping it in a TimeoutError instance
151
+ abortController.abort(new pTimeout_1.TimeoutError(`request timed out after ${timeoutSeconds} sec`));
152
+ }, timeoutSeconds * 1000);
153
+ }
146
154
  if (this.cfg.logRequest) {
147
155
  const { retryAttempt } = res.retryStatus;
148
156
  logger.log([' >>', signature, retryAttempt && `try#${retryAttempt + 1}/${req.retry.count + 1}`]
@@ -166,14 +174,30 @@ class Fetcher {
166
174
  // important to set it to undefined, otherwise it can keep the previous value (from previous try)
167
175
  res.fetchResponse = undefined;
168
176
  }
177
+ finally {
178
+ clearTimeout(timeoutId);
179
+ // Separate Timeout will be introduced to "download and parse the body"
180
+ }
169
181
  res.statusFamily = this.getStatusFamily(res);
170
182
  res.statusCode = res.fetchResponse?.status;
171
183
  if (res.fetchResponse?.ok) {
172
- await this.onOkResponse(res, timeout);
184
+ try {
185
+ // We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
186
+ await (0, pTimeout_1.pTimeout)(async () => await this.onOkResponse(res), {
187
+ timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
188
+ name: 'Fetcher.onOkResponse',
189
+ });
190
+ }
191
+ catch (err) {
192
+ // onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
193
+ res.err = (0, error_util_1._anyToError)(err);
194
+ res.ok = false;
195
+ await this.onNotOkResponse(res);
196
+ }
173
197
  }
174
198
  else {
175
199
  // !res.ok
176
- await this.onNotOkResponse(res, timeout);
200
+ await this.onNotOkResponse(res);
177
201
  }
178
202
  }
179
203
  for (const hook of this.cfg.hooks.afterResponse || []) {
@@ -181,31 +205,17 @@ class Fetcher {
181
205
  }
182
206
  return res;
183
207
  }
184
- async onOkResponse(res, timeout) {
208
+ async onOkResponse(res) {
185
209
  const { req } = res;
186
- const { mode } = res.req;
187
- if (mode === 'json') {
210
+ const { responseType } = res.req;
211
+ // This function is subject to a separate timeout to "download and parse the data"
212
+ if (responseType === 'json') {
188
213
  if (res.fetchResponse.body) {
189
214
  const text = await res.fetchResponse.text();
190
215
  if (text) {
191
- try {
192
- res.body = text;
193
- res.body = (0, json_util_1._jsonParse)(text, req.jsonReviver);
194
- }
195
- catch (err) {
196
- // Error while parsing json
197
- // res.err = _anyToError(err, HttpRequestError, {
198
- // requestUrl: res.req.url,
199
- // requestBaseUrl: this.cfg.baseUrl,
200
- // requestMethod: res.req.init.method,
201
- // requestSignature: res.signature,
202
- // requestDuration: Date.now() - started,
203
- // responseStatusCode: res.fetchResponse.status,
204
- // } satisfies HttpRequestErrorData)
205
- res.err = (0, error_util_1._anyToError)(err);
206
- res.ok = false;
207
- return await this.onNotOkResponse(res, timeout);
208
- }
216
+ res.body = text;
217
+ res.body = (0, json_util_1._jsonParse)(text, req.jsonReviver);
218
+ // Error while parsing json can happen - it'll be handled upstream
209
219
  }
210
220
  else {
211
221
  // Body had a '' (empty string)
@@ -218,24 +228,22 @@ class Fetcher {
218
228
  res.body = {};
219
229
  }
220
230
  }
221
- else if (mode === 'text') {
231
+ else if (responseType === 'text') {
222
232
  res.body = res.fetchResponse.body ? await res.fetchResponse.text() : '';
223
233
  }
224
- else if (mode === 'arrayBuffer') {
234
+ else if (responseType === 'arrayBuffer') {
225
235
  res.body = res.fetchResponse.body ? await res.fetchResponse.arrayBuffer() : {};
226
236
  }
227
- else if (mode === 'blob') {
237
+ else if (responseType === 'blob') {
228
238
  res.body = res.fetchResponse.body ? await res.fetchResponse.blob() : {};
229
239
  }
230
- else if (mode === 'readableStream') {
240
+ else if (responseType === 'readableStream') {
231
241
  res.body = res.fetchResponse.body;
232
242
  if (res.body === null) {
233
- res.err = new Error(`fetchResponse.body is null`);
234
- res.ok = false;
235
- return await this.onNotOkResponse(res, timeout);
243
+ // Error is to be handled upstream
244
+ throw new Error(`fetchResponse.body is null`);
236
245
  }
237
246
  }
238
- clearTimeout(timeout);
239
247
  res.retryStatus.retryStopped = true;
240
248
  // res.err can happen on `failed to fetch` type of error, e.g JSON.parse, CORS, unexpected redirect
241
249
  if (!res.err && this.cfg.logResponse) {
@@ -261,8 +269,7 @@ class Fetcher {
261
269
  async callNativeFetch(url, init) {
262
270
  return await globalThis.fetch(url, init);
263
271
  }
264
- async onNotOkResponse(res, timeout) {
265
- clearTimeout(timeout);
272
+ async onNotOkResponse(res) {
266
273
  let cause;
267
274
  if (res.err) {
268
275
  // This is only possible on JSON.parse error, or CORS error,
@@ -328,7 +335,11 @@ class Fetcher {
328
335
  return;
329
336
  retryStatus.retryAttempt++;
330
337
  retryStatus.retryTimeout = (0, number_util_1._clamp)(retryStatus.retryTimeout * timeoutMultiplier, 0, timeoutMax);
331
- await (0, pDelay_1.pDelay)(this.getRetryTimeout(res));
338
+ const timeout = this.getRetryTimeout(res);
339
+ if (res.req.debug) {
340
+ this.cfg.logger.log(` .. ${res.signature} waiting ${(0, time_util_1._ms)(timeout)}`);
341
+ }
342
+ await (0, pDelay_1.pDelay)(timeout);
332
343
  }
333
344
  getRetryTimeout(res) {
334
345
  let timeout = 0;
@@ -383,8 +394,9 @@ class Fetcher {
383
394
  if (statusFamily === 3 && !retry3xx)
384
395
  return false;
385
396
  // should not retry on `unexpected redirect` in error.cause.cause
386
- if (res.err?.cause?.cause?.message?.includes('unexpected redirect'))
397
+ if (res.err?.cause?.cause?.message?.includes('unexpected redirect')) {
387
398
  return false;
399
+ }
388
400
  return true; // default is true
389
401
  }
390
402
  getStatusFamily(res) {
@@ -422,14 +434,14 @@ class Fetcher {
422
434
  }
423
435
  normalizeCfg(cfg) {
424
436
  if (cfg.baseUrl?.endsWith('/')) {
425
- console.warn(`Fetcher: baseUrl should not end with /`);
437
+ console.warn(`Fetcher: baseUrl should not end with slash: ${cfg.baseUrl}`);
426
438
  cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
427
439
  }
428
440
  const { debug = false } = cfg;
429
441
  const norm = (0, object_util_1._merge)({
430
442
  baseUrl: '',
431
443
  inputUrl: '',
432
- mode: 'void',
444
+ responseType: 'void',
433
445
  searchParams: {},
434
446
  timeoutSeconds: 30,
435
447
  retryPost: false,
@@ -448,7 +460,10 @@ class Fetcher {
448
460
  retry: { ...defRetryOptions },
449
461
  init: {
450
462
  method: cfg.method || 'GET',
451
- headers: cfg.headers || {},
463
+ headers: {
464
+ 'user-agent': 'fetcher',
465
+ ...cfg.headers,
466
+ },
452
467
  credentials: cfg.credentials,
453
468
  redirect: cfg.redirect,
454
469
  },
@@ -464,12 +479,13 @@ class Fetcher {
464
479
  'retryPost',
465
480
  'retry4xx',
466
481
  'retry5xx',
467
- 'mode',
482
+ 'responseType',
468
483
  'jsonReviver',
469
484
  'logRequest',
470
485
  'logRequestBody',
471
486
  'logResponse',
472
487
  'logResponseBody',
488
+ 'debug',
473
489
  ]),
474
490
  started: Date.now(),
475
491
  ...(0, object_util_1._omit)(opt, ['method', 'headers', 'credentials']),
@@ -514,9 +530,20 @@ class Fetcher {
514
530
  req.init.body = opt.text;
515
531
  req.init.headers['content-type'] = 'text/plain';
516
532
  }
533
+ else if (opt.form) {
534
+ if (opt.form instanceof URLSearchParams || opt.form instanceof FormData) {
535
+ req.init.body = opt.form;
536
+ }
537
+ else {
538
+ req.init.body = new URLSearchParams(opt.form);
539
+ }
540
+ req.init.headers['content-type'] = 'application/x-www-form-urlencoded';
541
+ }
517
542
  else if (opt.body !== undefined) {
518
543
  req.init.body = opt.body;
519
544
  }
545
+ // Unless `accept` header was already set - set it based on responseType
546
+ req.init.headers['accept'] ||= acceptByResponseType[req.responseType];
520
547
  return req;
521
548
  }
522
549
  }
@@ -1,8 +1,8 @@
1
1
  import type { CommonLogger } from '../log/commonLogger';
2
2
  import type { Promisable } from '../typeFest';
3
- import type { Reviver, UnixTimestampMillisNumber } from '../types';
3
+ import type { AnyObject, NumberOfMilliseconds, Reviver, UnixTimestampMillisNumber } from '../types';
4
4
  import type { HttpMethod, HttpStatusFamily } from './http.model';
5
- export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'redirect' | 'credentials'> {
5
+ export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl' | 'logRequest' | 'logRequestBody' | 'logResponse' | 'logResponseBody' | 'debug' | 'redirect' | 'credentials'> {
6
6
  logger: CommonLogger;
7
7
  searchParams: Record<string, any>;
8
8
  }
@@ -68,13 +68,13 @@ export interface FetcherCfg {
68
68
  }
69
69
  export interface FetcherRetryStatus {
70
70
  retryAttempt: number;
71
- retryTimeout: number;
71
+ retryTimeout: NumberOfMilliseconds;
72
72
  retryStopped: boolean;
73
73
  }
74
74
  export interface FetcherRetryOptions {
75
75
  count: number;
76
- timeout: number;
77
- timeoutMax: number;
76
+ timeout: NumberOfMilliseconds;
77
+ timeoutMax: NumberOfMilliseconds;
78
78
  timeoutMultiplier: number;
79
79
  }
80
80
  export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers' | 'baseUrl' | 'url'> {
@@ -88,7 +88,7 @@ export interface FetcherRequest extends Omit<FetcherOptions, 'method' | 'headers
88
88
  */
89
89
  fullUrl: string;
90
90
  init: RequestInitNormalized;
91
- mode: FetcherMode;
91
+ responseType: FetcherResponseType;
92
92
  timeoutSeconds: number;
93
93
  retry: FetcherRetryOptions;
94
94
  retryPost: boolean;
@@ -112,14 +112,29 @@ export interface FetcherOptions {
112
112
  * so both should finish within this single timeout (not each).
113
113
  */
114
114
  timeoutSeconds?: number;
115
- json?: any;
116
- text?: string;
117
115
  /**
118
116
  * Supports all the types that RequestInit.body supports.
119
117
  *
120
118
  * Useful when you want to e.g pass FormData.
121
119
  */
122
120
  body?: Blob | BufferSource | FormData | URLSearchParams | string;
121
+ /**
122
+ * Same as `body`, but also conveniently sets the
123
+ * Content-Type header to `text/plain`
124
+ */
125
+ text?: string;
126
+ /**
127
+ * Same as `body`, but:
128
+ * 1. JSON.stringifies the passed variable
129
+ * 2. Conveniently sets the Content-Type header to `application/json`
130
+ */
131
+ json?: any;
132
+ /**
133
+ * Same as `body`, but:
134
+ * 1. Transforms the passed plain js object into URLSearchParams and passes it to `body`
135
+ * 2. Conveniently sets the Content-Type header to `application/x-www-form-urlencoded`
136
+ */
137
+ form?: FormData | URLSearchParams | AnyObject;
123
138
  credentials?: RequestCredentials;
124
139
  /**
125
140
  * Default to 'follow'.
@@ -128,7 +143,7 @@ export interface FetcherOptions {
128
143
  */
129
144
  redirect?: RequestRedirect;
130
145
  headers?: Record<string, any>;
131
- mode?: FetcherMode;
146
+ responseType?: FetcherResponseType;
132
147
  searchParams?: Record<string, any>;
133
148
  /**
134
149
  * Default is 2 retries (3 tries in total).
@@ -157,6 +172,10 @@ export interface FetcherOptions {
157
172
  logRequestBody?: boolean;
158
173
  logResponse?: boolean;
159
174
  logResponseBody?: boolean;
175
+ /**
176
+ * If true - enables all possible logging.
177
+ */
178
+ debug?: boolean;
160
179
  }
161
180
  export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
162
181
  method: HttpMethod;
@@ -185,4 +204,4 @@ export interface FetcherErrorResponse<BODY = unknown> {
185
204
  signature: string;
186
205
  }
187
206
  export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
188
- export type FetcherMode = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob' | 'readableStream';
207
+ export type FetcherResponseType = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob' | 'readableStream';
package/dist/index.d.ts CHANGED
@@ -80,6 +80,7 @@ export * from './http/fetcher';
80
80
  export * from './http/fetcher.model';
81
81
  export * from './string/hash.util';
82
82
  export * from './env/buildInfo';
83
+ export * from './form.util';
83
84
  export * from './zod/zod.util';
84
85
  export * from './zod/zod.shared.schemas';
85
86
  import { z, ZodSchema, ZodError, ZodIssue } from 'zod';
package/dist/index.js CHANGED
@@ -84,6 +84,7 @@ tslib_1.__exportStar(require("./http/fetcher"), exports);
84
84
  tslib_1.__exportStar(require("./http/fetcher.model"), exports);
85
85
  tslib_1.__exportStar(require("./string/hash.util"), exports);
86
86
  tslib_1.__exportStar(require("./env/buildInfo"), exports);
87
+ tslib_1.__exportStar(require("./form.util"), exports);
87
88
  tslib_1.__exportStar(require("./zod/zod.util"), exports);
88
89
  tslib_1.__exportStar(require("./zod/zod.shared.schemas"), exports);
89
90
  const zod_1 = require("zod");
@@ -1,6 +1,6 @@
1
1
  import { AppError } from '../error/app.error';
2
2
  import type { ErrorData, ErrorObject } from '../error/error.model';
3
- import type { AnyAsyncFunction } from '../types';
3
+ import type { AnyAsyncFunction, NumberOfMilliseconds } from '../types';
4
4
  export declare class TimeoutError extends AppError {
5
5
  constructor(message: string, data?: {}, cause?: ErrorObject);
6
6
  }
@@ -8,7 +8,7 @@ export interface PTimeoutOptions {
8
8
  /**
9
9
  * Timeout in milliseconds.
10
10
  */
11
- timeout: number;
11
+ timeout: NumberOfMilliseconds;
12
12
  /**
13
13
  * If set - will be included in the error message.
14
14
  * Can be used to identify the place in the code that failed.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Convert any object to FormData.
3
+ * Please note that every key and value of FormData is `string`.
4
+ * Even if you pass a number - it'll be converted to string.
5
+ * Think URLSearchParams.
6
+ */
7
+ export function objectToFormData(obj = {}) {
8
+ const fd = new FormData();
9
+ Object.entries(obj).forEach(([k, v]) => fd.append(k, v));
10
+ return fd;
11
+ }
12
+ export function formDataToObject(formData) {
13
+ return Object.fromEntries(formData);
14
+ }