accessio 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -207
- package/cjs/core/accessioError.cjs.map +1 -1
- package/cjs/core/buildURL.cjs +11 -8
- package/cjs/core/buildURL.cjs.map +1 -1
- package/cjs/core/fetchAdapter.cjs +9 -1
- package/cjs/core/fetchAdapter.cjs.map +1 -1
- package/cjs/core/mergeConfig.cjs +1 -1
- package/cjs/core/mergeConfig.cjs.map +1 -1
- package/cjs/core/request.cjs +43 -15
- package/cjs/core/request.cjs.map +1 -1
- package/cjs/defaults/transforms.cjs +4 -1
- package/cjs/defaults/transforms.cjs.map +1 -1
- package/cjs/helpers/flattenHeaders.cjs +16 -3
- package/cjs/helpers/flattenHeaders.cjs.map +1 -1
- package/cjs/helpers/memoryCache.cjs +26 -1
- package/cjs/helpers/memoryCache.cjs.map +1 -1
- package/cjs/helpers/parseHeaders.cjs +9 -6
- package/cjs/helpers/parseHeaders.cjs.map +1 -1
- package/cjs/helpers/transformData.cjs +1 -1
- package/cjs/helpers/transformData.cjs.map +1 -1
- package/index.d.ts +7 -0
- package/package.json +4 -3
- package/src/core/accessioError.ts +2 -2
- package/src/core/buildURL.ts +16 -13
- package/src/core/fetchAdapter.ts +14 -1
- package/src/core/mergeConfig.ts +1 -1
- package/src/core/request.ts +54 -16
- package/src/defaults/transforms.ts +4 -1
- package/src/helpers/flattenHeaders.ts +17 -3
- package/src/helpers/memoryCache.ts +33 -1
- package/src/helpers/parseHeaders.ts +10 -7
- package/src/helpers/transformData.ts +1 -1
- package/src/types.ts +6 -0
package/src/core/request.ts
CHANGED
|
@@ -12,28 +12,35 @@ import type { AccessioRequestConfig, AccessioResponse, TransformFunction } from
|
|
|
12
12
|
type HeadersConfig = Record<string, Record<string, string | string[]>>;
|
|
13
13
|
type FlatHeaders = Record<string, string | string[]>;
|
|
14
14
|
|
|
15
|
-
function lookupHeader(headers: FlatHeaders, name: string): string {
|
|
16
|
-
const target = name.toLowerCase();
|
|
17
|
-
for (const k of Object.keys(headers)) {
|
|
18
|
-
if (k.toLowerCase() === target) {
|
|
19
|
-
const v = headers[k];
|
|
20
|
-
return Array.isArray(v) ? v.join(',') : (v ?? '');
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return '';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
15
|
function buildCacheKey(
|
|
27
16
|
config: AccessioRequestConfig,
|
|
28
17
|
fullURL: string,
|
|
29
18
|
flatHeaders: FlatHeaders,
|
|
30
19
|
): string {
|
|
20
|
+
if (typeof config.cacheKeySerializer === 'function') {
|
|
21
|
+
return config.cacheKeySerializer(config, fullURL, flatHeaders);
|
|
22
|
+
}
|
|
31
23
|
const method = (config.method || 'GET').toUpperCase();
|
|
32
|
-
const auth = lookupHeader(flatHeaders, 'authorization');
|
|
33
|
-
const accept = lookupHeader(flatHeaders, 'accept');
|
|
34
24
|
const withCreds = config.withCredentials ? '1' : '0';
|
|
35
25
|
const respType = config.responseType || 'json';
|
|
36
|
-
|
|
26
|
+
|
|
27
|
+
// Sort and serialize headers dynamically to prevent collisions,
|
|
28
|
+
// excluding environment-specific transient headers.
|
|
29
|
+
const serializedHeaders = Object.keys(flatHeaders)
|
|
30
|
+
.sort()
|
|
31
|
+
.filter(
|
|
32
|
+
(k) =>
|
|
33
|
+
!['user-agent', 'connection', 'host', 'content-length', 'accept-encoding'].includes(
|
|
34
|
+
k.toLowerCase(),
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
.map((k) => {
|
|
38
|
+
const val = flatHeaders[k];
|
|
39
|
+
return `${k.toLowerCase()}=${Array.isArray(val) ? val.join(',') : val}`;
|
|
40
|
+
})
|
|
41
|
+
.join('&');
|
|
42
|
+
|
|
43
|
+
return `${method}:${fullURL}|h:${serializedHeaders}|c=${withCreds}|t=${respType}`;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
function buildTransformArray(
|
|
@@ -126,8 +133,39 @@ export default async function dispatchRequest(
|
|
|
126
133
|
if (isGet && config.dedupe) {
|
|
127
134
|
const inflight = activeRequests.get(cacheKey);
|
|
128
135
|
if (inflight) {
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
try {
|
|
137
|
+
const shared = await inflight;
|
|
138
|
+
const response = finalizeResponse(shared, config);
|
|
139
|
+
const settled = await new Promise<AccessioResponse>((resolve, reject) => {
|
|
140
|
+
settle(
|
|
141
|
+
resolve as (value: AccessioResponse) => void,
|
|
142
|
+
reject as (reason: AccessioError) => void,
|
|
143
|
+
response,
|
|
144
|
+
config,
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (config.hooks?.onRequestResponse) {
|
|
149
|
+
await config.hooks.onRequestResponse(settled);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return settled;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
let finalError = error;
|
|
155
|
+
if (error instanceof AccessioError) {
|
|
156
|
+
finalError = AccessioError.from(
|
|
157
|
+
error,
|
|
158
|
+
error.code || 'ERR_DEDUPE',
|
|
159
|
+
config,
|
|
160
|
+
error.request,
|
|
161
|
+
error.response,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (config.hooks?.onRequestError && finalError instanceof AccessioError) {
|
|
165
|
+
await config.hooks.onRequestError(finalError);
|
|
166
|
+
}
|
|
167
|
+
throw finalError;
|
|
168
|
+
}
|
|
131
169
|
}
|
|
132
170
|
}
|
|
133
171
|
|
|
@@ -39,7 +39,10 @@ export function defaultTransformRequest(
|
|
|
39
39
|
return data;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export function defaultTransformResponse(data: unknown): unknown {
|
|
42
|
+
export function defaultTransformResponse(data: unknown, headers?: any, config?: any): unknown {
|
|
43
|
+
if (config && config.responseType === 'text') {
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
43
46
|
if (typeof data === 'string') {
|
|
44
47
|
try {
|
|
45
48
|
return JSON.parse(data);
|
|
@@ -49,17 +49,31 @@ export function flattenHeaders(
|
|
|
49
49
|
const merged: Record<string, string | string[]> = {};
|
|
50
50
|
const methodLower = (method || 'get').toLowerCase();
|
|
51
51
|
|
|
52
|
+
const setHeader = (target: Record<string, string | string[]>, key: string, value: any) => {
|
|
53
|
+
const keyLower = key.toLowerCase();
|
|
54
|
+
for (const existingKey of Object.keys(target)) {
|
|
55
|
+
if (existingKey.toLowerCase() === keyLower) {
|
|
56
|
+
delete target[existingKey];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
target[key] = value;
|
|
60
|
+
};
|
|
61
|
+
|
|
52
62
|
if (headers['common']) {
|
|
53
|
-
Object.
|
|
63
|
+
Object.entries(headers['common']).forEach(([k, v]) => {
|
|
64
|
+
setHeader(merged, k, v);
|
|
65
|
+
});
|
|
54
66
|
}
|
|
55
67
|
|
|
56
68
|
if (headers[methodLower]) {
|
|
57
|
-
Object.
|
|
69
|
+
Object.entries(headers[methodLower]).forEach(([k, v]) => {
|
|
70
|
+
setHeader(merged, k, v);
|
|
71
|
+
});
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
for (const key in headers) {
|
|
61
75
|
if (Object.prototype.hasOwnProperty.call(headers, key) && !METHOD_KEYS.has(key)) {
|
|
62
|
-
merged
|
|
76
|
+
setHeader(merged, key, headers[key]);
|
|
63
77
|
}
|
|
64
78
|
}
|
|
65
79
|
|
|
@@ -2,6 +2,11 @@ import type { CacheProvider } from '../types';
|
|
|
2
2
|
|
|
3
3
|
class MemoryCache implements CacheProvider {
|
|
4
4
|
private cache = new Map<string, { value: any; expiry: number | null }>();
|
|
5
|
+
private maxItems: number;
|
|
6
|
+
|
|
7
|
+
constructor(maxItems: number = 1000) {
|
|
8
|
+
this.maxItems = maxItems;
|
|
9
|
+
}
|
|
5
10
|
|
|
6
11
|
get(key: string) {
|
|
7
12
|
const item = this.cache.get(key);
|
|
@@ -14,7 +19,33 @@ class MemoryCache implements CacheProvider {
|
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
set(key: string, value: any, ttl?: number) {
|
|
17
|
-
const
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
|
|
24
|
+
if (this.cache.has(key)) {
|
|
25
|
+
const expiry = ttl ? now + ttl : null;
|
|
26
|
+
this.cache.set(key, { value, expiry });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Proactively evict up to 5 of the oldest items to prevent memory build-up without O(N) cost.
|
|
31
|
+
// Map entries are ordered by insertion, so the oldest are checked first.
|
|
32
|
+
let count = 0;
|
|
33
|
+
for (const [k, item] of this.cache.entries()) {
|
|
34
|
+
if (count++ >= 5) break;
|
|
35
|
+
if (item.expiry && now > item.expiry) {
|
|
36
|
+
this.cache.delete(k);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Evict the oldest item if we are still at limit
|
|
41
|
+
if (this.cache.size >= this.maxItems) {
|
|
42
|
+
const oldest = this.cache.keys().next().value;
|
|
43
|
+
if (oldest !== undefined) {
|
|
44
|
+
this.cache.delete(oldest);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const expiry = ttl ? now + ttl : null;
|
|
18
49
|
this.cache.set(key, { value, expiry });
|
|
19
50
|
}
|
|
20
51
|
|
|
@@ -28,3 +59,4 @@ class MemoryCache implements CacheProvider {
|
|
|
28
59
|
}
|
|
29
60
|
|
|
30
61
|
export const defaultMemoryCache = new MemoryCache();
|
|
62
|
+
export { MemoryCache };
|
|
@@ -3,16 +3,19 @@ export default function parseHeaders(headers: any): Record<string, string | stri
|
|
|
3
3
|
|
|
4
4
|
if (!headers) return parsed;
|
|
5
5
|
|
|
6
|
-
const addHeader = (key: string, value: string) => {
|
|
6
|
+
const addHeader = (key: string, value: string | string[]) => {
|
|
7
7
|
const k = key.toLowerCase();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const values = Array.isArray(value) ? value : [value];
|
|
9
|
+
for (const val of values) {
|
|
10
|
+
if (parsed[k]) {
|
|
11
|
+
if (Array.isArray(parsed[k])) {
|
|
12
|
+
(parsed[k] as string[]).push(val);
|
|
13
|
+
} else {
|
|
14
|
+
parsed[k] = [parsed[k] as string, val];
|
|
15
|
+
}
|
|
11
16
|
} else {
|
|
12
|
-
parsed[k] =
|
|
17
|
+
parsed[k] = val;
|
|
13
18
|
}
|
|
14
|
-
} else {
|
|
15
|
-
parsed[k] = value;
|
|
16
19
|
}
|
|
17
20
|
};
|
|
18
21
|
|
|
@@ -17,7 +17,7 @@ export default async function transformData(
|
|
|
17
17
|
for (const transform of transforms) {
|
|
18
18
|
if (typeof transform === 'function') {
|
|
19
19
|
try {
|
|
20
|
-
result = await transform(result, headers);
|
|
20
|
+
result = await transform(result, headers, config);
|
|
21
21
|
} catch (err) {
|
|
22
22
|
throw AccessioError.from(
|
|
23
23
|
err instanceof Error ? err : new Error(String(err)),
|
package/src/types.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type ParamsSerializer = (params: Record<string, unknown>) => string;
|
|
|
14
14
|
export type TransformFunction = (
|
|
15
15
|
data: unknown,
|
|
16
16
|
headers: Record<string, string | string[]>,
|
|
17
|
+
config?: any,
|
|
17
18
|
) => unknown | Promise<unknown>;
|
|
18
19
|
|
|
19
20
|
export type RetryConditionFunction = (error: AccessioError) => boolean;
|
|
@@ -91,6 +92,11 @@ export interface AccessioRequestConfig {
|
|
|
91
92
|
dedupe?: boolean;
|
|
92
93
|
cache?: boolean | CacheProvider;
|
|
93
94
|
cacheTTL?: number;
|
|
95
|
+
cacheKeySerializer?: (
|
|
96
|
+
config: AccessioRequestConfig,
|
|
97
|
+
fullURL: string,
|
|
98
|
+
headers: Record<string, string | string[]>,
|
|
99
|
+
) => string;
|
|
94
100
|
onDownloadProgress?: (progressEvent: { loaded: number; total: number }) => void;
|
|
95
101
|
hooks?: AccessioHooks;
|
|
96
102
|
schema?: SchemaValidator;
|