@djvlc/openapi-client-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -0
- package/dist/index.d.mts +996 -0
- package/dist/index.d.ts +996 -0
- package/dist/index.js +1396 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1327 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1327 @@
|
|
|
1
|
+
// src/version.ts
|
|
2
|
+
var SDK_VERSION = true ? "1.0.0" : "0.0.0-dev";
|
|
3
|
+
var SDK_NAME = true ? "@djvlc/openapi-client-core" : "@djvlc/openapi-client-core";
|
|
4
|
+
function getSdkInfo() {
|
|
5
|
+
return {
|
|
6
|
+
name: SDK_NAME,
|
|
7
|
+
version: SDK_VERSION
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/errors.ts
|
|
12
|
+
var DjvApiError = class _DjvApiError extends Error {
|
|
13
|
+
constructor(message, code, status, traceId, details, response) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.traceId = traceId;
|
|
18
|
+
this.details = details;
|
|
19
|
+
this.response = response;
|
|
20
|
+
/** 标识这是一个 API 错误(用于 instanceof 替代方案) */
|
|
21
|
+
this.isApiError = true;
|
|
22
|
+
/** 错误名称 */
|
|
23
|
+
this.name = "DjvApiError";
|
|
24
|
+
Object.setPrototypeOf(this, _DjvApiError.prototype);
|
|
25
|
+
}
|
|
26
|
+
// ============================================================
|
|
27
|
+
// 增强方法 - P1 优化
|
|
28
|
+
// ============================================================
|
|
29
|
+
/**
|
|
30
|
+
* 是否为认证错误 (401 Unauthorized)
|
|
31
|
+
*/
|
|
32
|
+
isUnauthorized() {
|
|
33
|
+
return this.status === 401;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 是否为权限错误 (403 Forbidden)
|
|
37
|
+
*/
|
|
38
|
+
isForbidden() {
|
|
39
|
+
return this.status === 403;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 是否为认证相关错误 (401 或 403)
|
|
43
|
+
*/
|
|
44
|
+
isAuthError() {
|
|
45
|
+
return this.status === 401 || this.status === 403;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 是否为客户端错误 (4xx)
|
|
49
|
+
*/
|
|
50
|
+
isClientError() {
|
|
51
|
+
return this.status >= 400 && this.status < 500;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 是否为服务端错误 (5xx)
|
|
55
|
+
*/
|
|
56
|
+
isServerError() {
|
|
57
|
+
return this.status >= 500;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 是否为限流错误 (429 Too Many Requests)
|
|
61
|
+
*/
|
|
62
|
+
isRateLimited() {
|
|
63
|
+
return this.status === 429;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 是否为资源未找到 (404 Not Found)
|
|
67
|
+
*/
|
|
68
|
+
isNotFound() {
|
|
69
|
+
return this.status === 404;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 是否为请求冲突 (409 Conflict)
|
|
73
|
+
*/
|
|
74
|
+
isConflict() {
|
|
75
|
+
return this.status === 409;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 是否为请求验证失败 (400 Bad Request 或 422 Unprocessable Entity)
|
|
79
|
+
*/
|
|
80
|
+
isValidationError() {
|
|
81
|
+
return this.status === 400 || this.status === 422;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 获取 Retry-After 头的值(秒)
|
|
85
|
+
*
|
|
86
|
+
* 适用于 429 限流响应和 503 服务不可用响应
|
|
87
|
+
*
|
|
88
|
+
* @returns 重试等待秒数,如果没有 Retry-After 头则返回 null
|
|
89
|
+
*/
|
|
90
|
+
getRetryAfter() {
|
|
91
|
+
const header = this.response?.headers.get("Retry-After");
|
|
92
|
+
if (!header) return null;
|
|
93
|
+
const seconds = parseInt(header, 10);
|
|
94
|
+
if (!isNaN(seconds)) {
|
|
95
|
+
return seconds;
|
|
96
|
+
}
|
|
97
|
+
const date = Date.parse(header);
|
|
98
|
+
if (!isNaN(date)) {
|
|
99
|
+
return Math.max(0, Math.ceil((date - Date.now()) / 1e3));
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 获取重试延迟(毫秒)
|
|
105
|
+
*
|
|
106
|
+
* 优先使用 Retry-After 头,否则返回默认值
|
|
107
|
+
*
|
|
108
|
+
* @param defaultMs 默认延迟毫秒数
|
|
109
|
+
*/
|
|
110
|
+
getRetryDelayMs(defaultMs = 1e3) {
|
|
111
|
+
const retryAfter = this.getRetryAfter();
|
|
112
|
+
return retryAfter !== null ? retryAfter * 1e3 : defaultMs;
|
|
113
|
+
}
|
|
114
|
+
// ============================================================
|
|
115
|
+
// 静态方法
|
|
116
|
+
// ============================================================
|
|
117
|
+
/**
|
|
118
|
+
* 类型保护:判断是否为 DjvApiError
|
|
119
|
+
*/
|
|
120
|
+
static is(error) {
|
|
121
|
+
if (error instanceof _DjvApiError) return true;
|
|
122
|
+
if (error == null || typeof error !== "object") return false;
|
|
123
|
+
return "isApiError" in error && error.isApiError === true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 从响应创建错误
|
|
127
|
+
*/
|
|
128
|
+
static async fromResponse(response) {
|
|
129
|
+
let body = {};
|
|
130
|
+
try {
|
|
131
|
+
body = await response.json();
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
const message = body.message || response.statusText || `HTTP ${response.status}`;
|
|
135
|
+
const code = body.code || response.status;
|
|
136
|
+
const traceId = body.traceId || body.requestId || response.headers.get("x-request-id") || void 0;
|
|
137
|
+
return new _DjvApiError(message, code, response.status, traceId, body.details, response);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 转为 JSON(便于日志记录)
|
|
141
|
+
*/
|
|
142
|
+
toJSON() {
|
|
143
|
+
return {
|
|
144
|
+
name: this.name,
|
|
145
|
+
message: this.message,
|
|
146
|
+
code: this.code,
|
|
147
|
+
status: this.status,
|
|
148
|
+
traceId: this.traceId,
|
|
149
|
+
details: this.details
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var NetworkError = class _NetworkError extends Error {
|
|
154
|
+
constructor(message, cause) {
|
|
155
|
+
super(message);
|
|
156
|
+
/** 标识这是一个网络错误 */
|
|
157
|
+
this.isNetworkError = true;
|
|
158
|
+
/** 错误名称 */
|
|
159
|
+
this.name = "NetworkError";
|
|
160
|
+
this.originalCause = cause;
|
|
161
|
+
if (cause && "cause" in Error.prototype) {
|
|
162
|
+
this.cause = cause;
|
|
163
|
+
}
|
|
164
|
+
Object.setPrototypeOf(this, _NetworkError.prototype);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 获取原始错误
|
|
168
|
+
*/
|
|
169
|
+
get cause() {
|
|
170
|
+
return this.originalCause;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 类型保护:判断是否为 NetworkError(包括子类)
|
|
174
|
+
*/
|
|
175
|
+
static is(error) {
|
|
176
|
+
if (error instanceof _NetworkError) return true;
|
|
177
|
+
if (error == null || typeof error !== "object") return false;
|
|
178
|
+
return "isNetworkError" in error && error.isNetworkError === true;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 从原始错误创建
|
|
182
|
+
*/
|
|
183
|
+
static fromError(error) {
|
|
184
|
+
if (error instanceof _NetworkError) return error;
|
|
185
|
+
const cause = error instanceof Error ? error : void 0;
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
if (cause?.name === "AbortError") {
|
|
188
|
+
return new AbortError("Request was aborted");
|
|
189
|
+
}
|
|
190
|
+
return new _NetworkError(message, cause);
|
|
191
|
+
}
|
|
192
|
+
toJSON() {
|
|
193
|
+
return {
|
|
194
|
+
name: this.name,
|
|
195
|
+
message: this.message,
|
|
196
|
+
cause: this.cause?.message
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var TimeoutError = class _TimeoutError extends NetworkError {
|
|
201
|
+
constructor(timeoutMs, url) {
|
|
202
|
+
super(`Request timeout after ${timeoutMs}ms${url ? `: ${url}` : ""}`);
|
|
203
|
+
this.timeoutMs = timeoutMs;
|
|
204
|
+
this.url = url;
|
|
205
|
+
/** 标识这是一个超时错误 */
|
|
206
|
+
this.isTimeout = true;
|
|
207
|
+
this.name = "TimeoutError";
|
|
208
|
+
Object.setPrototypeOf(this, _TimeoutError.prototype);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 类型保护:判断是否为 TimeoutError
|
|
212
|
+
*/
|
|
213
|
+
static is(error) {
|
|
214
|
+
if (error instanceof _TimeoutError) return true;
|
|
215
|
+
if (error == null || typeof error !== "object") return false;
|
|
216
|
+
return "isTimeout" in error && error.isTimeout === true;
|
|
217
|
+
}
|
|
218
|
+
toJSON() {
|
|
219
|
+
return {
|
|
220
|
+
name: this.name,
|
|
221
|
+
message: this.message,
|
|
222
|
+
timeoutMs: this.timeoutMs,
|
|
223
|
+
url: this.url
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var AbortError = class _AbortError extends NetworkError {
|
|
228
|
+
constructor(message = "Request was aborted") {
|
|
229
|
+
super(message);
|
|
230
|
+
/** 标识这是一个取消错误 */
|
|
231
|
+
this.isAbort = true;
|
|
232
|
+
this.name = "AbortError";
|
|
233
|
+
Object.setPrototypeOf(this, _AbortError.prototype);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 类型保护:判断是否为 AbortError
|
|
237
|
+
*/
|
|
238
|
+
static is(error) {
|
|
239
|
+
if (error instanceof _AbortError) return true;
|
|
240
|
+
if (error == null || typeof error !== "object") return false;
|
|
241
|
+
return "isAbort" in error && error.isAbort === true;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function isRetryableError(error) {
|
|
245
|
+
if (AbortError.is(error)) return false;
|
|
246
|
+
if (TimeoutError.is(error) || NetworkError.is(error)) return true;
|
|
247
|
+
if (DjvApiError.is(error)) {
|
|
248
|
+
const retryableStatuses = [408, 429, 500, 502, 503, 504];
|
|
249
|
+
return retryableStatuses.includes(error.status);
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
function getRetryDelay(error, defaultMs = 1e3) {
|
|
254
|
+
if (DjvApiError.is(error)) {
|
|
255
|
+
return error.getRetryDelayMs(defaultMs);
|
|
256
|
+
}
|
|
257
|
+
return defaultMs;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/utils.ts
|
|
261
|
+
function stripTrailingSlash(url) {
|
|
262
|
+
return url.replace(/\/$/, "");
|
|
263
|
+
}
|
|
264
|
+
function normalizeHeaders(initHeaders) {
|
|
265
|
+
return new Headers(initHeaders ?? {});
|
|
266
|
+
}
|
|
267
|
+
function mergeHeaders(...headersList) {
|
|
268
|
+
const result = new Headers();
|
|
269
|
+
for (const headers of headersList) {
|
|
270
|
+
if (!headers) continue;
|
|
271
|
+
const normalized = new Headers(headers);
|
|
272
|
+
normalized.forEach((value, key) => {
|
|
273
|
+
result.set(key, value);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
function delay(ms) {
|
|
279
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
280
|
+
}
|
|
281
|
+
function calculateBackoffDelay(attempt, baseDelayMs, maxDelayMs, exponential = true) {
|
|
282
|
+
if (!exponential) return baseDelayMs;
|
|
283
|
+
const exponentialDelay = baseDelayMs * Math.pow(2, attempt - 1);
|
|
284
|
+
const jitter = Math.random() * 0.3 * exponentialDelay;
|
|
285
|
+
const delayWithJitter = exponentialDelay + jitter;
|
|
286
|
+
return Math.min(delayWithJitter, maxDelayMs);
|
|
287
|
+
}
|
|
288
|
+
function getPlatform() {
|
|
289
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
290
|
+
return `node/${process.versions.node}`;
|
|
291
|
+
}
|
|
292
|
+
if (typeof Deno !== "undefined") {
|
|
293
|
+
return `deno/${Deno.version?.deno ?? "unknown"}`;
|
|
294
|
+
}
|
|
295
|
+
if (typeof navigator !== "undefined") {
|
|
296
|
+
const ua = navigator.userAgent;
|
|
297
|
+
if (ua.includes("Chrome")) return "chrome";
|
|
298
|
+
if (ua.includes("Firefox")) return "firefox";
|
|
299
|
+
if (ua.includes("Safari")) return "safari";
|
|
300
|
+
if (ua.includes("Edge")) return "edge";
|
|
301
|
+
return "browser";
|
|
302
|
+
}
|
|
303
|
+
return "unknown";
|
|
304
|
+
}
|
|
305
|
+
function buildUserAgent(sdkName, sdkVersion) {
|
|
306
|
+
const platform = getPlatform();
|
|
307
|
+
return `${sdkName}/${sdkVersion} (${platform})`;
|
|
308
|
+
}
|
|
309
|
+
function isBrowser() {
|
|
310
|
+
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
311
|
+
}
|
|
312
|
+
async function safeParseJson(response) {
|
|
313
|
+
try {
|
|
314
|
+
return await response.json();
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function cloneResponse(response) {
|
|
320
|
+
return response.clone();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/fetch.ts
|
|
324
|
+
var DEFAULT_RETRY_OPTIONS = {
|
|
325
|
+
maxRetries: 0,
|
|
326
|
+
retryDelayMs: 1e3,
|
|
327
|
+
exponentialBackoff: true,
|
|
328
|
+
maxDelayMs: 3e4,
|
|
329
|
+
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
330
|
+
shouldRetry: () => true,
|
|
331
|
+
onRetry: () => {
|
|
332
|
+
},
|
|
333
|
+
respectRetryAfter: true
|
|
334
|
+
// P1: 默认尊重 Retry-After 头
|
|
335
|
+
};
|
|
336
|
+
function createFetchWithTimeout(fetchImpl, timeoutMs) {
|
|
337
|
+
return (async (input, init) => {
|
|
338
|
+
const controller = new AbortController();
|
|
339
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
340
|
+
const externalSignal = init?.signal;
|
|
341
|
+
if (externalSignal) {
|
|
342
|
+
if (externalSignal.aborted) {
|
|
343
|
+
clearTimeout(timeoutId);
|
|
344
|
+
throw new AbortError("Request was aborted");
|
|
345
|
+
}
|
|
346
|
+
externalSignal.addEventListener("abort", () => {
|
|
347
|
+
clearTimeout(timeoutId);
|
|
348
|
+
controller.abort();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
const response = await fetchImpl(input, {
|
|
353
|
+
...init,
|
|
354
|
+
signal: controller.signal
|
|
355
|
+
});
|
|
356
|
+
return response;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
359
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
360
|
+
if (externalSignal?.aborted) {
|
|
361
|
+
throw new AbortError("Request was aborted");
|
|
362
|
+
}
|
|
363
|
+
throw new TimeoutError(timeoutMs, url);
|
|
364
|
+
}
|
|
365
|
+
throw NetworkError.fromError(error);
|
|
366
|
+
} finally {
|
|
367
|
+
clearTimeout(timeoutId);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function getRetryDelayFromResponse(response, error, attempt, opts) {
|
|
372
|
+
if (opts.respectRetryAfter && response) {
|
|
373
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
374
|
+
if (retryAfter) {
|
|
375
|
+
const seconds = parseInt(retryAfter, 10);
|
|
376
|
+
if (!isNaN(seconds)) {
|
|
377
|
+
return Math.min(seconds * 1e3, opts.maxDelayMs);
|
|
378
|
+
}
|
|
379
|
+
const date = Date.parse(retryAfter);
|
|
380
|
+
if (!isNaN(date)) {
|
|
381
|
+
const delayMs = Math.max(0, date - Date.now());
|
|
382
|
+
return Math.min(delayMs, opts.maxDelayMs);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (DjvApiError.is(error)) {
|
|
387
|
+
const errorDelay = error.getRetryDelayMs(0);
|
|
388
|
+
if (errorDelay > 0) {
|
|
389
|
+
return Math.min(errorDelay, opts.maxDelayMs);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return calculateBackoffDelay(
|
|
393
|
+
attempt,
|
|
394
|
+
opts.retryDelayMs,
|
|
395
|
+
opts.maxDelayMs,
|
|
396
|
+
opts.exponentialBackoff
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
function createFetchWithRetry(fetchImpl, options = {}, logger) {
|
|
400
|
+
const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
|
|
401
|
+
return (async (input, init) => {
|
|
402
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
403
|
+
let lastError;
|
|
404
|
+
let lastResponse = null;
|
|
405
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
406
|
+
try {
|
|
407
|
+
const response = await fetchImpl(input, init);
|
|
408
|
+
lastResponse = response;
|
|
409
|
+
if (!response.ok && attempt < opts.maxRetries) {
|
|
410
|
+
if (opts.retryableStatuses.includes(response.status)) {
|
|
411
|
+
const error = await DjvApiError.fromResponse(response.clone());
|
|
412
|
+
if (opts.shouldRetry(error, attempt + 1)) {
|
|
413
|
+
const delayMs = getRetryDelayFromResponse(
|
|
414
|
+
response,
|
|
415
|
+
error,
|
|
416
|
+
attempt + 1,
|
|
417
|
+
opts
|
|
418
|
+
);
|
|
419
|
+
opts.onRetry(error, attempt + 1, delayMs);
|
|
420
|
+
logger?.retry?.(url, attempt + 1, error);
|
|
421
|
+
await delay(delayMs);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return response;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
lastError = error;
|
|
429
|
+
if (AbortError.is(error)) {
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
if (attempt >= opts.maxRetries) {
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
if (!isRetryableError(error) || !opts.shouldRetry(error, attempt + 1)) {
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
const delayMs = getRetryDelayFromResponse(
|
|
439
|
+
lastResponse,
|
|
440
|
+
error,
|
|
441
|
+
attempt + 1,
|
|
442
|
+
opts
|
|
443
|
+
);
|
|
444
|
+
opts.onRetry(error, attempt + 1, delayMs);
|
|
445
|
+
logger?.retry?.(url, attempt + 1, error);
|
|
446
|
+
await delay(delayMs);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
throw lastError;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function createEnhancedFetch(options) {
|
|
453
|
+
const {
|
|
454
|
+
fetchApi = fetch,
|
|
455
|
+
timeoutMs = 3e4,
|
|
456
|
+
retry,
|
|
457
|
+
logger
|
|
458
|
+
} = options;
|
|
459
|
+
let enhancedFetch = fetchApi;
|
|
460
|
+
enhancedFetch = createFetchWithTimeout(enhancedFetch, timeoutMs);
|
|
461
|
+
if (retry && retry.maxRetries && retry.maxRetries > 0) {
|
|
462
|
+
enhancedFetch = createFetchWithRetry(enhancedFetch, retry, logger);
|
|
463
|
+
}
|
|
464
|
+
if (logger) {
|
|
465
|
+
const wrappedFetch = enhancedFetch;
|
|
466
|
+
enhancedFetch = (async (input, init) => {
|
|
467
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
468
|
+
const startTime = Date.now();
|
|
469
|
+
logger.request?.(url, init ?? {});
|
|
470
|
+
try {
|
|
471
|
+
const response = await wrappedFetch(input, init);
|
|
472
|
+
const durationMs = Date.now() - startTime;
|
|
473
|
+
logger.response?.(url, response, durationMs);
|
|
474
|
+
return response;
|
|
475
|
+
} catch (error) {
|
|
476
|
+
logger.error?.(url, error);
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return enhancedFetch;
|
|
482
|
+
}
|
|
483
|
+
async function executeRequest(fetchFn, url, init, options) {
|
|
484
|
+
const finalInit = { ...init };
|
|
485
|
+
let timeoutController;
|
|
486
|
+
let timeoutId;
|
|
487
|
+
if (options?.timeoutMs) {
|
|
488
|
+
timeoutController = new AbortController();
|
|
489
|
+
timeoutId = setTimeout(() => timeoutController.abort(), options.timeoutMs);
|
|
490
|
+
if (options.signal) {
|
|
491
|
+
options.signal.addEventListener("abort", () => {
|
|
492
|
+
clearTimeout(timeoutId);
|
|
493
|
+
timeoutController.abort();
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
finalInit.signal = timeoutController.signal;
|
|
497
|
+
} else if (options?.signal) {
|
|
498
|
+
finalInit.signal = options.signal;
|
|
499
|
+
}
|
|
500
|
+
try {
|
|
501
|
+
const response = await fetchFn(url, finalInit);
|
|
502
|
+
if (!response.ok) {
|
|
503
|
+
throw await DjvApiError.fromResponse(response);
|
|
504
|
+
}
|
|
505
|
+
return response;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (DjvApiError.is(error) || NetworkError.is(error)) {
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
511
|
+
if (options?.signal?.aborted) {
|
|
512
|
+
throw new AbortError("Request was aborted");
|
|
513
|
+
}
|
|
514
|
+
if (options?.timeoutMs) {
|
|
515
|
+
throw new TimeoutError(options.timeoutMs, url);
|
|
516
|
+
}
|
|
517
|
+
throw new AbortError();
|
|
518
|
+
}
|
|
519
|
+
throw NetworkError.fromError(error);
|
|
520
|
+
} finally {
|
|
521
|
+
if (timeoutId) {
|
|
522
|
+
clearTimeout(timeoutId);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/middleware.ts
|
|
528
|
+
function createHeadersMiddleware(opts) {
|
|
529
|
+
return {
|
|
530
|
+
async pre(context) {
|
|
531
|
+
const headers = normalizeHeaders(context.init.headers);
|
|
532
|
+
if (opts.defaultHeaders) {
|
|
533
|
+
for (const [key, value] of Object.entries(opts.defaultHeaders)) {
|
|
534
|
+
headers.set(key, value);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (opts.getApiKey) {
|
|
538
|
+
const apiKey = await opts.getApiKey();
|
|
539
|
+
if (apiKey) {
|
|
540
|
+
headers.set("X-Api-Key", apiKey);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (opts.getAuthToken) {
|
|
544
|
+
const token = await opts.getAuthToken();
|
|
545
|
+
if (token) {
|
|
546
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (opts.getTraceHeaders) {
|
|
550
|
+
const traceHeaders = opts.getTraceHeaders();
|
|
551
|
+
for (const [key, value] of Object.entries(traceHeaders)) {
|
|
552
|
+
headers.set(key, value);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (opts.sdkInfo) {
|
|
556
|
+
headers.set("X-SDK-Version", `${opts.sdkInfo.name}@${opts.sdkInfo.version}`);
|
|
557
|
+
headers.set("User-Agent", buildUserAgent(opts.sdkInfo.name, opts.sdkInfo.version));
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
...context,
|
|
561
|
+
init: {
|
|
562
|
+
...context.init,
|
|
563
|
+
headers
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function createLoggingMiddleware(opts) {
|
|
570
|
+
const requestStartTimes = /* @__PURE__ */ new Map();
|
|
571
|
+
return {
|
|
572
|
+
pre(context) {
|
|
573
|
+
const requestId = `${context.url}-${Date.now()}`;
|
|
574
|
+
requestStartTimes.set(requestId, Date.now());
|
|
575
|
+
context.meta = { ...context.meta, requestId };
|
|
576
|
+
opts.onRequest?.(context.url, context.init);
|
|
577
|
+
return context;
|
|
578
|
+
},
|
|
579
|
+
post(context) {
|
|
580
|
+
const requestId = context.meta?.requestId;
|
|
581
|
+
const startTime = requestStartTimes.get(requestId);
|
|
582
|
+
const durationMs = startTime ? Date.now() - startTime : 0;
|
|
583
|
+
requestStartTimes.delete(requestId);
|
|
584
|
+
opts.onResponse?.(context.url, context.response, durationMs);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function applyMiddlewareChain(middlewares, context) {
|
|
589
|
+
let currentContext = context;
|
|
590
|
+
for (const middleware of middlewares) {
|
|
591
|
+
if (middleware.pre) {
|
|
592
|
+
currentContext = await middleware.pre(currentContext);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return currentContext;
|
|
596
|
+
}
|
|
597
|
+
async function applyPostMiddlewareChain(middlewares, context) {
|
|
598
|
+
for (let i = middlewares.length - 1; i >= 0; i--) {
|
|
599
|
+
const middleware = middlewares[i];
|
|
600
|
+
if (middleware.post) {
|
|
601
|
+
await middleware.post(context);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function createDefaultMiddlewares(opts, sdkInfo) {
|
|
606
|
+
const middlewares = [];
|
|
607
|
+
middlewares.push(
|
|
608
|
+
createHeadersMiddleware({
|
|
609
|
+
getAuthToken: opts.getAuthToken,
|
|
610
|
+
getApiKey: opts.getApiKey,
|
|
611
|
+
getTraceHeaders: opts.getTraceHeaders,
|
|
612
|
+
defaultHeaders: opts.defaultHeaders,
|
|
613
|
+
sdkInfo
|
|
614
|
+
})
|
|
615
|
+
);
|
|
616
|
+
if (opts.debug || opts.logger) {
|
|
617
|
+
middlewares.push(
|
|
618
|
+
createLoggingMiddleware({
|
|
619
|
+
onRequest: (url, init) => {
|
|
620
|
+
if (opts.debug) {
|
|
621
|
+
console.log(`[API] -> ${init.method ?? "GET"} ${url}`);
|
|
622
|
+
}
|
|
623
|
+
opts.logger?.request?.(url, init);
|
|
624
|
+
},
|
|
625
|
+
onResponse: (url, response, durationMs) => {
|
|
626
|
+
if (opts.debug) {
|
|
627
|
+
console.log(`[API] <- ${response.status} ${url} (${durationMs}ms)`);
|
|
628
|
+
}
|
|
629
|
+
opts.logger?.response?.(url, response, durationMs);
|
|
630
|
+
},
|
|
631
|
+
onError: (url, error) => {
|
|
632
|
+
if (opts.debug) {
|
|
633
|
+
console.error(`[API] !! ${url}`, error);
|
|
634
|
+
}
|
|
635
|
+
opts.logger?.error?.(url, error);
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
return middlewares;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/interceptors.ts
|
|
644
|
+
var InterceptorManager = class {
|
|
645
|
+
constructor(interceptors) {
|
|
646
|
+
this.requestInterceptors = [];
|
|
647
|
+
this.responseInterceptors = [];
|
|
648
|
+
this.errorInterceptors = [];
|
|
649
|
+
if (interceptors?.request) {
|
|
650
|
+
this.requestInterceptors.push(...interceptors.request);
|
|
651
|
+
}
|
|
652
|
+
if (interceptors?.response) {
|
|
653
|
+
this.responseInterceptors.push(...interceptors.response);
|
|
654
|
+
}
|
|
655
|
+
if (interceptors?.error) {
|
|
656
|
+
this.errorInterceptors.push(...interceptors.error);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* 添加请求拦截器
|
|
661
|
+
* @returns 移除拦截器的函数
|
|
662
|
+
*/
|
|
663
|
+
addRequestInterceptor(interceptor) {
|
|
664
|
+
this.requestInterceptors.push(interceptor);
|
|
665
|
+
return () => {
|
|
666
|
+
const index = this.requestInterceptors.indexOf(interceptor);
|
|
667
|
+
if (index >= 0) {
|
|
668
|
+
this.requestInterceptors.splice(index, 1);
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* 添加响应拦截器
|
|
674
|
+
* @returns 移除拦截器的函数
|
|
675
|
+
*/
|
|
676
|
+
addResponseInterceptor(interceptor) {
|
|
677
|
+
this.responseInterceptors.push(interceptor);
|
|
678
|
+
return () => {
|
|
679
|
+
const index = this.responseInterceptors.indexOf(interceptor);
|
|
680
|
+
if (index >= 0) {
|
|
681
|
+
this.responseInterceptors.splice(index, 1);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* 添加错误拦截器
|
|
687
|
+
* @returns 移除拦截器的函数
|
|
688
|
+
*/
|
|
689
|
+
addErrorInterceptor(interceptor) {
|
|
690
|
+
this.errorInterceptors.push(interceptor);
|
|
691
|
+
return () => {
|
|
692
|
+
const index = this.errorInterceptors.indexOf(interceptor);
|
|
693
|
+
if (index >= 0) {
|
|
694
|
+
this.errorInterceptors.splice(index, 1);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* 执行请求拦截器链
|
|
700
|
+
*/
|
|
701
|
+
async runRequestInterceptors(context) {
|
|
702
|
+
let currentContext = context;
|
|
703
|
+
for (const interceptor of this.requestInterceptors) {
|
|
704
|
+
currentContext = await interceptor(currentContext);
|
|
705
|
+
}
|
|
706
|
+
return currentContext;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* 执行响应拦截器链
|
|
710
|
+
*/
|
|
711
|
+
async runResponseInterceptors(response, context) {
|
|
712
|
+
let currentResponse = response;
|
|
713
|
+
for (const interceptor of this.responseInterceptors) {
|
|
714
|
+
currentResponse = await interceptor(currentResponse, context);
|
|
715
|
+
}
|
|
716
|
+
return currentResponse;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* 执行错误拦截器链
|
|
720
|
+
*/
|
|
721
|
+
async runErrorInterceptors(error, context) {
|
|
722
|
+
let currentError = error;
|
|
723
|
+
for (const interceptor of this.errorInterceptors) {
|
|
724
|
+
try {
|
|
725
|
+
currentError = await interceptor(currentError, context);
|
|
726
|
+
} catch (e) {
|
|
727
|
+
currentError = e;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return currentError;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* 清除所有拦截器
|
|
734
|
+
*/
|
|
735
|
+
clear() {
|
|
736
|
+
this.requestInterceptors = [];
|
|
737
|
+
this.responseInterceptors = [];
|
|
738
|
+
this.errorInterceptors = [];
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
function createTokenRefreshInterceptor(options) {
|
|
742
|
+
const {
|
|
743
|
+
refreshToken,
|
|
744
|
+
shouldRefresh = (response) => response.status === 401,
|
|
745
|
+
maxRetries = 1
|
|
746
|
+
} = options;
|
|
747
|
+
let refreshPromise = null;
|
|
748
|
+
let retryCount = 0;
|
|
749
|
+
return async (response, context) => {
|
|
750
|
+
if (!shouldRefresh(response) || retryCount >= maxRetries) {
|
|
751
|
+
retryCount = 0;
|
|
752
|
+
return response;
|
|
753
|
+
}
|
|
754
|
+
retryCount++;
|
|
755
|
+
if (!refreshPromise) {
|
|
756
|
+
refreshPromise = refreshToken().finally(() => {
|
|
757
|
+
refreshPromise = null;
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
const newToken = await refreshPromise;
|
|
762
|
+
const headers = new Headers(context.init.headers);
|
|
763
|
+
headers.set("Authorization", `Bearer ${newToken}`);
|
|
764
|
+
const newResponse = await fetch(context.url, {
|
|
765
|
+
...context.init,
|
|
766
|
+
headers
|
|
767
|
+
});
|
|
768
|
+
retryCount = 0;
|
|
769
|
+
return newResponse;
|
|
770
|
+
} catch {
|
|
771
|
+
retryCount = 0;
|
|
772
|
+
return response;
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function createDedupeInterceptor() {
|
|
777
|
+
const pendingRequests = /* @__PURE__ */ new Map();
|
|
778
|
+
const getKey = (context) => {
|
|
779
|
+
return `${context.init.method ?? "GET"}:${context.url}`;
|
|
780
|
+
};
|
|
781
|
+
return {
|
|
782
|
+
interceptor: (context) => {
|
|
783
|
+
const key = getKey(context);
|
|
784
|
+
const pending = pendingRequests.get(key);
|
|
785
|
+
if (pending) {
|
|
786
|
+
return pending;
|
|
787
|
+
}
|
|
788
|
+
return context;
|
|
789
|
+
},
|
|
790
|
+
clear: () => {
|
|
791
|
+
pendingRequests.clear();
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function createCacheInterceptor(options) {
|
|
796
|
+
const {
|
|
797
|
+
ttlMs = 6e4,
|
|
798
|
+
shouldCache = (context) => context.init.method === "GET" || !context.init.method,
|
|
799
|
+
maxSize = 100
|
|
800
|
+
} = options;
|
|
801
|
+
const cache = /* @__PURE__ */ new Map();
|
|
802
|
+
const getKey = (context) => {
|
|
803
|
+
return `${context.init.method ?? "GET"}:${context.url}`;
|
|
804
|
+
};
|
|
805
|
+
const isExpired = (entry) => {
|
|
806
|
+
return Date.now() - entry.timestamp > ttlMs;
|
|
807
|
+
};
|
|
808
|
+
const evictOldest = () => {
|
|
809
|
+
if (cache.size >= maxSize) {
|
|
810
|
+
const oldestKey = cache.keys().next().value;
|
|
811
|
+
if (oldestKey) {
|
|
812
|
+
cache.delete(oldestKey);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
return {
|
|
817
|
+
requestInterceptor: (context) => {
|
|
818
|
+
if (!shouldCache(context)) {
|
|
819
|
+
return context;
|
|
820
|
+
}
|
|
821
|
+
const key = getKey(context);
|
|
822
|
+
const entry = cache.get(key);
|
|
823
|
+
if (entry && !isExpired(entry)) {
|
|
824
|
+
context.meta = { ...context.meta, fromCache: true, cachedResponse: entry.response.clone() };
|
|
825
|
+
}
|
|
826
|
+
return context;
|
|
827
|
+
},
|
|
828
|
+
responseInterceptor: (response, context) => {
|
|
829
|
+
if (!shouldCache(context) || !response.ok) {
|
|
830
|
+
return response;
|
|
831
|
+
}
|
|
832
|
+
const key = getKey(context);
|
|
833
|
+
evictOldest();
|
|
834
|
+
cache.set(key, {
|
|
835
|
+
response: response.clone(),
|
|
836
|
+
timestamp: Date.now()
|
|
837
|
+
});
|
|
838
|
+
return response;
|
|
839
|
+
},
|
|
840
|
+
clear: () => {
|
|
841
|
+
cache.clear();
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/dedupe.ts
|
|
847
|
+
function createRequestDeduper(options = {}) {
|
|
848
|
+
const {
|
|
849
|
+
keyGenerator = defaultKeyGenerator,
|
|
850
|
+
getOnly = true,
|
|
851
|
+
onDedupe
|
|
852
|
+
} = options;
|
|
853
|
+
const inflightRequests = /* @__PURE__ */ new Map();
|
|
854
|
+
const pendingCounts = /* @__PURE__ */ new Map();
|
|
855
|
+
function defaultKeyGenerator(url, init) {
|
|
856
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
857
|
+
if (getOnly && method !== "GET") {
|
|
858
|
+
return `${method}:${url}:${Date.now()}:${Math.random()}`;
|
|
859
|
+
}
|
|
860
|
+
return `${method}:${url}`;
|
|
861
|
+
}
|
|
862
|
+
return {
|
|
863
|
+
/**
|
|
864
|
+
* 包装 fetch 函数,添加请求去重能力
|
|
865
|
+
*/
|
|
866
|
+
wrap: (fetchFn) => {
|
|
867
|
+
return (async (input, init) => {
|
|
868
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
869
|
+
const key = keyGenerator(url, init);
|
|
870
|
+
const inflight = inflightRequests.get(key);
|
|
871
|
+
if (inflight) {
|
|
872
|
+
pendingCounts.set(key, (pendingCounts.get(key) ?? 1) + 1);
|
|
873
|
+
onDedupe?.(key, pendingCounts.get(key));
|
|
874
|
+
return inflight.then((response) => response.clone());
|
|
875
|
+
}
|
|
876
|
+
const promise = fetchFn(input, init).then((response) => {
|
|
877
|
+
return response;
|
|
878
|
+
}).finally(() => {
|
|
879
|
+
inflightRequests.delete(key);
|
|
880
|
+
pendingCounts.delete(key);
|
|
881
|
+
});
|
|
882
|
+
inflightRequests.set(key, promise);
|
|
883
|
+
pendingCounts.set(key, 1);
|
|
884
|
+
return promise;
|
|
885
|
+
});
|
|
886
|
+
},
|
|
887
|
+
/**
|
|
888
|
+
* 获取当前进行中的请求数量
|
|
889
|
+
*/
|
|
890
|
+
getPendingCount() {
|
|
891
|
+
return inflightRequests.size;
|
|
892
|
+
},
|
|
893
|
+
/**
|
|
894
|
+
* 获取所有进行中的请求 key
|
|
895
|
+
*/
|
|
896
|
+
getPendingKeys() {
|
|
897
|
+
return Array.from(inflightRequests.keys());
|
|
898
|
+
},
|
|
899
|
+
/**
|
|
900
|
+
* 清除所有进行中的请求记录
|
|
901
|
+
*
|
|
902
|
+
* 注意:这不会取消请求,只是清除去重映射
|
|
903
|
+
*/
|
|
904
|
+
clear() {
|
|
905
|
+
inflightRequests.clear();
|
|
906
|
+
pendingCounts.clear();
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/metrics.ts
|
|
912
|
+
function createMetricsCollector(options = {}) {
|
|
913
|
+
const {
|
|
914
|
+
maxMetrics = 1e4,
|
|
915
|
+
ttlMs = 36e5,
|
|
916
|
+
// 1 小时
|
|
917
|
+
autoCleanup = true,
|
|
918
|
+
cleanupIntervalMs = 6e4,
|
|
919
|
+
// 1 分钟
|
|
920
|
+
onMetrics
|
|
921
|
+
} = options;
|
|
922
|
+
const metricsStore = [];
|
|
923
|
+
let cleanupTimer = null;
|
|
924
|
+
if (autoCleanup) {
|
|
925
|
+
cleanupTimer = setInterval(() => {
|
|
926
|
+
const now = Date.now();
|
|
927
|
+
const cutoff = now - ttlMs;
|
|
928
|
+
let i = 0;
|
|
929
|
+
while (i < metricsStore.length && metricsStore[i].endTime < cutoff) {
|
|
930
|
+
i++;
|
|
931
|
+
}
|
|
932
|
+
if (i > 0) {
|
|
933
|
+
metricsStore.splice(0, i);
|
|
934
|
+
}
|
|
935
|
+
}, cleanupIntervalMs);
|
|
936
|
+
}
|
|
937
|
+
function collect(metrics) {
|
|
938
|
+
if (metricsStore.length >= maxMetrics) {
|
|
939
|
+
metricsStore.shift();
|
|
940
|
+
}
|
|
941
|
+
metricsStore.push(metrics);
|
|
942
|
+
onMetrics?.(metrics);
|
|
943
|
+
}
|
|
944
|
+
function getMetrics() {
|
|
945
|
+
return [...metricsStore];
|
|
946
|
+
}
|
|
947
|
+
function flush() {
|
|
948
|
+
const result = [...metricsStore];
|
|
949
|
+
metricsStore.length = 0;
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
function summary() {
|
|
953
|
+
if (metricsStore.length === 0) {
|
|
954
|
+
return {
|
|
955
|
+
totalRequests: 0,
|
|
956
|
+
successCount: 0,
|
|
957
|
+
failureCount: 0,
|
|
958
|
+
successRate: 0,
|
|
959
|
+
avgDurationMs: 0,
|
|
960
|
+
p50Ms: 0,
|
|
961
|
+
p90Ms: 0,
|
|
962
|
+
p95Ms: 0,
|
|
963
|
+
p99Ms: 0,
|
|
964
|
+
minMs: 0,
|
|
965
|
+
maxMs: 0,
|
|
966
|
+
statusCodeCounts: {},
|
|
967
|
+
pathCounts: {},
|
|
968
|
+
timeRange: { start: 0, end: 0 }
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const durations = metricsStore.map((m) => m.durationMs).sort((a, b) => a - b);
|
|
972
|
+
const successCount = metricsStore.filter((m) => m.success).length;
|
|
973
|
+
const failureCount = metricsStore.length - successCount;
|
|
974
|
+
const statusCodeCounts = {};
|
|
975
|
+
const pathCounts = {};
|
|
976
|
+
for (const m of metricsStore) {
|
|
977
|
+
statusCodeCounts[m.status] = (statusCodeCounts[m.status] ?? 0) + 1;
|
|
978
|
+
pathCounts[m.path] = (pathCounts[m.path] ?? 0) + 1;
|
|
979
|
+
}
|
|
980
|
+
const getPercentile = (arr, p) => {
|
|
981
|
+
const index = Math.floor(arr.length * p);
|
|
982
|
+
return arr[Math.min(index, arr.length - 1)] ?? 0;
|
|
983
|
+
};
|
|
984
|
+
return {
|
|
985
|
+
totalRequests: metricsStore.length,
|
|
986
|
+
successCount,
|
|
987
|
+
failureCount,
|
|
988
|
+
successRate: successCount / metricsStore.length,
|
|
989
|
+
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
990
|
+
p50Ms: getPercentile(durations, 0.5),
|
|
991
|
+
p90Ms: getPercentile(durations, 0.9),
|
|
992
|
+
p95Ms: getPercentile(durations, 0.95),
|
|
993
|
+
p99Ms: getPercentile(durations, 0.99),
|
|
994
|
+
minMs: durations[0] ?? 0,
|
|
995
|
+
maxMs: durations[durations.length - 1] ?? 0,
|
|
996
|
+
statusCodeCounts,
|
|
997
|
+
pathCounts,
|
|
998
|
+
timeRange: {
|
|
999
|
+
start: metricsStore[0]?.startTime ?? 0,
|
|
1000
|
+
end: metricsStore[metricsStore.length - 1]?.endTime ?? 0
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function getByPath(path) {
|
|
1005
|
+
return metricsStore.filter((m) => m.path === path);
|
|
1006
|
+
}
|
|
1007
|
+
function getByTimeRange(startTime, endTime) {
|
|
1008
|
+
return metricsStore.filter(
|
|
1009
|
+
(m) => m.startTime >= startTime && m.endTime <= endTime
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
function clear() {
|
|
1013
|
+
metricsStore.length = 0;
|
|
1014
|
+
}
|
|
1015
|
+
function destroy() {
|
|
1016
|
+
if (cleanupTimer) {
|
|
1017
|
+
clearInterval(cleanupTimer);
|
|
1018
|
+
cleanupTimer = null;
|
|
1019
|
+
}
|
|
1020
|
+
clear();
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
collect,
|
|
1024
|
+
getMetrics,
|
|
1025
|
+
flush,
|
|
1026
|
+
summary,
|
|
1027
|
+
getByPath,
|
|
1028
|
+
getByTimeRange,
|
|
1029
|
+
clear,
|
|
1030
|
+
destroy
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function generateRequestId() {
|
|
1034
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1035
|
+
}
|
|
1036
|
+
function extractPath(url) {
|
|
1037
|
+
try {
|
|
1038
|
+
const parsed = new URL(url);
|
|
1039
|
+
return parsed.pathname;
|
|
1040
|
+
} catch {
|
|
1041
|
+
const match = url.match(/^[^?#]*/);
|
|
1042
|
+
return match?.[0] ?? url;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function createMetricsMiddleware(collector) {
|
|
1046
|
+
const requestStarts = /* @__PURE__ */ new Map();
|
|
1047
|
+
return {
|
|
1048
|
+
pre(context) {
|
|
1049
|
+
const requestId = generateRequestId();
|
|
1050
|
+
requestStarts.set(requestId, Date.now());
|
|
1051
|
+
return {
|
|
1052
|
+
...context,
|
|
1053
|
+
meta: {
|
|
1054
|
+
...context.meta,
|
|
1055
|
+
metricsRequestId: requestId
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
},
|
|
1059
|
+
post(context) {
|
|
1060
|
+
const requestId = context.meta?.metricsRequestId;
|
|
1061
|
+
if (!requestId) return;
|
|
1062
|
+
const startTime = requestStarts.get(requestId);
|
|
1063
|
+
if (!startTime) return;
|
|
1064
|
+
requestStarts.delete(requestId);
|
|
1065
|
+
const endTime = Date.now();
|
|
1066
|
+
const metrics = {
|
|
1067
|
+
requestId,
|
|
1068
|
+
url: context.url,
|
|
1069
|
+
path: extractPath(context.url),
|
|
1070
|
+
method: (context.init.method ?? "GET").toUpperCase(),
|
|
1071
|
+
startTime,
|
|
1072
|
+
endTime,
|
|
1073
|
+
durationMs: endTime - startTime,
|
|
1074
|
+
status: context.response.status,
|
|
1075
|
+
success: context.response.ok,
|
|
1076
|
+
fromCache: context.meta?.fromCache,
|
|
1077
|
+
retryCount: context.meta?.retryCount,
|
|
1078
|
+
traceId: context.response.headers.get("x-request-id") ?? void 0
|
|
1079
|
+
};
|
|
1080
|
+
const contentLength = context.response.headers.get("content-length");
|
|
1081
|
+
if (contentLength) {
|
|
1082
|
+
metrics.responseSize = parseInt(contentLength, 10);
|
|
1083
|
+
}
|
|
1084
|
+
if (context.init.body) {
|
|
1085
|
+
if (typeof context.init.body === "string") {
|
|
1086
|
+
metrics.requestSize = new TextEncoder().encode(context.init.body).length;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
collector.collect(metrics);
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// src/logger.ts
|
|
1095
|
+
function createConsoleLogger(options = {}) {
|
|
1096
|
+
const {
|
|
1097
|
+
prefix = "[DJV-API]",
|
|
1098
|
+
enabled = true,
|
|
1099
|
+
level = "info",
|
|
1100
|
+
timestamp = true,
|
|
1101
|
+
includeBody = false
|
|
1102
|
+
} = options;
|
|
1103
|
+
const levels = ["debug", "info", "warn", "error"];
|
|
1104
|
+
const currentLevelIndex = levels.indexOf(level);
|
|
1105
|
+
const shouldLog = (logLevel) => {
|
|
1106
|
+
return enabled && levels.indexOf(logLevel) >= currentLevelIndex;
|
|
1107
|
+
};
|
|
1108
|
+
const formatTime = () => {
|
|
1109
|
+
if (!timestamp) return "";
|
|
1110
|
+
return `[${(/* @__PURE__ */ new Date()).toISOString()}] `;
|
|
1111
|
+
};
|
|
1112
|
+
const formatMethod = (init) => {
|
|
1113
|
+
return init.method ?? "GET";
|
|
1114
|
+
};
|
|
1115
|
+
return {
|
|
1116
|
+
request: (url, init) => {
|
|
1117
|
+
if (!shouldLog("debug")) return;
|
|
1118
|
+
const method = formatMethod(init);
|
|
1119
|
+
console.log(`${formatTime()}${prefix} -> ${method} ${url}`);
|
|
1120
|
+
if (includeBody && init.body) {
|
|
1121
|
+
try {
|
|
1122
|
+
const body = typeof init.body === "string" ? JSON.parse(init.body) : init.body;
|
|
1123
|
+
console.log(`${prefix} Body:`, body);
|
|
1124
|
+
} catch {
|
|
1125
|
+
console.log(`${prefix} Body: [non-JSON]`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
response: (url, response, durationMs) => {
|
|
1130
|
+
if (!shouldLog("info")) return;
|
|
1131
|
+
const status = response.status;
|
|
1132
|
+
const statusEmoji = status >= 200 && status < 300 ? "\u2713" : "\u2717";
|
|
1133
|
+
console.log(
|
|
1134
|
+
`${formatTime()}${prefix} <- ${statusEmoji} ${status} ${url} (${durationMs}ms)`
|
|
1135
|
+
);
|
|
1136
|
+
},
|
|
1137
|
+
error: (url, error) => {
|
|
1138
|
+
if (!shouldLog("error")) return;
|
|
1139
|
+
console.error(`${formatTime()}${prefix} !! ${url}`, error);
|
|
1140
|
+
},
|
|
1141
|
+
retry: (url, attempt, error) => {
|
|
1142
|
+
if (!shouldLog("warn")) return;
|
|
1143
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1144
|
+
console.warn(
|
|
1145
|
+
`${formatTime()}${prefix} \u21BB Retry #${attempt} ${url} (${errorMessage})`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function createSilentLogger() {
|
|
1151
|
+
return {
|
|
1152
|
+
request: () => {
|
|
1153
|
+
},
|
|
1154
|
+
response: () => {
|
|
1155
|
+
},
|
|
1156
|
+
error: () => {
|
|
1157
|
+
},
|
|
1158
|
+
retry: () => {
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function createCustomLogger(handlers) {
|
|
1163
|
+
return {
|
|
1164
|
+
request: (url, init) => {
|
|
1165
|
+
handlers.onRequest?.({
|
|
1166
|
+
url,
|
|
1167
|
+
method: init.method ?? "GET",
|
|
1168
|
+
timestamp: Date.now()
|
|
1169
|
+
});
|
|
1170
|
+
},
|
|
1171
|
+
response: (url, response, durationMs) => {
|
|
1172
|
+
handlers.onResponse?.({
|
|
1173
|
+
url,
|
|
1174
|
+
status: response.status,
|
|
1175
|
+
durationMs,
|
|
1176
|
+
timestamp: Date.now()
|
|
1177
|
+
});
|
|
1178
|
+
},
|
|
1179
|
+
error: (url, error) => {
|
|
1180
|
+
handlers.onError?.({
|
|
1181
|
+
url,
|
|
1182
|
+
error,
|
|
1183
|
+
timestamp: Date.now()
|
|
1184
|
+
});
|
|
1185
|
+
},
|
|
1186
|
+
retry: (url, attempt, error) => {
|
|
1187
|
+
handlers.onRetry?.({
|
|
1188
|
+
url,
|
|
1189
|
+
attempt,
|
|
1190
|
+
error,
|
|
1191
|
+
timestamp: Date.now()
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
function combineLoggers(...loggers) {
|
|
1197
|
+
return {
|
|
1198
|
+
request: (url, init) => {
|
|
1199
|
+
for (const logger of loggers) {
|
|
1200
|
+
logger.request?.(url, init);
|
|
1201
|
+
}
|
|
1202
|
+
},
|
|
1203
|
+
response: (url, response, durationMs) => {
|
|
1204
|
+
for (const logger of loggers) {
|
|
1205
|
+
logger.response?.(url, response, durationMs);
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
error: (url, error) => {
|
|
1209
|
+
for (const logger of loggers) {
|
|
1210
|
+
logger.error?.(url, error);
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
retry: (url, attempt, error) => {
|
|
1214
|
+
for (const logger of loggers) {
|
|
1215
|
+
logger.retry?.(url, attempt, error);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/configuration.ts
|
|
1222
|
+
var Configuration = class _Configuration {
|
|
1223
|
+
constructor(params = {}) {
|
|
1224
|
+
this.basePath = params.basePath ?? "";
|
|
1225
|
+
this.fetchApi = params.fetchApi ?? fetch;
|
|
1226
|
+
this.middleware = params.middleware ?? [];
|
|
1227
|
+
this.credentials = params.credentials;
|
|
1228
|
+
this.interceptors = new InterceptorManager();
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* 从 ClientOptions 创建 Configuration
|
|
1232
|
+
*/
|
|
1233
|
+
static fromClientOptions(opts, sdkInfo) {
|
|
1234
|
+
const config = new _Configuration();
|
|
1235
|
+
config.basePath = stripTrailingSlash(opts.baseUrl);
|
|
1236
|
+
config.credentials = opts.credentials ?? "omit";
|
|
1237
|
+
config.sdkInfo = sdkInfo;
|
|
1238
|
+
config.fetchApi = createEnhancedFetch({
|
|
1239
|
+
fetchApi: opts.fetchApi,
|
|
1240
|
+
timeoutMs: opts.timeoutMs ?? 3e4,
|
|
1241
|
+
retry: opts.retry,
|
|
1242
|
+
logger: opts.logger
|
|
1243
|
+
});
|
|
1244
|
+
config.middleware = [
|
|
1245
|
+
...createDefaultMiddlewares(opts, sdkInfo),
|
|
1246
|
+
...opts.middleware ?? []
|
|
1247
|
+
];
|
|
1248
|
+
if (opts.interceptors) {
|
|
1249
|
+
config.interceptors = new InterceptorManager(opts.interceptors);
|
|
1250
|
+
}
|
|
1251
|
+
return config;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
function createConfiguration(opts, sdkInfo) {
|
|
1255
|
+
return Configuration.fromClientOptions(opts, sdkInfo);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/client-factory.ts
|
|
1259
|
+
function createClient(apiClasses, opts, sdkInfo) {
|
|
1260
|
+
const config = Configuration.fromClientOptions(opts, sdkInfo);
|
|
1261
|
+
const apiInstances = {};
|
|
1262
|
+
for (const [name, ApiClass] of Object.entries(apiClasses)) {
|
|
1263
|
+
if (typeof ApiClass === "function" && name.endsWith("Api")) {
|
|
1264
|
+
apiInstances[name] = new ApiClass(config);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return {
|
|
1268
|
+
config,
|
|
1269
|
+
apis: apiInstances,
|
|
1270
|
+
...apiInstances
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
function filterApiClasses(exports) {
|
|
1274
|
+
const result = {};
|
|
1275
|
+
for (const [name, value] of Object.entries(exports)) {
|
|
1276
|
+
if (typeof value === "function" && name.endsWith("Api")) {
|
|
1277
|
+
result[name] = value;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return result;
|
|
1281
|
+
}
|
|
1282
|
+
export {
|
|
1283
|
+
AbortError,
|
|
1284
|
+
Configuration,
|
|
1285
|
+
DjvApiError,
|
|
1286
|
+
InterceptorManager,
|
|
1287
|
+
NetworkError,
|
|
1288
|
+
SDK_NAME,
|
|
1289
|
+
SDK_VERSION,
|
|
1290
|
+
TimeoutError,
|
|
1291
|
+
applyMiddlewareChain,
|
|
1292
|
+
applyPostMiddlewareChain,
|
|
1293
|
+
buildUserAgent,
|
|
1294
|
+
calculateBackoffDelay,
|
|
1295
|
+
cloneResponse,
|
|
1296
|
+
combineLoggers,
|
|
1297
|
+
createCacheInterceptor,
|
|
1298
|
+
createClient,
|
|
1299
|
+
createConfiguration,
|
|
1300
|
+
createConsoleLogger,
|
|
1301
|
+
createCustomLogger,
|
|
1302
|
+
createDedupeInterceptor,
|
|
1303
|
+
createDefaultMiddlewares,
|
|
1304
|
+
createEnhancedFetch,
|
|
1305
|
+
createFetchWithRetry,
|
|
1306
|
+
createFetchWithTimeout,
|
|
1307
|
+
createHeadersMiddleware,
|
|
1308
|
+
createLoggingMiddleware,
|
|
1309
|
+
createMetricsCollector,
|
|
1310
|
+
createMetricsMiddleware,
|
|
1311
|
+
createRequestDeduper,
|
|
1312
|
+
createSilentLogger,
|
|
1313
|
+
createTokenRefreshInterceptor,
|
|
1314
|
+
delay,
|
|
1315
|
+
executeRequest,
|
|
1316
|
+
filterApiClasses,
|
|
1317
|
+
getPlatform,
|
|
1318
|
+
getRetryDelay,
|
|
1319
|
+
getSdkInfo,
|
|
1320
|
+
isBrowser,
|
|
1321
|
+
isRetryableError,
|
|
1322
|
+
mergeHeaders,
|
|
1323
|
+
normalizeHeaders,
|
|
1324
|
+
safeParseJson,
|
|
1325
|
+
stripTrailingSlash
|
|
1326
|
+
};
|
|
1327
|
+
//# sourceMappingURL=index.mjs.map
|