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.
Files changed (50) hide show
  1. package/README.md +98 -1
  2. package/cjs/accessio.cjs +102 -10
  3. package/cjs/accessio.cjs.map +1 -1
  4. package/cjs/core/accessioError.cjs +1 -0
  5. package/cjs/core/accessioError.cjs.map +1 -1
  6. package/cjs/core/buildURL.cjs +16 -2
  7. package/cjs/core/buildURL.cjs.map +1 -1
  8. package/cjs/core/fetchAdapter.cjs +224 -0
  9. package/cjs/core/fetchAdapter.cjs.map +1 -0
  10. package/cjs/core/mergeConfig.cjs +2 -2
  11. package/cjs/core/mergeConfig.cjs.map +1 -1
  12. package/cjs/core/request.cjs +74 -199
  13. package/cjs/core/request.cjs.map +1 -1
  14. package/cjs/core/retry.cjs +23 -4
  15. package/cjs/core/retry.cjs.map +1 -1
  16. package/cjs/defaults/transforms.cjs.map +1 -1
  17. package/cjs/helpers/auth.cjs +45 -0
  18. package/cjs/helpers/auth.cjs.map +1 -0
  19. package/cjs/helpers/flattenHeaders.cjs +78 -0
  20. package/cjs/helpers/flattenHeaders.cjs.map +1 -0
  21. package/cjs/helpers/memoryCache.cjs +51 -0
  22. package/cjs/helpers/memoryCache.cjs.map +1 -0
  23. package/cjs/helpers/parseHeaders.cjs +16 -4
  24. package/cjs/helpers/parseHeaders.cjs.map +1 -1
  25. package/cjs/helpers/rateLimiter.cjs +18 -8
  26. package/cjs/helpers/rateLimiter.cjs.map +1 -1
  27. package/cjs/helpers/toFormData.cjs +50 -0
  28. package/cjs/helpers/toFormData.cjs.map +1 -0
  29. package/cjs/helpers/transformData.cjs +2 -2
  30. package/cjs/helpers/transformData.cjs.map +1 -1
  31. package/cjs/index.cjs +4 -1
  32. package/cjs/index.cjs.map +1 -1
  33. package/package.json +4 -3
  34. package/src/accessio.ts +126 -10
  35. package/src/core/accessioError.ts +1 -0
  36. package/src/core/buildURL.ts +17 -2
  37. package/src/core/fetchAdapter.ts +227 -0
  38. package/src/core/mergeConfig.ts +2 -2
  39. package/src/core/request.ts +100 -250
  40. package/src/core/retry.ts +26 -6
  41. package/src/defaults/transforms.ts +4 -1
  42. package/src/helpers/auth.ts +26 -0
  43. package/src/helpers/flattenHeaders.ts +59 -0
  44. package/src/helpers/memoryCache.ts +30 -0
  45. package/src/helpers/parseHeaders.ts +19 -6
  46. package/src/helpers/rateLimiter.ts +18 -8
  47. package/src/helpers/toFormData.ts +25 -0
  48. package/src/helpers/transformData.ts +4 -4
  49. package/src/index.ts +4 -1
  50. package/src/types.ts +32 -3
@@ -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+\-.]*:)?\/\//i.test(url) || /^([a-z][a-z\d+\-.]*:)/i.test(url);
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
- const serialized = serializeParams(params as Record<string, unknown>, paramsSerializer);
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
+ }
@@ -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
 
@@ -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
- const METHOD_KEYS = new Set<string>([
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
- function setBasicAuth(config: AccessioRequestConfig, headers: Record<string, string>): void {
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
- let encoded: string;
69
- if (typeof Buffer !== 'undefined') {
70
- encoded = Buffer.from(credentials).toString('base64');
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
- const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);
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
- setBasicAuth(config, flatHeaders);
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
- const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
129
- if (
130
- methodsWithBody.includes(fetchOptions.method!) &&
131
- requestData !== undefined &&
132
- requestData !== null
133
- ) {
134
- fetchOptions.body = requestData as BodyInit;
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.withCredentials) {
138
- fetchOptions.credentials = 'include';
53
+ if (isGet && config.dedupe) {
54
+ if (activeRequests.has(cacheKey)) {
55
+ return activeRequests.get(cacheKey)!;
56
+ }
139
57
  }
140
58
 
141
- if (config.dispatcher) {
142
- (fetchOptions as any).dispatcher = config.dispatcher;
143
- }
144
- if (config.agent) {
145
- (fetchOptions as any).agent = config.agent;
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
- let abortController: AbortController | null = null;
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
- const timeoutValue = Number(config.timeout);
154
- if (!isNaN(timeoutValue) && timeoutValue > 0) {
155
- abortController = new AbortController();
74
+ const fetchOptions: RequestInit = {
75
+ method: (config.method || 'GET').toUpperCase(),
76
+ headers: buildFetchHeaders(flatHeaders),
77
+ };
156
78
 
157
- timeoutId = setTimeout(() => {
158
- isTimedOut = true;
159
- abortController!.abort(
160
- new AccessioError(
161
- `timeout of ${timeoutValue}ms exceeded`,
162
- AccessioError.ETIMEDOUT,
163
- config,
164
- null,
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.signal) {
171
- if (typeof AbortSignal.any === 'function') {
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
- const requestStartTime = Date.now();
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
- return fetch(fullURL, fetchOptions)
198
- .then(async (fetchResponse) => {
199
- let responseData: unknown;
200
- const responseType = config.responseType || 'json';
99
+ const requestStartTime = Date.now();
201
100
 
202
- const contentLength = fetchResponse.headers.get('content-length');
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
- try {
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
- const responseHeaders = parseHeaders(fetchResponse.headers);
105
+ response.data = await transformData(
106
+ responseTransforms,
107
+ response.data,
108
+ response.headers,
109
+ config,
110
+ );
230
111
 
231
- const responseTransforms = buildTransformArray(config.transformResponse);
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
- responseData = transformData(responseTransforms, responseData, responseHeaders, config);
122
+ const promise = performRequest();
234
123
 
235
- const response: AccessioResponse = {
236
- data: responseData,
237
- status: fetchResponse.status,
238
- statusText: fetchResponse.statusText,
239
- headers: responseHeaders,
240
- config: config,
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
- return new Promise<AccessioResponse>((resolve, reject) => {
246
- settle(
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
- if (error instanceof Error && error.name === 'AbortError') {
260
- if (isTimedOut) {
261
- throw new AccessioError(
262
- `timeout of ${config.timeout}ms exceeded`,
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
- if (
273
- error instanceof TypeError &&
274
- (error.message.toLowerCase().includes('url') ||
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
- throw AccessioError.from(
287
- error instanceof Error ? error : new Error(String(error)),
288
- AccessioError.ERR_NETWORK,
289
- config,
290
- null,
291
- null,
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
  }