accessio 1.0.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/LICENSE +21 -0
- package/README.md +487 -0
- package/cjs/accessio.cjs +208 -0
- package/cjs/accessio.cjs.map +1 -0
- package/cjs/constants/errorCodes.cjs +76 -0
- package/cjs/constants/errorCodes.cjs.map +1 -0
- package/cjs/core/accessioError.cjs +93 -0
- package/cjs/core/accessioError.cjs.map +1 -0
- package/cjs/core/buildURL.cjs +100 -0
- package/cjs/core/buildURL.cjs.map +1 -0
- package/cjs/core/mergeConfig.cjs +73 -0
- package/cjs/core/mergeConfig.cjs.map +1 -0
- package/cjs/core/request.cjs +259 -0
- package/cjs/core/request.cjs.map +1 -0
- package/cjs/core/retry.cjs +109 -0
- package/cjs/core/retry.cjs.map +1 -0
- package/cjs/defaults/index.cjs +55 -0
- package/cjs/defaults/index.cjs.map +1 -0
- package/cjs/defaults/transforms.cjs +59 -0
- package/cjs/defaults/transforms.cjs.map +1 -0
- package/cjs/helpers/debug.cjs +96 -0
- package/cjs/helpers/debug.cjs.map +1 -0
- package/cjs/helpers/parseHeaders.cjs +52 -0
- package/cjs/helpers/parseHeaders.cjs.map +1 -0
- package/cjs/helpers/rateLimiter.cjs +98 -0
- package/cjs/helpers/rateLimiter.cjs.map +1 -0
- package/cjs/helpers/settle.cjs +50 -0
- package/cjs/helpers/settle.cjs.map +1 -0
- package/cjs/helpers/transformData.cjs +57 -0
- package/cjs/helpers/transformData.cjs.map +1 -0
- package/cjs/index.cjs +121 -0
- package/cjs/index.cjs.map +1 -0
- package/cjs/interceptors/interceptorManager.cjs +31 -0
- package/cjs/interceptors/interceptorManager.cjs.map +1 -0
- package/index.d.ts +454 -0
- package/package.json +116 -0
- package/src/accessio.ts +251 -0
- package/src/constants/errorCodes.ts +29 -0
- package/src/core/accessioError.ts +74 -0
- package/src/core/buildURL.ts +99 -0
- package/src/core/mergeConfig.ts +78 -0
- package/src/core/request.ts +284 -0
- package/src/core/retry.ts +117 -0
- package/src/defaults/index.ts +36 -0
- package/src/defaults/transforms.ts +44 -0
- package/src/helpers/debug.ts +103 -0
- package/src/helpers/parseHeaders.ts +35 -0
- package/src/helpers/rateLimiter.ts +96 -0
- package/src/helpers/settle.ts +26 -0
- package/src/helpers/transformData.ts +36 -0
- package/src/index.ts +102 -0
- package/src/interceptors/interceptorManager.ts +5 -0
- package/src/types.ts +159 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import buildURL from './buildURL';
|
|
2
|
+
import AccessioError from './accessioError';
|
|
3
|
+
import parseHeaders from '../helpers/parseHeaders';
|
|
4
|
+
import transformData from '../helpers/transformData';
|
|
5
|
+
import settle from '../helpers/settle';
|
|
6
|
+
import type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';
|
|
7
|
+
|
|
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 (
|
|
40
|
+
Object.prototype.hasOwnProperty.call(headers, key) &&
|
|
41
|
+
!METHOD_KEYS.has(key)
|
|
42
|
+
) {
|
|
43
|
+
merged[key] = headers[key] as unknown as string;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function removeContentType(headers: Record<string, string>): void {
|
|
51
|
+
const key = Object.keys(headers).find(
|
|
52
|
+
(k) => k.toLowerCase() === 'content-type',
|
|
53
|
+
);
|
|
54
|
+
if (key) {
|
|
55
|
+
delete headers[key];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildTransformArray(
|
|
60
|
+
transform: TransformFunction | TransformFunction[] | undefined,
|
|
61
|
+
): TransformFunction[] {
|
|
62
|
+
if (!transform) return [];
|
|
63
|
+
if (Array.isArray(transform)) return transform;
|
|
64
|
+
return [transform];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default function dispatchRequest(
|
|
68
|
+
config: AccessioRequestConfig,
|
|
69
|
+
): Promise<AccessioResponse> {
|
|
70
|
+
const fullURL =
|
|
71
|
+
config._builtUrl ||
|
|
72
|
+
buildURL(
|
|
73
|
+
config.url ?? '',
|
|
74
|
+
config.baseURL,
|
|
75
|
+
config.params as Record<string, unknown> | undefined,
|
|
76
|
+
config.paramsSerializer,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const flatHeaders = flattenHeaders(
|
|
80
|
+
config.headers as HeadersConfig | undefined,
|
|
81
|
+
config.method,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const requestTransforms = buildTransformArray(config.transformRequest);
|
|
85
|
+
|
|
86
|
+
const requestData = transformData(
|
|
87
|
+
requestTransforms,
|
|
88
|
+
config.data,
|
|
89
|
+
flatHeaders,
|
|
90
|
+
config,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
requestData === null ||
|
|
95
|
+
requestData === undefined ||
|
|
96
|
+
(typeof FormData !== 'undefined' && requestData instanceof FormData)
|
|
97
|
+
) {
|
|
98
|
+
removeContentType(flatHeaders);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (config.auth) {
|
|
102
|
+
const username = config.auth.username || '';
|
|
103
|
+
const password = config.auth.password || '';
|
|
104
|
+
const credentials = `${username}:${password}`;
|
|
105
|
+
|
|
106
|
+
let encoded: string;
|
|
107
|
+
if (typeof Buffer !== 'undefined') {
|
|
108
|
+
encoded = Buffer.from(credentials).toString('base64');
|
|
109
|
+
} else {
|
|
110
|
+
const bytes = new TextEncoder().encode(credentials);
|
|
111
|
+
const binString = Array.from(bytes, (x) =>
|
|
112
|
+
String.fromCodePoint(x),
|
|
113
|
+
).join('');
|
|
114
|
+
encoded = btoa(binString);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
flatHeaders['Authorization'] = `Basic ${encoded}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const fetchOptions: RequestInit = {
|
|
121
|
+
method: (config.method || 'GET').toUpperCase(),
|
|
122
|
+
headers: flatHeaders,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];
|
|
126
|
+
if (
|
|
127
|
+
methodsWithBody.includes(fetchOptions.method!) &&
|
|
128
|
+
requestData !== undefined &&
|
|
129
|
+
requestData !== null
|
|
130
|
+
) {
|
|
131
|
+
fetchOptions.body = requestData as BodyInit;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (config.withCredentials) {
|
|
135
|
+
fetchOptions.credentials = 'include';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let abortController: AbortController | null = null;
|
|
139
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
140
|
+
let isTimedOut = false;
|
|
141
|
+
let onUserAbort: (() => void) | null = null;
|
|
142
|
+
|
|
143
|
+
if (config.timeout && config.timeout > 0) {
|
|
144
|
+
abortController = new AbortController();
|
|
145
|
+
|
|
146
|
+
timeoutId = setTimeout(() => {
|
|
147
|
+
isTimedOut = true;
|
|
148
|
+
abortController!.abort(
|
|
149
|
+
new AccessioError(
|
|
150
|
+
`timeout of ${config.timeout}ms exceeded`,
|
|
151
|
+
AccessioError.ETIMEDOUT,
|
|
152
|
+
config,
|
|
153
|
+
null,
|
|
154
|
+
null,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}, config.timeout);
|
|
158
|
+
|
|
159
|
+
if (config.signal) {
|
|
160
|
+
if (typeof AbortSignal.any === 'function') {
|
|
161
|
+
fetchOptions.signal = AbortSignal.any([
|
|
162
|
+
config.signal,
|
|
163
|
+
abortController.signal,
|
|
164
|
+
]);
|
|
165
|
+
} else {
|
|
166
|
+
if (config.signal.aborted) {
|
|
167
|
+
abortController.abort(config.signal.reason);
|
|
168
|
+
} else {
|
|
169
|
+
onUserAbort = () => {
|
|
170
|
+
abortController!.abort(config.signal!.reason);
|
|
171
|
+
};
|
|
172
|
+
config.signal.addEventListener('abort', onUserAbort, {
|
|
173
|
+
once: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
fetchOptions.signal = abortController.signal;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
fetchOptions.signal = abortController.signal;
|
|
180
|
+
}
|
|
181
|
+
} else if (config.signal) {
|
|
182
|
+
fetchOptions.signal = config.signal;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const requestStartTime = Date.now();
|
|
186
|
+
|
|
187
|
+
return fetch(fullURL, fetchOptions)
|
|
188
|
+
.then(async (fetchResponse) => {
|
|
189
|
+
let responseData: unknown;
|
|
190
|
+
const responseType = config.responseType || 'json';
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
switch (responseType) {
|
|
194
|
+
case 'arraybuffer':
|
|
195
|
+
responseData = await fetchResponse.arrayBuffer();
|
|
196
|
+
break;
|
|
197
|
+
case 'blob':
|
|
198
|
+
responseData = await fetchResponse.blob();
|
|
199
|
+
break;
|
|
200
|
+
case 'text':
|
|
201
|
+
responseData = await fetchResponse.text();
|
|
202
|
+
break;
|
|
203
|
+
case 'stream':
|
|
204
|
+
responseData = fetchResponse.body;
|
|
205
|
+
break;
|
|
206
|
+
case 'json':
|
|
207
|
+
default:
|
|
208
|
+
responseData = await fetchResponse.text();
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
} catch (readError) {
|
|
212
|
+
throw AccessioError.from(
|
|
213
|
+
readError as Error,
|
|
214
|
+
AccessioError.ERR_BAD_RESPONSE,
|
|
215
|
+
config,
|
|
216
|
+
fetchResponse,
|
|
217
|
+
null,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const responseHeaders = parseHeaders(fetchResponse.headers);
|
|
222
|
+
|
|
223
|
+
const responseTransforms = buildTransformArray(config.transformResponse);
|
|
224
|
+
|
|
225
|
+
responseData = transformData(
|
|
226
|
+
responseTransforms,
|
|
227
|
+
responseData,
|
|
228
|
+
responseHeaders,
|
|
229
|
+
config,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const response: AccessioResponse = {
|
|
233
|
+
data: responseData,
|
|
234
|
+
status: fetchResponse.status,
|
|
235
|
+
statusText: fetchResponse.statusText,
|
|
236
|
+
headers: responseHeaders,
|
|
237
|
+
config: config,
|
|
238
|
+
request: fetchResponse,
|
|
239
|
+
duration: Date.now() - requestStartTime,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return new Promise<AccessioResponse>((resolve, reject) => {
|
|
243
|
+
settle(resolve as (value: AccessioResponse) => void, reject as (reason: AccessioError) => void, response, config);
|
|
244
|
+
});
|
|
245
|
+
})
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
if (error instanceof AccessioError) {
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
252
|
+
if (isTimedOut) {
|
|
253
|
+
throw new AccessioError(
|
|
254
|
+
`timeout of ${config.timeout}ms exceeded`,
|
|
255
|
+
AccessioError.ETIMEDOUT,
|
|
256
|
+
config,
|
|
257
|
+
null,
|
|
258
|
+
null,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
throw new AccessioError(
|
|
262
|
+
'Request aborted',
|
|
263
|
+
AccessioError.ERR_CANCELED,
|
|
264
|
+
config,
|
|
265
|
+
null,
|
|
266
|
+
null,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
throw AccessioError.from(
|
|
271
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
272
|
+
AccessioError.ERR_NETWORK,
|
|
273
|
+
config,
|
|
274
|
+
null,
|
|
275
|
+
null,
|
|
276
|
+
);
|
|
277
|
+
})
|
|
278
|
+
.finally(() => {
|
|
279
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
280
|
+
if (config.signal && onUserAbort) {
|
|
281
|
+
config.signal.removeEventListener('abort', onUserAbort);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ERR_CANCELED,
|
|
3
|
+
ERR_NETWORK,
|
|
4
|
+
ETIMEDOUT,
|
|
5
|
+
} from '../constants/errorCodes';
|
|
6
|
+
import type {
|
|
7
|
+
AccessioRequestConfig,
|
|
8
|
+
AccessioResponse,
|
|
9
|
+
AccessioError,
|
|
10
|
+
RetryConditionFunction,
|
|
11
|
+
OnRetryFunction,
|
|
12
|
+
} from '../types';
|
|
13
|
+
|
|
14
|
+
function defaultRetryCondition(error: any): boolean {
|
|
15
|
+
if (error.code === ERR_CANCELED) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (error.code === ERR_NETWORK) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (error.code === ETIMEDOUT) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (error.response && error.response.status >= 500) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function calculateDelay(attempt: number, baseDelay: number): number {
|
|
35
|
+
const exponentialDelay = baseDelay * Math.pow(2, attempt);
|
|
36
|
+
const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
|
|
37
|
+
return Math.round(exponentialDelay + jitter);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sleep(ms: number, options?: { signal?: AbortSignal }): Promise<void> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
let onAbort: (() => void) | undefined;
|
|
43
|
+
|
|
44
|
+
const timeoutId = setTimeout(() => {
|
|
45
|
+
if (options?.signal && onAbort) {
|
|
46
|
+
options.signal.removeEventListener('abort', onAbort);
|
|
47
|
+
}
|
|
48
|
+
resolve();
|
|
49
|
+
}, ms);
|
|
50
|
+
|
|
51
|
+
if (options?.signal) {
|
|
52
|
+
if (options.signal.aborted) {
|
|
53
|
+
clearTimeout(timeoutId);
|
|
54
|
+
return reject(
|
|
55
|
+
options.signal.reason || new Error('Sleep aborted'),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onAbort = () => {
|
|
60
|
+
clearTimeout(timeoutId);
|
|
61
|
+
reject(options.signal!.reason || new Error('Sleep aborted'));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
options.signal.addEventListener('abort', onAbort, { once: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function retryRequest(
|
|
70
|
+
dispatchFn: (config: AccessioRequestConfig) => Promise<any>,
|
|
71
|
+
config: AccessioRequestConfig,
|
|
72
|
+
): Promise<any> {
|
|
73
|
+
const maxRetries = config.retry ?? 0;
|
|
74
|
+
|
|
75
|
+
if (maxRetries <= 0) {
|
|
76
|
+
return dispatchFn(config);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const retryDelay = config.retryDelay ?? 1000;
|
|
80
|
+
const retryCondition: RetryConditionFunction =
|
|
81
|
+
config.retryCondition ?? defaultRetryCondition;
|
|
82
|
+
|
|
83
|
+
let lastError: any;
|
|
84
|
+
|
|
85
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
const response = await dispatchFn(config);
|
|
88
|
+
return response;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
lastError = error;
|
|
91
|
+
|
|
92
|
+
const isLastAttempt = attempt >= maxRetries;
|
|
93
|
+
const shouldRetry = !isLastAttempt && retryCondition(error as AccessioError);
|
|
94
|
+
|
|
95
|
+
if (!shouldRetry) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const delay = calculateDelay(attempt, retryDelay);
|
|
100
|
+
|
|
101
|
+
if (typeof config.onRetry === 'function') {
|
|
102
|
+
(config.onRetry as OnRetryFunction)(
|
|
103
|
+
attempt + 1,
|
|
104
|
+
error as AccessioError,
|
|
105
|
+
config,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await sleep(delay, { signal: config.signal });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw lastError;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { defaultRetryCondition, calculateDelay };
|
|
117
|
+
export default retryRequest;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defaultTransformRequest, defaultTransformResponse } from './transforms';
|
|
2
|
+
import type { AccessioRequestConfig } from '../types';
|
|
3
|
+
|
|
4
|
+
const defaults: AccessioRequestConfig = {
|
|
5
|
+
method: 'get',
|
|
6
|
+
timeout: 0,
|
|
7
|
+
headers: {
|
|
8
|
+
common: {
|
|
9
|
+
Accept: 'application/json, text/plain, */*',
|
|
10
|
+
},
|
|
11
|
+
delete: {},
|
|
12
|
+
get: {},
|
|
13
|
+
head: {},
|
|
14
|
+
options: {},
|
|
15
|
+
post: {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
put: {
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
},
|
|
21
|
+
patch: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
transformRequest: [defaultTransformRequest],
|
|
26
|
+
transformResponse: [defaultTransformResponse],
|
|
27
|
+
validateStatus: function defaultValidateStatus(
|
|
28
|
+
status: number,
|
|
29
|
+
): boolean {
|
|
30
|
+
return status >= 200 && status < 300;
|
|
31
|
+
},
|
|
32
|
+
responseType: 'json',
|
|
33
|
+
withCredentials: false,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default defaults;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function defaultTransformRequest(
|
|
2
|
+
data: unknown,
|
|
3
|
+
headers: Record<string, string>,
|
|
4
|
+
): unknown {
|
|
5
|
+
if (data === null || data === undefined) {
|
|
6
|
+
return data;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (
|
|
10
|
+
typeof data === 'string' ||
|
|
11
|
+
data instanceof ArrayBuffer ||
|
|
12
|
+
(typeof Blob !== 'undefined' && data instanceof Blob) ||
|
|
13
|
+
(typeof FormData !== 'undefined' && data instanceof FormData) ||
|
|
14
|
+
(typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) ||
|
|
15
|
+
(typeof ReadableStream !== 'undefined' && data instanceof ReadableStream)
|
|
16
|
+
) {
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof data === 'object') {
|
|
21
|
+
if (headers && typeof headers === 'object') {
|
|
22
|
+
const hasContentType = Object.keys(headers).some(
|
|
23
|
+
(key) => key.toLowerCase() === 'content-type',
|
|
24
|
+
);
|
|
25
|
+
if (!hasContentType) {
|
|
26
|
+
headers['Content-Type'] = 'application/json';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return JSON.stringify(data);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function defaultTransformResponse(data: unknown): unknown {
|
|
36
|
+
if (typeof data === 'string') {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(data);
|
|
39
|
+
} catch {
|
|
40
|
+
// Not JSON — return as-is
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { AccessioRequestConfig, AccessioResponse } from '../types';
|
|
2
|
+
import AccessioError from '../core/accessioError';
|
|
3
|
+
|
|
4
|
+
function formatBytes(bytes: number): string {
|
|
5
|
+
if (bytes === 0) return '0 B';
|
|
6
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
7
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
8
|
+
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sanitizeConfigForLog(
|
|
12
|
+
config: AccessioRequestConfig,
|
|
13
|
+
): { params: Record<string, unknown> | undefined; timeout: number | undefined; retry: number | undefined } {
|
|
14
|
+
return {
|
|
15
|
+
params: config.params,
|
|
16
|
+
timeout: config.timeout,
|
|
17
|
+
retry: config.retry,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function logRequest(
|
|
22
|
+
config: AccessioRequestConfig,
|
|
23
|
+
fullUrl: string,
|
|
24
|
+
): void {
|
|
25
|
+
if (!config.debug) return;
|
|
26
|
+
|
|
27
|
+
const safe = sanitizeConfigForLog(config);
|
|
28
|
+
|
|
29
|
+
const method = (config.method || 'GET').toUpperCase();
|
|
30
|
+
const url = fullUrl || config.url || '';
|
|
31
|
+
|
|
32
|
+
const parts: string[] = [`🐦⬛ [Accessio] → ${method} ${url}`];
|
|
33
|
+
|
|
34
|
+
if (safe.params && Object.keys(safe.params).length > 0) {
|
|
35
|
+
parts.push(` Params: ${JSON.stringify(safe.params)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (config.data && typeof config.data === 'object') {
|
|
39
|
+
const preview = JSON.stringify(config.data);
|
|
40
|
+
const truncated =
|
|
41
|
+
preview.length > 200 ? `${preview.substring(0, 200)}...` : preview;
|
|
42
|
+
parts.push(` Body: ${truncated}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (safe.timeout) {
|
|
46
|
+
parts.push(` Timeout: ${safe.timeout}ms`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (safe.retry) {
|
|
50
|
+
parts.push(` Retry: ${safe.retry}x`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(parts.join('\n'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function logResponse(response: AccessioResponse): void {
|
|
57
|
+
if (!response.config || !response.config.debug) return;
|
|
58
|
+
const status = response.status;
|
|
59
|
+
const statusText = response.statusText || '';
|
|
60
|
+
const duration =
|
|
61
|
+
response.duration != null ? `${response.duration}ms` : '??';
|
|
62
|
+
|
|
63
|
+
const statusIcon =
|
|
64
|
+
status >= 200 && status < 300
|
|
65
|
+
? '✅'
|
|
66
|
+
: status >= 400
|
|
67
|
+
? '❌'
|
|
68
|
+
: '⚠️';
|
|
69
|
+
|
|
70
|
+
const parts: string[] = [
|
|
71
|
+
`🐦⬛ [Accessio] ← ${statusIcon} ${status} ${statusText} (${duration})`,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
if (response.data) {
|
|
75
|
+
try {
|
|
76
|
+
const size =
|
|
77
|
+
typeof response.data === 'string'
|
|
78
|
+
? response.data.length
|
|
79
|
+
: JSON.stringify(response.data).length;
|
|
80
|
+
parts.push(` Size: ~${formatBytes(size)}`);
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(parts.join('\n'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function logError(error: AccessioError, config?: AccessioRequestConfig): void {
|
|
90
|
+
if (!config || !config.debug) return;
|
|
91
|
+
|
|
92
|
+
const parts: string[] = [`🐦⬛ [Accessio] ← ❌ ERROR: ${error.message}`];
|
|
93
|
+
|
|
94
|
+
if (error.code) {
|
|
95
|
+
parts.push(` Code: ${error.code}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (error.response) {
|
|
99
|
+
parts.push(` Status: ${error.response.status}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(parts.join('\n'));
|
|
103
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export default function parseHeaders(
|
|
2
|
+
headers: any,
|
|
3
|
+
): Record<string, string> {
|
|
4
|
+
const parsed: Record<string, string> = {};
|
|
5
|
+
|
|
6
|
+
if (!headers) return parsed;
|
|
7
|
+
|
|
8
|
+
if (typeof headers.forEach === 'function') {
|
|
9
|
+
headers.forEach((value: string, key: string) => {
|
|
10
|
+
parsed[key.toLowerCase()] = value;
|
|
11
|
+
});
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof headers === 'string') {
|
|
16
|
+
headers.split('\n').forEach((line: string) => {
|
|
17
|
+
const index = line.indexOf(':');
|
|
18
|
+
if (index > 0) {
|
|
19
|
+
const key = line.substring(0, index).trim().toLowerCase();
|
|
20
|
+
const value = line.substring(index + 1).trim();
|
|
21
|
+
parsed[key] = value;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
return parsed;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof headers === 'object') {
|
|
28
|
+
Object.keys(headers).forEach((key) => {
|
|
29
|
+
parsed[key.toLowerCase()] = headers[key];
|
|
30
|
+
});
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RateLimiter,
|
|
3
|
+
AccessioRequestConfig,
|
|
4
|
+
AccessioResponse,
|
|
5
|
+
} from '../types';
|
|
6
|
+
|
|
7
|
+
interface QueueItem {
|
|
8
|
+
resolve: () => void;
|
|
9
|
+
reject: (reason: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRateLimiter(maxConcurrent: number = Infinity): RateLimiter {
|
|
13
|
+
if (
|
|
14
|
+
maxConcurrent !== Infinity &&
|
|
15
|
+
(!Number.isInteger(maxConcurrent) || maxConcurrent < 1)
|
|
16
|
+
) {
|
|
17
|
+
throw new RangeError(
|
|
18
|
+
`[Accessio] maxConcurrent must be a positive integer or Infinity, got: ${maxConcurrent}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
let active = 0;
|
|
22
|
+
let destroyed = false;
|
|
23
|
+
const queue: QueueItem[] = [];
|
|
24
|
+
|
|
25
|
+
function acquire(): Promise<void> {
|
|
26
|
+
if (destroyed) {
|
|
27
|
+
return Promise.reject(
|
|
28
|
+
new Error('[Accessio] Rate limiter has been destroyed'),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (active < maxConcurrent) {
|
|
33
|
+
active++;
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
queue.push({ resolve, reject });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function release(): void {
|
|
43
|
+
if (destroyed) return;
|
|
44
|
+
|
|
45
|
+
if (active <= 0) return;
|
|
46
|
+
|
|
47
|
+
active--;
|
|
48
|
+
|
|
49
|
+
if (queue.length > 0 && active < maxConcurrent) {
|
|
50
|
+
active++;
|
|
51
|
+
const next = queue.shift();
|
|
52
|
+
next?.resolve();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function destroy(): void {
|
|
57
|
+
destroyed = true;
|
|
58
|
+
const reason = new Error(
|
|
59
|
+
'[Accessio] Rate limiter destroyed — pending request cancelled',
|
|
60
|
+
);
|
|
61
|
+
while (queue.length > 0) {
|
|
62
|
+
const next = queue.shift();
|
|
63
|
+
next?.reject(reason);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
acquire,
|
|
69
|
+
release,
|
|
70
|
+
destroy,
|
|
71
|
+
get pending() {
|
|
72
|
+
return queue.length;
|
|
73
|
+
},
|
|
74
|
+
get active() {
|
|
75
|
+
return active;
|
|
76
|
+
},
|
|
77
|
+
get destroyed() {
|
|
78
|
+
return destroyed;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function rateLimitedRequest<T = unknown>(
|
|
84
|
+
dispatchFn: (config: AccessioRequestConfig) => Promise<AccessioResponse<T>>,
|
|
85
|
+
limiter: RateLimiter,
|
|
86
|
+
config: AccessioRequestConfig,
|
|
87
|
+
): Promise<AccessioResponse<T>> {
|
|
88
|
+
await limiter.acquire();
|
|
89
|
+
try {
|
|
90
|
+
return await dispatchFn(config);
|
|
91
|
+
} finally {
|
|
92
|
+
limiter.release();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default createRateLimiter;
|