accessio 1.1.1 → 1.2.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/README.md +98 -1
- package/cjs/accessio.cjs +102 -10
- package/cjs/accessio.cjs.map +1 -1
- package/cjs/core/accessioError.cjs +1 -0
- package/cjs/core/accessioError.cjs.map +1 -1
- package/cjs/core/buildURL.cjs +16 -2
- package/cjs/core/buildURL.cjs.map +1 -1
- package/cjs/core/fetchAdapter.cjs +224 -0
- package/cjs/core/fetchAdapter.cjs.map +1 -0
- package/cjs/core/mergeConfig.cjs +2 -2
- package/cjs/core/mergeConfig.cjs.map +1 -1
- package/cjs/core/request.cjs +74 -199
- package/cjs/core/request.cjs.map +1 -1
- package/cjs/core/retry.cjs +23 -4
- package/cjs/core/retry.cjs.map +1 -1
- package/cjs/defaults/transforms.cjs.map +1 -1
- package/cjs/helpers/auth.cjs +45 -0
- package/cjs/helpers/auth.cjs.map +1 -0
- package/cjs/helpers/flattenHeaders.cjs +78 -0
- package/cjs/helpers/flattenHeaders.cjs.map +1 -0
- package/cjs/helpers/memoryCache.cjs +51 -0
- package/cjs/helpers/memoryCache.cjs.map +1 -0
- package/cjs/helpers/parseHeaders.cjs +16 -4
- package/cjs/helpers/parseHeaders.cjs.map +1 -1
- package/cjs/helpers/rateLimiter.cjs +18 -8
- package/cjs/helpers/rateLimiter.cjs.map +1 -1
- package/cjs/helpers/toFormData.cjs +50 -0
- package/cjs/helpers/toFormData.cjs.map +1 -0
- package/cjs/helpers/transformData.cjs +2 -2
- package/cjs/helpers/transformData.cjs.map +1 -1
- package/cjs/index.cjs +4 -1
- package/cjs/index.cjs.map +1 -1
- package/package.json +4 -3
- package/src/accessio.ts +126 -10
- package/src/core/accessioError.ts +1 -0
- package/src/core/buildURL.ts +17 -2
- package/src/core/fetchAdapter.ts +227 -0
- package/src/core/mergeConfig.ts +2 -2
- package/src/core/request.ts +100 -250
- package/src/core/retry.ts +26 -6
- package/src/defaults/transforms.ts +4 -1
- package/src/helpers/auth.ts +26 -0
- package/src/helpers/flattenHeaders.ts +59 -0
- package/src/helpers/memoryCache.ts +30 -0
- package/src/helpers/parseHeaders.ts +19 -6
- package/src/helpers/rateLimiter.ts +18 -8
- package/src/helpers/toFormData.ts +25 -0
- package/src/helpers/transformData.ts +4 -4
- package/src/index.ts +4 -1
- package/src/types.ts +32 -3
package/src/core/buildURL.ts
CHANGED
|
@@ -64,7 +64,7 @@ function combineURLs(baseURL: string, relativeURL: string): string {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function isAbsoluteURL(url: string): boolean {
|
|
67
|
-
return /^([a-z][a-z\d+\-.]*:)
|
|
67
|
+
return /^([a-z][a-z\d+\-.]*:)/i.test(url);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export default function buildURL(
|
|
@@ -75,7 +75,22 @@ export default function buildURL(
|
|
|
75
75
|
): string {
|
|
76
76
|
let fullURL = baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url || '';
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
let finalParams = params;
|
|
79
|
+
if (params && typeof params === 'object') {
|
|
80
|
+
const unusedParams = { ...params };
|
|
81
|
+
fullURL = fullURL.replace(/(?::([a-zA-Z0-9_]+))|(?:{([a-zA-Z0-9_]+)})/g, (match, p1, p2) => {
|
|
82
|
+
const key = p1 || p2;
|
|
83
|
+
if (key && unusedParams[key] !== undefined) {
|
|
84
|
+
const val = unusedParams[key];
|
|
85
|
+
delete unusedParams[key];
|
|
86
|
+
return encodeURIComponent(String(val));
|
|
87
|
+
}
|
|
88
|
+
return match;
|
|
89
|
+
});
|
|
90
|
+
finalParams = unusedParams;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const serialized = serializeParams(finalParams as Record<string, unknown>, paramsSerializer);
|
|
79
94
|
if (serialized) {
|
|
80
95
|
const hashIndex = fullURL.indexOf('#');
|
|
81
96
|
if (hashIndex !== -1) {
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import AccessioError from './accessioError';
|
|
2
|
+
import parseHeaders from '../helpers/parseHeaders';
|
|
3
|
+
import type { AccessioRequestConfig, AccessioResponse } from '../types';
|
|
4
|
+
|
|
5
|
+
async function readResponseData(
|
|
6
|
+
fetchResponse: Response,
|
|
7
|
+
config: AccessioRequestConfig,
|
|
8
|
+
): Promise<unknown> {
|
|
9
|
+
const responseType = config.responseType || 'json';
|
|
10
|
+
switch (responseType) {
|
|
11
|
+
case 'arraybuffer':
|
|
12
|
+
return await fetchResponse.arrayBuffer();
|
|
13
|
+
case 'blob':
|
|
14
|
+
return await fetchResponse.blob();
|
|
15
|
+
case 'stream':
|
|
16
|
+
return fetchResponse.body;
|
|
17
|
+
case 'json':
|
|
18
|
+
default: {
|
|
19
|
+
const contentType = fetchResponse.headers.get('content-type') || '';
|
|
20
|
+
if (contentType.includes('application/json')) {
|
|
21
|
+
if (typeof fetchResponse.clone === 'function') {
|
|
22
|
+
const text = await fetchResponse.clone().text();
|
|
23
|
+
return text ? await fetchResponse.json() : '';
|
|
24
|
+
} else {
|
|
25
|
+
const text = await fetchResponse.text();
|
|
26
|
+
return text ? JSON.parse(text) : '';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return await fetchResponse.text();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function fetchAdapter(
|
|
35
|
+
config: AccessioRequestConfig,
|
|
36
|
+
fullURL: string,
|
|
37
|
+
fetchOptions: RequestInit,
|
|
38
|
+
requestStartTime: number,
|
|
39
|
+
): Promise<AccessioResponse> {
|
|
40
|
+
let abortController: AbortController | null = null;
|
|
41
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
42
|
+
let isTimedOut = false;
|
|
43
|
+
let onUserAbort: (() => void) | null = null;
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
config.timeout !== undefined &&
|
|
47
|
+
(typeof config.timeout !== 'number' || isNaN(config.timeout) || config.timeout < 0)
|
|
48
|
+
) {
|
|
49
|
+
throw new AccessioError(
|
|
50
|
+
`Invalid timeout value: ${config.timeout}`,
|
|
51
|
+
AccessioError.ERR_BAD_OPTION_VALUE,
|
|
52
|
+
config,
|
|
53
|
+
null,
|
|
54
|
+
null,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timeoutValue = Number(config.timeout);
|
|
59
|
+
if (!isNaN(timeoutValue) && timeoutValue > 0) {
|
|
60
|
+
abortController = new AbortController();
|
|
61
|
+
|
|
62
|
+
timeoutId = setTimeout(() => {
|
|
63
|
+
isTimedOut = true;
|
|
64
|
+
abortController!.abort(
|
|
65
|
+
new AccessioError(
|
|
66
|
+
`timeout of ${timeoutValue}ms exceeded`,
|
|
67
|
+
AccessioError.ETIMEDOUT,
|
|
68
|
+
config,
|
|
69
|
+
null,
|
|
70
|
+
null,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
}, timeoutValue);
|
|
74
|
+
|
|
75
|
+
if (config.signal) {
|
|
76
|
+
if (typeof AbortSignal.any === 'function') {
|
|
77
|
+
fetchOptions.signal = AbortSignal.any([config.signal, abortController.signal]);
|
|
78
|
+
} else {
|
|
79
|
+
if (config.signal.aborted) {
|
|
80
|
+
abortController.abort(config.signal.reason);
|
|
81
|
+
} else {
|
|
82
|
+
onUserAbort = () => {
|
|
83
|
+
if (!isTimedOut && abortController) {
|
|
84
|
+
abortController.abort(config.signal!.reason);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
config.signal.addEventListener('abort', onUserAbort, {
|
|
88
|
+
once: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
fetchOptions.signal = abortController.signal;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
fetchOptions.signal = abortController.signal;
|
|
95
|
+
}
|
|
96
|
+
} else if (config.signal) {
|
|
97
|
+
fetchOptions.signal = config.signal;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const fetchImpl = config.fetch || fetch;
|
|
102
|
+
let fetchResponse = await fetchImpl(fullURL, fetchOptions);
|
|
103
|
+
|
|
104
|
+
if (config.onDownloadProgress && fetchResponse.body && config.responseType !== 'stream') {
|
|
105
|
+
const contentLength = fetchResponse.headers.get('content-length');
|
|
106
|
+
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
107
|
+
let loaded = 0;
|
|
108
|
+
|
|
109
|
+
const reader = fetchResponse.body.getReader();
|
|
110
|
+
const stream = new ReadableStream({
|
|
111
|
+
async start(controller) {
|
|
112
|
+
try {
|
|
113
|
+
while (true) {
|
|
114
|
+
const { done, value } = await reader.read();
|
|
115
|
+
if (done) {
|
|
116
|
+
controller.close();
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
loaded += value.byteLength;
|
|
120
|
+
config.onDownloadProgress!({ loaded, total });
|
|
121
|
+
controller.enqueue(value);
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
controller.error(e);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
fetchResponse = new Response(stream, {
|
|
130
|
+
headers: fetchResponse.headers,
|
|
131
|
+
status: fetchResponse.status,
|
|
132
|
+
statusText: fetchResponse.statusText,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let responseData: unknown;
|
|
137
|
+
|
|
138
|
+
const contentLength = fetchResponse.headers.get('content-length');
|
|
139
|
+
if (
|
|
140
|
+
contentLength &&
|
|
141
|
+
config.maxContentLength &&
|
|
142
|
+
parseInt(contentLength, 10) > config.maxContentLength
|
|
143
|
+
) {
|
|
144
|
+
throw new AccessioError(
|
|
145
|
+
`maxContentLength size of ${config.maxContentLength} exceeded`,
|
|
146
|
+
AccessioError.ERR_BAD_RESPONSE,
|
|
147
|
+
config,
|
|
148
|
+
fetchResponse,
|
|
149
|
+
null,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
responseData = await readResponseData(fetchResponse, config);
|
|
155
|
+
if (config.schema) {
|
|
156
|
+
if (typeof config.schema.parseAsync === 'function') {
|
|
157
|
+
responseData = await config.schema.parseAsync(responseData);
|
|
158
|
+
} else {
|
|
159
|
+
responseData = config.schema.parse(responseData);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch (readError) {
|
|
163
|
+
throw AccessioError.from(
|
|
164
|
+
readError as Error,
|
|
165
|
+
AccessioError.ERR_BAD_RESPONSE,
|
|
166
|
+
config,
|
|
167
|
+
fetchResponse,
|
|
168
|
+
null,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const responseHeaders = parseHeaders(fetchResponse.headers);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
data: responseData,
|
|
176
|
+
status: fetchResponse.status,
|
|
177
|
+
statusText: fetchResponse.statusText,
|
|
178
|
+
headers: responseHeaders,
|
|
179
|
+
config: config,
|
|
180
|
+
request: fetchResponse,
|
|
181
|
+
duration: Date.now() - requestStartTime,
|
|
182
|
+
};
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof AccessioError) {
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
189
|
+
if (isTimedOut) {
|
|
190
|
+
throw new AccessioError(
|
|
191
|
+
`timeout of ${config.timeout}ms exceeded`,
|
|
192
|
+
AccessioError.ETIMEDOUT,
|
|
193
|
+
config,
|
|
194
|
+
null,
|
|
195
|
+
null,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
throw new AccessioError('Request aborted', AccessioError.ERR_CANCELED, config, null, null);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
error instanceof TypeError &&
|
|
203
|
+
(error.message.toLowerCase().includes('url') || error.message.toLowerCase().includes('fetch'))
|
|
204
|
+
) {
|
|
205
|
+
throw new AccessioError(
|
|
206
|
+
`Invalid URL: ${fullURL}`,
|
|
207
|
+
AccessioError.ERR_INVALID_URL,
|
|
208
|
+
config,
|
|
209
|
+
null,
|
|
210
|
+
null,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw AccessioError.from(
|
|
215
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
216
|
+
AccessioError.ERR_NETWORK,
|
|
217
|
+
config,
|
|
218
|
+
null,
|
|
219
|
+
null,
|
|
220
|
+
);
|
|
221
|
+
} finally {
|
|
222
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
223
|
+
if (config.signal && onUserAbort) {
|
|
224
|
+
config.signal.removeEventListener('abort', onUserAbort);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
package/src/core/mergeConfig.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AccessioRequestConfig } from '../types';
|
|
2
2
|
|
|
3
3
|
function deepMerge(...sources: any[]): Record<string, any> {
|
|
4
|
-
const result: Record<string, any> =
|
|
4
|
+
const result: Record<string, any> = Object.create(null);
|
|
5
5
|
|
|
6
6
|
for (const source of sources) {
|
|
7
7
|
if (!source || typeof source !== 'object') continue;
|
|
@@ -43,7 +43,7 @@ export default function mergeConfig(
|
|
|
43
43
|
config1: AccessioRequestConfig = {},
|
|
44
44
|
config2: AccessioRequestConfig = {},
|
|
45
45
|
): AccessioRequestConfig {
|
|
46
|
-
const merged: any =
|
|
46
|
+
const merged: any = Object.create(null);
|
|
47
47
|
|
|
48
48
|
const allKeys = new Set<string>([...Object.keys(config1), ...Object.keys(config2)]);
|
|
49
49
|
|
package/src/core/request.ts
CHANGED
|
@@ -1,55 +1,14 @@
|
|
|
1
1
|
import buildURL from './buildURL';
|
|
2
2
|
import AccessioError from './accessioError';
|
|
3
|
-
import parseHeaders from '../helpers/parseHeaders';
|
|
4
3
|
import transformData from '../helpers/transformData';
|
|
5
4
|
import settle from '../helpers/settle';
|
|
5
|
+
import { flattenHeaders, removeContentType, buildFetchHeaders } from '../helpers/flattenHeaders';
|
|
6
|
+
import { setBasicAuth } from '../helpers/auth';
|
|
7
|
+
import fetchAdapter from './fetchAdapter';
|
|
8
|
+
import { defaultMemoryCache } from '../helpers/memoryCache';
|
|
6
9
|
import type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
'common',
|
|
10
|
-
'delete',
|
|
11
|
-
'get',
|
|
12
|
-
'head',
|
|
13
|
-
'options',
|
|
14
|
-
'post',
|
|
15
|
-
'put',
|
|
16
|
-
'patch',
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
type HeadersConfig = Record<string, Record<string, string>>;
|
|
20
|
-
|
|
21
|
-
function flattenHeaders(
|
|
22
|
-
headers: HeadersConfig | undefined,
|
|
23
|
-
method?: string,
|
|
24
|
-
): Record<string, string> {
|
|
25
|
-
if (!headers) return {};
|
|
26
|
-
|
|
27
|
-
const merged: Record<string, string> = {};
|
|
28
|
-
const methodLower = (method || 'get').toLowerCase();
|
|
29
|
-
|
|
30
|
-
if (headers['common']) {
|
|
31
|
-
Object.assign(merged, headers['common']);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (headers[methodLower]) {
|
|
35
|
-
Object.assign(merged, headers[methodLower]);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
for (const key in headers) {
|
|
39
|
-
if (Object.prototype.hasOwnProperty.call(headers, key) && !METHOD_KEYS.has(key)) {
|
|
40
|
-
merged[key] = headers[key] as unknown as string;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return merged;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function removeContentType(headers: Record<string, string>): void {
|
|
48
|
-
const keys = Object.keys(headers).filter((k) => k.toLowerCase() === 'content-type');
|
|
49
|
-
for (const key of keys) {
|
|
50
|
-
delete headers[key];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
11
|
+
type HeadersConfig = Record<string, Record<string, string | string[]>>;
|
|
53
12
|
|
|
54
13
|
function buildTransformArray(
|
|
55
14
|
transform: TransformFunction | TransformFunction[] | undefined,
|
|
@@ -59,42 +18,11 @@ function buildTransformArray(
|
|
|
59
18
|
return [transform];
|
|
60
19
|
}
|
|
61
20
|
|
|
62
|
-
|
|
63
|
-
if (!config.auth) return;
|
|
64
|
-
const username = config.auth.username || '';
|
|
65
|
-
const password = config.auth.password || '';
|
|
66
|
-
const credentials = `${username}:${password}`;
|
|
21
|
+
const activeRequests = new Map<string, Promise<AccessioResponse>>();
|
|
67
22
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} else {
|
|
72
|
-
encoded = btoa(
|
|
73
|
-
encodeURIComponent(credentials).replace(/%([0-9A-F]{2})/g, (match, p1) => {
|
|
74
|
-
return String.fromCharCode(parseInt(p1, 16));
|
|
75
|
-
}),
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
headers['Authorization'] = `Basic ${encoded}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function readResponseData(fetchResponse: Response, responseType: string): Promise<unknown> {
|
|
82
|
-
switch (responseType) {
|
|
83
|
-
case 'arraybuffer':
|
|
84
|
-
return await fetchResponse.arrayBuffer();
|
|
85
|
-
case 'blob':
|
|
86
|
-
return await fetchResponse.blob();
|
|
87
|
-
case 'text':
|
|
88
|
-
return await fetchResponse.text();
|
|
89
|
-
case 'stream':
|
|
90
|
-
return fetchResponse.body;
|
|
91
|
-
case 'json':
|
|
92
|
-
default:
|
|
93
|
-
return await fetchResponse.text();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export default function dispatchRequest(config: AccessioRequestConfig): Promise<AccessioResponse> {
|
|
23
|
+
export default async function dispatchRequest(
|
|
24
|
+
config: AccessioRequestConfig,
|
|
25
|
+
): Promise<AccessioResponse> {
|
|
98
26
|
const fullURL =
|
|
99
27
|
config._builtUrl ||
|
|
100
28
|
buildURL(
|
|
@@ -104,197 +32,119 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
|
|
|
104
32
|
config.paramsSerializer,
|
|
105
33
|
);
|
|
106
34
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const requestTransforms = buildTransformArray(config.transformRequest);
|
|
110
|
-
|
|
111
|
-
const requestData = transformData(requestTransforms, config.data, flatHeaders, config);
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
requestData === null ||
|
|
115
|
-
requestData === undefined ||
|
|
116
|
-
(typeof FormData !== 'undefined' && requestData instanceof FormData)
|
|
117
|
-
) {
|
|
118
|
-
removeContentType(flatHeaders);
|
|
35
|
+
if (config.hooks?.onBeforeRequest) {
|
|
36
|
+
await config.hooks.onBeforeRequest(config);
|
|
119
37
|
}
|
|
120
38
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const fetchOptions: RequestInit = {
|
|
124
|
-
method: (config.method || 'GET').toUpperCase(),
|
|
125
|
-
headers: flatHeaders,
|
|
126
|
-
};
|
|
39
|
+
const isGet = (config.method || 'GET').toUpperCase() === 'GET';
|
|
40
|
+
const cacheKey = isGet ? `GET:${fullURL}` : '';
|
|
127
41
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
42
|
+
if (isGet && config.cache) {
|
|
43
|
+
const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;
|
|
44
|
+
const cached = await cacheProvider.get(cacheKey);
|
|
45
|
+
if (cached) {
|
|
46
|
+
if (config.hooks?.onRequestResponse) {
|
|
47
|
+
await config.hooks.onRequestResponse(cached);
|
|
48
|
+
}
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
135
51
|
}
|
|
136
52
|
|
|
137
|
-
if (config.
|
|
138
|
-
|
|
53
|
+
if (isGet && config.dedupe) {
|
|
54
|
+
if (activeRequests.has(cacheKey)) {
|
|
55
|
+
return activeRequests.get(cacheKey)!;
|
|
56
|
+
}
|
|
139
57
|
}
|
|
140
58
|
|
|
141
|
-
|
|
142
|
-
(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
59
|
+
const performRequest = async () => {
|
|
60
|
+
const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);
|
|
61
|
+
const requestTransforms = buildTransformArray(config.transformRequest);
|
|
62
|
+
const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
requestData === null ||
|
|
66
|
+
requestData === undefined ||
|
|
67
|
+
(typeof FormData !== 'undefined' && requestData instanceof FormData)
|
|
68
|
+
) {
|
|
69
|
+
removeContentType(flatHeaders);
|
|
70
|
+
}
|
|
147
71
|
|
|
148
|
-
|
|
149
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
150
|
-
let isTimedOut = false;
|
|
151
|
-
let onUserAbort: (() => void) | null = null;
|
|
72
|
+
setBasicAuth(config, flatHeaders);
|
|
152
73
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
74
|
+
const fetchOptions: RequestInit = {
|
|
75
|
+
method: (config.method || 'GET').toUpperCase(),
|
|
76
|
+
headers: buildFetchHeaders(flatHeaders),
|
|
77
|
+
};
|
|
156
78
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
null,
|
|
166
|
-
),
|
|
167
|
-
);
|
|
168
|
-
}, timeoutValue);
|
|
79
|
+
const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
80
|
+
if (
|
|
81
|
+
methodsWithBody.includes(fetchOptions.method!) &&
|
|
82
|
+
requestData !== undefined &&
|
|
83
|
+
requestData !== null
|
|
84
|
+
) {
|
|
85
|
+
fetchOptions.body = requestData as BodyInit;
|
|
86
|
+
}
|
|
169
87
|
|
|
170
|
-
if (config.
|
|
171
|
-
|
|
172
|
-
fetchOptions.signal = AbortSignal.any([config.signal, abortController.signal]);
|
|
173
|
-
} else {
|
|
174
|
-
if (config.signal.aborted) {
|
|
175
|
-
abortController.abort(config.signal.reason);
|
|
176
|
-
} else {
|
|
177
|
-
onUserAbort = () => {
|
|
178
|
-
if (!isTimedOut && abortController) {
|
|
179
|
-
abortController.abort(config.signal!.reason);
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
config.signal.addEventListener('abort', onUserAbort, {
|
|
183
|
-
once: true,
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
fetchOptions.signal = abortController.signal;
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
fetchOptions.signal = abortController.signal;
|
|
88
|
+
if (config.withCredentials) {
|
|
89
|
+
fetchOptions.credentials = 'include';
|
|
190
90
|
}
|
|
191
|
-
} else if (config.signal) {
|
|
192
|
-
fetchOptions.signal = config.signal;
|
|
193
|
-
}
|
|
194
91
|
|
|
195
|
-
|
|
92
|
+
if (config.dispatcher) {
|
|
93
|
+
(fetchOptions as any).dispatcher = config.dispatcher;
|
|
94
|
+
}
|
|
95
|
+
if (config.agent) {
|
|
96
|
+
(fetchOptions as any).agent = config.agent;
|
|
97
|
+
}
|
|
196
98
|
|
|
197
|
-
|
|
198
|
-
.then(async (fetchResponse) => {
|
|
199
|
-
let responseData: unknown;
|
|
200
|
-
const responseType = config.responseType || 'json';
|
|
99
|
+
const requestStartTime = Date.now();
|
|
201
100
|
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
contentLength &&
|
|
205
|
-
config.maxContentLength &&
|
|
206
|
-
parseInt(contentLength, 10) > config.maxContentLength
|
|
207
|
-
) {
|
|
208
|
-
throw new AccessioError(
|
|
209
|
-
`maxContentLength size of ${config.maxContentLength} exceeded`,
|
|
210
|
-
AccessioError.ERR_BAD_RESPONSE,
|
|
211
|
-
config,
|
|
212
|
-
fetchResponse,
|
|
213
|
-
null,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
101
|
+
const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
|
|
216
102
|
|
|
217
|
-
|
|
218
|
-
responseData = await readResponseData(fetchResponse, responseType);
|
|
219
|
-
} catch (readError) {
|
|
220
|
-
throw AccessioError.from(
|
|
221
|
-
readError as Error,
|
|
222
|
-
AccessioError.ERR_BAD_RESPONSE,
|
|
223
|
-
config,
|
|
224
|
-
fetchResponse,
|
|
225
|
-
null,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
103
|
+
const responseTransforms = buildTransformArray(config.transformResponse);
|
|
228
104
|
|
|
229
|
-
|
|
105
|
+
response.data = await transformData(
|
|
106
|
+
responseTransforms,
|
|
107
|
+
response.data,
|
|
108
|
+
response.headers,
|
|
109
|
+
config,
|
|
110
|
+
);
|
|
230
111
|
|
|
231
|
-
|
|
112
|
+
return new Promise<AccessioResponse>((resolve, reject) => {
|
|
113
|
+
settle(
|
|
114
|
+
resolve as (value: AccessioResponse) => void,
|
|
115
|
+
reject as (reason: AccessioError) => void,
|
|
116
|
+
response,
|
|
117
|
+
config,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
};
|
|
232
121
|
|
|
233
|
-
|
|
122
|
+
const promise = performRequest();
|
|
234
123
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
request: fetchResponse,
|
|
242
|
-
duration: Date.now() - requestStartTime,
|
|
243
|
-
};
|
|
124
|
+
if (isGet && config.dedupe) {
|
|
125
|
+
activeRequests.set(cacheKey, promise);
|
|
126
|
+
promise.finally(() => {
|
|
127
|
+
activeRequests.delete(cacheKey);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
244
130
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
resolve as (value: AccessioResponse) => void,
|
|
248
|
-
reject as (reason: AccessioError) => void,
|
|
249
|
-
response,
|
|
250
|
-
config,
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
})
|
|
254
|
-
.catch((error) => {
|
|
255
|
-
if (error instanceof AccessioError) {
|
|
256
|
-
throw error;
|
|
257
|
-
}
|
|
131
|
+
try {
|
|
132
|
+
const response = await promise;
|
|
258
133
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
AccessioError.ETIMEDOUT,
|
|
264
|
-
config,
|
|
265
|
-
null,
|
|
266
|
-
null,
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
throw new AccessioError('Request aborted', AccessioError.ERR_CANCELED, config, null, null);
|
|
270
|
-
}
|
|
134
|
+
if (isGet && config.cache) {
|
|
135
|
+
const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;
|
|
136
|
+
await cacheProvider.set(cacheKey, response, config.cacheTTL);
|
|
137
|
+
}
|
|
271
138
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
error.message.toLowerCase().includes('fetch'))
|
|
276
|
-
) {
|
|
277
|
-
throw new AccessioError(
|
|
278
|
-
`Invalid URL: ${fullURL}`,
|
|
279
|
-
AccessioError.ERR_INVALID_URL,
|
|
280
|
-
config,
|
|
281
|
-
null,
|
|
282
|
-
null,
|
|
283
|
-
);
|
|
284
|
-
}
|
|
139
|
+
if (config.hooks?.onRequestResponse) {
|
|
140
|
+
await config.hooks.onRequestResponse(response);
|
|
141
|
+
}
|
|
285
142
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
})
|
|
294
|
-
.finally(() => {
|
|
295
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
296
|
-
if (config.signal && onUserAbort) {
|
|
297
|
-
config.signal.removeEventListener('abort', onUserAbort);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
143
|
+
return response;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (config.hooks?.onRequestError && error instanceof AccessioError) {
|
|
146
|
+
await config.hooks.onRequestError(error);
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
300
150
|
}
|