accessio 1.1.1 → 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.
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) => {
@@ -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(
@@ -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 keys = Object.keys(headers).filter((k) => k.toLowerCase() === 'content-type');
49
- for (const key of keys) {
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,42 +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
- 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> {
20
+ export default async function dispatchRequest(
21
+ config: AccessioRequestConfig,
22
+ ): Promise<AccessioResponse> {
98
23
  const fullURL =
99
24
  config._builtUrl ||
100
25
  buildURL(
@@ -108,7 +33,7 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
108
33
 
109
34
  const requestTransforms = buildTransformArray(config.transformRequest);
110
35
 
111
- const requestData = transformData(requestTransforms, config.data, flatHeaders, config);
36
+ const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);
112
37
 
113
38
  if (
114
39
  requestData === null ||
@@ -122,7 +47,7 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
122
47
 
123
48
  const fetchOptions: RequestInit = {
124
49
  method: (config.method || 'GET').toUpperCase(),
125
- headers: flatHeaders,
50
+ headers: buildFetchHeaders(flatHeaders),
126
51
  };
127
52
 
128
53
  const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
@@ -145,156 +70,20 @@ export default function dispatchRequest(config: AccessioRequestConfig): Promise<
145
70
  (fetchOptions as any).agent = config.agent;
146
71
  }
147
72
 
148
- let abortController: AbortController | null = null;
149
- let timeoutId: ReturnType<typeof setTimeout> | null = null;
150
- let isTimedOut = false;
151
- let onUserAbort: (() => void) | null = null;
152
-
153
- const timeoutValue = Number(config.timeout);
154
- if (!isNaN(timeoutValue) && timeoutValue > 0) {
155
- abortController = new AbortController();
156
-
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);
169
-
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;
190
- }
191
- } else if (config.signal) {
192
- fetchOptions.signal = config.signal;
193
- }
194
-
195
73
  const requestStartTime = Date.now();
196
74
 
197
- return fetch(fullURL, fetchOptions)
198
- .then(async (fetchResponse) => {
199
- let responseData: unknown;
200
- const responseType = config.responseType || 'json';
75
+ const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
201
76
 
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
- }
77
+ const responseTransforms = buildTransformArray(config.transformResponse);
216
78
 
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
- }
79
+ response.data = await transformData(responseTransforms, response.data, response.headers, config);
228
80
 
229
- const responseHeaders = parseHeaders(fetchResponse.headers);
230
-
231
- const responseTransforms = buildTransformArray(config.transformResponse);
232
-
233
- responseData = transformData(responseTransforms, responseData, responseHeaders, config);
234
-
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
- };
244
-
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
- }
258
-
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
- }
271
-
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
- }
285
-
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
- });
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
+ });
300
89
  }
@@ -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
  }
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ const METHOD_KEYS = new Set<string>([
2
+ 'common',
3
+ 'delete',
4
+ 'get',
5
+ 'head',
6
+ 'options',
7
+ 'post',
8
+ 'put',
9
+ 'patch',
10
+ ]);
11
+
12
+ type HeadersConfig = Record<string, Record<string, string | string[]>>;
13
+
14
+ export function flattenHeaders(
15
+ headers: HeadersConfig | undefined,
16
+ method?: string,
17
+ ): Record<string, string | string[]> {
18
+ if (!headers) return {};
19
+
20
+ const merged: Record<string, string | string[]> = {};
21
+ const methodLower = (method || 'get').toLowerCase();
22
+
23
+ if (headers['common']) {
24
+ Object.assign(merged, headers['common']);
25
+ }
26
+
27
+ if (headers[methodLower]) {
28
+ Object.assign(merged, headers[methodLower]);
29
+ }
30
+
31
+ for (const key in headers) {
32
+ if (Object.prototype.hasOwnProperty.call(headers, key) && !METHOD_KEYS.has(key)) {
33
+ merged[key] = headers[key] as unknown as string | string[];
34
+ }
35
+ }
36
+
37
+ return merged;
38
+ }
39
+
40
+ export function removeContentType(headers: Record<string, string | string[]>): void {
41
+ const keys = Object.keys(headers).filter((k) => k.toLowerCase() === 'content-type');
42
+ for (const key of keys) {
43
+ delete headers[key];
44
+ }
45
+ }
46
+
47
+ export function buildFetchHeaders(headers: Record<string, string | string[]>): Headers {
48
+ const fetchHeaders = new Headers();
49
+ for (const [key, value] of Object.entries(headers)) {
50
+ if (Array.isArray(value)) {
51
+ for (const v of value) {
52
+ fetchHeaders.append(key, v);
53
+ }
54
+ } else {
55
+ fetchHeaders.set(key, value);
56
+ }
57
+ }
58
+ return fetchHeaders;
59
+ }