accessio 1.2.0 → 1.3.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 +21 -21
- package/cjs/core/accessioError.cjs +27 -3
- package/cjs/core/accessioError.cjs.map +1 -1
- package/cjs/core/fetchAdapter.cjs +13 -6
- package/cjs/core/fetchAdapter.cjs.map +1 -1
- package/cjs/core/request.cjs +26 -3
- package/cjs/core/request.cjs.map +1 -1
- package/cjs/core/retry.cjs +25 -0
- package/cjs/core/retry.cjs.map +1 -1
- package/cjs/helpers/flattenHeaders.cjs +37 -0
- package/cjs/helpers/flattenHeaders.cjs.map +1 -1
- package/cjs/helpers/rateLimiter.cjs +11 -22
- package/cjs/helpers/rateLimiter.cjs.map +1 -1
- package/cjs/helpers/transformData.cjs +2 -2
- package/cjs/helpers/transformData.cjs.map +1 -1
- package/index.d.ts +7 -0
- package/package.json +1 -1
- package/src/core/accessioError.ts +25 -1
- package/src/core/fetchAdapter.ts +15 -6
- package/src/core/request.ts +32 -3
- package/src/core/retry.ts +19 -1
- package/src/helpers/flattenHeaders.ts +30 -0
- package/src/helpers/rateLimiter.ts +11 -24
- package/src/helpers/transformData.ts +2 -1
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Fast, flexible HTTP client — simple, modular, and dependency-free.**
|
|
4
4
|
|
|
5
|
-
`
|
|
5
|
+
`Accessio` is a lightweight, modern HTTP client built on top of the native `fetch` API. It provides a familiar, Promise-based interface with advanced features like interceptors, automatic retries, rate limiting, and structured debug logging, all while maintaining **zero external dependencies**.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -69,22 +69,22 @@ console.log(`User created in ${response.duration}ms`);
|
|
|
69
69
|
|
|
70
70
|
### Request Methods
|
|
71
71
|
|
|
72
|
-
| Method
|
|
73
|
-
|
|
|
74
|
-
| `accessio(config)`
|
|
75
|
-
| `accessio.get(url, config?)`
|
|
76
|
-
| `accessio.post(url, data?, config?)`
|
|
77
|
-
| `accessio.put(url, data?, config?)`
|
|
78
|
-
| `accessio.patch(url, data?, config?)`
|
|
79
|
-
| `accessio.delete(url, config?)`
|
|
80
|
-
| `accessio.head(url, config?)`
|
|
81
|
-
| `accessio.options(url, config?)`
|
|
82
|
-
| `accessio.postForm(url, data?, config?)`
|
|
83
|
-
| `accessio.putForm(url, data?, config?)`
|
|
84
|
-
| `accessio.patchForm(url, data?, config?)
|
|
85
|
-
| `accessio.stream(url, config?)`
|
|
86
|
-
| `accessio.autoPaginate(url, config?)`
|
|
87
|
-
| `accessio.gql(url, query, vars?, config?)
|
|
72
|
+
| Method | Description |
|
|
73
|
+
| :----------------------------------------- | :--------------------------------------- |
|
|
74
|
+
| `accessio(config)` | Generic request using config object |
|
|
75
|
+
| `accessio.get(url, config?)` | GET request |
|
|
76
|
+
| `accessio.post(url, data?, config?)` | POST request |
|
|
77
|
+
| `accessio.put(url, data?, config?)` | PUT request |
|
|
78
|
+
| `accessio.patch(url, data?, config?)` | PATCH request |
|
|
79
|
+
| `accessio.delete(url, config?)` | DELETE request |
|
|
80
|
+
| `accessio.head(url, config?)` | HEAD request |
|
|
81
|
+
| `accessio.options(url, config?)` | OPTIONS request |
|
|
82
|
+
| `accessio.postForm(url, data?, config?)` | POST request with `multipart/form-data` |
|
|
83
|
+
| `accessio.putForm(url, data?, config?)` | PUT request with `multipart/form-data` |
|
|
84
|
+
| `accessio.patchForm(url, data?, config?)` | PATCH request with `multipart/form-data` |
|
|
85
|
+
| `accessio.stream(url, config?)` | Server-Sent Events (SSE) streaming |
|
|
86
|
+
| `accessio.autoPaginate(url, config?)` | Async iterator for paginated endpoints |
|
|
87
|
+
| `accessio.gql(url, query, vars?, config?)` | GraphQL query/mutation wrapper |
|
|
88
88
|
|
|
89
89
|
### Configuration Options
|
|
90
90
|
|
|
@@ -146,7 +146,7 @@ accessio.interceptors.response.use(
|
|
|
146
146
|
|
|
147
147
|
### Error Handling
|
|
148
148
|
|
|
149
|
-
`
|
|
149
|
+
`Accessio` provides a structured error object with specific codes to help you handle failures gracefully.
|
|
150
150
|
|
|
151
151
|
| Code | Description |
|
|
152
152
|
| :----------------- | :---------------------------- |
|
|
@@ -160,7 +160,7 @@ accessio.interceptors.response.use(
|
|
|
160
160
|
|
|
161
161
|
### Automatic Retries
|
|
162
162
|
|
|
163
|
-
`
|
|
163
|
+
`Accessio` includes a powerful retry mechanism that handles network errors and 5xx responses automatically.
|
|
164
164
|
|
|
165
165
|
```typescript
|
|
166
166
|
const response = await accessio.get('/flaky-endpoint', {
|
|
@@ -207,7 +207,7 @@ Prevent duplicate requests and cache responses to improve performance.
|
|
|
207
207
|
```typescript
|
|
208
208
|
const api = accessio.create({
|
|
209
209
|
dedupe: true, // Prevents identical requests while one is pending
|
|
210
|
-
cache: true,
|
|
210
|
+
cache: true, // Caches responses in memory
|
|
211
211
|
cacheTTL: 5 * 60 * 1000, // Cache for 5 minutes
|
|
212
212
|
});
|
|
213
213
|
```
|
|
@@ -269,7 +269,7 @@ accessio.create({
|
|
|
269
269
|
onBeforeRequest: (config) => console.log('Starting request...'),
|
|
270
270
|
onRequestResponse: (response) => console.log('Request succeeded!'),
|
|
271
271
|
onRequestError: (error) => console.error('Request failed!'),
|
|
272
|
-
}
|
|
272
|
+
},
|
|
273
273
|
});
|
|
274
274
|
```
|
|
275
275
|
|
|
@@ -29,10 +29,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var accessioError_exports = {};
|
|
30
30
|
__export(accessioError_exports, {
|
|
31
31
|
AccessioError: () => AccessioError,
|
|
32
|
-
default: () => accessioError_default
|
|
32
|
+
default: () => accessioError_default,
|
|
33
|
+
redactConfig: () => redactConfig
|
|
33
34
|
});
|
|
34
35
|
module.exports = __toCommonJS(accessioError_exports);
|
|
35
36
|
var import_errorCodes = __toESM(require("../constants/errorCodes"), 1);
|
|
37
|
+
function redactHeaders(headers) {
|
|
38
|
+
if (!headers || typeof headers !== "object") return headers;
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const key of Object.keys(headers)) {
|
|
41
|
+
const value = headers[key];
|
|
42
|
+
if (/^authorization$/i.test(key)) {
|
|
43
|
+
out[key] = "[REDACTED]";
|
|
44
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
45
|
+
out[key] = redactHeaders(value);
|
|
46
|
+
} else {
|
|
47
|
+
out[key] = value;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function redactConfig(config) {
|
|
53
|
+
if (!config) return config;
|
|
54
|
+
const clone = { ...config };
|
|
55
|
+
if ("auth" in clone) delete clone.auth;
|
|
56
|
+
if (clone.headers) clone.headers = redactHeaders(clone.headers);
|
|
57
|
+
return clone;
|
|
58
|
+
}
|
|
36
59
|
class AccessioError extends Error {
|
|
37
60
|
static ERR_BAD_OPTION_VALUE = import_errorCodes.default.ERR_BAD_OPTION_VALUE;
|
|
38
61
|
static ERR_BAD_OPTION = import_errorCodes.default.ERR_BAD_OPTION;
|
|
@@ -56,7 +79,7 @@ class AccessioError extends Error {
|
|
|
56
79
|
super(message);
|
|
57
80
|
this.name = "AccessioError";
|
|
58
81
|
this.code = code ?? null;
|
|
59
|
-
this.config = config ?? null;
|
|
82
|
+
this.config = redactConfig(config ?? null);
|
|
60
83
|
this.request = request ?? null;
|
|
61
84
|
this.response = response ?? null;
|
|
62
85
|
this.isAccessioError = true;
|
|
@@ -83,6 +106,7 @@ class AccessioError extends Error {
|
|
|
83
106
|
var accessioError_default = AccessioError;
|
|
84
107
|
// Annotate the CommonJS export names for ESM import in node:
|
|
85
108
|
0 && (module.exports = {
|
|
86
|
-
AccessioError
|
|
109
|
+
AccessioError,
|
|
110
|
+
redactConfig
|
|
87
111
|
});
|
|
88
112
|
//# sourceMappingURL=accessioError.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/accessioError.ts"],"sourcesContent":["import ErrorCodes from '../constants/errorCodes';\nimport type { AccessioRequestConfig, AccessioResponse } from '../types';\n\nexport class AccessioError extends Error {\n static ERR_BAD_OPTION_VALUE: string = ErrorCodes.ERR_BAD_OPTION_VALUE;\n static ERR_BAD_OPTION: string = ErrorCodes.ERR_BAD_OPTION;\n static ECONNABORTED: string = ErrorCodes.ECONNABORTED;\n static ETIMEDOUT: string = ErrorCodes.ETIMEDOUT;\n static ERR_NETWORK: string = ErrorCodes.ERR_NETWORK;\n static ERR_FR_TOO_MANY_REDIRECTS: string = ErrorCodes.ERR_FR_TOO_MANY_REDIRECTS;\n static ERR_BAD_RESPONSE: string = ErrorCodes.ERR_BAD_RESPONSE;\n static ERR_BAD_REQUEST: string = ErrorCodes.ERR_BAD_REQUEST;\n static ERR_CANCELED: string = ErrorCodes.ERR_CANCELED;\n static ERR_NOT_SUPPORT: string = ErrorCodes.ERR_NOT_SUPPORT;\n static ERR_INVALID_URL: string = ErrorCodes.ERR_INVALID_URL;\n\n readonly code: string | null;\n readonly config: AccessioRequestConfig | null;\n readonly request: unknown;\n readonly response: AccessioResponse | null;\n readonly isAccessioError: true;\n cause?: Error;\n override name = 'AccessioError' as const;\n\n constructor(\n message: string,\n code: string | null,\n config: AccessioRequestConfig | null,\n request: unknown,\n response: AccessioResponse | null,\n ) {\n super(message);\n this.name = 'AccessioError';\n this.code = code ?? null;\n this.config = config ?? null;\n this.request = request ?? null;\n this.response = response ?? null;\n this.isAccessioError = true;\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, AccessioError);\n }\n }\n\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n status: this.response ? this.response.status : null,\n config: this.config,\n };\n }\n\n static from(\n error: Error,\n code: string,\n config: AccessioRequestConfig | null,\n request: unknown,\n response: AccessioResponse | null,\n ): AccessioError {\n const accessioError = new AccessioError(error.message, code, config, request, response);\n accessioError.cause = error;\n accessioError.stack = error.stack;\n return accessioError;\n }\n}\n\nexport default AccessioError;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAuB;
|
|
1
|
+
{"version":3,"sources":["../../src/core/accessioError.ts"],"sourcesContent":["import ErrorCodes from '../constants/errorCodes';\nimport type { AccessioRequestConfig, AccessioResponse } from '../types';\n\nfunction redactHeaders(headers: unknown): unknown {\n if (!headers || typeof headers !== 'object') return headers;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(headers as Record<string, unknown>)) {\n const value = (headers as Record<string, unknown>)[key];\n if (/^authorization$/i.test(key)) {\n out[key] = '[REDACTED]';\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\n out[key] = redactHeaders(value);\n } else {\n out[key] = value;\n }\n }\n return out;\n}\n\nexport function redactConfig(config: AccessioRequestConfig | null): AccessioRequestConfig | null {\n if (!config) return config;\n const clone = { ...config } as AccessioRequestConfig & { auth?: unknown };\n if ('auth' in clone) delete clone.auth;\n if (clone.headers) clone.headers = redactHeaders(clone.headers) as typeof clone.headers;\n return clone;\n}\n\nexport class AccessioError extends Error {\n static ERR_BAD_OPTION_VALUE: string = ErrorCodes.ERR_BAD_OPTION_VALUE;\n static ERR_BAD_OPTION: string = ErrorCodes.ERR_BAD_OPTION;\n static ECONNABORTED: string = ErrorCodes.ECONNABORTED;\n static ETIMEDOUT: string = ErrorCodes.ETIMEDOUT;\n static ERR_NETWORK: string = ErrorCodes.ERR_NETWORK;\n static ERR_FR_TOO_MANY_REDIRECTS: string = ErrorCodes.ERR_FR_TOO_MANY_REDIRECTS;\n static ERR_BAD_RESPONSE: string = ErrorCodes.ERR_BAD_RESPONSE;\n static ERR_BAD_REQUEST: string = ErrorCodes.ERR_BAD_REQUEST;\n static ERR_CANCELED: string = ErrorCodes.ERR_CANCELED;\n static ERR_NOT_SUPPORT: string = ErrorCodes.ERR_NOT_SUPPORT;\n static ERR_INVALID_URL: string = ErrorCodes.ERR_INVALID_URL;\n\n readonly code: string | null;\n readonly config: AccessioRequestConfig | null;\n readonly request: unknown;\n readonly response: AccessioResponse | null;\n readonly isAccessioError: true;\n cause?: Error;\n override name = 'AccessioError' as const;\n\n constructor(\n message: string,\n code: string | null,\n config: AccessioRequestConfig | null,\n request: unknown,\n response: AccessioResponse | null,\n ) {\n super(message);\n this.name = 'AccessioError';\n this.code = code ?? null;\n this.config = redactConfig(config ?? null);\n this.request = request ?? null;\n this.response = response ?? null;\n this.isAccessioError = true;\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, AccessioError);\n }\n }\n\n toJSON(): Record<string, unknown> {\n return {\n name: this.name,\n message: this.message,\n code: this.code,\n status: this.response ? this.response.status : null,\n config: this.config,\n };\n }\n\n static from(\n error: Error,\n code: string,\n config: AccessioRequestConfig | null,\n request: unknown,\n response: AccessioResponse | null,\n ): AccessioError {\n const accessioError = new AccessioError(error.message, code, config, request, response);\n accessioError.cause = error;\n accessioError.stack = error.stack;\n return accessioError;\n }\n}\n\nexport default AccessioError;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBAAuB;AAGvB,SAAS,cAAc,SAA2B;AAChD,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,MAA+B,CAAC;AACtC,aAAW,OAAO,OAAO,KAAK,OAAkC,GAAG;AACjE,UAAM,QAAS,QAAoC,GAAG;AACtD,QAAI,mBAAmB,KAAK,GAAG,GAAG;AAChC,UAAI,GAAG,IAAI;AAAA,IACb,WAAW,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,KAAK,GAAG;AACtE,UAAI,GAAG,IAAI,cAAc,KAAK;AAAA,IAChC,OAAO;AACL,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,aAAa,QAAoE;AAC/F,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,QAAQ,EAAE,GAAG,OAAO;AAC1B,MAAI,UAAU,MAAO,QAAO,MAAM;AAClC,MAAI,MAAM,QAAS,OAAM,UAAU,cAAc,MAAM,OAAO;AAC9D,SAAO;AACT;AAEO,MAAM,sBAAsB,MAAM;AAAA,EACvC,OAAO,uBAA+B,kBAAAA,QAAW;AAAA,EACjD,OAAO,iBAAyB,kBAAAA,QAAW;AAAA,EAC3C,OAAO,eAAuB,kBAAAA,QAAW;AAAA,EACzC,OAAO,YAAoB,kBAAAA,QAAW;AAAA,EACtC,OAAO,cAAsB,kBAAAA,QAAW;AAAA,EACxC,OAAO,4BAAoC,kBAAAA,QAAW;AAAA,EACtD,OAAO,mBAA2B,kBAAAA,QAAW;AAAA,EAC7C,OAAO,kBAA0B,kBAAAA,QAAW;AAAA,EAC5C,OAAO,eAAuB,kBAAAA,QAAW;AAAA,EACzC,OAAO,kBAA0B,kBAAAA,QAAW;AAAA,EAC5C,OAAO,kBAA0B,kBAAAA,QAAW;AAAA,EAEnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EACS,OAAO;AAAA,EAEhB,YACE,SACA,MACA,QACA,SACA,UACA;AACA,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO,QAAQ;AACpB,SAAK,SAAS,aAAa,UAAU,IAAI;AACzC,SAAK,UAAU,WAAW;AAC1B,SAAK,WAAW,YAAY;AAC5B,SAAK,kBAAkB;AAEvB,QAAI,MAAM,mBAAmB;AAC3B,YAAM,kBAAkB,MAAM,aAAa;AAAA,IAC7C;AAAA,EACF;AAAA,EAEA,SAAkC;AAChC,WAAO;AAAA,MACL,MAAM,KAAK;AAAA,MACX,SAAS,KAAK;AAAA,MACd,MAAM,KAAK;AAAA,MACX,QAAQ,KAAK,WAAW,KAAK,SAAS,SAAS;AAAA,MAC/C,QAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAAA,EAEA,OAAO,KACL,OACA,MACA,QACA,SACA,UACe;AACf,UAAM,gBAAgB,IAAI,cAAc,MAAM,SAAS,MAAM,QAAQ,SAAS,QAAQ;AACtF,kBAAc,QAAQ;AACtB,kBAAc,QAAQ,MAAM;AAC5B,WAAO;AAAA,EACT;AACF;AAEA,IAAO,wBAAQ;","names":["ErrorCodes"]}
|
|
@@ -46,12 +46,18 @@ async function readResponseData(fetchResponse, config) {
|
|
|
46
46
|
default: {
|
|
47
47
|
const contentType = fetchResponse.headers.get("content-type") || "";
|
|
48
48
|
if (contentType.includes("application/json")) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
const text = await fetchResponse.text();
|
|
50
|
+
if (!text) return "";
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(text);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new import_accessioError.default(
|
|
55
|
+
`Failed to parse JSON response: ${err.message}. Raw body: ${text.length > 500 ? text.slice(0, 500) + "\u2026" : text}`,
|
|
56
|
+
import_accessioError.default.ERR_BAD_RESPONSE,
|
|
57
|
+
config,
|
|
58
|
+
fetchResponse,
|
|
59
|
+
null
|
|
60
|
+
);
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
return await fetchResponse.text();
|
|
@@ -164,6 +170,7 @@ async function fetchAdapter(config, fullURL, fetchOptions, requestStartTime) {
|
|
|
164
170
|
}
|
|
165
171
|
}
|
|
166
172
|
} catch (readError) {
|
|
173
|
+
if (readError instanceof import_accessioError.default) throw readError;
|
|
167
174
|
throw import_accessioError.default.from(
|
|
168
175
|
readError,
|
|
169
176
|
import_accessioError.default.ERR_BAD_RESPONSE,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/fetchAdapter.ts"],"sourcesContent":["import AccessioError from './accessioError';\nimport parseHeaders from '../helpers/parseHeaders';\nimport type { AccessioRequestConfig, AccessioResponse } from '../types';\n\nasync function readResponseData(\n fetchResponse: Response,\n config: AccessioRequestConfig,\n): Promise<unknown> {\n const responseType = config.responseType || 'json';\n switch (responseType) {\n case 'arraybuffer':\n return await fetchResponse.arrayBuffer();\n case 'blob':\n return await fetchResponse.blob();\n case 'stream':\n return fetchResponse.body;\n case 'json':\n default: {\n const contentType = fetchResponse.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n if (typeof fetchResponse.clone === 'function') {\n const text = await fetchResponse.clone().text();\n return text ? await fetchResponse.json() : '';\n } else {\n const text = await fetchResponse.text();\n return text ? JSON.parse(text) : '';\n }\n }\n return await fetchResponse.text();\n }\n }\n}\n\nexport default async function fetchAdapter(\n config: AccessioRequestConfig,\n fullURL: string,\n fetchOptions: RequestInit,\n requestStartTime: number,\n): Promise<AccessioResponse> {\n let abortController: AbortController | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let isTimedOut = false;\n let onUserAbort: (() => void) | null = null;\n\n if (\n config.timeout !== undefined &&\n (typeof config.timeout !== 'number' || isNaN(config.timeout) || config.timeout < 0)\n ) {\n throw new AccessioError(\n `Invalid timeout value: ${config.timeout}`,\n AccessioError.ERR_BAD_OPTION_VALUE,\n config,\n null,\n null,\n );\n }\n\n const timeoutValue = Number(config.timeout);\n if (!isNaN(timeoutValue) && timeoutValue > 0) {\n abortController = new AbortController();\n\n timeoutId = setTimeout(() => {\n isTimedOut = true;\n abortController!.abort(\n new AccessioError(\n `timeout of ${timeoutValue}ms exceeded`,\n AccessioError.ETIMEDOUT,\n config,\n null,\n null,\n ),\n );\n }, timeoutValue);\n\n if (config.signal) {\n if (typeof AbortSignal.any === 'function') {\n fetchOptions.signal = AbortSignal.any([config.signal, abortController.signal]);\n } else {\n if (config.signal.aborted) {\n abortController.abort(config.signal.reason);\n } else {\n onUserAbort = () => {\n if (!isTimedOut && abortController) {\n abortController.abort(config.signal!.reason);\n }\n };\n config.signal.addEventListener('abort', onUserAbort, {\n once: true,\n });\n }\n fetchOptions.signal = abortController.signal;\n }\n } else {\n fetchOptions.signal = abortController.signal;\n }\n } else if (config.signal) {\n fetchOptions.signal = config.signal;\n }\n\n try {\n const fetchImpl = config.fetch || fetch;\n let fetchResponse = await fetchImpl(fullURL, fetchOptions);\n\n if (config.onDownloadProgress && fetchResponse.body && config.responseType !== 'stream') {\n const contentLength = fetchResponse.headers.get('content-length');\n const total = contentLength ? parseInt(contentLength, 10) : 0;\n let loaded = 0;\n\n const reader = fetchResponse.body.getReader();\n const stream = new ReadableStream({\n async start(controller) {\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n break;\n }\n loaded += value.byteLength;\n config.onDownloadProgress!({ loaded, total });\n controller.enqueue(value);\n }\n } catch (e) {\n controller.error(e);\n }\n },\n });\n\n fetchResponse = new Response(stream, {\n headers: fetchResponse.headers,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n });\n }\n\n let responseData: unknown;\n\n const contentLength = fetchResponse.headers.get('content-length');\n if (\n contentLength &&\n config.maxContentLength &&\n parseInt(contentLength, 10) > config.maxContentLength\n ) {\n throw new AccessioError(\n `maxContentLength size of ${config.maxContentLength} exceeded`,\n AccessioError.ERR_BAD_RESPONSE,\n config,\n fetchResponse,\n null,\n );\n }\n\n try {\n responseData = await readResponseData(fetchResponse, config);\n if (config.schema) {\n if (typeof config.schema.parseAsync === 'function') {\n responseData = await config.schema.parseAsync(responseData);\n } else {\n responseData = config.schema.parse(responseData);\n }\n }\n } catch (readError) {\n throw AccessioError.from(\n readError as Error,\n AccessioError.ERR_BAD_RESPONSE,\n config,\n fetchResponse,\n null,\n );\n }\n\n const responseHeaders = parseHeaders(fetchResponse.headers);\n\n return {\n data: responseData,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n headers: responseHeaders,\n config: config,\n request: fetchResponse,\n duration: Date.now() - requestStartTime,\n };\n } catch (error) {\n if (error instanceof AccessioError) {\n throw error;\n }\n\n if (error instanceof Error && error.name === 'AbortError') {\n if (isTimedOut) {\n throw new AccessioError(\n `timeout of ${config.timeout}ms exceeded`,\n AccessioError.ETIMEDOUT,\n config,\n null,\n null,\n );\n }\n throw new AccessioError('Request aborted', AccessioError.ERR_CANCELED, config, null, null);\n }\n\n if (\n error instanceof TypeError &&\n (error.message.toLowerCase().includes('url') || error.message.toLowerCase().includes('fetch'))\n ) {\n throw new AccessioError(\n `Invalid URL: ${fullURL}`,\n AccessioError.ERR_INVALID_URL,\n config,\n null,\n null,\n );\n }\n\n throw AccessioError.from(\n error instanceof Error ? error : new Error(String(error)),\n AccessioError.ERR_NETWORK,\n config,\n null,\n null,\n );\n } finally {\n if (timeoutId) clearTimeout(timeoutId);\n if (config.signal && onUserAbort) {\n config.signal.removeEventListener('abort', onUserAbort);\n }\n }\n}\n"],"mappings":"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","names":["AccessioError","contentLength","parseHeaders"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/fetchAdapter.ts"],"sourcesContent":["import AccessioError from './accessioError';\nimport parseHeaders from '../helpers/parseHeaders';\nimport type { AccessioRequestConfig, AccessioResponse } from '../types';\n\nasync function readResponseData(\n fetchResponse: Response,\n config: AccessioRequestConfig,\n): Promise<unknown> {\n const responseType = config.responseType || 'json';\n switch (responseType) {\n case 'arraybuffer':\n return await fetchResponse.arrayBuffer();\n case 'blob':\n return await fetchResponse.blob();\n case 'stream':\n return fetchResponse.body;\n case 'json':\n default: {\n const contentType = fetchResponse.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n const text = await fetchResponse.text();\n if (!text) return '';\n try {\n return JSON.parse(text);\n } catch (err) {\n throw new AccessioError(\n `Failed to parse JSON response: ${(err as Error).message}. Raw body: ${\n text.length > 500 ? text.slice(0, 500) + '…' : text\n }`,\n AccessioError.ERR_BAD_RESPONSE,\n config,\n fetchResponse,\n null,\n );\n }\n }\n return await fetchResponse.text();\n }\n }\n}\n\nexport default async function fetchAdapter(\n config: AccessioRequestConfig,\n fullURL: string,\n fetchOptions: RequestInit,\n requestStartTime: number,\n): Promise<AccessioResponse> {\n let abortController: AbortController | null = null;\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let isTimedOut = false;\n let onUserAbort: (() => void) | null = null;\n\n if (\n config.timeout !== undefined &&\n (typeof config.timeout !== 'number' || isNaN(config.timeout) || config.timeout < 0)\n ) {\n throw new AccessioError(\n `Invalid timeout value: ${config.timeout}`,\n AccessioError.ERR_BAD_OPTION_VALUE,\n config,\n null,\n null,\n );\n }\n\n const timeoutValue = Number(config.timeout);\n if (!isNaN(timeoutValue) && timeoutValue > 0) {\n abortController = new AbortController();\n\n timeoutId = setTimeout(() => {\n isTimedOut = true;\n abortController!.abort(\n new AccessioError(\n `timeout of ${timeoutValue}ms exceeded`,\n AccessioError.ETIMEDOUT,\n config,\n null,\n null,\n ),\n );\n }, timeoutValue);\n\n if (config.signal) {\n if (typeof AbortSignal.any === 'function') {\n fetchOptions.signal = AbortSignal.any([config.signal, abortController.signal]);\n } else {\n if (config.signal.aborted) {\n abortController.abort(config.signal.reason);\n } else {\n onUserAbort = () => {\n if (!isTimedOut && abortController) {\n abortController.abort(config.signal!.reason);\n }\n };\n config.signal.addEventListener('abort', onUserAbort, {\n once: true,\n });\n }\n fetchOptions.signal = abortController.signal;\n }\n } else {\n fetchOptions.signal = abortController.signal;\n }\n } else if (config.signal) {\n fetchOptions.signal = config.signal;\n }\n\n try {\n const fetchImpl = config.fetch || fetch;\n let fetchResponse = await fetchImpl(fullURL, fetchOptions);\n\n if (config.onDownloadProgress && fetchResponse.body && config.responseType !== 'stream') {\n const contentLength = fetchResponse.headers.get('content-length');\n const total = contentLength ? parseInt(contentLength, 10) : 0;\n let loaded = 0;\n\n const reader = fetchResponse.body.getReader();\n const stream = new ReadableStream({\n async start(controller) {\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n break;\n }\n loaded += value.byteLength;\n config.onDownloadProgress!({ loaded, total });\n controller.enqueue(value);\n }\n } catch (e) {\n controller.error(e);\n }\n },\n });\n\n fetchResponse = new Response(stream, {\n headers: fetchResponse.headers,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n });\n }\n\n let responseData: unknown;\n\n const contentLength = fetchResponse.headers.get('content-length');\n if (\n contentLength &&\n config.maxContentLength &&\n parseInt(contentLength, 10) > config.maxContentLength\n ) {\n throw new AccessioError(\n `maxContentLength size of ${config.maxContentLength} exceeded`,\n AccessioError.ERR_BAD_RESPONSE,\n config,\n fetchResponse,\n null,\n );\n }\n\n try {\n responseData = await readResponseData(fetchResponse, config);\n if (config.schema) {\n if (typeof config.schema.parseAsync === 'function') {\n responseData = await config.schema.parseAsync(responseData);\n } else {\n responseData = config.schema.parse(responseData);\n }\n }\n } catch (readError) {\n if (readError instanceof AccessioError) throw readError;\n throw AccessioError.from(\n readError as Error,\n AccessioError.ERR_BAD_RESPONSE,\n config,\n fetchResponse,\n null,\n );\n }\n\n const responseHeaders = parseHeaders(fetchResponse.headers);\n\n return {\n data: responseData,\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n headers: responseHeaders,\n config: config,\n request: fetchResponse,\n duration: Date.now() - requestStartTime,\n };\n } catch (error) {\n if (error instanceof AccessioError) {\n throw error;\n }\n\n if (error instanceof Error && error.name === 'AbortError') {\n if (isTimedOut) {\n throw new AccessioError(\n `timeout of ${config.timeout}ms exceeded`,\n AccessioError.ETIMEDOUT,\n config,\n null,\n null,\n );\n }\n throw new AccessioError('Request aborted', AccessioError.ERR_CANCELED, config, null, null);\n }\n\n if (\n error instanceof TypeError &&\n (error.message.toLowerCase().includes('url') || error.message.toLowerCase().includes('fetch'))\n ) {\n throw new AccessioError(\n `Invalid URL: ${fullURL}`,\n AccessioError.ERR_INVALID_URL,\n config,\n null,\n null,\n );\n }\n\n throw AccessioError.from(\n error instanceof Error ? error : new Error(String(error)),\n AccessioError.ERR_NETWORK,\n config,\n null,\n null,\n );\n } finally {\n if (timeoutId) clearTimeout(timeoutId);\n if (config.signal && onUserAbort) {\n config.signal.removeEventListener('abort', onUserAbort);\n }\n }\n}\n"],"mappings":"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","names":["AccessioError","contentLength","parseHeaders"]}
|
package/cjs/core/request.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(request_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(request_exports);
|
|
34
34
|
var import_buildURL = __toESM(require("./buildURL"), 1);
|
|
35
35
|
var import_accessioError = __toESM(require("./accessioError"), 1);
|
|
36
|
+
var import_errorCodes = require("../constants/errorCodes");
|
|
36
37
|
var import_transformData = __toESM(require("../helpers/transformData"), 1);
|
|
37
38
|
var import_settle = __toESM(require("../helpers/settle"), 1);
|
|
38
39
|
var import_flattenHeaders = require("../helpers/flattenHeaders");
|
|
@@ -44,6 +45,24 @@ function buildTransformArray(transform) {
|
|
|
44
45
|
if (Array.isArray(transform)) return transform;
|
|
45
46
|
return [transform];
|
|
46
47
|
}
|
|
48
|
+
const DEFAULT_ALLOWED_PROTOCOLS = ["http:", "https:"];
|
|
49
|
+
function assertAllowedProtocol(fullURL, config) {
|
|
50
|
+
if (config.allowedProtocols === null) return;
|
|
51
|
+
const allowed = config.allowedProtocols ?? DEFAULT_ALLOWED_PROTOCOLS;
|
|
52
|
+
let scheme = null;
|
|
53
|
+
const match = /^([a-z][a-z\d+\-.]*):/i.exec(fullURL);
|
|
54
|
+
if (match) scheme = match[1].toLowerCase() + ":";
|
|
55
|
+
if (!scheme) return;
|
|
56
|
+
if (!allowed.includes(scheme)) {
|
|
57
|
+
throw new import_accessioError.default(
|
|
58
|
+
`URL protocol "${scheme}" is not allowed. Allowed: ${allowed.join(", ")}. Set config.allowedProtocols to extend, or null to disable the check.`,
|
|
59
|
+
import_errorCodes.ERR_BAD_OPTION,
|
|
60
|
+
config,
|
|
61
|
+
null,
|
|
62
|
+
null
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
47
66
|
const activeRequests = /* @__PURE__ */ new Map();
|
|
48
67
|
async function dispatchRequest(config) {
|
|
49
68
|
const fullURL = config._builtUrl || (0, import_buildURL.default)(
|
|
@@ -52,6 +71,7 @@ async function dispatchRequest(config) {
|
|
|
52
71
|
config.params,
|
|
53
72
|
config.paramsSerializer
|
|
54
73
|
);
|
|
74
|
+
assertAllowedProtocol(fullURL, config);
|
|
55
75
|
if (config.hooks?.onBeforeRequest) {
|
|
56
76
|
await config.hooks.onBeforeRequest(config);
|
|
57
77
|
}
|
|
@@ -99,12 +119,14 @@ async function dispatchRequest(config) {
|
|
|
99
119
|
}
|
|
100
120
|
const requestStartTime = Date.now();
|
|
101
121
|
const response = await (0, import_fetchAdapter.default)(config, fullURL, fetchOptions, requestStartTime);
|
|
122
|
+
response.config = (0, import_accessioError.redactConfig)(response.config);
|
|
102
123
|
const responseTransforms = buildTransformArray(config.transformResponse);
|
|
103
124
|
response.data = await (0, import_transformData.default)(
|
|
104
125
|
responseTransforms,
|
|
105
126
|
response.data,
|
|
106
127
|
response.headers,
|
|
107
|
-
config
|
|
128
|
+
config,
|
|
129
|
+
"response"
|
|
108
130
|
);
|
|
109
131
|
return new Promise((resolve, reject) => {
|
|
110
132
|
(0, import_settle.default)(
|
|
@@ -118,9 +140,10 @@ async function dispatchRequest(config) {
|
|
|
118
140
|
const promise = performRequest();
|
|
119
141
|
if (isGet && config.dedupe) {
|
|
120
142
|
activeRequests.set(cacheKey, promise);
|
|
121
|
-
|
|
143
|
+
const cleanup = () => {
|
|
122
144
|
activeRequests.delete(cacheKey);
|
|
123
|
-
}
|
|
145
|
+
};
|
|
146
|
+
promise.then(cleanup, cleanup);
|
|
124
147
|
}
|
|
125
148
|
try {
|
|
126
149
|
const response = await promise;
|
package/cjs/core/request.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/request.ts"],"sourcesContent":["import buildURL from './buildURL';\nimport AccessioError from './accessioError';\nimport transformData from '../helpers/transformData';\nimport settle from '../helpers/settle';\nimport { flattenHeaders, removeContentType, buildFetchHeaders } from '../helpers/flattenHeaders';\nimport { setBasicAuth } from '../helpers/auth';\nimport fetchAdapter from './fetchAdapter';\nimport { defaultMemoryCache } from '../helpers/memoryCache';\nimport type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';\n\ntype HeadersConfig = Record<string, Record<string, string | string[]>>;\n\nfunction buildTransformArray(\n transform: TransformFunction | TransformFunction[] | undefined,\n): TransformFunction[] {\n if (!transform) return [];\n if (Array.isArray(transform)) return transform;\n return [transform];\n}\n\nconst activeRequests = new Map<string, Promise<AccessioResponse>>();\n\nexport default async function dispatchRequest(\n config: AccessioRequestConfig,\n): Promise<AccessioResponse> {\n const fullURL =\n config._builtUrl ||\n buildURL(\n config.url ?? '',\n config.baseURL,\n config.params as Record<string, unknown> | undefined,\n config.paramsSerializer,\n );\n\n if (config.hooks?.onBeforeRequest) {\n await config.hooks.onBeforeRequest(config);\n }\n\n const isGet = (config.method || 'GET').toUpperCase() === 'GET';\n const cacheKey = isGet ? `GET:${fullURL}` : '';\n\n if (isGet && config.cache) {\n const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;\n const cached = await cacheProvider.get(cacheKey);\n if (cached) {\n if (config.hooks?.onRequestResponse) {\n await config.hooks.onRequestResponse(cached);\n }\n return cached;\n }\n }\n\n if (isGet && config.dedupe) {\n if (activeRequests.has(cacheKey)) {\n return activeRequests.get(cacheKey)!;\n }\n }\n\n const performRequest = async () => {\n const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);\n const requestTransforms = buildTransformArray(config.transformRequest);\n const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);\n\n if (\n requestData === null ||\n requestData === undefined ||\n (typeof FormData !== 'undefined' && requestData instanceof FormData)\n ) {\n removeContentType(flatHeaders);\n }\n\n setBasicAuth(config, flatHeaders);\n\n const fetchOptions: RequestInit = {\n method: (config.method || 'GET').toUpperCase(),\n headers: buildFetchHeaders(flatHeaders),\n };\n\n const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];\n if (\n methodsWithBody.includes(fetchOptions.method!) &&\n requestData !== undefined &&\n requestData !== null\n ) {\n fetchOptions.body = requestData as BodyInit;\n }\n\n if (config.withCredentials) {\n fetchOptions.credentials = 'include';\n }\n\n if (config.dispatcher) {\n (fetchOptions as any).dispatcher = config.dispatcher;\n }\n if (config.agent) {\n (fetchOptions as any).agent = config.agent;\n }\n\n const requestStartTime = Date.now();\n\n const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);\n\n const responseTransforms = buildTransformArray(config.transformResponse);\n\n response.data = await transformData(\n responseTransforms,\n response.data,\n response.headers,\n config,\n );\n\n return new Promise<AccessioResponse>((resolve, reject) => {\n settle(\n resolve as (value: AccessioResponse) => void,\n reject as (reason: AccessioError) => void,\n response,\n config,\n );\n });\n };\n\n const promise = performRequest();\n\n if (isGet && config.dedupe) {\n activeRequests.set(cacheKey, promise);\n
|
|
1
|
+
{"version":3,"sources":["../../src/core/request.ts"],"sourcesContent":["import buildURL from './buildURL';\nimport AccessioError, { redactConfig } from './accessioError';\nimport { ERR_BAD_OPTION } from '../constants/errorCodes';\nimport transformData from '../helpers/transformData';\nimport settle from '../helpers/settle';\nimport { flattenHeaders, removeContentType, buildFetchHeaders } from '../helpers/flattenHeaders';\nimport { setBasicAuth } from '../helpers/auth';\nimport fetchAdapter from './fetchAdapter';\nimport { defaultMemoryCache } from '../helpers/memoryCache';\nimport type { AccessioRequestConfig, AccessioResponse, TransformFunction } from '../types';\n\ntype HeadersConfig = Record<string, Record<string, string | string[]>>;\n\nfunction buildTransformArray(\n transform: TransformFunction | TransformFunction[] | undefined,\n): TransformFunction[] {\n if (!transform) return [];\n if (Array.isArray(transform)) return transform;\n return [transform];\n}\n\nconst DEFAULT_ALLOWED_PROTOCOLS = ['http:', 'https:'];\n\nfunction assertAllowedProtocol(fullURL: string, config: AccessioRequestConfig): void {\n if (config.allowedProtocols === null) return;\n const allowed = config.allowedProtocols ?? DEFAULT_ALLOWED_PROTOCOLS;\n\n let scheme: string | null = null;\n const match = /^([a-z][a-z\\d+\\-.]*):/i.exec(fullURL);\n if (match) scheme = match[1].toLowerCase() + ':';\n if (!scheme) return;\n\n if (!allowed.includes(scheme)) {\n throw new AccessioError(\n `URL protocol \"${scheme}\" is not allowed. Allowed: ${allowed.join(', ')}. ` +\n 'Set config.allowedProtocols to extend, or null to disable the check.',\n ERR_BAD_OPTION,\n config,\n null,\n null,\n );\n }\n}\n\nconst activeRequests = new Map<string, Promise<AccessioResponse>>();\n\nexport default async function dispatchRequest(\n config: AccessioRequestConfig,\n): Promise<AccessioResponse> {\n const fullURL =\n config._builtUrl ||\n buildURL(\n config.url ?? '',\n config.baseURL,\n config.params as Record<string, unknown> | undefined,\n config.paramsSerializer,\n );\n\n assertAllowedProtocol(fullURL, config);\n\n if (config.hooks?.onBeforeRequest) {\n await config.hooks.onBeforeRequest(config);\n }\n\n const isGet = (config.method || 'GET').toUpperCase() === 'GET';\n const cacheKey = isGet ? `GET:${fullURL}` : '';\n\n if (isGet && config.cache) {\n const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;\n const cached = await cacheProvider.get(cacheKey);\n if (cached) {\n if (config.hooks?.onRequestResponse) {\n await config.hooks.onRequestResponse(cached);\n }\n return cached;\n }\n }\n\n if (isGet && config.dedupe) {\n if (activeRequests.has(cacheKey)) {\n return activeRequests.get(cacheKey)!;\n }\n }\n\n const performRequest = async () => {\n const flatHeaders = flattenHeaders(config.headers as HeadersConfig | undefined, config.method);\n const requestTransforms = buildTransformArray(config.transformRequest);\n const requestData = await transformData(requestTransforms, config.data, flatHeaders, config);\n\n if (\n requestData === null ||\n requestData === undefined ||\n (typeof FormData !== 'undefined' && requestData instanceof FormData)\n ) {\n removeContentType(flatHeaders);\n }\n\n setBasicAuth(config, flatHeaders);\n\n const fetchOptions: RequestInit = {\n method: (config.method || 'GET').toUpperCase(),\n headers: buildFetchHeaders(flatHeaders),\n };\n\n const methodsWithBody = ['POST', 'PUT', 'PATCH', 'DELETE'];\n if (\n methodsWithBody.includes(fetchOptions.method!) &&\n requestData !== undefined &&\n requestData !== null\n ) {\n fetchOptions.body = requestData as BodyInit;\n }\n\n if (config.withCredentials) {\n fetchOptions.credentials = 'include';\n }\n\n if (config.dispatcher) {\n (fetchOptions as any).dispatcher = config.dispatcher;\n }\n if (config.agent) {\n (fetchOptions as any).agent = config.agent;\n }\n\n const requestStartTime = Date.now();\n\n const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);\n response.config = redactConfig(response.config) as typeof response.config;\n\n const responseTransforms = buildTransformArray(config.transformResponse);\n\n response.data = await transformData(\n responseTransforms,\n response.data,\n response.headers,\n config,\n 'response',\n );\n\n return new Promise<AccessioResponse>((resolve, reject) => {\n settle(\n resolve as (value: AccessioResponse) => void,\n reject as (reason: AccessioError) => void,\n response,\n config,\n );\n });\n };\n\n const promise = performRequest();\n\n if (isGet && config.dedupe) {\n activeRequests.set(cacheKey, promise);\n const cleanup = () => {\n activeRequests.delete(cacheKey);\n };\n promise.then(cleanup, cleanup);\n }\n\n try {\n const response = await promise;\n\n if (isGet && config.cache) {\n const cacheProvider = typeof config.cache === 'object' ? config.cache : defaultMemoryCache;\n await cacheProvider.set(cacheKey, response, config.cacheTTL);\n }\n\n if (config.hooks?.onRequestResponse) {\n await config.hooks.onRequestResponse(response);\n }\n\n return response;\n } catch (error) {\n if (config.hooks?.onRequestError && error instanceof AccessioError) {\n await config.hooks.onRequestError(error);\n }\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,sBAAqB;AACrB,2BAA4C;AAC5C,wBAA+B;AAC/B,2BAA0B;AAC1B,oBAAmB;AACnB,4BAAqE;AACrE,kBAA6B;AAC7B,0BAAyB;AACzB,yBAAmC;AAKnC,SAAS,oBACP,WACqB;AACrB,MAAI,CAAC,UAAW,QAAO,CAAC;AACxB,MAAI,MAAM,QAAQ,SAAS,EAAG,QAAO;AACrC,SAAO,CAAC,SAAS;AACnB;AAEA,MAAM,4BAA4B,CAAC,SAAS,QAAQ;AAEpD,SAAS,sBAAsB,SAAiB,QAAqC;AACnF,MAAI,OAAO,qBAAqB,KAAM;AACtC,QAAM,UAAU,OAAO,oBAAoB;AAE3C,MAAI,SAAwB;AAC5B,QAAM,QAAQ,yBAAyB,KAAK,OAAO;AACnD,MAAI,MAAO,UAAS,MAAM,CAAC,EAAE,YAAY,IAAI;AAC7C,MAAI,CAAC,OAAQ;AAEb,MAAI,CAAC,QAAQ,SAAS,MAAM,GAAG;AAC7B,UAAM,IAAI,qBAAAA;AAAA,MACR,iBAAiB,MAAM,8BAA8B,QAAQ,KAAK,IAAI,CAAC;AAAA,MAEvE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,iBAAiB,oBAAI,IAAuC;AAElE,eAAO,gBACL,QAC2B;AAC3B,QAAM,UACJ,OAAO,iBACP,gBAAAC;AAAA,IACE,OAAO,OAAO;AAAA,IACd,OAAO;AAAA,IACP,OAAO;AAAA,IACP,OAAO;AAAA,EACT;AAEF,wBAAsB,SAAS,MAAM;AAErC,MAAI,OAAO,OAAO,iBAAiB;AACjC,UAAM,OAAO,MAAM,gBAAgB,MAAM;AAAA,EAC3C;AAEA,QAAM,SAAS,OAAO,UAAU,OAAO,YAAY,MAAM;AACzD,QAAM,WAAW,QAAQ,OAAO,OAAO,KAAK;AAE5C,MAAI,SAAS,OAAO,OAAO;AACzB,UAAM,gBAAgB,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AACxE,UAAM,SAAS,MAAM,cAAc,IAAI,QAAQ;AAC/C,QAAI,QAAQ;AACV,UAAI,OAAO,OAAO,mBAAmB;AACnC,cAAM,OAAO,MAAM,kBAAkB,MAAM;AAAA,MAC7C;AACA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,SAAS,OAAO,QAAQ;AAC1B,QAAI,eAAe,IAAI,QAAQ,GAAG;AAChC,aAAO,eAAe,IAAI,QAAQ;AAAA,IACpC;AAAA,EACF;AAEA,QAAM,iBAAiB,YAAY;AACjC,UAAM,kBAAc,sCAAe,OAAO,SAAsC,OAAO,MAAM;AAC7F,UAAM,oBAAoB,oBAAoB,OAAO,gBAAgB;AACrE,UAAM,cAAc,UAAM,qBAAAC,SAAc,mBAAmB,OAAO,MAAM,aAAa,MAAM;AAE3F,QACE,gBAAgB,QAChB,gBAAgB,UACf,OAAO,aAAa,eAAe,uBAAuB,UAC3D;AACA,mDAAkB,WAAW;AAAA,IAC/B;AAEA,kCAAa,QAAQ,WAAW;AAEhC,UAAM,eAA4B;AAAA,MAChC,SAAS,OAAO,UAAU,OAAO,YAAY;AAAA,MAC7C,aAAS,yCAAkB,WAAW;AAAA,IACxC;AAEA,UAAM,kBAAkB,CAAC,QAAQ,OAAO,SAAS,QAAQ;AACzD,QACE,gBAAgB,SAAS,aAAa,MAAO,KAC7C,gBAAgB,UAChB,gBAAgB,MAChB;AACA,mBAAa,OAAO;AAAA,IACtB;AAEA,QAAI,OAAO,iBAAiB;AAC1B,mBAAa,cAAc;AAAA,IAC7B;AAEA,QAAI,OAAO,YAAY;AACrB,MAAC,aAAqB,aAAa,OAAO;AAAA,IAC5C;AACA,QAAI,OAAO,OAAO;AAChB,MAAC,aAAqB,QAAQ,OAAO;AAAA,IACvC;AAEA,UAAM,mBAAmB,KAAK,IAAI;AAElC,UAAM,WAAW,UAAM,oBAAAC,SAAa,QAAQ,SAAS,cAAc,gBAAgB;AACnF,aAAS,aAAS,mCAAa,SAAS,MAAM;AAE9C,UAAM,qBAAqB,oBAAoB,OAAO,iBAAiB;AAEvE,aAAS,OAAO,UAAM,qBAAAD;AAAA,MACpB;AAAA,MACA,SAAS;AAAA,MACT,SAAS;AAAA,MACT;AAAA,MACA;AAAA,IACF;AAEA,WAAO,IAAI,QAA0B,CAAC,SAAS,WAAW;AACxD,wBAAAE;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,eAAe;AAE/B,MAAI,SAAS,OAAO,QAAQ;AAC1B,mBAAe,IAAI,UAAU,OAAO;AACpC,UAAM,UAAU,MAAM;AACpB,qBAAe,OAAO,QAAQ;AAAA,IAChC;AACA,YAAQ,KAAK,SAAS,OAAO;AAAA,EAC/B;AAEA,MAAI;AACF,UAAM,WAAW,MAAM;AAEvB,QAAI,SAAS,OAAO,OAAO;AACzB,YAAM,gBAAgB,OAAO,OAAO,UAAU,WAAW,OAAO,QAAQ;AACxE,YAAM,cAAc,IAAI,UAAU,UAAU,OAAO,QAAQ;AAAA,IAC7D;AAEA,QAAI,OAAO,OAAO,mBAAmB;AACnC,YAAM,OAAO,MAAM,kBAAkB,QAAQ;AAAA,IAC/C;AAEA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,QAAI,OAAO,OAAO,kBAAkB,iBAAiB,qBAAAJ,SAAe;AAClE,YAAM,OAAO,MAAM,eAAe,KAAK;AAAA,IACzC;AACA,UAAM;AAAA,EACR;AACF;","names":["AccessioError","buildURL","transformData","fetchAdapter","settle"]}
|
package/cjs/core/retry.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var retry_exports = {};
|
|
20
30
|
__export(retry_exports, {
|
|
@@ -24,6 +34,12 @@ __export(retry_exports, {
|
|
|
24
34
|
});
|
|
25
35
|
module.exports = __toCommonJS(retry_exports);
|
|
26
36
|
var import_errorCodes = require("../constants/errorCodes");
|
|
37
|
+
var import_accessioError = __toESM(require("./accessioError"), 1);
|
|
38
|
+
function isUnretriableBody(data) {
|
|
39
|
+
if (data == null) return false;
|
|
40
|
+
if (typeof ReadableStream !== "undefined" && data instanceof ReadableStream) return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
27
43
|
function defaultRetryCondition(error) {
|
|
28
44
|
if (error.code === import_errorCodes.ERR_CANCELED) {
|
|
29
45
|
return false;
|
|
@@ -86,6 +102,15 @@ async function retryRequest(dispatchFn, config) {
|
|
|
86
102
|
if (!shouldRetry) {
|
|
87
103
|
throw error;
|
|
88
104
|
}
|
|
105
|
+
if (isUnretriableBody(config.data)) {
|
|
106
|
+
throw new import_accessioError.default(
|
|
107
|
+
"Request body is a ReadableStream and cannot be retried after consumption. Buffer the stream upstream or set retry: 0 for this call.",
|
|
108
|
+
import_errorCodes.ERR_BAD_OPTION,
|
|
109
|
+
config,
|
|
110
|
+
null,
|
|
111
|
+
error.response ?? null
|
|
112
|
+
);
|
|
113
|
+
}
|
|
89
114
|
let delay = calculateDelay(attempt, retryDelay);
|
|
90
115
|
if (config.retryOn429 && error.response?.status === 429) {
|
|
91
116
|
const headers = error.response?.headers;
|
package/cjs/core/retry.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/retry.ts"],"sourcesContent":["import { ERR_CANCELED, ERR_NETWORK } from '../constants/errorCodes';\nimport type {\n AccessioRequestConfig,\n AccessioError,\n RetryConditionFunction,\n OnRetryFunction,\n} from '../types';\n\nfunction defaultRetryCondition(error: any): boolean {\n if (error.code === ERR_CANCELED) {\n return false;\n }\n\n if (error.code === ERR_NETWORK) {\n return true;\n }\n\n if (error.response && error.response.status >= 500) {\n return true;\n }\n\n if (error.config?.retryOn429 && error.response && error.response.status === 429) {\n return true;\n }\n\n return false;\n}\n\nfunction calculateDelay(attempt: number, baseDelay: number): number {\n const exponentialDelay = baseDelay * Math.pow(2, attempt);\n const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);\n return Math.round(exponentialDelay + jitter);\n}\n\nfunction sleep(ms: number, options?: { signal?: AbortSignal }): Promise<void> {\n return new Promise((resolve, reject) => {\n let onAbort: (() => void) | undefined;\n\n const timeoutId = setTimeout(() => {\n if (options?.signal && onAbort) {\n options.signal.removeEventListener('abort', onAbort);\n }\n resolve();\n }, ms);\n\n if (options?.signal) {\n if (options.signal.aborted) {\n clearTimeout(timeoutId);\n return reject(options.signal.reason || new Error('Sleep aborted'));\n }\n\n onAbort = () => {\n clearTimeout(timeoutId);\n reject(options.signal!.reason || new Error('Sleep aborted'));\n };\n\n options.signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\nasync function retryRequest(\n dispatchFn: (config: AccessioRequestConfig) => Promise<any>,\n config: AccessioRequestConfig,\n): Promise<any> {\n const maxRetries = config.retry ?? 0;\n\n if (maxRetries <= 0 && !config.retryOn429) {\n return dispatchFn(config);\n }\n\n const retryDelay = config.retryDelay ?? 1000;\n const retryCondition: RetryConditionFunction = config.retryCondition ?? defaultRetryCondition;\n\n let lastError: any;\n const actualMaxRetries = Math.max(maxRetries, config.retryOn429 ? 3 : 0);\n\n for (let attempt = 0; attempt <= actualMaxRetries; attempt++) {\n try {\n const response = await dispatchFn(config);\n return response;\n } catch (error) {\n lastError = error;\n\n const isLastAttempt = attempt >= actualMaxRetries;\n const shouldRetry = !isLastAttempt && retryCondition(error as AccessioError);\n\n if (!shouldRetry) {\n throw error;\n }\n\n let delay = calculateDelay(attempt, retryDelay);\n\n if (config.retryOn429 && (error as any).response?.status === 429) {\n const headers = (error as any).response?.headers;\n const retryAfterStr = headers?.['retry-after'] || headers?.['Retry-After'];\n if (retryAfterStr) {\n const parsed = parseInt(retryAfterStr, 10);\n if (!isNaN(parsed)) {\n delay = parsed * 1000;\n } else {\n const date = new Date(retryAfterStr);\n if (!isNaN(date.getTime())) {\n delay = Math.max(0, date.getTime() - Date.now());\n }\n }\n }\n }\n\n if (typeof config.onRetry === 'function') {\n (config.onRetry as OnRetryFunction)(attempt + 1, error as AccessioError, config);\n }\n\n await sleep(delay, { signal: config.signal });\n }\n }\n\n throw lastError;\n}\n\nexport { defaultRetryCondition, calculateDelay };\nexport default retryRequest;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/core/retry.ts"],"sourcesContent":["import { ERR_BAD_OPTION, ERR_CANCELED, ERR_NETWORK } from '../constants/errorCodes';\nimport AccessioErrorClass from './accessioError';\nimport type {\n AccessioRequestConfig,\n AccessioError,\n RetryConditionFunction,\n OnRetryFunction,\n} from '../types';\n\nfunction isUnretriableBody(data: unknown): boolean {\n if (data == null) return false;\n if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) return true;\n return false;\n}\n\nfunction defaultRetryCondition(error: any): boolean {\n if (error.code === ERR_CANCELED) {\n return false;\n }\n\n if (error.code === ERR_NETWORK) {\n return true;\n }\n\n if (error.response && error.response.status >= 500) {\n return true;\n }\n\n if (error.config?.retryOn429 && error.response && error.response.status === 429) {\n return true;\n }\n\n return false;\n}\n\nfunction calculateDelay(attempt: number, baseDelay: number): number {\n const exponentialDelay = baseDelay * Math.pow(2, attempt);\n const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);\n return Math.round(exponentialDelay + jitter);\n}\n\nfunction sleep(ms: number, options?: { signal?: AbortSignal }): Promise<void> {\n return new Promise((resolve, reject) => {\n let onAbort: (() => void) | undefined;\n\n const timeoutId = setTimeout(() => {\n if (options?.signal && onAbort) {\n options.signal.removeEventListener('abort', onAbort);\n }\n resolve();\n }, ms);\n\n if (options?.signal) {\n if (options.signal.aborted) {\n clearTimeout(timeoutId);\n return reject(options.signal.reason || new Error('Sleep aborted'));\n }\n\n onAbort = () => {\n clearTimeout(timeoutId);\n reject(options.signal!.reason || new Error('Sleep aborted'));\n };\n\n options.signal.addEventListener('abort', onAbort, { once: true });\n }\n });\n}\n\nasync function retryRequest(\n dispatchFn: (config: AccessioRequestConfig) => Promise<any>,\n config: AccessioRequestConfig,\n): Promise<any> {\n const maxRetries = config.retry ?? 0;\n\n if (maxRetries <= 0 && !config.retryOn429) {\n return dispatchFn(config);\n }\n\n const retryDelay = config.retryDelay ?? 1000;\n const retryCondition: RetryConditionFunction = config.retryCondition ?? defaultRetryCondition;\n\n let lastError: any;\n const actualMaxRetries = Math.max(maxRetries, config.retryOn429 ? 3 : 0);\n\n for (let attempt = 0; attempt <= actualMaxRetries; attempt++) {\n try {\n const response = await dispatchFn(config);\n return response;\n } catch (error) {\n lastError = error;\n\n const isLastAttempt = attempt >= actualMaxRetries;\n const shouldRetry = !isLastAttempt && retryCondition(error as AccessioError);\n\n if (!shouldRetry) {\n throw error;\n }\n\n if (isUnretriableBody(config.data)) {\n throw new AccessioErrorClass(\n 'Request body is a ReadableStream and cannot be retried after consumption. ' +\n 'Buffer the stream upstream or set retry: 0 for this call.',\n ERR_BAD_OPTION,\n config,\n null,\n (error as AccessioError).response ?? null,\n );\n }\n\n let delay = calculateDelay(attempt, retryDelay);\n\n if (config.retryOn429 && (error as any).response?.status === 429) {\n const headers = (error as any).response?.headers;\n const retryAfterStr = headers?.['retry-after'] || headers?.['Retry-After'];\n if (retryAfterStr) {\n const parsed = parseInt(retryAfterStr, 10);\n if (!isNaN(parsed)) {\n delay = parsed * 1000;\n } else {\n const date = new Date(retryAfterStr);\n if (!isNaN(date.getTime())) {\n delay = Math.max(0, date.getTime() - Date.now());\n }\n }\n }\n }\n\n if (typeof config.onRetry === 'function') {\n (config.onRetry as OnRetryFunction)(attempt + 1, error as AccessioError, config);\n }\n\n await sleep(delay, { signal: config.signal });\n }\n }\n\n throw lastError;\n}\n\nexport { defaultRetryCondition, calculateDelay };\nexport default retryRequest;\n"],"mappings":"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","names":["AccessioErrorClass"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var flattenHeaders_exports = {};
|
|
20
30
|
__export(flattenHeaders_exports, {
|
|
@@ -23,6 +33,32 @@ __export(flattenHeaders_exports, {
|
|
|
23
33
|
removeContentType: () => removeContentType
|
|
24
34
|
});
|
|
25
35
|
module.exports = __toCommonJS(flattenHeaders_exports);
|
|
36
|
+
var import_accessioError = __toESM(require("../core/accessioError"), 1);
|
|
37
|
+
var import_errorCodes = require("../constants/errorCodes");
|
|
38
|
+
const HEADER_FORBIDDEN_CHAR = /[\r\n\0]/;
|
|
39
|
+
function assertSafeHeader(name, value) {
|
|
40
|
+
if (typeof name !== "string" || HEADER_FORBIDDEN_CHAR.test(name)) {
|
|
41
|
+
throw new import_accessioError.default(
|
|
42
|
+
`Invalid header name "${String(name)}": CR, LF and NUL are not allowed`,
|
|
43
|
+
import_errorCodes.ERR_BAD_OPTION,
|
|
44
|
+
null,
|
|
45
|
+
null,
|
|
46
|
+
null
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const values = Array.isArray(value) ? value : [value];
|
|
50
|
+
for (const v of values) {
|
|
51
|
+
if (typeof v === "string" && HEADER_FORBIDDEN_CHAR.test(v)) {
|
|
52
|
+
throw new import_accessioError.default(
|
|
53
|
+
`Invalid value for header "${name}": CR, LF and NUL are not allowed`,
|
|
54
|
+
import_errorCodes.ERR_BAD_OPTION,
|
|
55
|
+
null,
|
|
56
|
+
null,
|
|
57
|
+
null
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
26
62
|
const METHOD_KEYS = /* @__PURE__ */ new Set([
|
|
27
63
|
"common",
|
|
28
64
|
"delete",
|
|
@@ -59,6 +95,7 @@ function removeContentType(headers) {
|
|
|
59
95
|
function buildFetchHeaders(headers) {
|
|
60
96
|
const fetchHeaders = new Headers();
|
|
61
97
|
for (const [key, value] of Object.entries(headers)) {
|
|
98
|
+
assertSafeHeader(key, value);
|
|
62
99
|
if (Array.isArray(value)) {
|
|
63
100
|
for (const v of value) {
|
|
64
101
|
fetchHeaders.append(key, v);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/helpers/flattenHeaders.ts"],"sourcesContent":["const METHOD_KEYS = new Set<string>([\n 'common',\n 'delete',\n 'get',\n 'head',\n 'options',\n 'post',\n 'put',\n 'patch',\n]);\n\ntype HeadersConfig = Record<string, Record<string, string | string[]>>;\n\nexport function flattenHeaders(\n headers: HeadersConfig | undefined,\n method?: string,\n): Record<string, string | string[]> {\n if (!headers) return {};\n\n const merged: Record<string, string | string[]> = {};\n const methodLower = (method || 'get').toLowerCase();\n\n if (headers['common']) {\n Object.assign(merged, headers['common']);\n }\n\n if (headers[methodLower]) {\n Object.assign(merged, headers[methodLower]);\n }\n\n for (const key in headers) {\n if (Object.prototype.hasOwnProperty.call(headers, key) && !METHOD_KEYS.has(key)) {\n merged[key] = headers[key] as unknown as string | string[];\n }\n }\n\n return merged;\n}\n\nexport function removeContentType(headers: Record<string, string | string[]>): void {\n const keys = Object.keys(headers).filter((k) => k.toLowerCase() === 'content-type');\n for (const key of keys) {\n delete headers[key];\n }\n}\n\nexport function buildFetchHeaders(headers: Record<string, string | string[]>): Headers {\n const fetchHeaders = new Headers();\n for (const [key, value] of Object.entries(headers)) {\n if (Array.isArray(value)) {\n for (const v of value) {\n fetchHeaders.append(key, v);\n }\n } else {\n fetchHeaders.set(key, value);\n }\n }\n return fetchHeaders;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/helpers/flattenHeaders.ts"],"sourcesContent":["import AccessioError from '../core/accessioError';\nimport { ERR_BAD_OPTION } from '../constants/errorCodes';\n\nconst HEADER_FORBIDDEN_CHAR = /[\\r\\n\\0]/;\n\nfunction assertSafeHeader(name: string, value: string | string[]): void {\n if (typeof name !== 'string' || HEADER_FORBIDDEN_CHAR.test(name)) {\n throw new AccessioError(\n `Invalid header name \"${String(name)}\": CR, LF and NUL are not allowed`,\n ERR_BAD_OPTION,\n null,\n null,\n null,\n );\n }\n const values = Array.isArray(value) ? value : [value];\n for (const v of values) {\n if (typeof v === 'string' && HEADER_FORBIDDEN_CHAR.test(v)) {\n throw new AccessioError(\n `Invalid value for header \"${name}\": CR, LF and NUL are not allowed`,\n ERR_BAD_OPTION,\n null,\n null,\n null,\n );\n }\n }\n}\n\nconst METHOD_KEYS = new Set<string>([\n 'common',\n 'delete',\n 'get',\n 'head',\n 'options',\n 'post',\n 'put',\n 'patch',\n]);\n\ntype HeadersConfig = Record<string, Record<string, string | string[]>>;\n\nexport function flattenHeaders(\n headers: HeadersConfig | undefined,\n method?: string,\n): Record<string, string | string[]> {\n if (!headers) return {};\n\n const merged: Record<string, string | string[]> = {};\n const methodLower = (method || 'get').toLowerCase();\n\n if (headers['common']) {\n Object.assign(merged, headers['common']);\n }\n\n if (headers[methodLower]) {\n Object.assign(merged, headers[methodLower]);\n }\n\n for (const key in headers) {\n if (Object.prototype.hasOwnProperty.call(headers, key) && !METHOD_KEYS.has(key)) {\n merged[key] = headers[key] as unknown as string | string[];\n }\n }\n\n return merged;\n}\n\nexport function removeContentType(headers: Record<string, string | string[]>): void {\n const keys = Object.keys(headers).filter((k) => k.toLowerCase() === 'content-type');\n for (const key of keys) {\n delete headers[key];\n }\n}\n\nexport function buildFetchHeaders(headers: Record<string, string | string[]>): Headers {\n const fetchHeaders = new Headers();\n for (const [key, value] of Object.entries(headers)) {\n assertSafeHeader(key, value);\n if (Array.isArray(value)) {\n for (const v of value) {\n fetchHeaders.append(key, v);\n }\n } else {\n fetchHeaders.set(key, value);\n }\n }\n return fetchHeaders;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2BAA0B;AAC1B,wBAA+B;AAE/B,MAAM,wBAAwB;AAE9B,SAAS,iBAAiB,MAAc,OAAgC;AACtE,MAAI,OAAO,SAAS,YAAY,sBAAsB,KAAK,IAAI,GAAG;AAChE,UAAM,IAAI,qBAAAA;AAAA,MACR,wBAAwB,OAAO,IAAI,CAAC;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK;AACpD,aAAW,KAAK,QAAQ;AACtB,QAAI,OAAO,MAAM,YAAY,sBAAsB,KAAK,CAAC,GAAG;AAC1D,YAAM,IAAI,qBAAAA;AAAA,QACR,6BAA6B,IAAI;AAAA,QACjC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,MAAM,cAAc,oBAAI,IAAY;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAIM,SAAS,eACd,SACA,QACmC;AACnC,MAAI,CAAC,QAAS,QAAO,CAAC;AAEtB,QAAM,SAA4C,CAAC;AACnD,QAAM,eAAe,UAAU,OAAO,YAAY;AAElD,MAAI,QAAQ,QAAQ,GAAG;AACrB,WAAO,OAAO,QAAQ,QAAQ,QAAQ,CAAC;AAAA,EACzC;AAEA,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,OAAO,QAAQ,QAAQ,WAAW,CAAC;AAAA,EAC5C;AAEA,aAAW,OAAO,SAAS;AACzB,QAAI,OAAO,UAAU,eAAe,KAAK,SAAS,GAAG,KAAK,CAAC,YAAY,IAAI,GAAG,GAAG;AAC/E,aAAO,GAAG,IAAI,QAAQ,GAAG;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,kBAAkB,SAAkD;AAClF,QAAM,OAAO,OAAO,KAAK,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,YAAY,MAAM,cAAc;AAClF,aAAW,OAAO,MAAM;AACtB,WAAO,QAAQ,GAAG;AAAA,EACpB;AACF;AAEO,SAAS,kBAAkB,SAAqD;AACrF,QAAM,eAAe,IAAI,QAAQ;AACjC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,qBAAiB,KAAK,KAAK;AAC3B,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,iBAAW,KAAK,OAAO;AACrB,qBAAa,OAAO,KAAK,CAAC;AAAA,MAC5B;AAAA,IACF,OAAO;AACL,mBAAa,IAAI,KAAK,KAAK;AAAA,IAC7B;AAAA,EACF;AACA,SAAO;AACT;","names":["AccessioError"]}
|
|
@@ -36,10 +36,7 @@ function createRateLimiter(maxConcurrent = Infinity, maxQueueSize = Infinity) {
|
|
|
36
36
|
}
|
|
37
37
|
let active = 0;
|
|
38
38
|
let destroyed = false;
|
|
39
|
-
|
|
40
|
-
let tail = 0;
|
|
41
|
-
let pendingCount = 0;
|
|
42
|
-
const queue = {};
|
|
39
|
+
const queue = [];
|
|
43
40
|
function acquire() {
|
|
44
41
|
if (destroyed) {
|
|
45
42
|
return Promise.reject(new Error("[Accessio] Rate limiter has been destroyed"));
|
|
@@ -48,38 +45,30 @@ function createRateLimiter(maxConcurrent = Infinity, maxQueueSize = Infinity) {
|
|
|
48
45
|
active++;
|
|
49
46
|
return Promise.resolve();
|
|
50
47
|
}
|
|
51
|
-
if (
|
|
48
|
+
if (queue.length >= maxQueueSize) {
|
|
52
49
|
return Promise.reject(
|
|
53
50
|
new Error(`[Accessio] Rate limiter queue size exceeded maxQueueSize (${maxQueueSize})`)
|
|
54
51
|
);
|
|
55
52
|
}
|
|
56
53
|
return new Promise((resolve, reject) => {
|
|
57
|
-
queue
|
|
58
|
-
pendingCount++;
|
|
54
|
+
queue.push({ resolve, reject });
|
|
59
55
|
});
|
|
60
56
|
}
|
|
61
57
|
function release() {
|
|
62
58
|
if (destroyed) return;
|
|
63
59
|
if (active <= 0) return;
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
delete queue[head];
|
|
69
|
-
head++;
|
|
70
|
-
pendingCount--;
|
|
71
|
-
next?.resolve();
|
|
60
|
+
const next = queue.shift();
|
|
61
|
+
if (next) {
|
|
62
|
+
next.resolve();
|
|
63
|
+
return;
|
|
72
64
|
}
|
|
65
|
+
active--;
|
|
73
66
|
}
|
|
74
67
|
function destroy() {
|
|
75
68
|
destroyed = true;
|
|
76
69
|
const reason = new Error("[Accessio] Rate limiter destroyed \u2014 pending request cancelled");
|
|
77
|
-
while (
|
|
78
|
-
|
|
79
|
-
delete queue[head];
|
|
80
|
-
head++;
|
|
81
|
-
pendingCount--;
|
|
82
|
-
next?.reject(reason);
|
|
70
|
+
while (queue.length > 0) {
|
|
71
|
+
queue.shift().reject(reason);
|
|
83
72
|
}
|
|
84
73
|
}
|
|
85
74
|
return {
|
|
@@ -87,7 +76,7 @@ function createRateLimiter(maxConcurrent = Infinity, maxQueueSize = Infinity) {
|
|
|
87
76
|
release,
|
|
88
77
|
destroy,
|
|
89
78
|
get pending() {
|
|
90
|
-
return
|
|
79
|
+
return queue.length;
|
|
91
80
|
},
|
|
92
81
|
get active() {
|
|
93
82
|
return active;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/helpers/rateLimiter.ts"],"sourcesContent":["import type { RateLimiter, AccessioRequestConfig, AccessioResponse } from '../types';\n\ninterface QueueItem {\n resolve: () => void;\n reject: (reason: Error) => void;\n}\n\nexport function createRateLimiter(\n maxConcurrent: number = Infinity,\n maxQueueSize: number = Infinity,\n): RateLimiter {\n if (maxConcurrent !== Infinity && (!Number.isInteger(maxConcurrent) || maxConcurrent < 1)) {\n throw new RangeError(\n `[Accessio] maxConcurrent must be a positive integer or Infinity, got: ${maxConcurrent}`,\n );\n }\n if (maxQueueSize !== Infinity && (!Number.isInteger(maxQueueSize) || maxQueueSize < 1)) {\n throw new RangeError(\n `[Accessio] maxQueueSize must be a positive integer or Infinity, got: ${maxQueueSize}`,\n );\n }\n let active = 0;\n let destroyed = false;\n
|
|
1
|
+
{"version":3,"sources":["../../src/helpers/rateLimiter.ts"],"sourcesContent":["import type { RateLimiter, AccessioRequestConfig, AccessioResponse } from '../types';\n\ninterface QueueItem {\n resolve: () => void;\n reject: (reason: Error) => void;\n}\n\nexport function createRateLimiter(\n maxConcurrent: number = Infinity,\n maxQueueSize: number = Infinity,\n): RateLimiter {\n if (maxConcurrent !== Infinity && (!Number.isInteger(maxConcurrent) || maxConcurrent < 1)) {\n throw new RangeError(\n `[Accessio] maxConcurrent must be a positive integer or Infinity, got: ${maxConcurrent}`,\n );\n }\n if (maxQueueSize !== Infinity && (!Number.isInteger(maxQueueSize) || maxQueueSize < 1)) {\n throw new RangeError(\n `[Accessio] maxQueueSize must be a positive integer or Infinity, got: ${maxQueueSize}`,\n );\n }\n let active = 0;\n let destroyed = false;\n const queue: QueueItem[] = [];\n\n function acquire(): Promise<void> {\n if (destroyed) {\n return Promise.reject(new Error('[Accessio] Rate limiter has been destroyed'));\n }\n\n if (active < maxConcurrent) {\n active++;\n return Promise.resolve();\n }\n\n if (queue.length >= maxQueueSize) {\n return Promise.reject(\n new Error(`[Accessio] Rate limiter queue size exceeded maxQueueSize (${maxQueueSize})`),\n );\n }\n\n return new Promise((resolve, reject) => {\n queue.push({ resolve, reject });\n });\n }\n\n function release(): void {\n if (destroyed) return;\n if (active <= 0) return;\n\n const next = queue.shift();\n if (next) {\n next.resolve();\n return;\n }\n active--;\n }\n\n function destroy(): void {\n destroyed = true;\n const reason = new Error('[Accessio] Rate limiter destroyed — pending request cancelled');\n while (queue.length > 0) {\n queue.shift()!.reject(reason);\n }\n }\n\n return {\n acquire,\n release,\n destroy,\n get pending() {\n return queue.length;\n },\n get active() {\n return active;\n },\n get destroyed() {\n return destroyed;\n },\n };\n}\n\nexport async function rateLimitedRequest<T = unknown>(\n dispatchFn: (config: AccessioRequestConfig) => Promise<AccessioResponse<T>>,\n limiter: RateLimiter,\n config: AccessioRequestConfig,\n): Promise<AccessioResponse<T>> {\n await limiter.acquire();\n try {\n return await dispatchFn(config);\n } finally {\n limiter.release();\n }\n}\n\nexport default createRateLimiter;\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,SAAS,kBACd,gBAAwB,UACxB,eAAuB,UACV;AACb,MAAI,kBAAkB,aAAa,CAAC,OAAO,UAAU,aAAa,KAAK,gBAAgB,IAAI;AACzF,UAAM,IAAI;AAAA,MACR,yEAAyE,aAAa;AAAA,IACxF;AAAA,EACF;AACA,MAAI,iBAAiB,aAAa,CAAC,OAAO,UAAU,YAAY,KAAK,eAAe,IAAI;AACtF,UAAM,IAAI;AAAA,MACR,wEAAwE,YAAY;AAAA,IACtF;AAAA,EACF;AACA,MAAI,SAAS;AACb,MAAI,YAAY;AAChB,QAAM,QAAqB,CAAC;AAE5B,WAAS,UAAyB;AAChC,QAAI,WAAW;AACb,aAAO,QAAQ,OAAO,IAAI,MAAM,4CAA4C,CAAC;AAAA,IAC/E;AAEA,QAAI,SAAS,eAAe;AAC1B;AACA,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,QAAI,MAAM,UAAU,cAAc;AAChC,aAAO,QAAQ;AAAA,QACb,IAAI,MAAM,6DAA6D,YAAY,GAAG;AAAA,MACxF;AAAA,IACF;AAEA,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,KAAK,EAAE,SAAS,OAAO,CAAC;AAAA,IAChC,CAAC;AAAA,EACH;AAEA,WAAS,UAAgB;AACvB,QAAI,UAAW;AACf,QAAI,UAAU,EAAG;AAEjB,UAAM,OAAO,MAAM,MAAM;AACzB,QAAI,MAAM;AACR,WAAK,QAAQ;AACb;AAAA,IACF;AACA;AAAA,EACF;AAEA,WAAS,UAAgB;AACvB,gBAAY;AACZ,UAAM,SAAS,IAAI,MAAM,oEAA+D;AACxF,WAAO,MAAM,SAAS,GAAG;AACvB,YAAM,MAAM,EAAG,OAAO,MAAM;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,IAAI,UAAU;AACZ,aAAO,MAAM;AAAA,IACf;AAAA,IACA,IAAI,SAAS;AACX,aAAO;AAAA,IACT;AAAA,IACA,IAAI,YAAY;AACd,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,eAAsB,mBACpB,YACA,SACA,QAC8B;AAC9B,QAAM,QAAQ,QAAQ;AACtB,MAAI;AACF,WAAO,MAAM,WAAW,MAAM;AAAA,EAChC,UAAE;AACA,YAAQ,QAAQ;AAAA,EAClB;AACF;AAEA,IAAO,sBAAQ;","names":[]}
|
|
@@ -32,7 +32,7 @@ __export(transformData_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(transformData_exports);
|
|
34
34
|
var import_accessioError = __toESM(require("../core/accessioError"), 1);
|
|
35
|
-
async function transformData(transforms, data, headers, config) {
|
|
35
|
+
async function transformData(transforms, data, headers, config, direction = "request") {
|
|
36
36
|
if (!transforms || !Array.isArray(transforms)) {
|
|
37
37
|
return data;
|
|
38
38
|
}
|
|
@@ -44,7 +44,7 @@ async function transformData(transforms, data, headers, config) {
|
|
|
44
44
|
} catch (err) {
|
|
45
45
|
throw import_accessioError.default.from(
|
|
46
46
|
err instanceof Error ? err : new Error(String(err)),
|
|
47
|
-
import_accessioError.default.ERR_BAD_REQUEST,
|
|
47
|
+
direction === "response" ? import_accessioError.default.ERR_BAD_RESPONSE : import_accessioError.default.ERR_BAD_REQUEST,
|
|
48
48
|
config ?? null,
|
|
49
49
|
null,
|
|
50
50
|
null
|
|
@@ -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 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,
|
|
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 direction: 'request' | 'response' = 'request',\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 direction === 'response' ? AccessioError.ERR_BAD_RESPONSE : 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,QACA,YAAoC,WAClB;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,cAAc,aAAa,qBAAAA,QAAc,mBAAmB,qBAAAA,QAAc;AAAA,UAC1E,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":["AccessioError"]}
|
package/index.d.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface AccessioRequestConfig {
|
|
|
55
55
|
/** Include credentials in cross-site requests */
|
|
56
56
|
withCredentials?: boolean;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* URL protocols accepted by the client. Defaults to `["http:", "https:"]`.
|
|
60
|
+
* Pass an extended array to allow more (e.g. `["http:", "https:", "ws:"]`),
|
|
61
|
+
* or `null` to disable the check entirely.
|
|
62
|
+
*/
|
|
63
|
+
allowedProtocols?: string[] | null;
|
|
64
|
+
|
|
58
65
|
/** Expected response data type */
|
|
59
66
|
responseType?: "json" | "text" | "blob" | "arraybuffer" | "stream";
|
|
60
67
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import ErrorCodes from '../constants/errorCodes';
|
|
2
2
|
import type { AccessioRequestConfig, AccessioResponse } from '../types';
|
|
3
3
|
|
|
4
|
+
function redactHeaders(headers: unknown): unknown {
|
|
5
|
+
if (!headers || typeof headers !== 'object') return headers;
|
|
6
|
+
const out: Record<string, unknown> = {};
|
|
7
|
+
for (const key of Object.keys(headers as Record<string, unknown>)) {
|
|
8
|
+
const value = (headers as Record<string, unknown>)[key];
|
|
9
|
+
if (/^authorization$/i.test(key)) {
|
|
10
|
+
out[key] = '[REDACTED]';
|
|
11
|
+
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
12
|
+
out[key] = redactHeaders(value);
|
|
13
|
+
} else {
|
|
14
|
+
out[key] = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function redactConfig(config: AccessioRequestConfig | null): AccessioRequestConfig | null {
|
|
21
|
+
if (!config) return config;
|
|
22
|
+
const clone = { ...config } as AccessioRequestConfig & { auth?: unknown };
|
|
23
|
+
if ('auth' in clone) delete clone.auth;
|
|
24
|
+
if (clone.headers) clone.headers = redactHeaders(clone.headers) as typeof clone.headers;
|
|
25
|
+
return clone;
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
export class AccessioError extends Error {
|
|
5
29
|
static ERR_BAD_OPTION_VALUE: string = ErrorCodes.ERR_BAD_OPTION_VALUE;
|
|
6
30
|
static ERR_BAD_OPTION: string = ErrorCodes.ERR_BAD_OPTION;
|
|
@@ -32,7 +56,7 @@ export class AccessioError extends Error {
|
|
|
32
56
|
super(message);
|
|
33
57
|
this.name = 'AccessioError';
|
|
34
58
|
this.code = code ?? null;
|
|
35
|
-
this.config = config ?? null;
|
|
59
|
+
this.config = redactConfig(config ?? null);
|
|
36
60
|
this.request = request ?? null;
|
|
37
61
|
this.response = response ?? null;
|
|
38
62
|
this.isAccessioError = true;
|
package/src/core/fetchAdapter.ts
CHANGED
|
@@ -18,12 +18,20 @@ async function readResponseData(
|
|
|
18
18
|
default: {
|
|
19
19
|
const contentType = fetchResponse.headers.get('content-type') || '';
|
|
20
20
|
if (contentType.includes('application/json')) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const text = await fetchResponse.text();
|
|
22
|
+
if (!text) return '';
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(text);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new AccessioError(
|
|
27
|
+
`Failed to parse JSON response: ${(err as Error).message}. Raw body: ${
|
|
28
|
+
text.length > 500 ? text.slice(0, 500) + '…' : text
|
|
29
|
+
}`,
|
|
30
|
+
AccessioError.ERR_BAD_RESPONSE,
|
|
31
|
+
config,
|
|
32
|
+
fetchResponse,
|
|
33
|
+
null,
|
|
34
|
+
);
|
|
27
35
|
}
|
|
28
36
|
}
|
|
29
37
|
return await fetchResponse.text();
|
|
@@ -160,6 +168,7 @@ export default async function fetchAdapter(
|
|
|
160
168
|
}
|
|
161
169
|
}
|
|
162
170
|
} catch (readError) {
|
|
171
|
+
if (readError instanceof AccessioError) throw readError;
|
|
163
172
|
throw AccessioError.from(
|
|
164
173
|
readError as Error,
|
|
165
174
|
AccessioError.ERR_BAD_RESPONSE,
|
package/src/core/request.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import buildURL from './buildURL';
|
|
2
|
-
import AccessioError from './accessioError';
|
|
2
|
+
import AccessioError, { redactConfig } from './accessioError';
|
|
3
|
+
import { ERR_BAD_OPTION } from '../constants/errorCodes';
|
|
3
4
|
import transformData from '../helpers/transformData';
|
|
4
5
|
import settle from '../helpers/settle';
|
|
5
6
|
import { flattenHeaders, removeContentType, buildFetchHeaders } from '../helpers/flattenHeaders';
|
|
@@ -18,6 +19,29 @@ function buildTransformArray(
|
|
|
18
19
|
return [transform];
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
const DEFAULT_ALLOWED_PROTOCOLS = ['http:', 'https:'];
|
|
23
|
+
|
|
24
|
+
function assertAllowedProtocol(fullURL: string, config: AccessioRequestConfig): void {
|
|
25
|
+
if (config.allowedProtocols === null) return;
|
|
26
|
+
const allowed = config.allowedProtocols ?? DEFAULT_ALLOWED_PROTOCOLS;
|
|
27
|
+
|
|
28
|
+
let scheme: string | null = null;
|
|
29
|
+
const match = /^([a-z][a-z\d+\-.]*):/i.exec(fullURL);
|
|
30
|
+
if (match) scheme = match[1].toLowerCase() + ':';
|
|
31
|
+
if (!scheme) return;
|
|
32
|
+
|
|
33
|
+
if (!allowed.includes(scheme)) {
|
|
34
|
+
throw new AccessioError(
|
|
35
|
+
`URL protocol "${scheme}" is not allowed. Allowed: ${allowed.join(', ')}. ` +
|
|
36
|
+
'Set config.allowedProtocols to extend, or null to disable the check.',
|
|
37
|
+
ERR_BAD_OPTION,
|
|
38
|
+
config,
|
|
39
|
+
null,
|
|
40
|
+
null,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
const activeRequests = new Map<string, Promise<AccessioResponse>>();
|
|
22
46
|
|
|
23
47
|
export default async function dispatchRequest(
|
|
@@ -32,6 +56,8 @@ export default async function dispatchRequest(
|
|
|
32
56
|
config.paramsSerializer,
|
|
33
57
|
);
|
|
34
58
|
|
|
59
|
+
assertAllowedProtocol(fullURL, config);
|
|
60
|
+
|
|
35
61
|
if (config.hooks?.onBeforeRequest) {
|
|
36
62
|
await config.hooks.onBeforeRequest(config);
|
|
37
63
|
}
|
|
@@ -99,6 +125,7 @@ export default async function dispatchRequest(
|
|
|
99
125
|
const requestStartTime = Date.now();
|
|
100
126
|
|
|
101
127
|
const response = await fetchAdapter(config, fullURL, fetchOptions, requestStartTime);
|
|
128
|
+
response.config = redactConfig(response.config) as typeof response.config;
|
|
102
129
|
|
|
103
130
|
const responseTransforms = buildTransformArray(config.transformResponse);
|
|
104
131
|
|
|
@@ -107,6 +134,7 @@ export default async function dispatchRequest(
|
|
|
107
134
|
response.data,
|
|
108
135
|
response.headers,
|
|
109
136
|
config,
|
|
137
|
+
'response',
|
|
110
138
|
);
|
|
111
139
|
|
|
112
140
|
return new Promise<AccessioResponse>((resolve, reject) => {
|
|
@@ -123,9 +151,10 @@ export default async function dispatchRequest(
|
|
|
123
151
|
|
|
124
152
|
if (isGet && config.dedupe) {
|
|
125
153
|
activeRequests.set(cacheKey, promise);
|
|
126
|
-
|
|
154
|
+
const cleanup = () => {
|
|
127
155
|
activeRequests.delete(cacheKey);
|
|
128
|
-
}
|
|
156
|
+
};
|
|
157
|
+
promise.then(cleanup, cleanup);
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
try {
|
package/src/core/retry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ERR_CANCELED, ERR_NETWORK } from '../constants/errorCodes';
|
|
1
|
+
import { ERR_BAD_OPTION, ERR_CANCELED, ERR_NETWORK } from '../constants/errorCodes';
|
|
2
|
+
import AccessioErrorClass from './accessioError';
|
|
2
3
|
import type {
|
|
3
4
|
AccessioRequestConfig,
|
|
4
5
|
AccessioError,
|
|
@@ -6,6 +7,12 @@ import type {
|
|
|
6
7
|
OnRetryFunction,
|
|
7
8
|
} from '../types';
|
|
8
9
|
|
|
10
|
+
function isUnretriableBody(data: unknown): boolean {
|
|
11
|
+
if (data == null) return false;
|
|
12
|
+
if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) return true;
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
function defaultRetryCondition(error: any): boolean {
|
|
10
17
|
if (error.code === ERR_CANCELED) {
|
|
11
18
|
return false;
|
|
@@ -89,6 +96,17 @@ async function retryRequest(
|
|
|
89
96
|
throw error;
|
|
90
97
|
}
|
|
91
98
|
|
|
99
|
+
if (isUnretriableBody(config.data)) {
|
|
100
|
+
throw new AccessioErrorClass(
|
|
101
|
+
'Request body is a ReadableStream and cannot be retried after consumption. ' +
|
|
102
|
+
'Buffer the stream upstream or set retry: 0 for this call.',
|
|
103
|
+
ERR_BAD_OPTION,
|
|
104
|
+
config,
|
|
105
|
+
null,
|
|
106
|
+
(error as AccessioError).response ?? null,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
let delay = calculateDelay(attempt, retryDelay);
|
|
93
111
|
|
|
94
112
|
if (config.retryOn429 && (error as any).response?.status === 429) {
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
import AccessioError from '../core/accessioError';
|
|
2
|
+
import { ERR_BAD_OPTION } from '../constants/errorCodes';
|
|
3
|
+
|
|
4
|
+
const HEADER_FORBIDDEN_CHAR = /[\r\n\0]/;
|
|
5
|
+
|
|
6
|
+
function assertSafeHeader(name: string, value: string | string[]): void {
|
|
7
|
+
if (typeof name !== 'string' || HEADER_FORBIDDEN_CHAR.test(name)) {
|
|
8
|
+
throw new AccessioError(
|
|
9
|
+
`Invalid header name "${String(name)}": CR, LF and NUL are not allowed`,
|
|
10
|
+
ERR_BAD_OPTION,
|
|
11
|
+
null,
|
|
12
|
+
null,
|
|
13
|
+
null,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const values = Array.isArray(value) ? value : [value];
|
|
17
|
+
for (const v of values) {
|
|
18
|
+
if (typeof v === 'string' && HEADER_FORBIDDEN_CHAR.test(v)) {
|
|
19
|
+
throw new AccessioError(
|
|
20
|
+
`Invalid value for header "${name}": CR, LF and NUL are not allowed`,
|
|
21
|
+
ERR_BAD_OPTION,
|
|
22
|
+
null,
|
|
23
|
+
null,
|
|
24
|
+
null,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
1
30
|
const METHOD_KEYS = new Set<string>([
|
|
2
31
|
'common',
|
|
3
32
|
'delete',
|
|
@@ -47,6 +76,7 @@ export function removeContentType(headers: Record<string, string | string[]>): v
|
|
|
47
76
|
export function buildFetchHeaders(headers: Record<string, string | string[]>): Headers {
|
|
48
77
|
const fetchHeaders = new Headers();
|
|
49
78
|
for (const [key, value] of Object.entries(headers)) {
|
|
79
|
+
assertSafeHeader(key, value);
|
|
50
80
|
if (Array.isArray(value)) {
|
|
51
81
|
for (const v of value) {
|
|
52
82
|
fetchHeaders.append(key, v);
|
|
@@ -21,10 +21,7 @@ export function createRateLimiter(
|
|
|
21
21
|
}
|
|
22
22
|
let active = 0;
|
|
23
23
|
let destroyed = false;
|
|
24
|
-
|
|
25
|
-
let tail = 0;
|
|
26
|
-
let pendingCount = 0;
|
|
27
|
-
const queue: Record<number, QueueItem> = {};
|
|
24
|
+
const queue: QueueItem[] = [];
|
|
28
25
|
|
|
29
26
|
function acquire(): Promise<void> {
|
|
30
27
|
if (destroyed) {
|
|
@@ -36,44 +33,34 @@ export function createRateLimiter(
|
|
|
36
33
|
return Promise.resolve();
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
if (
|
|
36
|
+
if (queue.length >= maxQueueSize) {
|
|
40
37
|
return Promise.reject(
|
|
41
38
|
new Error(`[Accessio] Rate limiter queue size exceeded maxQueueSize (${maxQueueSize})`),
|
|
42
39
|
);
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
return new Promise((resolve, reject) => {
|
|
46
|
-
queue
|
|
47
|
-
pendingCount++;
|
|
43
|
+
queue.push({ resolve, reject });
|
|
48
44
|
});
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
function release(): void {
|
|
52
48
|
if (destroyed) return;
|
|
53
|
-
|
|
54
49
|
if (active <= 0) return;
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const next = queue[head];
|
|
61
|
-
delete queue[head];
|
|
62
|
-
head++;
|
|
63
|
-
pendingCount--;
|
|
64
|
-
next?.resolve();
|
|
51
|
+
const next = queue.shift();
|
|
52
|
+
if (next) {
|
|
53
|
+
next.resolve();
|
|
54
|
+
return;
|
|
65
55
|
}
|
|
56
|
+
active--;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
59
|
function destroy(): void {
|
|
69
60
|
destroyed = true;
|
|
70
61
|
const reason = new Error('[Accessio] Rate limiter destroyed — pending request cancelled');
|
|
71
|
-
while (
|
|
72
|
-
|
|
73
|
-
delete queue[head];
|
|
74
|
-
head++;
|
|
75
|
-
pendingCount--;
|
|
76
|
-
next?.reject(reason);
|
|
62
|
+
while (queue.length > 0) {
|
|
63
|
+
queue.shift()!.reject(reason);
|
|
77
64
|
}
|
|
78
65
|
}
|
|
79
66
|
|
|
@@ -82,7 +69,7 @@ export function createRateLimiter(
|
|
|
82
69
|
release,
|
|
83
70
|
destroy,
|
|
84
71
|
get pending() {
|
|
85
|
-
return
|
|
72
|
+
return queue.length;
|
|
86
73
|
},
|
|
87
74
|
get active() {
|
|
88
75
|
return active;
|
|
@@ -6,6 +6,7 @@ export default async function transformData(
|
|
|
6
6
|
data: unknown,
|
|
7
7
|
headers: Record<string, string | string[]>,
|
|
8
8
|
config?: AccessioRequestConfig,
|
|
9
|
+
direction: 'request' | 'response' = 'request',
|
|
9
10
|
): Promise<unknown> {
|
|
10
11
|
if (!transforms || !Array.isArray(transforms)) {
|
|
11
12
|
return data;
|
|
@@ -20,7 +21,7 @@ export default async function transformData(
|
|
|
20
21
|
} catch (err) {
|
|
21
22
|
throw AccessioError.from(
|
|
22
23
|
err instanceof Error ? err : new Error(String(err)),
|
|
23
|
-
AccessioError.ERR_BAD_REQUEST,
|
|
24
|
+
direction === 'response' ? AccessioError.ERR_BAD_RESPONSE : AccessioError.ERR_BAD_REQUEST,
|
|
24
25
|
config ?? null,
|
|
25
26
|
null,
|
|
26
27
|
null,
|