accessio 1.3.0 → 1.4.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/cjs/accessio.cjs +68 -84
- package/cjs/accessio.cjs.map +1 -1
- package/cjs/core/accessioError.cjs +23 -1
- package/cjs/core/accessioError.cjs.map +1 -1
- package/cjs/core/buildURL.cjs +10 -4
- package/cjs/core/buildURL.cjs.map +1 -1
- package/cjs/core/fetchAdapter.cjs +122 -106
- package/cjs/core/fetchAdapter.cjs.map +1 -1
- package/cjs/core/request.cjs +73 -23
- package/cjs/core/request.cjs.map +1 -1
- package/cjs/helpers/debug.cjs +7 -1
- package/cjs/helpers/debug.cjs.map +1 -1
- package/cjs/helpers/settle.cjs +1 -1
- package/cjs/helpers/settle.cjs.map +1 -1
- package/cjs/interceptors/interceptorManager.cjs +25 -18
- package/cjs/interceptors/interceptorManager.cjs.map +1 -1
- package/index.d.ts +82 -21
- package/package.json +2 -2
- package/src/accessio.ts +104 -98
- package/src/core/accessioError.ts +26 -1
- package/src/core/buildURL.ts +14 -4
- package/src/core/fetchAdapter.ts +152 -125
- package/src/core/request.ts +85 -27
- package/src/helpers/debug.ts +7 -2
- package/src/helpers/settle.ts +1 -1
- package/src/interceptors/interceptorManager.ts +26 -19
|
@@ -23,41 +23,48 @@ __export(interceptorManager_exports, {
|
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(interceptorManager_exports);
|
|
25
25
|
class InterceptorManager {
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
_handlers;
|
|
27
|
+
_nextId;
|
|
28
28
|
constructor() {
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
29
|
+
this._handlers = /* @__PURE__ */ new Map();
|
|
30
|
+
this._nextId = 0;
|
|
31
31
|
}
|
|
32
32
|
use(fulfilled, rejected, options = {}) {
|
|
33
|
-
this.
|
|
33
|
+
const id = this._nextId++;
|
|
34
|
+
this._handlers.set(id, {
|
|
34
35
|
fulfilled: fulfilled || null,
|
|
35
36
|
rejected: rejected || null,
|
|
36
37
|
synchronous: options.synchronous || false,
|
|
37
38
|
runWhen: options.runWhen || null
|
|
38
39
|
});
|
|
39
|
-
|
|
40
|
-
return this.handlers.length - 1;
|
|
40
|
+
return id;
|
|
41
41
|
}
|
|
42
42
|
eject(id) {
|
|
43
|
-
|
|
44
|
-
this.handlers[id] = null;
|
|
45
|
-
this._activeCount--;
|
|
46
|
-
}
|
|
43
|
+
this._handlers.delete(id);
|
|
47
44
|
}
|
|
48
45
|
clear() {
|
|
49
|
-
this.
|
|
50
|
-
this._activeCount = 0;
|
|
46
|
+
this._handlers.clear();
|
|
51
47
|
}
|
|
52
48
|
forEach(fn) {
|
|
53
|
-
for (const handler of this.
|
|
54
|
-
|
|
55
|
-
fn(handler);
|
|
56
|
-
}
|
|
49
|
+
for (const handler of this._handlers.values()) {
|
|
50
|
+
fn(handler);
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
53
|
get size() {
|
|
60
|
-
return this.
|
|
54
|
+
return this._handlers.size;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Snapshot view for backward-compat introspection. Slot index = interceptor ID;
|
|
58
|
+
* ejected IDs appear as `null`. Reading this builds a fresh array each time —
|
|
59
|
+
* prefer `forEach`/`size` in hot paths.
|
|
60
|
+
*/
|
|
61
|
+
get handlers() {
|
|
62
|
+
const max = this._nextId;
|
|
63
|
+
const out = new Array(max);
|
|
64
|
+
for (let i = 0; i < max; i++) {
|
|
65
|
+
out[i] = this._handlers.get(i) ?? null;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
70
|
var interceptorManager_default = InterceptorManager;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/interceptors/interceptorManager.ts"],"sourcesContent":["import type { TransformFunction, InterceptorHandler, InterceptorOptions } from '../types';\n\nexport class InterceptorManager {\n
|
|
1
|
+
{"version":3,"sources":["../../src/interceptors/interceptorManager.ts"],"sourcesContent":["import type { TransformFunction, InterceptorHandler, InterceptorOptions } from '../types';\n\nexport class InterceptorManager {\n private _handlers: Map<number, InterceptorHandler>;\n private _nextId: number;\n\n constructor() {\n this._handlers = new Map();\n this._nextId = 0;\n }\n\n use(\n fulfilled: TransformFunction | null,\n rejected?: ((error: unknown) => unknown) | null,\n options: InterceptorOptions = {},\n ): number {\n const id = this._nextId++;\n this._handlers.set(id, {\n fulfilled: fulfilled || null,\n rejected: rejected || null,\n synchronous: options.synchronous || false,\n runWhen: options.runWhen || null,\n });\n return id;\n }\n\n eject(id: number): void {\n this._handlers.delete(id);\n }\n\n clear(): void {\n this._handlers.clear();\n }\n\n forEach(fn: (handler: InterceptorHandler) => void): void {\n for (const handler of this._handlers.values()) {\n fn(handler);\n }\n }\n\n get size(): number {\n return this._handlers.size;\n }\n\n /**\n * Snapshot view for backward-compat introspection. Slot index = interceptor ID;\n * ejected IDs appear as `null`. Reading this builds a fresh array each time —\n * prefer `forEach`/`size` in hot paths.\n */\n get handlers(): Array<InterceptorHandler | null> {\n const max = this._nextId;\n const out: Array<InterceptorHandler | null> = new Array(max);\n for (let i = 0; i < max; i++) {\n out[i] = this._handlers.get(i) ?? null;\n }\n return out;\n }\n}\n\nexport default InterceptorManager;\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEO,MAAM,mBAAmB;AAAA,EACtB;AAAA,EACA;AAAA,EAER,cAAc;AACZ,SAAK,YAAY,oBAAI,IAAI;AACzB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEA,IACE,WACA,UACA,UAA8B,CAAC,GACvB;AACR,UAAM,KAAK,KAAK;AAChB,SAAK,UAAU,IAAI,IAAI;AAAA,MACrB,WAAW,aAAa;AAAA,MACxB,UAAU,YAAY;AAAA,MACtB,aAAa,QAAQ,eAAe;AAAA,MACpC,SAAS,QAAQ,WAAW;AAAA,IAC9B,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAkB;AACtB,SAAK,UAAU,OAAO,EAAE;AAAA,EAC1B;AAAA,EAEA,QAAc;AACZ,SAAK,UAAU,MAAM;AAAA,EACvB;AAAA,EAEA,QAAQ,IAAiD;AACvD,eAAW,WAAW,KAAK,UAAU,OAAO,GAAG;AAC7C,SAAG,OAAO;AAAA,IACZ;AAAA,EACF;AAAA,EAEA,IAAI,OAAe;AACjB,WAAO,KAAK,UAAU;AAAA,EACxB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,WAA6C;AAC/C,UAAM,MAAM,KAAK;AACjB,UAAM,MAAwC,IAAI,MAAM,GAAG;AAC3D,aAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,UAAI,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,KAAK;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AACF;AAEA,IAAO,6BAAQ;","names":[]}
|
package/index.d.ts
CHANGED
|
@@ -6,19 +6,44 @@
|
|
|
6
6
|
* Avoids ambiguous intersection of Record<string, string> & { common?, get?, ... }
|
|
7
7
|
* which TypeScript accepts but can confuse type consumers.
|
|
8
8
|
*/
|
|
9
|
+
export type HeaderValue = string | string[];
|
|
10
|
+
|
|
9
11
|
export type AccessioHeaders = {
|
|
10
|
-
common?: Record<string,
|
|
11
|
-
get?: Record<string,
|
|
12
|
-
post?: Record<string,
|
|
13
|
-
put?: Record<string,
|
|
14
|
-
patch?: Record<string,
|
|
15
|
-
delete?: Record<string,
|
|
16
|
-
head?: Record<string,
|
|
17
|
-
options?: Record<string,
|
|
18
|
-
/** Any additional custom headers */
|
|
19
|
-
[key: string]:
|
|
12
|
+
common?: Record<string, HeaderValue>;
|
|
13
|
+
get?: Record<string, HeaderValue>;
|
|
14
|
+
post?: Record<string, HeaderValue>;
|
|
15
|
+
put?: Record<string, HeaderValue>;
|
|
16
|
+
patch?: Record<string, HeaderValue>;
|
|
17
|
+
delete?: Record<string, HeaderValue>;
|
|
18
|
+
head?: Record<string, HeaderValue>;
|
|
19
|
+
options?: Record<string, HeaderValue>;
|
|
20
|
+
/** Any additional custom headers (flat or per-method-nested) */
|
|
21
|
+
[key: string]: HeaderValue | Record<string, HeaderValue> | undefined;
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
export interface AccessioHooks {
|
|
25
|
+
onBeforeRequest?: (config: AccessioRequestConfig) => void | Promise<void>;
|
|
26
|
+
onRequestResponse?: (response: AccessioResponse) => void | Promise<void>;
|
|
27
|
+
onRequestError?: (error: AccessioError) => void | Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CacheProvider {
|
|
31
|
+
get(key: string): Promise<any> | any;
|
|
32
|
+
set(key: string, value: any, ttl?: number): Promise<void> | void;
|
|
33
|
+
delete(key: string): Promise<void> | void;
|
|
34
|
+
clear(): Promise<void> | void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SchemaValidator<T = any> {
|
|
38
|
+
parse(data: unknown): T;
|
|
39
|
+
parseAsync?(data: unknown): Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type TransformFunction = (
|
|
43
|
+
data: any,
|
|
44
|
+
headers: Record<string, HeaderValue>,
|
|
45
|
+
) => any | Promise<any>;
|
|
46
|
+
|
|
22
47
|
export interface AccessioRequestConfig {
|
|
23
48
|
/** Request URL (path or full URL) */
|
|
24
49
|
url?: string;
|
|
@@ -66,17 +91,13 @@ export interface AccessioRequestConfig {
|
|
|
66
91
|
responseType?: "json" | "text" | "blob" | "arraybuffer" | "stream";
|
|
67
92
|
|
|
68
93
|
/** Transform functions applied to request data */
|
|
69
|
-
transformRequest?:
|
|
70
|
-
(data: any, headers?: Record<string, string>) => any
|
|
71
|
-
>;
|
|
94
|
+
transformRequest?: TransformFunction | TransformFunction[];
|
|
72
95
|
|
|
73
96
|
/** Transform functions applied to response data */
|
|
74
|
-
transformResponse?:
|
|
75
|
-
(data: any, headers?: Record<string, string>) => any
|
|
76
|
-
>;
|
|
97
|
+
transformResponse?: TransformFunction | TransformFunction[];
|
|
77
98
|
|
|
78
|
-
/** Function to determine if a status code should resolve or reject */
|
|
79
|
-
validateStatus?: (status: number) => boolean;
|
|
99
|
+
/** Function to determine if a status code should resolve or reject. Pass `null` to disable. */
|
|
100
|
+
validateStatus?: ((status: number) => boolean) | null;
|
|
80
101
|
|
|
81
102
|
/** AbortSignal for request cancellation */
|
|
82
103
|
signal?: AbortSignal;
|
|
@@ -105,6 +126,9 @@ export interface AccessioRequestConfig {
|
|
|
105
126
|
config: AccessioRequestConfig,
|
|
106
127
|
) => void;
|
|
107
128
|
|
|
129
|
+
/** Retry on HTTP 429 (Too Many Requests), honoring Retry-After when present */
|
|
130
|
+
retryOn429?: boolean;
|
|
131
|
+
|
|
108
132
|
// ── Debug ──────────────────────────────────────────
|
|
109
133
|
|
|
110
134
|
/** Enable debug logging for this request */
|
|
@@ -115,6 +139,43 @@ export interface AccessioRequestConfig {
|
|
|
115
139
|
/** Rate limiter instance to control concurrent requests */
|
|
116
140
|
rateLimiter?: RateLimiter;
|
|
117
141
|
|
|
142
|
+
// ── Caching / dedupe ───────────────────────────────
|
|
143
|
+
|
|
144
|
+
/** Dedupe in-flight identical GETs (cache/dedupe key is per fullURL+auth+accept+responseType+withCredentials) */
|
|
145
|
+
dedupe?: boolean;
|
|
146
|
+
|
|
147
|
+
/** Enable response caching for GETs. `true` uses the default in-memory cache; pass a provider to customize. */
|
|
148
|
+
cache?: boolean | CacheProvider;
|
|
149
|
+
|
|
150
|
+
/** TTL in ms for cached responses (when supported by the provider) */
|
|
151
|
+
cacheTTL?: number;
|
|
152
|
+
|
|
153
|
+
// ── Response handling ──────────────────────────────
|
|
154
|
+
|
|
155
|
+
/** Maximum allowed response Content-Length in bytes */
|
|
156
|
+
maxContentLength?: number;
|
|
157
|
+
|
|
158
|
+
/** Schema validator applied to response data (Zod-compatible: `parse` / `parseAsync`) */
|
|
159
|
+
schema?: SchemaValidator;
|
|
160
|
+
|
|
161
|
+
/** Progress callback for downloads. Wraps the response body in a passthrough ReadableStream. */
|
|
162
|
+
onDownloadProgress?: (progressEvent: { loaded: number; total: number }) => void;
|
|
163
|
+
|
|
164
|
+
// ── Lifecycle hooks ────────────────────────────────
|
|
165
|
+
|
|
166
|
+
hooks?: AccessioHooks;
|
|
167
|
+
|
|
168
|
+
// ── Adapter / runtime ──────────────────────────────
|
|
169
|
+
|
|
170
|
+
/** Custom fetch implementation (defaults to global `fetch`) */
|
|
171
|
+
fetch?: typeof fetch;
|
|
172
|
+
|
|
173
|
+
/** Undici dispatcher (Node.js) */
|
|
174
|
+
dispatcher?: unknown;
|
|
175
|
+
|
|
176
|
+
/** Node.js http(s).Agent */
|
|
177
|
+
agent?: unknown;
|
|
178
|
+
|
|
118
179
|
/** Allow any additional custom properties */
|
|
119
180
|
[key: string]: any;
|
|
120
181
|
}
|
|
@@ -129,8 +190,8 @@ export interface AccessioResponse<T = any> {
|
|
|
129
190
|
/** HTTP status text */
|
|
130
191
|
statusText: string;
|
|
131
192
|
|
|
132
|
-
/** Response headers (lowercased keys) */
|
|
133
|
-
headers: Record<string,
|
|
193
|
+
/** Response headers (lowercased keys; repeated headers become string arrays) */
|
|
194
|
+
headers: Record<string, HeaderValue>;
|
|
134
195
|
|
|
135
196
|
/** The config used for this request */
|
|
136
197
|
config: AccessioRequestConfig;
|
|
@@ -458,4 +519,4 @@ export const createRateLimiter: (maxConcurrent?: number) => RateLimiter;
|
|
|
458
519
|
|
|
459
520
|
export function logRequest(config: AccessioRequestConfig, fullUrl: string): void;
|
|
460
521
|
export function logResponse(response: AccessioResponse): void;
|
|
461
|
-
export function logError(error:
|
|
522
|
+
export function logError(error: AccessioError, config?: AccessioRequestConfig): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "accessio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Fast, flexible HTTP client — simple, modular, and dependency-free",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./cjs/index.cjs",
|
|
@@ -106,4 +106,4 @@
|
|
|
106
106
|
"typescript-eslint": "^8.59.3",
|
|
107
107
|
"vitest": "^3.1.0"
|
|
108
108
|
}
|
|
109
|
-
}
|
|
109
|
+
}
|
package/src/accessio.ts
CHANGED
|
@@ -15,6 +15,66 @@ import type {
|
|
|
15
15
|
} from './types';
|
|
16
16
|
import defaultsConfig from './defaults/index';
|
|
17
17
|
|
|
18
|
+
function runRequestInterceptorsSync(
|
|
19
|
+
startConfig: AccessioRequestConfig,
|
|
20
|
+
interceptors: InterceptorHandler[],
|
|
21
|
+
): Promise<AccessioRequestConfig> {
|
|
22
|
+
let cfg = startConfig;
|
|
23
|
+
let rejectReason: any = null;
|
|
24
|
+
let isRejected = false;
|
|
25
|
+
|
|
26
|
+
for (const interceptor of interceptors) {
|
|
27
|
+
if (!isRejected) {
|
|
28
|
+
try {
|
|
29
|
+
if (interceptor.fulfilled) {
|
|
30
|
+
cfg = (interceptor.fulfilled as any)(cfg) as AccessioRequestConfig;
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
rejectReason = err;
|
|
34
|
+
isRejected = true;
|
|
35
|
+
}
|
|
36
|
+
} else if (interceptor.rejected) {
|
|
37
|
+
try {
|
|
38
|
+
cfg = interceptor.rejected(rejectReason) as AccessioRequestConfig;
|
|
39
|
+
isRejected = false;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
rejectReason = err;
|
|
42
|
+
isRejected = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return isRejected ? Promise.reject(rejectReason) : Promise.resolve(cfg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runRequestInterceptorsAsync(
|
|
51
|
+
startConfig: AccessioRequestConfig,
|
|
52
|
+
interceptors: InterceptorHandler[],
|
|
53
|
+
): Promise<AccessioRequestConfig> {
|
|
54
|
+
let promise: Promise<any> = Promise.resolve(startConfig);
|
|
55
|
+
for (const interceptor of interceptors) {
|
|
56
|
+
promise = promise.then(
|
|
57
|
+
(value: any) => (interceptor.fulfilled ? (interceptor.fulfilled as any)(value) : value),
|
|
58
|
+
interceptor.rejected ?? undefined,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return promise as Promise<AccessioRequestConfig>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function dispatchAndRetry(cfg: AccessioRequestConfig): Promise<AccessioResponse> {
|
|
65
|
+
const fullUrl = buildURL(cfg.url ?? '', cfg.baseURL, cfg.params, cfg.paramsSerializer);
|
|
66
|
+
logRequest(cfg, fullUrl);
|
|
67
|
+
|
|
68
|
+
const enrichedCfg = fullUrl !== (cfg.url || '') ? { ...cfg, _builtUrl: fullUrl } : cfg;
|
|
69
|
+
|
|
70
|
+
const dispatchFn = cfg.rateLimiter
|
|
71
|
+
? (config: AccessioRequestConfig) =>
|
|
72
|
+
rateLimitedRequest(dispatchRequest, config.rateLimiter!, config)
|
|
73
|
+
: dispatchRequest;
|
|
74
|
+
|
|
75
|
+
return retryRequest(dispatchFn, enrichedCfg);
|
|
76
|
+
}
|
|
77
|
+
|
|
18
78
|
export class Accessio {
|
|
19
79
|
defaults: AccessioRequestConfig;
|
|
20
80
|
interceptors: Interceptors;
|
|
@@ -51,86 +111,17 @@ export class Accessio {
|
|
|
51
111
|
);
|
|
52
112
|
}
|
|
53
113
|
|
|
54
|
-
const requestInterceptors
|
|
55
|
-
|
|
56
|
-
let synchronousRequestInterceptors = true;
|
|
114
|
+
const { requestInterceptors, responseInterceptors, synchronous } =
|
|
115
|
+
this.collectInterceptors(mergedConfig);
|
|
57
116
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
|
|
63
|
-
requestInterceptors.unshift(interceptor);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
this.interceptors.response.forEach((interceptor: InterceptorHandler) => {
|
|
67
|
-
responseInterceptors.push(interceptor);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
let promise: Promise<any>;
|
|
71
|
-
|
|
72
|
-
if (synchronousRequestInterceptors) {
|
|
73
|
-
let newConfig = mergedConfig;
|
|
74
|
-
let rejectReason: any = null;
|
|
75
|
-
let isRejected = false;
|
|
76
|
-
|
|
77
|
-
for (const interceptor of requestInterceptors) {
|
|
78
|
-
if (!isRejected) {
|
|
79
|
-
try {
|
|
80
|
-
if (interceptor.fulfilled) {
|
|
81
|
-
newConfig = interceptor.fulfilled(newConfig);
|
|
82
|
-
}
|
|
83
|
-
} catch (err) {
|
|
84
|
-
rejectReason = err;
|
|
85
|
-
isRejected = true;
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
if (interceptor.rejected) {
|
|
89
|
-
try {
|
|
90
|
-
newConfig = interceptor.rejected(rejectReason);
|
|
91
|
-
isRejected = false;
|
|
92
|
-
} catch (err) {
|
|
93
|
-
rejectReason = err;
|
|
94
|
-
isRejected = true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (isRejected) {
|
|
101
|
-
promise = Promise.reject(rejectReason);
|
|
102
|
-
} else {
|
|
103
|
-
promise = Promise.resolve(newConfig);
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
promise = Promise.resolve(mergedConfig);
|
|
107
|
-
for (const interceptor of requestInterceptors) {
|
|
108
|
-
promise = promise.then((value: any) => {
|
|
109
|
-
if (interceptor.fulfilled) {
|
|
110
|
-
return interceptor.fulfilled(value);
|
|
111
|
-
}
|
|
112
|
-
return value;
|
|
113
|
-
}, interceptor.rejected);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
117
|
+
let promise: Promise<any> = synchronous
|
|
118
|
+
? runRequestInterceptorsSync(mergedConfig, requestInterceptors)
|
|
119
|
+
: runRequestInterceptorsAsync(mergedConfig, requestInterceptors);
|
|
116
120
|
|
|
117
|
-
promise = promise.then((cfg:
|
|
118
|
-
const fullUrl = buildURL(cfg.url ?? '', cfg.baseURL, cfg.params, cfg.paramsSerializer);
|
|
119
|
-
|
|
120
|
-
logRequest(cfg, fullUrl);
|
|
121
|
-
|
|
122
|
-
const enrichedCfg = fullUrl !== (cfg.url || '') ? { ...cfg, _builtUrl: fullUrl } : cfg;
|
|
123
|
-
|
|
124
|
-
const dispatchFn = cfg.rateLimiter
|
|
125
|
-
? (config: AccessioRequestConfig) =>
|
|
126
|
-
rateLimitedRequest(dispatchRequest, config.rateLimiter!, config)
|
|
127
|
-
: dispatchRequest;
|
|
128
|
-
|
|
129
|
-
return retryRequest(dispatchFn, enrichedCfg);
|
|
130
|
-
});
|
|
121
|
+
promise = promise.then((cfg: AccessioRequestConfig) => dispatchAndRetry(cfg));
|
|
131
122
|
|
|
132
123
|
promise = promise.then(
|
|
133
|
-
(value:
|
|
124
|
+
(value: AccessioResponse) => {
|
|
134
125
|
logResponse(value);
|
|
135
126
|
return value;
|
|
136
127
|
},
|
|
@@ -143,15 +134,37 @@ export class Accessio {
|
|
|
143
134
|
for (const interceptor of responseInterceptors) {
|
|
144
135
|
promise = promise.then((value: any) => {
|
|
145
136
|
if (interceptor.fulfilled) {
|
|
146
|
-
return interceptor.fulfilled(value);
|
|
137
|
+
return (interceptor.fulfilled as any)(value);
|
|
147
138
|
}
|
|
148
139
|
return value;
|
|
149
|
-
}, interceptor.rejected);
|
|
140
|
+
}, interceptor.rejected ?? undefined);
|
|
150
141
|
}
|
|
151
142
|
|
|
152
143
|
return promise;
|
|
153
144
|
}
|
|
154
145
|
|
|
146
|
+
private collectInterceptors(mergedConfig: AccessioRequestConfig): {
|
|
147
|
+
requestInterceptors: InterceptorHandler[];
|
|
148
|
+
responseInterceptors: InterceptorHandler[];
|
|
149
|
+
synchronous: boolean;
|
|
150
|
+
} {
|
|
151
|
+
const requestInterceptors: InterceptorHandler[] = [];
|
|
152
|
+
const responseInterceptors: InterceptorHandler[] = [];
|
|
153
|
+
let synchronous = true;
|
|
154
|
+
|
|
155
|
+
this.interceptors.request.forEach((interceptor: InterceptorHandler) => {
|
|
156
|
+
if (interceptor.runWhen && !interceptor.runWhen(mergedConfig)) return;
|
|
157
|
+
synchronous = synchronous && interceptor.synchronous;
|
|
158
|
+
requestInterceptors.unshift(interceptor);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
this.interceptors.response.forEach((interceptor: InterceptorHandler) => {
|
|
162
|
+
responseInterceptors.push(interceptor);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return { requestInterceptors, responseInterceptors, synchronous };
|
|
166
|
+
}
|
|
167
|
+
|
|
155
168
|
getUri(config?: AccessioRequestConfig): string {
|
|
156
169
|
const merged = mergeConfig(this.defaults, config);
|
|
157
170
|
return buildURL(merged.url ?? '', merged.baseURL, merged.params, merged.paramsSerializer);
|
|
@@ -197,7 +210,8 @@ export class Accessio {
|
|
|
197
210
|
return this.request<T>(mergeConfig(config || {}, { method: 'patch', url, data }));
|
|
198
211
|
}
|
|
199
212
|
|
|
200
|
-
|
|
213
|
+
private formRequest<T = any>(
|
|
214
|
+
method: 'post' | 'put' | 'patch',
|
|
201
215
|
url: string,
|
|
202
216
|
data?: any,
|
|
203
217
|
config?: AccessioRequestConfig,
|
|
@@ -205,7 +219,7 @@ export class Accessio {
|
|
|
205
219
|
const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
|
|
206
220
|
return this.request<T>(
|
|
207
221
|
mergeConfig(config || {}, {
|
|
208
|
-
method
|
|
222
|
+
method,
|
|
209
223
|
url,
|
|
210
224
|
data: formData,
|
|
211
225
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
@@ -213,20 +227,20 @@ export class Accessio {
|
|
|
213
227
|
);
|
|
214
228
|
}
|
|
215
229
|
|
|
230
|
+
postForm<T = any>(
|
|
231
|
+
url: string,
|
|
232
|
+
data?: any,
|
|
233
|
+
config?: AccessioRequestConfig,
|
|
234
|
+
): Promise<AccessioResponse<T>> {
|
|
235
|
+
return this.formRequest<T>('post', url, data, config);
|
|
236
|
+
}
|
|
237
|
+
|
|
216
238
|
putForm<T = any>(
|
|
217
239
|
url: string,
|
|
218
240
|
data?: any,
|
|
219
241
|
config?: AccessioRequestConfig,
|
|
220
242
|
): Promise<AccessioResponse<T>> {
|
|
221
|
-
|
|
222
|
-
return this.request<T>(
|
|
223
|
-
mergeConfig(config || {}, {
|
|
224
|
-
method: 'put',
|
|
225
|
-
url,
|
|
226
|
-
data: formData,
|
|
227
|
-
headers: { 'Content-Type': 'multipart/form-data' },
|
|
228
|
-
}),
|
|
229
|
-
);
|
|
243
|
+
return this.formRequest<T>('put', url, data, config);
|
|
230
244
|
}
|
|
231
245
|
|
|
232
246
|
patchForm<T = any>(
|
|
@@ -234,15 +248,7 @@ export class Accessio {
|
|
|
234
248
|
data?: any,
|
|
235
249
|
config?: AccessioRequestConfig,
|
|
236
250
|
): Promise<AccessioResponse<T>> {
|
|
237
|
-
|
|
238
|
-
return this.request<T>(
|
|
239
|
-
mergeConfig(config || {}, {
|
|
240
|
-
method: 'patch',
|
|
241
|
-
url,
|
|
242
|
-
data: formData,
|
|
243
|
-
headers: { 'Content-Type': 'multipart/form-data' },
|
|
244
|
-
}),
|
|
245
|
-
);
|
|
251
|
+
return this.formRequest<T>('patch', url, data, config);
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
async *stream<T = any>(
|
|
@@ -6,7 +6,7 @@ function redactHeaders(headers: unknown): unknown {
|
|
|
6
6
|
const out: Record<string, unknown> = {};
|
|
7
7
|
for (const key of Object.keys(headers as Record<string, unknown>)) {
|
|
8
8
|
const value = (headers as Record<string, unknown>)[key];
|
|
9
|
-
if (/^authorization$/i.test(key)) {
|
|
9
|
+
if (/^authorization$/i.test(key) || /^cookie$/i.test(key) || /^set-cookie$/i.test(key)) {
|
|
10
10
|
out[key] = '[REDACTED]';
|
|
11
11
|
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
12
12
|
out[key] = redactHeaders(value);
|
|
@@ -17,6 +17,31 @@ function redactHeaders(headers: unknown): unknown {
|
|
|
17
17
|
return out;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
const SENSITIVE_BODY_KEY =
|
|
21
|
+
/^(password|passwd|pwd|token|access_token|refresh_token|id_token|authorization|api[_-]?key|secret|client[_-]?secret|cookie|set[_-]?cookie|private[_-]?key|session)$/i;
|
|
22
|
+
|
|
23
|
+
export function redactBody(value: unknown, seen?: WeakSet<object>): unknown {
|
|
24
|
+
if (value === null || typeof value !== 'object') return value;
|
|
25
|
+
const visited = seen ?? new WeakSet<object>();
|
|
26
|
+
if (visited.has(value as object)) return '[Circular]';
|
|
27
|
+
visited.add(value as object);
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
return value.map((item) => redactBody(item, visited));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const out: Record<string, unknown> = {};
|
|
34
|
+
for (const key of Object.keys(value as Record<string, unknown>)) {
|
|
35
|
+
const v = (value as Record<string, unknown>)[key];
|
|
36
|
+
if (SENSITIVE_BODY_KEY.test(key)) {
|
|
37
|
+
out[key] = '[REDACTED]';
|
|
38
|
+
} else {
|
|
39
|
+
out[key] = redactBody(v, visited);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
20
45
|
export function redactConfig(config: AccessioRequestConfig | null): AccessioRequestConfig | null {
|
|
21
46
|
if (!config) return config;
|
|
22
47
|
const clone = { ...config } as AccessioRequestConfig & { auth?: unknown };
|
package/src/core/buildURL.ts
CHANGED
|
@@ -76,11 +76,19 @@ export default function buildURL(
|
|
|
76
76
|
let fullURL = baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url || '';
|
|
77
77
|
|
|
78
78
|
let finalParams = params;
|
|
79
|
-
if (params && typeof params === 'object') {
|
|
80
|
-
const unusedParams = {
|
|
79
|
+
if (params && typeof params === 'object' && !(params instanceof URLSearchParams)) {
|
|
80
|
+
const unusedParams: Record<string, unknown> = {};
|
|
81
|
+
for (const key of Object.keys(params)) {
|
|
82
|
+
if (key === '__proto__' || key === 'prototype' || key === 'constructor') continue;
|
|
83
|
+
unusedParams[key] = (params as Record<string, unknown>)[key];
|
|
84
|
+
}
|
|
81
85
|
fullURL = fullURL.replace(/(?::([a-zA-Z0-9_]+))|(?:{([a-zA-Z0-9_]+)})/g, (match, p1, p2) => {
|
|
82
86
|
const key = p1 || p2;
|
|
83
|
-
if (
|
|
87
|
+
if (
|
|
88
|
+
key &&
|
|
89
|
+
Object.prototype.hasOwnProperty.call(unusedParams, key) &&
|
|
90
|
+
unusedParams[key] !== undefined
|
|
91
|
+
) {
|
|
84
92
|
const val = unusedParams[key];
|
|
85
93
|
delete unusedParams[key];
|
|
86
94
|
return encodeURIComponent(String(val));
|
|
@@ -93,10 +101,12 @@ export default function buildURL(
|
|
|
93
101
|
const serialized = serializeParams(finalParams as Record<string, unknown>, paramsSerializer);
|
|
94
102
|
if (serialized) {
|
|
95
103
|
const hashIndex = fullURL.indexOf('#');
|
|
104
|
+
let fragment = '';
|
|
96
105
|
if (hashIndex !== -1) {
|
|
106
|
+
fragment = fullURL.slice(hashIndex);
|
|
97
107
|
fullURL = fullURL.slice(0, hashIndex);
|
|
98
108
|
}
|
|
99
|
-
fullURL += (fullURL.indexOf('?') === -1 ? '?' : '&') + serialized;
|
|
109
|
+
fullURL += (fullURL.indexOf('?') === -1 ? '?' : '&') + serialized + fragment;
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
return fullURL;
|