accessio 1.1.0 → 1.1.2

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 (39) hide show
  1. package/README.md +4 -2
  2. package/cjs/accessio.cjs +43 -7
  3. package/cjs/accessio.cjs.map +1 -1
  4. package/cjs/core/buildURL.cjs +2 -2
  5. package/cjs/core/buildURL.cjs.map +1 -1
  6. package/cjs/core/fetchAdapter.cjs +187 -0
  7. package/cjs/core/fetchAdapter.cjs.map +1 -0
  8. package/cjs/core/mergeConfig.cjs +2 -2
  9. package/cjs/core/mergeConfig.cjs.map +1 -1
  10. package/cjs/core/request.cjs +23 -171
  11. package/cjs/core/request.cjs.map +1 -1
  12. package/cjs/core/retry.cjs +0 -3
  13. package/cjs/core/retry.cjs.map +1 -1
  14. package/cjs/defaults/transforms.cjs +8 -1
  15. package/cjs/defaults/transforms.cjs.map +1 -1
  16. package/cjs/helpers/auth.cjs +45 -0
  17. package/cjs/helpers/auth.cjs.map +1 -0
  18. package/cjs/helpers/flattenHeaders.cjs +78 -0
  19. package/cjs/helpers/flattenHeaders.cjs.map +1 -0
  20. package/cjs/helpers/parseHeaders.cjs +16 -4
  21. package/cjs/helpers/parseHeaders.cjs.map +1 -1
  22. package/cjs/helpers/rateLimiter.cjs +20 -12
  23. package/cjs/helpers/rateLimiter.cjs.map +1 -1
  24. package/cjs/helpers/transformData.cjs +2 -2
  25. package/cjs/helpers/transformData.cjs.map +1 -1
  26. package/package.json +3 -3
  27. package/src/accessio.ts +46 -8
  28. package/src/core/buildURL.ts +2 -2
  29. package/src/core/fetchAdapter.ts +184 -0
  30. package/src/core/mergeConfig.ts +2 -2
  31. package/src/core/request.ts +28 -193
  32. package/src/core/retry.ts +0 -4
  33. package/src/defaults/transforms.ts +12 -2
  34. package/src/helpers/auth.ts +26 -0
  35. package/src/helpers/flattenHeaders.ts +59 -0
  36. package/src/helpers/parseHeaders.ts +19 -6
  37. package/src/helpers/rateLimiter.ts +20 -12
  38. package/src/helpers/transformData.ts +4 -4
  39. package/src/types.ts +9 -5
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/helpers/transformData.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport type { TransformFunction, AccessioRequestConfig } from '../types';\n\nexport default function transformData(\n transforms: TransformFunction | TransformFunction[] | undefined,\n data: unknown,\n headers: Record<string, string>,\n config?: AccessioRequestConfig,\n): unknown {\n if (!transforms || !Array.isArray(transforms)) {\n return data;\n }\n\n let result = data;\n\n for (const transform of transforms) {\n if (typeof transform === 'function') {\n try {\n result = transform(result, headers);\n } catch (err) {\n throw AccessioError.from(\n err instanceof Error ? err : new Error(String(err)),\n AccessioError.ERR_BAD_REQUEST,\n config ?? null,\n null,\n null,\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAGX,SAAR,cACL,YACA,MACA,SACA,QACS;AACT,MAAI,CAAC,cAAc,CAAC,MAAM,QAAQ,UAAU,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAS;AAEb,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY;AACnC,UAAI;AACF,iBAAS,UAAU,QAAQ,OAAO;AAAA,MACpC,SAAS,KAAK;AACZ,cAAM,qBAAAA,QAAc;AAAA,UAClB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,UAClD,qBAAAA,QAAc;AAAA,UACd,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["AccessioError"]}
1
+ {"version":3,"sources":["../../src/helpers/transformData.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport type { TransformFunction, AccessioRequestConfig } from '../types';\n\nexport default async function transformData(\n transforms: TransformFunction | TransformFunction[] | undefined,\n data: unknown,\n headers: Record<string, string | string[]>,\n config?: AccessioRequestConfig,\n): Promise<unknown> {\n if (!transforms || !Array.isArray(transforms)) {\n return data;\n }\n\n let result = data;\n\n for (const transform of transforms) {\n if (typeof transform === 'function') {\n try {\n result = await transform(result, headers);\n } catch (err) {\n throw AccessioError.from(\n err instanceof Error ? err : new Error(String(err)),\n AccessioError.ERR_BAD_REQUEST,\n config ?? null,\n null,\n null,\n );\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAG1B,eAAO,cACL,YACA,MACA,SACA,QACkB;AAClB,MAAI,CAAC,cAAc,CAAC,MAAM,QAAQ,UAAU,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,MAAI,SAAS;AAEb,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY;AACnC,UAAI;AACF,iBAAS,MAAM,UAAU,QAAQ,OAAO;AAAA,MAC1C,SAAS,KAAK;AACZ,cAAM,qBAAAA,QAAc;AAAA,UAClB,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,UAClD,qBAAAA,QAAc;AAAA,UACd,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["AccessioError"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "accessio",
3
- "version": "1.1.0",
4
- "description": "Fast, flexible HTTP client for Node.js and browsers — simple, modular, and dependency-free",
3
+ "version": "1.1.2",
4
+ "description": "Fast, flexible HTTP client — simple, modular, and dependency-free",
5
5
  "type": "module",
6
6
  "main": "./cjs/index.cjs",
7
7
  "module": "./src/index.ts",
@@ -77,7 +77,7 @@
77
77
  "test": "vitest run",
78
78
  "test:watch": "vitest",
79
79
  "test:coverage": "vitest run --coverage",
80
- "test:browser": "vitest run --config vitest.browser.config.js",
80
+ "test:browser": "vitest run --config vitest.browser.config.ts",
81
81
  "release:npm": "gh workflow run publish-npm.yml -f publish_tag=$(git describe --tags --abbrev=0)",
82
82
  "typecheck": "tsc --noEmit"
83
83
  },
package/src/accessio.ts CHANGED
@@ -52,11 +52,13 @@ export class Accessio {
52
52
 
53
53
  const requestInterceptors: any[] = [];
54
54
  const responseInterceptors: any[] = [];
55
+ let synchronousRequestInterceptors = true;
55
56
 
56
57
  this.interceptors.request.forEach((interceptor: InterceptorHandler) => {
57
58
  if (interceptor.runWhen && !interceptor.runWhen(mergedConfig)) {
58
59
  return;
59
60
  }
61
+ synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
60
62
  requestInterceptors.unshift(interceptor);
61
63
  });
62
64
 
@@ -64,15 +66,51 @@ export class Accessio {
64
66
  responseInterceptors.push(interceptor);
65
67
  });
66
68
 
67
- let promise: Promise<any> = Promise.resolve(mergedConfig);
68
-
69
- for (const interceptor of requestInterceptors) {
70
- promise = promise.then((value: any) => {
71
- if (interceptor.fulfilled) {
72
- return interceptor.fulfilled(value);
69
+ let promise: Promise<any>;
70
+
71
+ if (synchronousRequestInterceptors) {
72
+ let newConfig = mergedConfig;
73
+ let rejectReason: any = null;
74
+ let isRejected = false;
75
+
76
+ for (const interceptor of requestInterceptors) {
77
+ if (!isRejected) {
78
+ try {
79
+ if (interceptor.fulfilled) {
80
+ newConfig = interceptor.fulfilled(newConfig);
81
+ }
82
+ } catch (err) {
83
+ rejectReason = err;
84
+ isRejected = true;
85
+ }
86
+ } else {
87
+ if (interceptor.rejected) {
88
+ try {
89
+ newConfig = interceptor.rejected(rejectReason);
90
+ isRejected = false;
91
+ } catch (err) {
92
+ rejectReason = err;
93
+ isRejected = true;
94
+ }
95
+ }
73
96
  }
74
- return value;
75
- }, interceptor.rejected);
97
+ }
98
+
99
+ if (isRejected) {
100
+ promise = Promise.reject(rejectReason);
101
+ } else {
102
+ promise = Promise.resolve(newConfig);
103
+ }
104
+ } else {
105
+ promise = Promise.resolve(mergedConfig);
106
+ for (const interceptor of requestInterceptors) {
107
+ promise = promise.then((value: any) => {
108
+ if (interceptor.fulfilled) {
109
+ return interceptor.fulfilled(value);
110
+ }
111
+ return value;
112
+ }, interceptor.rejected);
113
+ }
76
114
  }
77
115
 
78
116
  promise = promise.then((cfg: any) => {
@@ -26,7 +26,7 @@ function serializeParams(
26
26
  if (typeof item === 'object' && item !== null) {
27
27
  encode(`${prefix}[${index}]`, item);
28
28
  } else {
29
- encode(`${prefix}[]`, item);
29
+ encode(prefix, item);
30
30
  }
31
31
  });
32
32
  } else if (typeof value === 'object' && !(value instanceof Date)) {
@@ -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);
67
+ return /^([a-z][a-z\d+\-.]*:)/i.test(url);
68
68
  }
69
69
 
70
70
  export default function buildURL(
@@ -0,0 +1,184 @@
1
+ import AccessioError from './accessioError';
2
+ import parseHeaders from '../helpers/parseHeaders';
3
+ import type { AccessioRequestConfig, AccessioResponse } from '../types';
4
+
5
+ async function readResponseData(fetchResponse: Response, responseType: string): Promise<unknown> {
6
+ switch (responseType) {
7
+ case 'arraybuffer':
8
+ return await fetchResponse.arrayBuffer();
9
+ case 'blob':
10
+ return await fetchResponse.blob();
11
+ case 'stream':
12
+ return fetchResponse.body;
13
+ case 'json':
14
+ default: {
15
+ const contentType = fetchResponse.headers.get('content-type') || '';
16
+ if (contentType.includes('application/json')) {
17
+ if (typeof fetchResponse.clone === 'function') {
18
+ const text = await fetchResponse.clone().text();
19
+ return text ? await fetchResponse.json() : '';
20
+ } else {
21
+ const text = await fetchResponse.text();
22
+ return text ? JSON.parse(text) : '';
23
+ }
24
+ }
25
+ return await fetchResponse.text();
26
+ }
27
+ }
28
+ }
29
+
30
+ export default async function fetchAdapter(
31
+ config: AccessioRequestConfig,
32
+ fullURL: string,
33
+ fetchOptions: RequestInit,
34
+ requestStartTime: number,
35
+ ): Promise<AccessioResponse> {
36
+ let abortController: AbortController | null = null;
37
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
38
+ let isTimedOut = false;
39
+ let onUserAbort: (() => void) | null = null;
40
+
41
+ if (
42
+ config.timeout !== undefined &&
43
+ (typeof config.timeout !== 'number' || isNaN(config.timeout) || config.timeout < 0)
44
+ ) {
45
+ throw new AccessioError(
46
+ `Invalid timeout value: ${config.timeout}`,
47
+ AccessioError.ERR_BAD_OPTION_VALUE,
48
+ config,
49
+ null,
50
+ null,
51
+ );
52
+ }
53
+
54
+ const timeoutValue = Number(config.timeout);
55
+ if (!isNaN(timeoutValue) && timeoutValue > 0) {
56
+ abortController = new AbortController();
57
+
58
+ timeoutId = setTimeout(() => {
59
+ isTimedOut = true;
60
+ abortController!.abort(
61
+ new AccessioError(
62
+ `timeout of ${timeoutValue}ms exceeded`,
63
+ AccessioError.ETIMEDOUT,
64
+ config,
65
+ null,
66
+ null,
67
+ ),
68
+ );
69
+ }, timeoutValue);
70
+
71
+ if (config.signal) {
72
+ if (typeof AbortSignal.any === 'function') {
73
+ fetchOptions.signal = AbortSignal.any([config.signal, abortController.signal]);
74
+ } else {
75
+ if (config.signal.aborted) {
76
+ abortController.abort(config.signal.reason);
77
+ } else {
78
+ onUserAbort = () => {
79
+ if (!isTimedOut && abortController) {
80
+ abortController.abort(config.signal!.reason);
81
+ }
82
+ };
83
+ config.signal.addEventListener('abort', onUserAbort, {
84
+ once: true,
85
+ });
86
+ }
87
+ fetchOptions.signal = abortController.signal;
88
+ }
89
+ } else {
90
+ fetchOptions.signal = abortController.signal;
91
+ }
92
+ } else if (config.signal) {
93
+ fetchOptions.signal = config.signal;
94
+ }
95
+
96
+ try {
97
+ const fetchResponse = await fetch(fullURL, fetchOptions);
98
+
99
+ let responseData: unknown;
100
+ const responseType = config.responseType || 'json';
101
+
102
+ const contentLength = fetchResponse.headers.get('content-length');
103
+ if (
104
+ contentLength &&
105
+ config.maxContentLength &&
106
+ parseInt(contentLength, 10) > config.maxContentLength
107
+ ) {
108
+ throw new AccessioError(
109
+ `maxContentLength size of ${config.maxContentLength} exceeded`,
110
+ AccessioError.ERR_BAD_RESPONSE,
111
+ config,
112
+ fetchResponse,
113
+ null,
114
+ );
115
+ }
116
+
117
+ try {
118
+ responseData = await readResponseData(fetchResponse, responseType);
119
+ } catch (readError) {
120
+ throw AccessioError.from(
121
+ readError as Error,
122
+ AccessioError.ERR_BAD_RESPONSE,
123
+ config,
124
+ fetchResponse,
125
+ null,
126
+ );
127
+ }
128
+
129
+ const responseHeaders = parseHeaders(fetchResponse.headers);
130
+
131
+ return {
132
+ data: responseData,
133
+ status: fetchResponse.status,
134
+ statusText: fetchResponse.statusText,
135
+ headers: responseHeaders,
136
+ config: config,
137
+ request: fetchResponse,
138
+ duration: Date.now() - requestStartTime,
139
+ };
140
+ } catch (error) {
141
+ if (error instanceof AccessioError) {
142
+ throw error;
143
+ }
144
+
145
+ if (error instanceof Error && error.name === 'AbortError') {
146
+ if (isTimedOut) {
147
+ throw new AccessioError(
148
+ `timeout of ${config.timeout}ms exceeded`,
149
+ AccessioError.ETIMEDOUT,
150
+ config,
151
+ null,
152
+ null,
153
+ );
154
+ }
155
+ throw new AccessioError('Request aborted', AccessioError.ERR_CANCELED, config, null, null);
156
+ }
157
+
158
+ if (
159
+ error instanceof TypeError &&
160
+ (error.message.toLowerCase().includes('url') || error.message.toLowerCase().includes('fetch'))
161
+ ) {
162
+ throw new AccessioError(
163
+ `Invalid URL: ${fullURL}`,
164
+ AccessioError.ERR_INVALID_URL,
165
+ config,
166
+ null,
167
+ null,
168
+ );
169
+ }
170
+
171
+ throw AccessioError.from(
172
+ error instanceof Error ? error : new Error(String(error)),
173
+ AccessioError.ERR_NETWORK,
174
+ config,
175
+ null,
176
+ null,
177
+ );
178
+ } finally {
179
+ if (timeoutId) clearTimeout(timeoutId);
180
+ if (config.signal && onUserAbort) {
181
+ config.signal.removeEventListener('abort', onUserAbort);
182
+ }
183
+ }
184
+ }
@@ -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,13 @@
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';
6
8
  import type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';
7
9
 
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 key = Object.keys(headers).find((k) => k.toLowerCase() === 'content-type');
49
- if (key) {
50
- delete headers[key];
51
- }
52
- }
10
+ type HeadersConfig = Record<string, Record<string, string | string[]>>;
53
11
 
54
12
  function buildTransformArray(
55
13
  transform: TransformFunction | TransformFunction[] | undefined,
@@ -59,35 +17,9 @@ function buildTransformArray(
59
17
  return [transform];
60
18
  }
61
19
 
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}`;
67
-
68
- let encoded: string;
69
- if (typeof Buffer !== 'undefined') {
70
- encoded = Buffer.from(credentials).toString('base64');
71
- } else {
72
- const bytes = new TextEncoder().encode(credentials);
73
- const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join('');
74
- encoded = btoa(binString);
75
- }
76
- headers['Authorization'] = `Basic ${encoded}`;
77
- }
78
-
79
- async function readResponseData(fetchResponse: Response, responseType: string): Promise<unknown> {
80
- switch (responseType) {
81
- case 'arraybuffer': return await fetchResponse.arrayBuffer();
82
- case 'blob': return await fetchResponse.blob();
83
- case 'text': return await fetchResponse.text();
84
- case 'stream': return fetchResponse.body;
85
- case 'json':
86
- default: return await fetchResponse.text();
87
- }
88
- }
89
-
90
- export default function dispatchRequest(config: AccessioRequestConfig): Promise<AccessioResponse> {
20
+ export default async function dispatchRequest(
21
+ config: AccessioRequestConfig,
22
+ ): Promise<AccessioResponse> {
91
23
  const fullURL =
92
24
  config._builtUrl ||
93
25
  buildURL(
@@ -101,7 +33,7 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
101
33
 
102
34
  const requestTransforms = buildTransformArray(config.transformRequest);
103
35
 
104
- const requestData = transformData(requestTransforms, config.data, flatHeaders, config);
36
+ const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);
105
37
 
106
38
  if (
107
39
  requestData === null ||
@@ -115,7 +47,7 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
115
47
 
116
48
  const fetchOptions: RequestInit = {
117
49
  method: (config.method || 'GET').toUpperCase(),
118
- headers: flatHeaders,
50
+ headers: buildFetchHeaders(flatHeaders),
119
51
  };
120
52
 
121
53
  const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
@@ -131,124 +63,27 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
131
63
  fetchOptions.credentials = 'include';
132
64
  }
133
65
 
134
- let abortController: AbortController | null = null;
135
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
136
- let isTimedOut = false;
137
- let onUserAbort: (() => void) | null = null;
138
-
139
- if (config.timeout && config.timeout > 0) {
140
- abortController = new AbortController();
141
-
142
- timeoutId = setTimeout(() => {
143
- isTimedOut = true;
144
- abortController!.abort(
145
- new AccessioError(
146
- `timeout of ${config.timeout}ms exceeded`,
147
- AccessioError.ETIMEDOUT,
148
- config,
149
- null,
150
- null,
151
- ),
152
- );
153
- }, config.timeout);
154
-
155
- if (config.signal) {
156
- if (typeof AbortSignal.any === 'function') {
157
- fetchOptions.signal = AbortSignal.any([config.signal, abortController.signal]);
158
- } else {
159
- if (config.signal.aborted) {
160
- abortController.abort(config.signal.reason);
161
- } else {
162
- onUserAbort = () => {
163
- abortController!.abort(config.signal!.reason);
164
- };
165
- config.signal.addEventListener('abort', onUserAbort, {
166
- once: true,
167
- });
168
- }
169
- fetchOptions.signal = abortController.signal;
170
- }
171
- } else {
172
- fetchOptions.signal = abortController.signal;
173
- }
174
- } else if (config.signal) {
175
- fetchOptions.signal = config.signal;
66
+ if (config.dispatcher) {
67
+ (fetchOptions as any).dispatcher = config.dispatcher;
68
+ }
69
+ if (config.agent) {
70
+ (fetchOptions as any).agent = config.agent;
176
71
  }
177
72
 
178
73
  const requestStartTime = Date.now();
179
74
 
180
- return fetch(fullURL, fetchOptions)
181
- .then(async (fetchResponse) => {
182
- let responseData: unknown;
183
- const responseType = config.responseType || 'json';
184
-
185
- try {
186
- responseData = await readResponseData(fetchResponse, responseType);
187
- } catch (readError) {
188
- throw AccessioError.from(
189
- readError as Error,
190
- AccessioError.ERR_BAD_RESPONSE,
191
- config,
192
- fetchResponse,
193
- null,
194
- );
195
- }
196
-
197
- const responseHeaders = parseHeaders(fetchResponse.headers);
198
-
199
- const responseTransforms = buildTransformArray(config.transformResponse);
200
-
201
- responseData = transformData(responseTransforms, responseData, responseHeaders, config);
202
-
203
- const response: AccessioResponse = {
204
- data: responseData,
205
- status: fetchResponse.status,
206
- statusText: fetchResponse.statusText,
207
- headers: responseHeaders,
208
- config: config,
209
- request: fetchResponse,
210
- duration: Date.now() - requestStartTime,
211
- };
212
-
213
- return new Promise<AccessioResponse>((resolve, reject) => {
214
- settle(
215
- resolve as (value: AccessioResponse) => void,
216
- reject as (reason: AccessioError) => void,
217
- response,
218
- config,
219
- );
220
- });
221
- })
222
- .catch((error) => {
223
- if (error instanceof AccessioError) {
224
- throw error;
225
- }
226
-
227
- if (error instanceof Error && error.name === 'AbortError') {
228
- if (isTimedOut) {
229
- throw new AccessioError(
230
- `timeout of ${config.timeout}ms exceeded`,
231
- AccessioError.ETIMEDOUT,
232
- config,
233
- null,
234
- null,
235
- );
236
- }
237
- throw new AccessioError('Request aborted', AccessioError.ERR_CANCELED, config, null, null);
238
- }
239
-
240
- throw AccessioError.from(
241
- error instanceof Error ? error : new Error(String(error)),
242
- AccessioError.ERR_NETWORK,
243
- config,
244
- null,
245
- null,
246
- );
247
- })
248
- .finally(() => {
249
- if (timeoutId) clearTimeout(timeoutId);
250
- if (config.signal && onUserAbort) {
251
- config.signal.removeEventListener('abort', onUserAbort);
252
- }
253
- });
75
+ const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
76
+
77
+ const responseTransforms = buildTransformArray(config.transformResponse);
78
+
79
+ response.data = await transformData(responseTransforms, response.data, response.headers, config);
80
+
81
+ return new Promise<AccessioResponse>((resolve, reject) => {
82
+ settle(
83
+ resolve as (value: AccessioResponse) => void,
84
+ reject as (reason: AccessioError) => void,
85
+ response,
86
+ config,
87
+ );
88
+ });
254
89
  }
package/src/core/retry.ts CHANGED
@@ -16,10 +16,6 @@ function defaultRetryCondition(error: any): boolean {
16
16
  return true;
17
17
  }
18
18
 
19
- if (error.code === ETIMEDOUT) {
20
- return true;
21
- }
22
-
23
19
  if (error.response && error.response.status >= 500) {
24
20
  return true;
25
21
  }
@@ -1,4 +1,7 @@
1
- export function defaultTransformRequest(data: unknown, headers: Record<string, string>): unknown {
1
+ export function defaultTransformRequest(
2
+ data: unknown,
3
+ headers: Record<string, string | string[]>,
4
+ ): unknown {
2
5
  if (data === null || data === undefined) {
3
6
  return data;
4
7
  }
@@ -23,7 +26,14 @@ export function defaultTransformRequest(data: unknown, headers: Record<string, s
23
26
  headers['Content-Type'] = 'application/json';
24
27
  }
25
28
  }
26
- return JSON.stringify(data);
29
+ try {
30
+ return JSON.stringify(data);
31
+ } catch (e: any) {
32
+ if (e instanceof TypeError && e.message.toLowerCase().includes('circular')) {
33
+ throw new Error('Accessio: Cannot stringify circular structure in request data');
34
+ }
35
+ throw e;
36
+ }
27
37
  }
28
38
 
29
39
  return data;
@@ -0,0 +1,26 @@
1
+ import type { AccessioRequestConfig } from '../types';
2
+
3
+ export function setBasicAuth(
4
+ config: AccessioRequestConfig,
5
+ headers: Record<string, string | string[]>,
6
+ ): void {
7
+ if (!config.auth) return;
8
+ const username = config.auth.username || '';
9
+ const password = config.auth.password || '';
10
+ const credentials = `${username}:${password}`;
11
+
12
+ let encoded: string;
13
+ if (typeof Buffer !== 'undefined') {
14
+ encoded = Buffer.from(credentials).toString('base64');
15
+ } else {
16
+ // Cryptic but effective UTF-8 to Base64 conversion for browsers lacking Buffer.
17
+ // encodeURIComponent converts non-ASCII to %XX, then we replace %XX with raw bytes
18
+ // before applying btoa.
19
+ encoded = btoa(
20
+ encodeURIComponent(credentials).replace(/%([0-9A-F]{2})/g, (match, p1) => {
21
+ return String.fromCharCode(parseInt(p1, 16));
22
+ }),
23
+ );
24
+ }
25
+ headers['Authorization'] = `Basic ${encoded}`;
26
+ }