@naturalcycles/js-lib 14.119.1 → 14.121.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.
@@ -2,11 +2,17 @@
2
2
  import { CommonLogger } from '../log/commonLogger';
3
3
  import type { Promisable } from '../typeFest';
4
4
  import type { HttpMethod, HttpStatusFamily } from './http.model';
5
- export interface FetcherNormalizedCfg extends FetcherCfg, FetcherRequest {
5
+ export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
6
6
  logger: CommonLogger;
7
7
  searchParams: Record<string, any>;
8
8
  }
9
+ export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>;
10
+ export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>;
11
+ export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>;
9
12
  export interface FetcherCfg {
13
+ /**
14
+ * Should **not** contain trailing slash.
15
+ */
10
16
  baseUrl?: string;
11
17
  /**
12
18
  * Default rule is that you **are allowed** to mutate req, res, res.retryStatus
@@ -17,22 +23,28 @@ export interface FetcherCfg {
17
23
  /**
18
24
  * Allows to mutate req.
19
25
  */
20
- beforeRequest?(req: FetcherRequest): Promisable<void>;
26
+ beforeRequest?: FetcherBeforeRequestHook[];
21
27
  /**
22
28
  * Allows to mutate res.
23
29
  * If you set `res.err` - it will be thrown.
24
30
  */
25
- beforeResponse?(res: FetcherResponse): Promisable<void>;
31
+ afterResponse?: FetcherAfterResponseHook[];
26
32
  /**
27
33
  * Allows to mutate res.retryStatus to override retry behavior.
28
34
  */
29
- beforeRetry?(res: FetcherResponse): Promisable<void>;
35
+ beforeRetry?: FetcherBeforeRetryHook[];
30
36
  };
37
+ /**
38
+ * If true - enables all possible logging.
39
+ */
31
40
  debug?: boolean;
32
41
  logRequest?: boolean;
33
42
  logRequestBody?: boolean;
34
43
  logResponse?: boolean;
35
44
  logResponseBody?: boolean;
45
+ /**
46
+ * Defaults to `console`.
47
+ */
36
48
  logger?: CommonLogger;
37
49
  }
38
50
  export interface FetcherRetryStatus {
@@ -68,7 +80,7 @@ export interface FetcherOptions {
68
80
  timeoutSeconds?: number;
69
81
  json?: any;
70
82
  text?: string;
71
- init?: Partial<RequestInitNormalized>;
83
+ credentials?: RequestCredentials;
72
84
  headers?: Record<string, any>;
73
85
  mode?: FetcherMode;
74
86
  searchParams?: Record<string, any>;
@@ -120,14 +132,30 @@ export type FetcherMode = 'json' | 'text';
120
132
  */
121
133
  export declare class Fetcher {
122
134
  private constructor();
135
+ /**
136
+ * Add BeforeRequest hook at the end of the hooks list.
137
+ */
138
+ onBeforeRequest(hook: FetcherBeforeRequestHook): this;
139
+ onAfterResponse(hook: FetcherAfterResponseHook): this;
140
+ onBeforeRetry(hook: FetcherBeforeRetryHook): this;
123
141
  cfg: FetcherNormalizedCfg;
124
142
  static create(cfg?: FetcherCfg & FetcherOptions): Fetcher;
125
- getJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
126
- postJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
127
- putJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
128
- patchJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
129
- deleteJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
130
- getText(url: string, opt?: FetcherOptions): Promise<string>;
143
+ get: (url: string, opt?: FetcherOptions) => Promise<void>;
144
+ post: (url: string, opt?: FetcherOptions) => Promise<void>;
145
+ put: (url: string, opt?: FetcherOptions) => Promise<void>;
146
+ patch: (url: string, opt?: FetcherOptions) => Promise<void>;
147
+ delete: (url: string, opt?: FetcherOptions) => Promise<void>;
148
+ head: (url: string, opt?: FetcherOptions) => Promise<void>;
149
+ getText: (url: string, opt?: FetcherOptions) => Promise<string>;
150
+ postText: (url: string, opt?: FetcherOptions) => Promise<string>;
151
+ putText: (url: string, opt?: FetcherOptions) => Promise<string>;
152
+ patchText: (url: string, opt?: FetcherOptions) => Promise<string>;
153
+ deleteText: (url: string, opt?: FetcherOptions) => Promise<string>;
154
+ getJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
155
+ postJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
156
+ putJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
157
+ patchJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
158
+ deleteJson: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>;
131
159
  fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T>;
132
160
  rawFetch<T = unknown>(url: string, rawOpt?: FetcherOptions): Promise<FetcherResponse<T>>;
133
161
  private processRetry;
@@ -9,6 +9,7 @@ const object_util_1 = require("../object/object.util");
9
9
  const pDelay_1 = require("../promise/pDelay");
10
10
  const json_util_1 = require("../string/json.util");
11
11
  const time_util_1 = require("../time/time.util");
12
+ const http_model_1 = require("./http.model");
12
13
  const defRetryOptions = {
13
14
  count: 2,
14
15
  timeout: 500,
@@ -24,50 +25,53 @@ const defRetryOptions = {
24
25
  class Fetcher {
25
26
  constructor(cfg = {}) {
26
27
  this.cfg = this.normalizeCfg(cfg);
27
- }
28
- static create(cfg = {}) {
29
- return new Fetcher(cfg);
30
- }
31
- async getJson(url, opt) {
32
- return await this.fetch(url, {
33
- ...opt,
34
- mode: 'json',
28
+ // Dynamically create all helper methods
29
+ http_model_1.HTTP_METHODS.forEach(method => {
30
+ // mode=void
31
+ this[method] = async (url, opt) => {
32
+ return await this.fetch(url, {
33
+ ...opt,
34
+ method,
35
+ });
36
+ };
37
+ this[`${method}Text`] = async (url, opt) => {
38
+ return await this.fetch(url, {
39
+ ...opt,
40
+ method,
41
+ mode: 'text',
42
+ });
43
+ };
44
+ this[`${method}Json`] = async (url, opt) => {
45
+ return await this.fetch(url, {
46
+ ...opt,
47
+ method,
48
+ mode: 'json',
49
+ });
50
+ };
35
51
  });
36
52
  }
37
- async postJson(url, opt) {
38
- return await this.fetch(url, {
39
- ...opt,
40
- method: 'post',
41
- mode: 'json',
42
- });
53
+ /**
54
+ * Add BeforeRequest hook at the end of the hooks list.
55
+ */
56
+ onBeforeRequest(hook) {
57
+ ;
58
+ (this.cfg.hooks.beforeRequest ||= []).push(hook);
59
+ return this;
43
60
  }
44
- async putJson(url, opt) {
45
- return await this.fetch(url, {
46
- ...opt,
47
- method: 'put',
48
- mode: 'json',
49
- });
61
+ onAfterResponse(hook) {
62
+ ;
63
+ (this.cfg.hooks.afterResponse ||= []).push(hook);
64
+ return this;
50
65
  }
51
- async patchJson(url, opt) {
52
- return await this.fetch(url, {
53
- ...opt,
54
- method: 'patch',
55
- mode: 'json',
56
- });
66
+ onBeforeRetry(hook) {
67
+ ;
68
+ (this.cfg.hooks.beforeRetry ||= []).push(hook);
69
+ return this;
57
70
  }
58
- async deleteJson(url, opt) {
59
- return await this.fetch(url, {
60
- ...opt,
61
- method: 'delete',
62
- mode: 'json',
63
- });
64
- }
65
- async getText(url, opt) {
66
- return await this.fetch(url, {
67
- ...opt,
68
- mode: 'text',
69
- });
71
+ static create(cfg = {}) {
72
+ return new Fetcher(cfg);
70
73
  }
74
+ // headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
71
75
  async fetch(url, opt) {
72
76
  const res = await this.rawFetch(url, opt);
73
77
  if (res.err) {
@@ -90,7 +94,9 @@ class Fetcher {
90
94
  abortController.abort(`timeout of ${timeoutSeconds} sec`);
91
95
  }, timeoutSeconds * 1000);
92
96
  }
93
- await this.cfg.hooks?.beforeRequest?.(req);
97
+ for await (const hook of this.cfg.hooks.beforeRequest || []) {
98
+ await hook(req);
99
+ }
94
100
  const res = {
95
101
  req,
96
102
  retryStatus: {
@@ -159,29 +165,12 @@ class Fetcher {
159
165
  url: req.url,
160
166
  // tryCount: req.tryCount,
161
167
  }));
162
- // We don't log errors when they are also thrown,
163
- // otherwise it gets logged twice: here, and upstream
164
- // if (this.cfg.logResponse) {
165
- // const { retryAttempt } = res.retryStatus
166
- // logger.error(
167
- // [
168
- // [
169
- // ' <<',
170
- // res.fetchResponse.status,
171
- // signature,
172
- // retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
173
- // _since(started),
174
- // ]
175
- // .filter(Boolean)
176
- // .join(' '),
177
- // _stringifyAny(body),
178
- // ].join('\n'),
179
- // )
180
- // }
181
168
  await this.processRetry(res);
182
169
  }
183
170
  }
184
- await this.cfg.hooks?.beforeResponse?.(res);
171
+ for await (const hook of this.cfg.hooks.afterResponse || []) {
172
+ await hook(res);
173
+ }
185
174
  return res;
186
175
  }
187
176
  async processRetry(res) {
@@ -189,7 +178,9 @@ class Fetcher {
189
178
  if (!this.shouldRetry(res)) {
190
179
  retryStatus.retryStopped = true;
191
180
  }
192
- await this.cfg.hooks?.beforeRetry?.(res);
181
+ for await (const hook of this.cfg.hooks.beforeRetry || []) {
182
+ await hook(res);
183
+ }
193
184
  const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
194
185
  if (retryStatus.retryAttempt >= count) {
195
186
  retryStatus.retryStopped = true;
@@ -210,9 +201,9 @@ class Fetcher {
210
201
  if (method === 'post' && !retryPost)
211
202
  return false;
212
203
  const { statusFamily } = res;
213
- if (statusFamily === '5xx' && !retry5xx)
204
+ if (statusFamily === 5 && !retry5xx)
214
205
  return false;
215
- if (statusFamily === '4xx' && !retry4xx)
206
+ if (statusFamily === 4 && !retry4xx)
216
207
  return false;
217
208
  return true; // default is true
218
209
  }
@@ -221,15 +212,15 @@ class Fetcher {
221
212
  if (!status)
222
213
  return;
223
214
  if (status >= 500)
224
- return '5xx';
215
+ return 5;
225
216
  if (status >= 400)
226
- return '4xx';
217
+ return 4;
227
218
  if (status >= 300)
228
- return '3xx';
219
+ return 3;
229
220
  if (status >= 200)
230
- return '2xx';
221
+ return 2;
231
222
  if (status >= 100)
232
- return '1xx';
223
+ return 1;
233
224
  }
234
225
  /**
235
226
  * Returns url without baseUrl and before ?queryString
@@ -245,8 +236,9 @@ class Fetcher {
245
236
  console.warn(`Fetcher: baseUrl should not end with /`);
246
237
  cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
247
238
  }
248
- const { debug } = cfg;
239
+ const { debug = false } = cfg;
249
240
  const norm = (0, object_util_1._merge)({
241
+ baseUrl: '',
250
242
  url: '',
251
243
  searchParams: {},
252
244
  timeoutSeconds: 30,
@@ -255,6 +247,7 @@ class Fetcher {
255
247
  retry4xx: false,
256
248
  retry5xx: true,
257
249
  logger: console,
250
+ debug,
258
251
  logRequest: debug,
259
252
  logRequestBody: debug,
260
253
  logResponse: debug,
@@ -264,6 +257,7 @@ class Fetcher {
264
257
  method: 'get',
265
258
  headers: {},
266
259
  },
260
+ hooks: {},
267
261
  }, cfg);
268
262
  norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
269
263
  return norm;
@@ -282,8 +276,11 @@ class Fetcher {
282
276
  ...retry,
283
277
  ...(0, object_util_1._filterUndefinedValues)(opt.retry || {}),
284
278
  },
285
- init: (0, object_util_1._merge)({ ...this.cfg.init }, opt.init, (0, object_util_1._filterUndefinedValues)({
279
+ init: (0, object_util_1._merge)({ ...this.cfg.init },
280
+ // opt.init,
281
+ (0, object_util_1._filterUndefinedValues)({
286
282
  method: opt.method,
283
+ credentials: opt.credentials,
287
284
  headers: (0, object_util_1._mapKeys)(opt.headers || {}, k => k.toLowerCase()),
288
285
  })),
289
286
  };
@@ -1,2 +1,3 @@
1
1
  export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head';
2
- export type HttpStatusFamily = '5xx' | '4xx' | '3xx' | '2xx' | '1xx';
2
+ export type HttpStatusFamily = 5 | 4 | 3 | 2 | 1;
3
+ export declare const HTTP_METHODS: HttpMethod[];
@@ -1,2 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HTTP_METHODS = void 0;
4
+ exports.HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
@@ -1,4 +1,5 @@
1
1
  /// <reference lib="dom"/>
2
+ import { __asyncValues } from "tslib";
2
3
  import { _anyToErrorObject } from '../error/error.util';
3
4
  import { HttpError } from '../error/http.error';
4
5
  import { _clamp } from '../number/number.util';
@@ -6,6 +7,7 @@ import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit,
6
7
  import { pDelay } from '../promise/pDelay';
7
8
  import { _jsonParseIfPossible } from '../string/json.util';
8
9
  import { _since } from '../time/time.util';
10
+ import { HTTP_METHODS } from './http.model';
9
11
  const defRetryOptions = {
10
12
  count: 2,
11
13
  timeout: 500,
@@ -21,28 +23,45 @@ const defRetryOptions = {
21
23
  export class Fetcher {
22
24
  constructor(cfg = {}) {
23
25
  this.cfg = this.normalizeCfg(cfg);
26
+ // Dynamically create all helper methods
27
+ HTTP_METHODS.forEach(method => {
28
+ // mode=void
29
+ this[method] = async (url, opt) => {
30
+ return await this.fetch(url, Object.assign(Object.assign({}, opt), { method }));
31
+ };
32
+ this[`${method}Text`] = async (url, opt) => {
33
+ return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'text' }));
34
+ };
35
+ this[`${method}Json`] = async (url, opt) => {
36
+ return await this.fetch(url, Object.assign(Object.assign({}, opt), { method, mode: 'json' }));
37
+ };
38
+ });
24
39
  }
25
- static create(cfg = {}) {
26
- return new Fetcher(cfg);
27
- }
28
- async getJson(url, opt) {
29
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: 'json' }));
30
- }
31
- async postJson(url, opt) {
32
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'post', mode: 'json' }));
33
- }
34
- async putJson(url, opt) {
35
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'put', mode: 'json' }));
40
+ /**
41
+ * Add BeforeRequest hook at the end of the hooks list.
42
+ */
43
+ onBeforeRequest(hook) {
44
+ var _a;
45
+ ;
46
+ ((_a = this.cfg.hooks).beforeRequest || (_a.beforeRequest = [])).push(hook);
47
+ return this;
36
48
  }
37
- async patchJson(url, opt) {
38
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'patch', mode: 'json' }));
49
+ onAfterResponse(hook) {
50
+ var _a;
51
+ ;
52
+ ((_a = this.cfg.hooks).afterResponse || (_a.afterResponse = [])).push(hook);
53
+ return this;
39
54
  }
40
- async deleteJson(url, opt) {
41
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { method: 'delete', mode: 'json' }));
55
+ onBeforeRetry(hook) {
56
+ var _a;
57
+ ;
58
+ ((_a = this.cfg.hooks).beforeRetry || (_a.beforeRetry = [])).push(hook);
59
+ return this;
42
60
  }
43
- async getText(url, opt) {
44
- return await this.fetch(url, Object.assign(Object.assign({}, opt), { mode: 'text' }));
61
+ static create(cfg = {}) {
62
+ return new Fetcher(cfg);
45
63
  }
64
+ // headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
46
65
  async fetch(url, opt) {
47
66
  const res = await this.rawFetch(url, opt);
48
67
  if (res.err) {
@@ -53,7 +72,7 @@ export class Fetcher {
53
72
  return res.body;
54
73
  }
55
74
  async rawFetch(url, rawOpt = {}) {
56
- var _a, _b, _c, _d;
75
+ var _a, e_1, _b, _c, _d, e_2, _e, _f;
57
76
  const { logger } = this.cfg;
58
77
  const req = this.normalizeOptions(url, rawOpt);
59
78
  const { timeoutSeconds, mode, init: { method }, } = req;
@@ -66,7 +85,26 @@ export class Fetcher {
66
85
  abortController.abort(`timeout of ${timeoutSeconds} sec`);
67
86
  }, timeoutSeconds * 1000);
68
87
  }
69
- await ((_b = (_a = this.cfg.hooks) === null || _a === void 0 ? void 0 : _a.beforeRequest) === null || _b === void 0 ? void 0 : _b.call(_a, req));
88
+ try {
89
+ for (var _g = true, _h = __asyncValues(this.cfg.hooks.beforeRequest || []), _j; _j = await _h.next(), _a = _j.done, !_a;) {
90
+ _c = _j.value;
91
+ _g = false;
92
+ try {
93
+ const hook = _c;
94
+ await hook(req);
95
+ }
96
+ finally {
97
+ _g = true;
98
+ }
99
+ }
100
+ }
101
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
102
+ finally {
103
+ try {
104
+ if (!_g && !_a && (_b = _h.return)) await _b.call(_h);
105
+ }
106
+ finally { if (e_1) throw e_1.error; }
107
+ }
70
108
  const res = {
71
109
  req,
72
110
  retryStatus: {
@@ -130,38 +168,57 @@ export class Fetcher {
130
168
  // Enabled, cause `data` is not printed by default when error is HttpError
131
169
  // method: req.method,
132
170
  url: req.url })));
133
- // We don't log errors when they are also thrown,
134
- // otherwise it gets logged twice: here, and upstream
135
- // if (this.cfg.logResponse) {
136
- // const { retryAttempt } = res.retryStatus
137
- // logger.error(
138
- // [
139
- // [
140
- // ' <<',
141
- // res.fetchResponse.status,
142
- // signature,
143
- // retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
144
- // _since(started),
145
- // ]
146
- // .filter(Boolean)
147
- // .join(' '),
148
- // _stringifyAny(body),
149
- // ].join('\n'),
150
- // )
151
- // }
152
171
  await this.processRetry(res);
153
172
  }
154
173
  }
155
- await ((_d = (_c = this.cfg.hooks) === null || _c === void 0 ? void 0 : _c.beforeResponse) === null || _d === void 0 ? void 0 : _d.call(_c, res));
174
+ try {
175
+ for (var _k = true, _l = __asyncValues(this.cfg.hooks.afterResponse || []), _m; _m = await _l.next(), _d = _m.done, !_d;) {
176
+ _f = _m.value;
177
+ _k = false;
178
+ try {
179
+ const hook = _f;
180
+ await hook(res);
181
+ }
182
+ finally {
183
+ _k = true;
184
+ }
185
+ }
186
+ }
187
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
188
+ finally {
189
+ try {
190
+ if (!_k && !_d && (_e = _l.return)) await _e.call(_l);
191
+ }
192
+ finally { if (e_2) throw e_2.error; }
193
+ }
156
194
  return res;
157
195
  }
158
196
  async processRetry(res) {
159
- var _a, _b;
197
+ var _a, e_3, _b, _c;
160
198
  const { retryStatus } = res;
161
199
  if (!this.shouldRetry(res)) {
162
200
  retryStatus.retryStopped = true;
163
201
  }
164
- await ((_b = (_a = this.cfg.hooks) === null || _a === void 0 ? void 0 : _a.beforeRetry) === null || _b === void 0 ? void 0 : _b.call(_a, res));
202
+ try {
203
+ for (var _d = true, _e = __asyncValues(this.cfg.hooks.beforeRetry || []), _f; _f = await _e.next(), _a = _f.done, !_a;) {
204
+ _c = _f.value;
205
+ _d = false;
206
+ try {
207
+ const hook = _c;
208
+ await hook(res);
209
+ }
210
+ finally {
211
+ _d = true;
212
+ }
213
+ }
214
+ }
215
+ catch (e_3_1) { e_3 = { error: e_3_1 }; }
216
+ finally {
217
+ try {
218
+ if (!_d && !_a && (_b = _e.return)) await _b.call(_e);
219
+ }
220
+ finally { if (e_3) throw e_3.error; }
221
+ }
165
222
  const { count, timeoutMultiplier, timeoutMax } = res.req.retry;
166
223
  if (retryStatus.retryAttempt >= count) {
167
224
  retryStatus.retryStopped = true;
@@ -182,9 +239,9 @@ export class Fetcher {
182
239
  if (method === 'post' && !retryPost)
183
240
  return false;
184
241
  const { statusFamily } = res;
185
- if (statusFamily === '5xx' && !retry5xx)
242
+ if (statusFamily === 5 && !retry5xx)
186
243
  return false;
187
- if (statusFamily === '4xx' && !retry4xx)
244
+ if (statusFamily === 4 && !retry4xx)
188
245
  return false;
189
246
  return true; // default is true
190
247
  }
@@ -194,15 +251,15 @@ export class Fetcher {
194
251
  if (!status)
195
252
  return;
196
253
  if (status >= 500)
197
- return '5xx';
254
+ return 5;
198
255
  if (status >= 400)
199
- return '4xx';
256
+ return 4;
200
257
  if (status >= 300)
201
- return '3xx';
258
+ return 3;
202
259
  if (status >= 200)
203
- return '2xx';
260
+ return 2;
204
261
  if (status >= 100)
205
- return '1xx';
262
+ return 1;
206
263
  }
207
264
  /**
208
265
  * Returns url without baseUrl and before ?queryString
@@ -219,8 +276,9 @@ export class Fetcher {
219
276
  console.warn(`Fetcher: baseUrl should not end with /`);
220
277
  cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1);
221
278
  }
222
- const { debug } = cfg;
279
+ const { debug = false } = cfg;
223
280
  const norm = _merge({
281
+ baseUrl: '',
224
282
  url: '',
225
283
  searchParams: {},
226
284
  timeoutSeconds: 30,
@@ -229,6 +287,7 @@ export class Fetcher {
229
287
  retry4xx: false,
230
288
  retry5xx: true,
231
289
  logger: console,
290
+ debug,
232
291
  logRequest: debug,
233
292
  logRequestBody: debug,
234
293
  logResponse: debug,
@@ -238,6 +297,7 @@ export class Fetcher {
238
297
  method: 'get',
239
298
  headers: {},
240
299
  },
300
+ hooks: {},
241
301
  }, cfg);
242
302
  norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
243
303
  return norm;
@@ -249,8 +309,11 @@ export class Fetcher {
249
309
  throwHttpErrors,
250
310
  retryPost,
251
311
  retry4xx,
252
- retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init), opt.init, _filterUndefinedValues({
312
+ retry5xx }, _omit(opt, ['method', 'headers'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign({}, this.cfg.init),
313
+ // opt.init,
314
+ _filterUndefinedValues({
253
315
  method: opt.method,
316
+ credentials: opt.credentials,
254
317
  headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
255
318
  })) });
256
319
  // setup url
@@ -1 +1 @@
1
- export {};
1
+ export const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.119.1",
3
+ "version": "14.121.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -15,14 +15,22 @@ import { pDelay } from '../promise/pDelay'
15
15
  import { _jsonParseIfPossible } from '../string/json.util'
16
16
  import { _since } from '../time/time.util'
17
17
  import type { Promisable } from '../typeFest'
18
+ import { HTTP_METHODS } from './http.model'
18
19
  import type { HttpMethod, HttpStatusFamily } from './http.model'
19
20
 
20
- export interface FetcherNormalizedCfg extends FetcherCfg, FetcherRequest {
21
+ export interface FetcherNormalizedCfg extends Required<FetcherCfg>, FetcherRequest {
21
22
  logger: CommonLogger
22
23
  searchParams: Record<string, any>
23
24
  }
24
25
 
26
+ export type FetcherBeforeRequestHook = (req: FetcherRequest) => Promisable<void>
27
+ export type FetcherAfterResponseHook = (res: FetcherResponse) => Promisable<void>
28
+ export type FetcherBeforeRetryHook = (res: FetcherResponse) => Promisable<void>
29
+
25
30
  export interface FetcherCfg {
31
+ /**
32
+ * Should **not** contain trailing slash.
33
+ */
26
34
  baseUrl?: string
27
35
 
28
36
  /**
@@ -34,23 +42,30 @@ export interface FetcherCfg {
34
42
  /**
35
43
  * Allows to mutate req.
36
44
  */
37
- beforeRequest?(req: FetcherRequest): Promisable<void>
45
+ beforeRequest?: FetcherBeforeRequestHook[]
38
46
  /**
39
47
  * Allows to mutate res.
40
48
  * If you set `res.err` - it will be thrown.
41
49
  */
42
- beforeResponse?(res: FetcherResponse): Promisable<void>
50
+ afterResponse?: FetcherAfterResponseHook[]
43
51
  /**
44
52
  * Allows to mutate res.retryStatus to override retry behavior.
45
53
  */
46
- beforeRetry?(res: FetcherResponse): Promisable<void>
54
+ beforeRetry?: FetcherBeforeRetryHook[]
47
55
  }
48
56
 
57
+ /**
58
+ * If true - enables all possible logging.
59
+ */
49
60
  debug?: boolean
50
61
  logRequest?: boolean
51
62
  logRequestBody?: boolean
52
63
  logResponse?: boolean
53
64
  logResponseBody?: boolean
65
+
66
+ /**
67
+ * Defaults to `console`.
68
+ */
54
69
  logger?: CommonLogger
55
70
  }
56
71
 
@@ -90,7 +105,13 @@ export interface FetcherOptions {
90
105
  timeoutSeconds?: number
91
106
  json?: any
92
107
  text?: string
93
- init?: Partial<RequestInitNormalized>
108
+
109
+ credentials?: RequestCredentials
110
+
111
+ // Removing RequestInit from options to simplify FetcherOptions interface.
112
+ // Will instead only add hand-picked useful options, such as `credentials`.
113
+ // init?: Partial<RequestInitNormalized>
114
+
94
115
  headers?: Record<string, any>
95
116
  mode?: FetcherMode // default to undefined (void response)
96
117
 
@@ -159,56 +180,88 @@ const defRetryOptions: FetcherRetryOptions = {
159
180
  export class Fetcher {
160
181
  private constructor(cfg: FetcherCfg & FetcherOptions = {}) {
161
182
  this.cfg = this.normalizeCfg(cfg)
162
- }
163
183
 
164
- public cfg: FetcherNormalizedCfg
184
+ // Dynamically create all helper methods
185
+ HTTP_METHODS.forEach(method => {
186
+ // mode=void
187
+ this[method] = async (url: string, opt?: FetcherOptions): Promise<void> => {
188
+ return await this.fetch<void>(url, {
189
+ ...opt,
190
+ method,
191
+ })
192
+ }
165
193
 
166
- static create(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
167
- return new Fetcher(cfg)
168
- }
194
+ // mode=text
195
+ ;(this as any)[`${method}Text`] = async (
196
+ url: string,
197
+ opt?: FetcherOptions,
198
+ ): Promise<string> => {
199
+ return await this.fetch<string>(url, {
200
+ ...opt,
201
+ method,
202
+ mode: 'text',
203
+ })
204
+ }
169
205
 
170
- async getJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
171
- return await this.fetch<T>(url, {
172
- ...opt,
173
- mode: 'json',
174
- })
175
- }
176
- async postJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
177
- return await this.fetch<T>(url, {
178
- ...opt,
179
- method: 'post',
180
- mode: 'json',
206
+ // mode=json
207
+ ;(this as any)[`${method}Json`] = async <T = unknown>(
208
+ url: string,
209
+ opt?: FetcherOptions,
210
+ ): Promise<T> => {
211
+ return await this.fetch<T>(url, {
212
+ ...opt,
213
+ method,
214
+ mode: 'json',
215
+ })
216
+ }
181
217
  })
182
218
  }
183
- async putJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
184
- return await this.fetch<T>(url, {
185
- ...opt,
186
- method: 'put',
187
- mode: 'json',
188
- })
219
+
220
+ /**
221
+ * Add BeforeRequest hook at the end of the hooks list.
222
+ */
223
+ onBeforeRequest(hook: FetcherBeforeRequestHook): this {
224
+ ;(this.cfg.hooks.beforeRequest ||= []).push(hook)
225
+ return this
189
226
  }
190
- async patchJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
191
- return await this.fetch<T>(url, {
192
- ...opt,
193
- method: 'patch',
194
- mode: 'json',
195
- })
227
+
228
+ onAfterResponse(hook: FetcherAfterResponseHook): this {
229
+ ;(this.cfg.hooks.afterResponse ||= []).push(hook)
230
+ return this
196
231
  }
197
- async deleteJson<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
198
- return await this.fetch<T>(url, {
199
- ...opt,
200
- method: 'delete',
201
- mode: 'json',
202
- })
232
+
233
+ onBeforeRetry(hook: FetcherBeforeRetryHook): this {
234
+ ;(this.cfg.hooks.beforeRetry ||= []).push(hook)
235
+ return this
203
236
  }
204
237
 
205
- async getText(url: string, opt?: FetcherOptions): Promise<string> {
206
- return await this.fetch<string>(url, {
207
- ...opt,
208
- mode: 'text',
209
- })
238
+ public cfg: FetcherNormalizedCfg
239
+
240
+ static create(cfg: FetcherCfg & FetcherOptions = {}): Fetcher {
241
+ return new Fetcher(cfg)
210
242
  }
211
243
 
244
+ // These methods are generated dynamically in the constructor
245
+ get!: (url: string, opt?: FetcherOptions) => Promise<void>
246
+ post!: (url: string, opt?: FetcherOptions) => Promise<void>
247
+ put!: (url: string, opt?: FetcherOptions) => Promise<void>
248
+ patch!: (url: string, opt?: FetcherOptions) => Promise<void>
249
+ delete!: (url: string, opt?: FetcherOptions) => Promise<void>
250
+ head!: (url: string, opt?: FetcherOptions) => Promise<void>
251
+
252
+ getText!: (url: string, opt?: FetcherOptions) => Promise<string>
253
+ postText!: (url: string, opt?: FetcherOptions) => Promise<string>
254
+ putText!: (url: string, opt?: FetcherOptions) => Promise<string>
255
+ patchText!: (url: string, opt?: FetcherOptions) => Promise<string>
256
+ deleteText!: (url: string, opt?: FetcherOptions) => Promise<string>
257
+
258
+ getJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
259
+ postJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
260
+ putJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
261
+ patchJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
262
+ deleteJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
263
+ // headJson!: <T = unknown>(url: string, opt?: FetcherOptions) => Promise<T>
264
+
212
265
  async fetch<T = unknown>(url: string, opt?: FetcherOptions): Promise<T> {
213
266
  const res = await this.rawFetch<T>(url, opt)
214
267
  if (res.err) {
@@ -241,7 +294,9 @@ export class Fetcher {
241
294
  }, timeoutSeconds * 1000) as any as number
242
295
  }
243
296
 
244
- await this.cfg.hooks?.beforeRequest?.(req)
297
+ for await (const hook of this.cfg.hooks.beforeRequest || []) {
298
+ await hook(req)
299
+ }
245
300
 
246
301
  const res: FetcherResponse<any> = {
247
302
  req,
@@ -330,31 +385,13 @@ export class Fetcher {
330
385
  }),
331
386
  )
332
387
 
333
- // We don't log errors when they are also thrown,
334
- // otherwise it gets logged twice: here, and upstream
335
- // if (this.cfg.logResponse) {
336
- // const { retryAttempt } = res.retryStatus
337
- // logger.error(
338
- // [
339
- // [
340
- // ' <<',
341
- // res.fetchResponse.status,
342
- // signature,
343
- // retryAttempt && `try#${retryAttempt + 1}/${req.retry.count}`,
344
- // _since(started),
345
- // ]
346
- // .filter(Boolean)
347
- // .join(' '),
348
- // _stringifyAny(body),
349
- // ].join('\n'),
350
- // )
351
- // }
352
-
353
388
  await this.processRetry(res)
354
389
  }
355
390
  }
356
391
 
357
- await this.cfg.hooks?.beforeResponse?.(res)
392
+ for await (const hook of this.cfg.hooks.afterResponse || []) {
393
+ await hook(res)
394
+ }
358
395
 
359
396
  return res
360
397
  }
@@ -366,7 +403,9 @@ export class Fetcher {
366
403
  retryStatus.retryStopped = true
367
404
  }
368
405
 
369
- await this.cfg.hooks?.beforeRetry?.(res)
406
+ for await (const hook of this.cfg.hooks.beforeRetry || []) {
407
+ await hook(res)
408
+ }
370
409
 
371
410
  const { count, timeoutMultiplier, timeoutMax } = res.req.retry
372
411
 
@@ -391,19 +430,19 @@ export class Fetcher {
391
430
  const { method } = res.req.init
392
431
  if (method === 'post' && !retryPost) return false
393
432
  const { statusFamily } = res
394
- if (statusFamily === '5xx' && !retry5xx) return false
395
- if (statusFamily === '4xx' && !retry4xx) return false
433
+ if (statusFamily === 5 && !retry5xx) return false
434
+ if (statusFamily === 4 && !retry4xx) return false
396
435
  return true // default is true
397
436
  }
398
437
 
399
438
  private getStatusFamily(res: FetcherResponse): HttpStatusFamily | undefined {
400
439
  const status = res.fetchResponse?.status
401
440
  if (!status) return
402
- if (status >= 500) return '5xx'
403
- if (status >= 400) return '4xx'
404
- if (status >= 300) return '3xx'
405
- if (status >= 200) return '2xx'
406
- if (status >= 100) return '1xx'
441
+ if (status >= 500) return 5
442
+ if (status >= 400) return 4
443
+ if (status >= 300) return 3
444
+ if (status >= 200) return 2
445
+ if (status >= 100) return 1
407
446
  }
408
447
 
409
448
  /**
@@ -421,10 +460,11 @@ export class Fetcher {
421
460
  console.warn(`Fetcher: baseUrl should not end with /`)
422
461
  cfg.baseUrl = cfg.baseUrl.slice(0, cfg.baseUrl.length - 1)
423
462
  }
424
- const { debug } = cfg
463
+ const { debug = false } = cfg
425
464
 
426
465
  const norm: FetcherNormalizedCfg = _merge(
427
466
  {
467
+ baseUrl: '',
428
468
  url: '',
429
469
  searchParams: {},
430
470
  timeoutSeconds: 30,
@@ -433,6 +473,7 @@ export class Fetcher {
433
473
  retry4xx: false,
434
474
  retry5xx: true,
435
475
  logger: console,
476
+ debug,
436
477
  logRequest: debug,
437
478
  logRequestBody: debug,
438
479
  logResponse: debug,
@@ -442,6 +483,7 @@ export class Fetcher {
442
483
  method: 'get',
443
484
  headers: {},
444
485
  },
486
+ hooks: {},
445
487
  },
446
488
  cfg,
447
489
  )
@@ -469,11 +511,12 @@ export class Fetcher {
469
511
  },
470
512
  init: _merge(
471
513
  { ...this.cfg.init },
472
- opt.init,
514
+ // opt.init,
473
515
  _filterUndefinedValues({
474
516
  method: opt.method,
517
+ credentials: opt.credentials,
475
518
  headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
476
- }),
519
+ } as RequestInit),
477
520
  ),
478
521
  }
479
522
 
@@ -1,3 +1,5 @@
1
1
  export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head'
2
2
 
3
- export type HttpStatusFamily = '5xx' | '4xx' | '3xx' | '2xx' | '1xx'
3
+ export type HttpStatusFamily = 5 | 4 | 3 | 2 | 1
4
+
5
+ export const HTTP_METHODS: HttpMethod[] = ['get', 'post', 'put', 'patch', 'delete', 'head']