@naturalcycles/js-lib 14.146.0 → 14.147.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/http/fetcher.d.ts +3 -4
- package/dist/http/fetcher.js +28 -26
- package/dist/http/fetcher.model.d.ts +24 -8
- package/dist-esm/http/fetcher.js +26 -27
- package/package.json +1 -1
- package/src/http/fetcher.model.ts +25 -8
- package/src/http/fetcher.ts +28 -30
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions,
|
|
2
|
+
import type { FetcherAfterResponseHook, FetcherBeforeRequestHook, FetcherBeforeRetryHook, FetcherCfg, FetcherNormalizedCfg, FetcherOptions, FetcherResponse } from './fetcher.model';
|
|
3
3
|
/**
|
|
4
4
|
* Experimental wrapper around Fetch.
|
|
5
5
|
* Works in both Browser and Node, using `globalThis.fetch`.
|
|
@@ -37,14 +37,13 @@ export declare class Fetcher {
|
|
|
37
37
|
* https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/
|
|
38
38
|
*/
|
|
39
39
|
getReadableStream(url: string, opt?: FetcherOptions<ReadableStream<Uint8Array>>): Promise<ReadableStream<Uint8Array>>;
|
|
40
|
-
fetch<T = unknown>(
|
|
40
|
+
fetch<T = unknown>(opt: FetcherOptions<T>): Promise<T>;
|
|
41
41
|
/**
|
|
42
42
|
* Returns FetcherResponse.
|
|
43
43
|
* Never throws, returns `err` property in the response instead.
|
|
44
44
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
45
45
|
*/
|
|
46
|
-
doFetch<T = unknown>(
|
|
47
|
-
doFetchRequest<T = unknown>(req: FetcherRequest<T>): Promise<FetcherResponse<T>>;
|
|
46
|
+
doFetch<T = unknown>(opt: FetcherOptions<T>): Promise<FetcherResponse<T>>;
|
|
48
47
|
private onOkResponse;
|
|
49
48
|
/**
|
|
50
49
|
* This method exists to be able to easily mock it.
|
package/dist/http/fetcher.js
CHANGED
|
@@ -31,7 +31,8 @@ class Fetcher {
|
|
|
31
31
|
http_model_1.HTTP_METHODS.forEach(method => {
|
|
32
32
|
const m = method.toLowerCase();
|
|
33
33
|
this[`${m}Void`] = async (url, opt) => {
|
|
34
|
-
return await this.fetch(
|
|
34
|
+
return await this.fetch({
|
|
35
|
+
url,
|
|
35
36
|
method,
|
|
36
37
|
mode: 'void',
|
|
37
38
|
...opt,
|
|
@@ -41,14 +42,16 @@ class Fetcher {
|
|
|
41
42
|
return // mode=text
|
|
42
43
|
;
|
|
43
44
|
this[`${m}Text`] = async (url, opt) => {
|
|
44
|
-
return await this.fetch(
|
|
45
|
+
return await this.fetch({
|
|
46
|
+
url,
|
|
45
47
|
method,
|
|
46
48
|
mode: 'text',
|
|
47
49
|
...opt,
|
|
48
50
|
});
|
|
49
51
|
};
|
|
50
52
|
this[m] = async (url, opt) => {
|
|
51
|
-
return await this.fetch(
|
|
53
|
+
return await this.fetch({
|
|
54
|
+
url,
|
|
52
55
|
method,
|
|
53
56
|
mode: 'json',
|
|
54
57
|
...opt,
|
|
@@ -85,13 +88,14 @@ class Fetcher {
|
|
|
85
88
|
* https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/
|
|
86
89
|
*/
|
|
87
90
|
async getReadableStream(url, opt) {
|
|
88
|
-
return await this.fetch(
|
|
91
|
+
return await this.fetch({
|
|
92
|
+
url,
|
|
89
93
|
mode: 'readableStream',
|
|
90
94
|
...opt,
|
|
91
95
|
});
|
|
92
96
|
}
|
|
93
|
-
async fetch(
|
|
94
|
-
const res = await this.doFetch(
|
|
97
|
+
async fetch(opt) {
|
|
98
|
+
const res = await this.doFetch(opt);
|
|
95
99
|
if (res.err) {
|
|
96
100
|
if (res.req.throwHttpErrors)
|
|
97
101
|
throw res.err;
|
|
@@ -104,11 +108,8 @@ class Fetcher {
|
|
|
104
108
|
* Never throws, returns `err` property in the response instead.
|
|
105
109
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
106
110
|
*/
|
|
107
|
-
async doFetch(
|
|
108
|
-
const req = this.normalizeOptions(
|
|
109
|
-
return await this.doFetchRequest(req);
|
|
110
|
-
}
|
|
111
|
-
async doFetchRequest(req) {
|
|
111
|
+
async doFetch(opt) {
|
|
112
|
+
const req = this.normalizeOptions(opt);
|
|
112
113
|
const { logger } = this.cfg;
|
|
113
114
|
const { timeoutSeconds, init: { method }, } = req;
|
|
114
115
|
// setup timeout
|
|
@@ -123,9 +124,9 @@ class Fetcher {
|
|
|
123
124
|
for (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
124
125
|
await hook(req);
|
|
125
126
|
}
|
|
126
|
-
const isFullUrl = req.
|
|
127
|
-
const fullUrl = isFullUrl ? new URL(req.
|
|
128
|
-
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.
|
|
127
|
+
const isFullUrl = req.fullUrl.includes('://');
|
|
128
|
+
const fullUrl = isFullUrl ? new URL(req.fullUrl) : undefined;
|
|
129
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.fullUrl;
|
|
129
130
|
const signature = [method, shortUrl].join(' ');
|
|
130
131
|
const res = {
|
|
131
132
|
req,
|
|
@@ -148,7 +149,7 @@ class Fetcher {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
try {
|
|
151
|
-
res.fetchResponse = await this.callNativeFetch(req.
|
|
152
|
+
res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init);
|
|
152
153
|
res.ok = res.fetchResponse.ok;
|
|
153
154
|
}
|
|
154
155
|
catch (err) {
|
|
@@ -171,9 +172,9 @@ class Fetcher {
|
|
|
171
172
|
await hook(res);
|
|
172
173
|
}
|
|
173
174
|
if (req.paginate && res.ok) {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
return await this.
|
|
175
|
+
const proceeed = await req.paginate(res, opt);
|
|
176
|
+
if (proceeed) {
|
|
177
|
+
return await this.doFetch(opt);
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
180
|
return res;
|
|
@@ -280,7 +281,7 @@ class Fetcher {
|
|
|
280
281
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
281
282
|
// method: req.method,
|
|
282
283
|
// tryCount: req.tryCount,
|
|
283
|
-
requestUrl: res.req.
|
|
284
|
+
requestUrl: res.req.fullUrl,
|
|
284
285
|
requestBaseUrl: this.cfg.baseUrl || null,
|
|
285
286
|
requestMethod: res.req.init.method,
|
|
286
287
|
requestSignature: res.signature,
|
|
@@ -387,7 +388,7 @@ class Fetcher {
|
|
|
387
388
|
const { debug = false } = cfg;
|
|
388
389
|
const norm = (0, object_util_1._merge)({
|
|
389
390
|
baseUrl: '',
|
|
390
|
-
|
|
391
|
+
inputUrl: '',
|
|
391
392
|
mode: 'void',
|
|
392
393
|
searchParams: {},
|
|
393
394
|
timeoutSeconds: 30,
|
|
@@ -415,12 +416,11 @@ class Fetcher {
|
|
|
415
416
|
norm.init.headers = (0, object_util_1._mapKeys)(norm.init.headers, k => k.toLowerCase());
|
|
416
417
|
return norm;
|
|
417
418
|
}
|
|
418
|
-
normalizeOptions(
|
|
419
|
+
normalizeOptions(opt) {
|
|
419
420
|
const { timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode, jsonReviver, } = this.cfg;
|
|
420
421
|
const req = {
|
|
421
422
|
started: Date.now(),
|
|
422
423
|
mode,
|
|
423
|
-
url,
|
|
424
424
|
timeoutSeconds,
|
|
425
425
|
throwHttpErrors,
|
|
426
426
|
retryPost,
|
|
@@ -428,6 +428,8 @@ class Fetcher {
|
|
|
428
428
|
retry5xx,
|
|
429
429
|
jsonReviver,
|
|
430
430
|
...(0, object_util_1._omit)(opt, ['method', 'headers', 'credentials']),
|
|
431
|
+
inputUrl: opt.url || '',
|
|
432
|
+
fullUrl: opt.url || '',
|
|
431
433
|
retry: {
|
|
432
434
|
...retry,
|
|
433
435
|
...(0, object_util_1._filterUndefinedValues)(opt.retry || {}),
|
|
@@ -444,11 +446,11 @@ class Fetcher {
|
|
|
444
446
|
// setup url
|
|
445
447
|
const baseUrl = opt.baseUrl || this.cfg.baseUrl;
|
|
446
448
|
if (baseUrl) {
|
|
447
|
-
if (
|
|
449
|
+
if (req.fullUrl.startsWith('/')) {
|
|
448
450
|
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
449
|
-
|
|
451
|
+
req.fullUrl = req.fullUrl.slice(1);
|
|
450
452
|
}
|
|
451
|
-
req.
|
|
453
|
+
req.fullUrl = `${baseUrl}/${req.inputUrl}`;
|
|
452
454
|
}
|
|
453
455
|
const searchParams = (0, object_util_1._filterUndefinedValues)({
|
|
454
456
|
...this.cfg.searchParams,
|
|
@@ -456,7 +458,7 @@ class Fetcher {
|
|
|
456
458
|
});
|
|
457
459
|
if (Object.keys(searchParams).length) {
|
|
458
460
|
const qs = new URLSearchParams(searchParams).toString();
|
|
459
|
-
req.
|
|
461
|
+
req.fullUrl += req.fullUrl.includes('?') ? '&' : '?' + qs;
|
|
460
462
|
}
|
|
461
463
|
// setup request body
|
|
462
464
|
if (opt.json !== undefined) {
|
|
@@ -2,7 +2,7 @@ import type { CommonLogger } from '../log/commonLogger';
|
|
|
2
2
|
import type { Promisable } from '../typeFest';
|
|
3
3
|
import type { Reviver, UnixTimestampMillisNumber } from '../types';
|
|
4
4
|
import type { HttpMethod, HttpStatusFamily } from './http.model';
|
|
5
|
-
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started'> {
|
|
5
|
+
export interface FetcherNormalizedCfg extends Required<FetcherCfg>, Omit<FetcherRequest, 'started' | 'fullUrl'> {
|
|
6
6
|
logger: CommonLogger;
|
|
7
7
|
searchParams: Record<string, any>;
|
|
8
8
|
}
|
|
@@ -77,8 +77,16 @@ export interface FetcherRetryOptions {
|
|
|
77
77
|
timeoutMax: number;
|
|
78
78
|
timeoutMultiplier: number;
|
|
79
79
|
}
|
|
80
|
-
export interface FetcherRequest<BODY = unknown> extends Omit<FetcherOptions<BODY>, 'method' | 'headers' | 'baseUrl'> {
|
|
81
|
-
|
|
80
|
+
export interface FetcherRequest<BODY = unknown> extends Omit<FetcherOptions<BODY>, 'method' | 'headers' | 'baseUrl' | 'url'> {
|
|
81
|
+
/**
|
|
82
|
+
* inputUrl is only the part that was passed in the request,
|
|
83
|
+
* without baseUrl or searchParams.
|
|
84
|
+
*/
|
|
85
|
+
inputUrl: string;
|
|
86
|
+
/**
|
|
87
|
+
* fullUrl includes baseUrl and searchParams.
|
|
88
|
+
*/
|
|
89
|
+
fullUrl: string;
|
|
82
90
|
init: RequestInitNormalized;
|
|
83
91
|
mode: FetcherMode;
|
|
84
92
|
throwHttpErrors: boolean;
|
|
@@ -91,6 +99,11 @@ export interface FetcherRequest<BODY = unknown> extends Omit<FetcherOptions<BODY
|
|
|
91
99
|
}
|
|
92
100
|
export interface FetcherOptions<BODY = unknown> {
|
|
93
101
|
method?: HttpMethod;
|
|
102
|
+
/**
|
|
103
|
+
* If defined - this `url` will override the original given `url`.
|
|
104
|
+
* baseUrl (and searchParams) will still modify it.
|
|
105
|
+
*/
|
|
106
|
+
url?: string;
|
|
94
107
|
baseUrl?: string;
|
|
95
108
|
throwHttpErrors?: boolean;
|
|
96
109
|
/**
|
|
@@ -138,15 +151,18 @@ export interface FetcherOptions<BODY = unknown> {
|
|
|
138
151
|
/**
|
|
139
152
|
* Allows to walk over multiple pages of results.
|
|
140
153
|
* Paginate take a function.
|
|
141
|
-
* Function has access to
|
|
154
|
+
* Function has access to FetcherResponse and FetcherOptions
|
|
142
155
|
* and has to make a decision to continue pagination or not.
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
156
|
+
*
|
|
157
|
+
* Return false to stop pagination.
|
|
158
|
+
* Return true to continue pagination.
|
|
159
|
+
* Feel free to mutate/modify opt (FetcherOptions), for example:
|
|
160
|
+
*
|
|
161
|
+
* opt.searchParams!['page']++
|
|
146
162
|
*
|
|
147
163
|
* @experimental
|
|
148
164
|
*/
|
|
149
|
-
paginate?: (
|
|
165
|
+
paginate?: (res: FetcherSuccessResponse<BODY>, opt: FetcherOptions<BODY>) => Promisable<boolean>;
|
|
150
166
|
}
|
|
151
167
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
152
168
|
method: HttpMethod;
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -28,16 +28,19 @@ export class Fetcher {
|
|
|
28
28
|
HTTP_METHODS.forEach(method => {
|
|
29
29
|
const m = method.toLowerCase();
|
|
30
30
|
this[`${m}Void`] = async (url, opt) => {
|
|
31
|
-
return await this.fetch(
|
|
31
|
+
return await this.fetch(Object.assign({ url,
|
|
32
|
+
method, mode: 'void' }, opt));
|
|
32
33
|
};
|
|
33
34
|
if (method === 'HEAD')
|
|
34
35
|
return // mode=text
|
|
35
36
|
;
|
|
36
37
|
this[`${m}Text`] = async (url, opt) => {
|
|
37
|
-
return await this.fetch(
|
|
38
|
+
return await this.fetch(Object.assign({ url,
|
|
39
|
+
method, mode: 'text' }, opt));
|
|
38
40
|
};
|
|
39
41
|
this[m] = async (url, opt) => {
|
|
40
|
-
return await this.fetch(
|
|
42
|
+
return await this.fetch(Object.assign({ url,
|
|
43
|
+
method, mode: 'json' }, opt));
|
|
41
44
|
};
|
|
42
45
|
});
|
|
43
46
|
}
|
|
@@ -73,10 +76,10 @@ export class Fetcher {
|
|
|
73
76
|
* https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/
|
|
74
77
|
*/
|
|
75
78
|
async getReadableStream(url, opt) {
|
|
76
|
-
return await this.fetch(
|
|
79
|
+
return await this.fetch(Object.assign({ url, mode: 'readableStream' }, opt));
|
|
77
80
|
}
|
|
78
|
-
async fetch(
|
|
79
|
-
const res = await this.doFetch(
|
|
81
|
+
async fetch(opt) {
|
|
82
|
+
const res = await this.doFetch(opt);
|
|
80
83
|
if (res.err) {
|
|
81
84
|
if (res.req.throwHttpErrors)
|
|
82
85
|
throw res.err;
|
|
@@ -89,12 +92,9 @@ export class Fetcher {
|
|
|
89
92
|
* Never throws, returns `err` property in the response instead.
|
|
90
93
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
91
94
|
*/
|
|
92
|
-
async doFetch(
|
|
93
|
-
const req = this.normalizeOptions(url, opt);
|
|
94
|
-
return await this.doFetchRequest(req);
|
|
95
|
-
}
|
|
96
|
-
async doFetchRequest(req) {
|
|
95
|
+
async doFetch(opt) {
|
|
97
96
|
var _a;
|
|
97
|
+
const req = this.normalizeOptions(opt);
|
|
98
98
|
const { logger } = this.cfg;
|
|
99
99
|
const { timeoutSeconds, init: { method }, } = req;
|
|
100
100
|
// setup timeout
|
|
@@ -109,9 +109,9 @@ export class Fetcher {
|
|
|
109
109
|
for (const hook of this.cfg.hooks.beforeRequest || []) {
|
|
110
110
|
await hook(req);
|
|
111
111
|
}
|
|
112
|
-
const isFullUrl = req.
|
|
113
|
-
const fullUrl = isFullUrl ? new URL(req.
|
|
114
|
-
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.
|
|
112
|
+
const isFullUrl = req.fullUrl.includes('://');
|
|
113
|
+
const fullUrl = isFullUrl ? new URL(req.fullUrl) : undefined;
|
|
114
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.fullUrl;
|
|
115
115
|
const signature = [method, shortUrl].join(' ');
|
|
116
116
|
const res = {
|
|
117
117
|
req,
|
|
@@ -134,7 +134,7 @@ export class Fetcher {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
try {
|
|
137
|
-
res.fetchResponse = await this.callNativeFetch(req.
|
|
137
|
+
res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init);
|
|
138
138
|
res.ok = res.fetchResponse.ok;
|
|
139
139
|
}
|
|
140
140
|
catch (err) {
|
|
@@ -157,9 +157,9 @@ export class Fetcher {
|
|
|
157
157
|
await hook(res);
|
|
158
158
|
}
|
|
159
159
|
if (req.paginate && res.ok) {
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
return await this.
|
|
160
|
+
const proceeed = await req.paginate(res, opt);
|
|
161
|
+
if (proceeed) {
|
|
162
|
+
return await this.doFetch(opt);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
return res;
|
|
@@ -267,7 +267,7 @@ export class Fetcher {
|
|
|
267
267
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
268
268
|
// method: req.method,
|
|
269
269
|
// tryCount: req.tryCount,
|
|
270
|
-
requestUrl: res.req.
|
|
270
|
+
requestUrl: res.req.fullUrl,
|
|
271
271
|
requestBaseUrl: this.cfg.baseUrl || null,
|
|
272
272
|
requestMethod: res.req.init.method,
|
|
273
273
|
requestSignature: res.signature,
|
|
@@ -378,7 +378,7 @@ export class Fetcher {
|
|
|
378
378
|
const { debug = false } = cfg;
|
|
379
379
|
const norm = _merge({
|
|
380
380
|
baseUrl: '',
|
|
381
|
-
|
|
381
|
+
inputUrl: '',
|
|
382
382
|
mode: 'void',
|
|
383
383
|
searchParams: {},
|
|
384
384
|
timeoutSeconds: 30,
|
|
@@ -406,32 +406,31 @@ export class Fetcher {
|
|
|
406
406
|
norm.init.headers = _mapKeys(norm.init.headers, k => k.toLowerCase());
|
|
407
407
|
return norm;
|
|
408
408
|
}
|
|
409
|
-
normalizeOptions(
|
|
409
|
+
normalizeOptions(opt) {
|
|
410
410
|
var _a, _b;
|
|
411
411
|
const { timeoutSeconds, throwHttpErrors, retryPost, retry4xx, retry5xx, retry, mode, jsonReviver, } = this.cfg;
|
|
412
412
|
const req = Object.assign(Object.assign({ started: Date.now(), mode,
|
|
413
|
-
url,
|
|
414
413
|
timeoutSeconds,
|
|
415
414
|
throwHttpErrors,
|
|
416
415
|
retryPost,
|
|
417
416
|
retry4xx,
|
|
418
417
|
retry5xx,
|
|
419
|
-
jsonReviver }, _omit(opt, ['method', 'headers', 'credentials'])), { retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign(Object.assign({}, this.cfg.init), { method: opt.method || this.cfg.init.method, credentials: opt.credentials || this.cfg.init.credentials, redirect: ((_b = (_a = opt.followRedirects) !== null && _a !== void 0 ? _a : this.cfg.followRedirects) !== null && _b !== void 0 ? _b : true) ? 'follow' : 'error' }), {
|
|
418
|
+
jsonReviver }, _omit(opt, ['method', 'headers', 'credentials'])), { inputUrl: opt.url || '', fullUrl: opt.url || '', retry: Object.assign(Object.assign({}, retry), _filterUndefinedValues(opt.retry || {})), init: _merge(Object.assign(Object.assign({}, this.cfg.init), { method: opt.method || this.cfg.init.method, credentials: opt.credentials || this.cfg.init.credentials, redirect: ((_b = (_a = opt.followRedirects) !== null && _a !== void 0 ? _a : this.cfg.followRedirects) !== null && _b !== void 0 ? _b : true) ? 'follow' : 'error' }), {
|
|
420
419
|
headers: _mapKeys(opt.headers || {}, k => k.toLowerCase()),
|
|
421
420
|
}) });
|
|
422
421
|
// setup url
|
|
423
422
|
const baseUrl = opt.baseUrl || this.cfg.baseUrl;
|
|
424
423
|
if (baseUrl) {
|
|
425
|
-
if (
|
|
424
|
+
if (req.fullUrl.startsWith('/')) {
|
|
426
425
|
console.warn(`Fetcher: url should not start with / when baseUrl is specified`);
|
|
427
|
-
|
|
426
|
+
req.fullUrl = req.fullUrl.slice(1);
|
|
428
427
|
}
|
|
429
|
-
req.
|
|
428
|
+
req.fullUrl = `${baseUrl}/${req.inputUrl}`;
|
|
430
429
|
}
|
|
431
430
|
const searchParams = _filterUndefinedValues(Object.assign(Object.assign({}, this.cfg.searchParams), opt.searchParams));
|
|
432
431
|
if (Object.keys(searchParams).length) {
|
|
433
432
|
const qs = new URLSearchParams(searchParams).toString();
|
|
434
|
-
req.
|
|
433
|
+
req.fullUrl += req.fullUrl.includes('?') ? '&' : '?' + qs;
|
|
435
434
|
}
|
|
436
435
|
// setup request body
|
|
437
436
|
if (opt.json !== undefined) {
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import type { HttpMethod, HttpStatusFamily } from './http.model'
|
|
|
5
5
|
|
|
6
6
|
export interface FetcherNormalizedCfg
|
|
7
7
|
extends Required<FetcherCfg>,
|
|
8
|
-
Omit<FetcherRequest, 'started'> {
|
|
8
|
+
Omit<FetcherRequest, 'started' | 'fullUrl'> {
|
|
9
9
|
logger: CommonLogger
|
|
10
10
|
searchParams: Record<string, any>
|
|
11
11
|
}
|
|
@@ -97,8 +97,16 @@ export interface FetcherRetryOptions {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
export interface FetcherRequest<BODY = unknown>
|
|
100
|
-
extends Omit<FetcherOptions<BODY>, 'method' | 'headers' | 'baseUrl'> {
|
|
101
|
-
|
|
100
|
+
extends Omit<FetcherOptions<BODY>, 'method' | 'headers' | 'baseUrl' | 'url'> {
|
|
101
|
+
/**
|
|
102
|
+
* inputUrl is only the part that was passed in the request,
|
|
103
|
+
* without baseUrl or searchParams.
|
|
104
|
+
*/
|
|
105
|
+
inputUrl: string
|
|
106
|
+
/**
|
|
107
|
+
* fullUrl includes baseUrl and searchParams.
|
|
108
|
+
*/
|
|
109
|
+
fullUrl: string
|
|
102
110
|
init: RequestInitNormalized
|
|
103
111
|
mode: FetcherMode
|
|
104
112
|
throwHttpErrors: boolean
|
|
@@ -113,6 +121,12 @@ export interface FetcherRequest<BODY = unknown>
|
|
|
113
121
|
export interface FetcherOptions<BODY = unknown> {
|
|
114
122
|
method?: HttpMethod
|
|
115
123
|
|
|
124
|
+
/**
|
|
125
|
+
* If defined - this `url` will override the original given `url`.
|
|
126
|
+
* baseUrl (and searchParams) will still modify it.
|
|
127
|
+
*/
|
|
128
|
+
url?: string
|
|
129
|
+
|
|
116
130
|
baseUrl?: string
|
|
117
131
|
|
|
118
132
|
throwHttpErrors?: boolean
|
|
@@ -173,15 +187,18 @@ export interface FetcherOptions<BODY = unknown> {
|
|
|
173
187
|
/**
|
|
174
188
|
* Allows to walk over multiple pages of results.
|
|
175
189
|
* Paginate take a function.
|
|
176
|
-
* Function has access to
|
|
190
|
+
* Function has access to FetcherResponse and FetcherOptions
|
|
177
191
|
* and has to make a decision to continue pagination or not.
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
192
|
+
*
|
|
193
|
+
* Return false to stop pagination.
|
|
194
|
+
* Return true to continue pagination.
|
|
195
|
+
* Feel free to mutate/modify opt (FetcherOptions), for example:
|
|
196
|
+
*
|
|
197
|
+
* opt.searchParams!['page']++
|
|
181
198
|
*
|
|
182
199
|
* @experimental
|
|
183
200
|
*/
|
|
184
|
-
paginate?: (
|
|
201
|
+
paginate?: (res: FetcherSuccessResponse<BODY>, opt: FetcherOptions<BODY>) => Promisable<boolean>
|
|
185
202
|
}
|
|
186
203
|
|
|
187
204
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
package/src/http/fetcher.ts
CHANGED
|
@@ -57,7 +57,8 @@ export class Fetcher {
|
|
|
57
57
|
url: string,
|
|
58
58
|
opt?: FetcherOptions<void>,
|
|
59
59
|
): Promise<void> => {
|
|
60
|
-
return await this.fetch<void>(
|
|
60
|
+
return await this.fetch<void>({
|
|
61
|
+
url,
|
|
61
62
|
method,
|
|
62
63
|
mode: 'void',
|
|
63
64
|
...opt,
|
|
@@ -69,7 +70,8 @@ export class Fetcher {
|
|
|
69
70
|
url: string,
|
|
70
71
|
opt?: FetcherOptions<string>,
|
|
71
72
|
): Promise<string> => {
|
|
72
|
-
return await this.fetch<string>(
|
|
73
|
+
return await this.fetch<string>({
|
|
74
|
+
url,
|
|
73
75
|
method,
|
|
74
76
|
mode: 'text',
|
|
75
77
|
...opt,
|
|
@@ -78,7 +80,8 @@ export class Fetcher {
|
|
|
78
80
|
|
|
79
81
|
// Default mode=json, but overridable
|
|
80
82
|
;(this as any)[m] = async <T = unknown>(url: string, opt?: FetcherOptions<T>): Promise<T> => {
|
|
81
|
-
return await this.fetch<T>(
|
|
83
|
+
return await this.fetch<T>({
|
|
84
|
+
url,
|
|
82
85
|
method,
|
|
83
86
|
mode: 'json',
|
|
84
87
|
...opt,
|
|
@@ -145,14 +148,15 @@ export class Fetcher {
|
|
|
145
148
|
url: string,
|
|
146
149
|
opt?: FetcherOptions<ReadableStream<Uint8Array>>,
|
|
147
150
|
): Promise<ReadableStream<Uint8Array>> {
|
|
148
|
-
return await this.fetch(
|
|
151
|
+
return await this.fetch({
|
|
152
|
+
url,
|
|
149
153
|
mode: 'readableStream',
|
|
150
154
|
...opt,
|
|
151
155
|
})
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
async fetch<T = unknown>(
|
|
155
|
-
const res = await this.doFetch<T>(
|
|
158
|
+
async fetch<T = unknown>(opt: FetcherOptions<T>): Promise<T> {
|
|
159
|
+
const res = await this.doFetch<T>(opt)
|
|
156
160
|
if (res.err) {
|
|
157
161
|
if (res.req.throwHttpErrors) throw res.err
|
|
158
162
|
return res as any
|
|
@@ -165,15 +169,8 @@ export class Fetcher {
|
|
|
165
169
|
* Never throws, returns `err` property in the response instead.
|
|
166
170
|
* Use this method instead of `throwHttpErrors: false` or try-catching.
|
|
167
171
|
*/
|
|
168
|
-
async doFetch<T = unknown>(
|
|
169
|
-
|
|
170
|
-
opt: FetcherOptions<T> = {},
|
|
171
|
-
): Promise<FetcherResponse<T>> {
|
|
172
|
-
const req = this.normalizeOptions(url, opt)
|
|
173
|
-
return await this.doFetchRequest<T>(req)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async doFetchRequest<T = unknown>(req: FetcherRequest<T>): Promise<FetcherResponse<T>> {
|
|
172
|
+
async doFetch<T = unknown>(opt: FetcherOptions<T>): Promise<FetcherResponse<T>> {
|
|
173
|
+
const req = this.normalizeOptions(opt)
|
|
177
174
|
const { logger } = this.cfg
|
|
178
175
|
const {
|
|
179
176
|
timeoutSeconds,
|
|
@@ -194,9 +191,9 @@ export class Fetcher {
|
|
|
194
191
|
await hook(req)
|
|
195
192
|
}
|
|
196
193
|
|
|
197
|
-
const isFullUrl = req.
|
|
198
|
-
const fullUrl = isFullUrl ? new URL(req.
|
|
199
|
-
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.
|
|
194
|
+
const isFullUrl = req.fullUrl.includes('://')
|
|
195
|
+
const fullUrl = isFullUrl ? new URL(req.fullUrl) : undefined
|
|
196
|
+
const shortUrl = fullUrl ? this.getShortUrl(fullUrl) : req.fullUrl
|
|
200
197
|
const signature = [method, shortUrl].join(' ')
|
|
201
198
|
|
|
202
199
|
const res = {
|
|
@@ -225,7 +222,7 @@ export class Fetcher {
|
|
|
225
222
|
}
|
|
226
223
|
|
|
227
224
|
try {
|
|
228
|
-
res.fetchResponse = await this.callNativeFetch(req.
|
|
225
|
+
res.fetchResponse = await this.callNativeFetch(req.fullUrl, req.init)
|
|
229
226
|
res.ok = res.fetchResponse.ok
|
|
230
227
|
} catch (err) {
|
|
231
228
|
// For example, CORS error would result in "TypeError: failed to fetch" here
|
|
@@ -249,9 +246,9 @@ export class Fetcher {
|
|
|
249
246
|
}
|
|
250
247
|
|
|
251
248
|
if (req.paginate && res.ok) {
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
254
|
-
return await this.
|
|
249
|
+
const proceeed = await req.paginate(res, opt)
|
|
250
|
+
if (proceeed) {
|
|
251
|
+
return await this.doFetch(opt)
|
|
255
252
|
}
|
|
256
253
|
}
|
|
257
254
|
|
|
@@ -372,7 +369,7 @@ export class Fetcher {
|
|
|
372
369
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
373
370
|
// method: req.method,
|
|
374
371
|
// tryCount: req.tryCount,
|
|
375
|
-
requestUrl: res.req.
|
|
372
|
+
requestUrl: res.req.fullUrl,
|
|
376
373
|
requestBaseUrl: this.cfg.baseUrl || (null as any),
|
|
377
374
|
requestMethod: res.req.init.method,
|
|
378
375
|
requestSignature: res.signature,
|
|
@@ -495,7 +492,7 @@ export class Fetcher {
|
|
|
495
492
|
const norm: FetcherNormalizedCfg = _merge(
|
|
496
493
|
{
|
|
497
494
|
baseUrl: '',
|
|
498
|
-
|
|
495
|
+
inputUrl: '',
|
|
499
496
|
mode: 'void',
|
|
500
497
|
searchParams: {},
|
|
501
498
|
timeoutSeconds: 30,
|
|
@@ -528,7 +525,7 @@ export class Fetcher {
|
|
|
528
525
|
return norm
|
|
529
526
|
}
|
|
530
527
|
|
|
531
|
-
private normalizeOptions<BODY>(
|
|
528
|
+
private normalizeOptions<BODY>(opt: FetcherOptions<BODY>): FetcherRequest<BODY> {
|
|
532
529
|
const {
|
|
533
530
|
timeoutSeconds,
|
|
534
531
|
throwHttpErrors,
|
|
@@ -543,7 +540,6 @@ export class Fetcher {
|
|
|
543
540
|
const req: FetcherRequest<BODY> = {
|
|
544
541
|
started: Date.now(),
|
|
545
542
|
mode,
|
|
546
|
-
url,
|
|
547
543
|
timeoutSeconds,
|
|
548
544
|
throwHttpErrors,
|
|
549
545
|
retryPost,
|
|
@@ -551,6 +547,8 @@ export class Fetcher {
|
|
|
551
547
|
retry5xx,
|
|
552
548
|
jsonReviver,
|
|
553
549
|
..._omit(opt, ['method', 'headers', 'credentials']),
|
|
550
|
+
inputUrl: opt.url || '',
|
|
551
|
+
fullUrl: opt.url || '',
|
|
554
552
|
retry: {
|
|
555
553
|
...retry,
|
|
556
554
|
..._filterUndefinedValues(opt.retry || {}),
|
|
@@ -571,11 +569,11 @@ export class Fetcher {
|
|
|
571
569
|
// setup url
|
|
572
570
|
const baseUrl = opt.baseUrl || this.cfg.baseUrl
|
|
573
571
|
if (baseUrl) {
|
|
574
|
-
if (
|
|
572
|
+
if (req.fullUrl.startsWith('/')) {
|
|
575
573
|
console.warn(`Fetcher: url should not start with / when baseUrl is specified`)
|
|
576
|
-
|
|
574
|
+
req.fullUrl = req.fullUrl.slice(1)
|
|
577
575
|
}
|
|
578
|
-
req.
|
|
576
|
+
req.fullUrl = `${baseUrl}/${req.inputUrl}`
|
|
579
577
|
}
|
|
580
578
|
|
|
581
579
|
const searchParams = _filterUndefinedValues({
|
|
@@ -585,7 +583,7 @@ export class Fetcher {
|
|
|
585
583
|
|
|
586
584
|
if (Object.keys(searchParams).length) {
|
|
587
585
|
const qs = new URLSearchParams(searchParams).toString()
|
|
588
|
-
req.
|
|
586
|
+
req.fullUrl += req.fullUrl.includes('?') ? '&' : '?' + qs
|
|
589
587
|
}
|
|
590
588
|
|
|
591
589
|
// setup request body
|