@vafast/api-client 0.2.0 → 0.2.3
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/dist/index.d.mts +231 -34
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +350 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
//#region src/types/index.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* 类型定义
|
|
3
|
+
* @vafast/api-client 类型定义
|
|
4
4
|
*/
|
|
5
5
|
/** HTTP 方法 */
|
|
6
6
|
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
7
|
-
/**
|
|
8
|
-
* 请求配置
|
|
9
|
-
*/
|
|
10
|
-
interface RequestConfig {
|
|
11
|
-
/** 请求头 */
|
|
12
|
-
headers?: Record<string, string>;
|
|
13
|
-
/** 超时时间(毫秒) */
|
|
14
|
-
timeout?: number;
|
|
15
|
-
/** 取消信号 */
|
|
16
|
-
signal?: AbortSignal;
|
|
17
|
-
}
|
|
18
7
|
/**
|
|
19
8
|
* API 错误 - Go 风格结构化错误
|
|
20
9
|
*/
|
|
@@ -43,16 +32,124 @@ interface ApiResponse<T = unknown> {
|
|
|
43
32
|
/** 错误信息,成功时为 null,失败时有值 */
|
|
44
33
|
error: ApiError | null;
|
|
45
34
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
interface
|
|
35
|
+
/**
|
|
36
|
+
* 请求配置(每次请求可传)
|
|
37
|
+
*/
|
|
38
|
+
interface RequestConfig {
|
|
39
|
+
/** 额外请求头(合并到全局) */
|
|
50
40
|
headers?: Record<string, string>;
|
|
51
|
-
|
|
52
|
-
onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;
|
|
53
|
-
onError?: (error: ApiError) => void;
|
|
41
|
+
/** 超时时间(毫秒),覆盖全局 */
|
|
54
42
|
timeout?: number;
|
|
43
|
+
/** 取消信号 */
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
/** 元数据(传递给中间件) */
|
|
46
|
+
meta?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 请求上下文 - 贯穿整个请求生命周期
|
|
50
|
+
*/
|
|
51
|
+
interface RequestContext {
|
|
52
|
+
/** HTTP 方法 */
|
|
53
|
+
method: string;
|
|
54
|
+
/** 请求路径(不含 baseURL) */
|
|
55
|
+
path: string;
|
|
56
|
+
/** 完整 URL */
|
|
57
|
+
url: URL;
|
|
58
|
+
/** 请求头(可修改) */
|
|
59
|
+
headers: Headers;
|
|
60
|
+
/** 请求体 */
|
|
61
|
+
body?: unknown;
|
|
62
|
+
/** 请求配置 */
|
|
63
|
+
config?: RequestConfig;
|
|
64
|
+
/** 元数据存储(中间件间共享状态) */
|
|
65
|
+
meta: Map<string, unknown>;
|
|
66
|
+
/** 当前重试次数 */
|
|
67
|
+
retryCount: number;
|
|
55
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* 响应上下文
|
|
71
|
+
*/
|
|
72
|
+
interface ResponseContext<T = unknown> {
|
|
73
|
+
/** 关联的请求上下文 */
|
|
74
|
+
request: RequestContext;
|
|
75
|
+
/** 原始 Response 对象 */
|
|
76
|
+
raw: Response | null;
|
|
77
|
+
/** 解析后的数据 */
|
|
78
|
+
data: T | null;
|
|
79
|
+
/** 错误信息 */
|
|
80
|
+
error: ApiError | null;
|
|
81
|
+
/** HTTP 状态码 */
|
|
82
|
+
status: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 中间件函数
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const logMiddleware: Middleware = async (ctx, next) => {
|
|
90
|
+
* console.log(`[${ctx.method}] ${ctx.path}`)
|
|
91
|
+
* const response = await next()
|
|
92
|
+
* console.log(`[${response.status}] ${ctx.path}`)
|
|
93
|
+
* return response
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
type Middleware = (ctx: RequestContext, next: () => Promise<ResponseContext>) => Promise<ResponseContext>;
|
|
98
|
+
/**
|
|
99
|
+
* 带名称的中间件(用于跳过/调试)
|
|
100
|
+
*/
|
|
101
|
+
interface NamedMiddleware extends Middleware {
|
|
102
|
+
/** 中间件名称 */
|
|
103
|
+
middlewareName?: string;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 中间件选项
|
|
107
|
+
*/
|
|
108
|
+
interface MiddlewareOptions {
|
|
109
|
+
/** 中间件名称 */
|
|
110
|
+
name?: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* HTTP 客户端接口
|
|
114
|
+
*/
|
|
115
|
+
interface Client {
|
|
116
|
+
/** 基础 URL */
|
|
117
|
+
readonly baseURL: string;
|
|
118
|
+
/** 添加中间件 */
|
|
119
|
+
use(middleware: Middleware): Client;
|
|
120
|
+
use(name: string, middleware: Middleware): Client;
|
|
121
|
+
/** 设置默认请求头 */
|
|
122
|
+
headers(h: Record<string, string>): Client;
|
|
123
|
+
/** 设置默认超时 */
|
|
124
|
+
timeout(ms: number): Client;
|
|
125
|
+
/** 发起请求 */
|
|
126
|
+
request<T = unknown>(method: string, path: string, body?: unknown, config?: RequestConfig): Promise<ApiResponse<T>>;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/core/client.d.ts
|
|
130
|
+
/**
|
|
131
|
+
* 定义带名称的中间件
|
|
132
|
+
*/
|
|
133
|
+
declare function defineMiddleware(fn: Middleware, options?: MiddlewareOptions): NamedMiddleware;
|
|
134
|
+
/**
|
|
135
|
+
* 创建 HTTP 客户端
|
|
136
|
+
*
|
|
137
|
+
* @param baseURL 基础 URL
|
|
138
|
+
* @returns 客户端实例
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const client = createClient('http://localhost:3000')
|
|
143
|
+
* .use(authMiddleware)
|
|
144
|
+
* .use('retry', retryMiddleware)
|
|
145
|
+
* .timeout(30000)
|
|
146
|
+
*
|
|
147
|
+
* const api = eden<Api>(client)
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
declare function createClient(baseURL: string): Client;
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/core/eden.d.ts
|
|
56
153
|
interface SSEEvent<T = unknown> {
|
|
57
154
|
event?: string;
|
|
58
155
|
data: T;
|
|
@@ -74,13 +171,13 @@ type InferStatic<T> = T extends {
|
|
|
74
171
|
static: infer S;
|
|
75
172
|
} ? S : T;
|
|
76
173
|
/** 从 Schema 对象提取各部分类型 */
|
|
77
|
-
type GetSchemaQuery<S> = S extends {
|
|
174
|
+
type GetSchemaQuery<S$1> = S$1 extends {
|
|
78
175
|
query: infer Q;
|
|
79
176
|
} ? InferStatic<Q> : undefined;
|
|
80
|
-
type GetSchemaBody<S> = S extends {
|
|
177
|
+
type GetSchemaBody<S$1> = S$1 extends {
|
|
81
178
|
body: infer B;
|
|
82
179
|
} ? InferStatic<B> : undefined;
|
|
83
|
-
type GetSchemaParams<S> = S extends {
|
|
180
|
+
type GetSchemaParams<S$1> = S$1 extends {
|
|
84
181
|
params: infer P;
|
|
85
182
|
} ? InferStatic<P> : undefined;
|
|
86
183
|
/**
|
|
@@ -91,7 +188,7 @@ type InferHandlerReturn<H$1> = H$1 extends ((...args: never[]) => infer R) ? R e
|
|
|
91
188
|
/** 移除开头斜杠:/users → users */
|
|
92
189
|
type TrimSlash<P$1 extends string> = P$1 extends `/${infer R}` ? R : P$1;
|
|
93
190
|
/** 检查是否是动态参数段::id → true */
|
|
94
|
-
type IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false;
|
|
191
|
+
type IsDynamicSegment<S$1 extends string> = S$1 extends `:${string}` ? true : false;
|
|
95
192
|
type Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] };
|
|
96
193
|
type SSEBrand = {
|
|
97
194
|
readonly __brand: 'SSE';
|
|
@@ -141,15 +238,19 @@ type DeepMerge<A, B$1> = { [K in keyof A | keyof B$1]: K extends keyof A & keyof
|
|
|
141
238
|
/** 递归合并路由数组为单一类型结构 */
|
|
142
239
|
type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First] ? RouteToTree<First> : T extends readonly [infer First, ...infer Rest] ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>> : {};
|
|
143
240
|
/**
|
|
144
|
-
* 从
|
|
241
|
+
* 从 defineRoutes 结果自动推断 Eden 契约
|
|
242
|
+
*
|
|
243
|
+
* 支持两种用法:
|
|
244
|
+
* 1. 直接从 defineRoutes 结果推断(推荐,无需 as const)
|
|
245
|
+
* 2. 从原始路由定义数组推断(需要 as const)
|
|
145
246
|
*
|
|
146
247
|
* @example
|
|
147
248
|
* ```typescript
|
|
148
249
|
* import { defineRoute, defineRoutes, Type } from 'vafast'
|
|
149
250
|
* import { eden, InferEden } from '@vafast/api-client'
|
|
150
251
|
*
|
|
151
|
-
* //
|
|
152
|
-
* const
|
|
252
|
+
* // 方式1:直接从 defineRoutes 结果推断(推荐)
|
|
253
|
+
* const routes = defineRoutes([
|
|
153
254
|
* defineRoute({
|
|
154
255
|
* method: 'GET',
|
|
155
256
|
* path: '/users',
|
|
@@ -162,14 +263,12 @@ type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First
|
|
|
162
263
|
* schema: { body: Type.Object({ name: Type.String() }) },
|
|
163
264
|
* handler: ({ body }) => ({ id: '1', name: body.name })
|
|
164
265
|
* })
|
|
165
|
-
* ]
|
|
266
|
+
* ])
|
|
166
267
|
*
|
|
167
|
-
* // 服务端
|
|
168
|
-
* const routes = defineRoutes(routeDefinitions)
|
|
169
268
|
* const server = new Server(routes)
|
|
170
269
|
*
|
|
171
|
-
* //
|
|
172
|
-
* type Api = InferEden<typeof
|
|
270
|
+
* // ✅ 类型推断自动工作,无需 as const!
|
|
271
|
+
* type Api = InferEden<typeof routes>
|
|
173
272
|
* const api = eden<Api>('http://localhost:3000')
|
|
174
273
|
*
|
|
175
274
|
* // 类型安全的调用
|
|
@@ -177,7 +276,9 @@ type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First
|
|
|
177
276
|
* const { data: user } = await api.users.post({ name: 'John' }) // ✅ body 类型推断
|
|
178
277
|
* ```
|
|
179
278
|
*/
|
|
180
|
-
type InferEden<T
|
|
279
|
+
type InferEden<T> = T extends {
|
|
280
|
+
__source: infer S extends readonly unknown[];
|
|
281
|
+
} ? MergeRoutes<S> : T extends readonly unknown[] ? MergeRoutes<T> : {};
|
|
181
282
|
interface MethodDef {
|
|
182
283
|
query?: unknown;
|
|
183
284
|
body?: unknown;
|
|
@@ -220,8 +321,104 @@ type EdenClient<T, HasParams extends boolean = false> = { [K in keyof T as K ext
|
|
|
220
321
|
} ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false> : EdenClient<T[K], false> } & Endpoint<T, HasParams>;
|
|
221
322
|
/**
|
|
222
323
|
* 创建 Eden 风格的类型安全 API 客户端
|
|
324
|
+
*
|
|
325
|
+
* @param client - Client 实例
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```typescript
|
|
329
|
+
* import { createClient, eden } from '@vafast/api-client'
|
|
330
|
+
*
|
|
331
|
+
* const client = createClient('http://localhost:3000')
|
|
332
|
+
* .use(authMiddleware)
|
|
333
|
+
* .use(retryMiddleware)
|
|
334
|
+
*
|
|
335
|
+
* const api = eden<Api>(client)
|
|
336
|
+
*
|
|
337
|
+
* const { data, error } = await api.users.find.post({ page: 1 })
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
declare function eden<T>(client: Client): EdenClient<T>;
|
|
341
|
+
//#endregion
|
|
342
|
+
//#region src/middlewares/timeout.d.ts
|
|
343
|
+
/**
|
|
344
|
+
* 创建超时中间件
|
|
345
|
+
*
|
|
346
|
+
* @param ms 超时时间(毫秒)
|
|
347
|
+
* @returns 超时中间件
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const client = createClient(BASE_URL)
|
|
352
|
+
* .use(timeoutMiddleware(30000)) // 30 秒超时
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
declare function timeoutMiddleware(ms: number): NamedMiddleware;
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/middlewares/retry.d.ts
|
|
358
|
+
/**
|
|
359
|
+
* 重试配置
|
|
360
|
+
*/
|
|
361
|
+
interface RetryOptions {
|
|
362
|
+
/** 重试次数,默认 3 */
|
|
363
|
+
count?: number;
|
|
364
|
+
/** 重试延迟(毫秒),默认 1000 */
|
|
365
|
+
delay?: number;
|
|
366
|
+
/** 指数退避,默认 true */
|
|
367
|
+
backoff?: boolean;
|
|
368
|
+
/** 触发重试的状态码,默认 [408, 429, 500, 502, 503, 504] */
|
|
369
|
+
on?: number[];
|
|
370
|
+
/** 自定义重试判断 */
|
|
371
|
+
shouldRetry?: (ctx: ResponseContext) => boolean;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 创建重试中间件
|
|
375
|
+
*
|
|
376
|
+
* @param options 重试配置
|
|
377
|
+
* @returns 重试中间件
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* const client = createClient(BASE_URL)
|
|
382
|
+
* .use(retryMiddleware({ count: 3, delay: 1000 }))
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
declare function retryMiddleware(options?: RetryOptions): NamedMiddleware;
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/middlewares/logger.d.ts
|
|
388
|
+
/**
|
|
389
|
+
* 日志配置
|
|
390
|
+
*/
|
|
391
|
+
interface LoggerOptions {
|
|
392
|
+
/** 请求日志回调 */
|
|
393
|
+
onRequest?: (ctx: RequestContext) => void;
|
|
394
|
+
/** 响应日志回调 */
|
|
395
|
+
onResponse?: (ctx: ResponseContext) => void;
|
|
396
|
+
/** 是否启用控制台日志,默认 true */
|
|
397
|
+
console?: boolean;
|
|
398
|
+
/** 日志前缀 */
|
|
399
|
+
prefix?: string;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 创建日志中间件
|
|
403
|
+
*
|
|
404
|
+
* @param options 日志配置
|
|
405
|
+
* @returns 日志中间件
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* // 使用默认控制台日志
|
|
410
|
+
* const client = createClient(BASE_URL)
|
|
411
|
+
* .use(loggerMiddleware())
|
|
412
|
+
*
|
|
413
|
+
* // 自定义日志
|
|
414
|
+
* const client = createClient(BASE_URL)
|
|
415
|
+
* .use(loggerMiddleware({
|
|
416
|
+
* onRequest: (ctx) => console.log(`[REQ] ${ctx.method} ${ctx.path}`),
|
|
417
|
+
* onResponse: (ctx) => console.log(`[RES] ${ctx.status} ${ctx.request.path}`),
|
|
418
|
+
* }))
|
|
419
|
+
* ```
|
|
223
420
|
*/
|
|
224
|
-
declare function
|
|
421
|
+
declare function loggerMiddleware(options?: LoggerOptions): NamedMiddleware;
|
|
225
422
|
//#endregion
|
|
226
|
-
export { type ApiError, type ApiResponse, type
|
|
423
|
+
export { type ApiError, type ApiResponse, type Client, type EdenClient, type HTTPMethod, type InferEden, type LoggerOptions, type Middleware, type MiddlewareOptions, type NamedMiddleware, type RequestConfig, type RequestContext, type ResponseContext, type RetryOptions, type SSEEvent, type SSESubscribeOptions, type SSESubscription, createClient, defineMiddleware, eden, loggerMiddleware, retryMiddleware, timeoutMiddleware };
|
|
227
424
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/index.ts","../src/core/eden.ts"],"sourcesContent":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/index.ts","../src/core/client.ts","../src/core/eden.ts","../src/middlewares/timeout.ts","../src/middlewares/retry.ts","../src/middlewares/logger.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,199 @@
|
|
|
1
|
+
//#region src/core/compose.ts
|
|
2
|
+
/**
|
|
3
|
+
* 组合多个中间件为单一函数
|
|
4
|
+
*
|
|
5
|
+
* @param middlewares 中间件数组
|
|
6
|
+
* @returns 组合后的中间件函数
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* const chain = compose([m1, m2, m3])
|
|
11
|
+
* const response = await chain(ctx, finalHandler)
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function compose(middlewares) {
|
|
15
|
+
for (const fn of middlewares) if (typeof fn !== "function") throw new TypeError("Middleware must be a function");
|
|
16
|
+
return function composedMiddleware(ctx, final) {
|
|
17
|
+
let index = -1;
|
|
18
|
+
function dispatch(i) {
|
|
19
|
+
if (i <= index) return Promise.reject(/* @__PURE__ */ new Error("next() called multiple times"));
|
|
20
|
+
index = i;
|
|
21
|
+
const fn = i < middlewares.length ? middlewares[i] : final;
|
|
22
|
+
if (!fn) return final();
|
|
23
|
+
try {
|
|
24
|
+
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
return Promise.reject(err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return dispatch(0);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/core/client.ts
|
|
35
|
+
/**
|
|
36
|
+
* 定义带名称的中间件
|
|
37
|
+
*/
|
|
38
|
+
function defineMiddleware(fn, options) {
|
|
39
|
+
const named = fn;
|
|
40
|
+
if (options?.name) named.middlewareName = options.name;
|
|
41
|
+
return named;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 创建成功响应
|
|
45
|
+
*/
|
|
46
|
+
function createSuccessResponse(data, ctx, raw) {
|
|
47
|
+
return {
|
|
48
|
+
request: ctx,
|
|
49
|
+
raw,
|
|
50
|
+
data,
|
|
51
|
+
error: null,
|
|
52
|
+
status: raw.status
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 创建错误响应
|
|
57
|
+
*/
|
|
58
|
+
function createErrorResponse(code, message, ctx, raw = null) {
|
|
59
|
+
return {
|
|
60
|
+
request: ctx,
|
|
61
|
+
raw,
|
|
62
|
+
data: null,
|
|
63
|
+
error: {
|
|
64
|
+
code,
|
|
65
|
+
message
|
|
66
|
+
},
|
|
67
|
+
status: raw?.status ?? 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 内部客户端实现类
|
|
72
|
+
*/
|
|
73
|
+
var ClientImpl = class {
|
|
74
|
+
baseURL;
|
|
75
|
+
middlewares = [];
|
|
76
|
+
defaultHeaders = {};
|
|
77
|
+
defaultTimeout = 3e4;
|
|
78
|
+
constructor(baseURL) {
|
|
79
|
+
this.baseURL = baseURL.replace(/\/+$/, "");
|
|
80
|
+
}
|
|
81
|
+
use(middlewareOrName, middleware) {
|
|
82
|
+
if (typeof middlewareOrName === "string") {
|
|
83
|
+
if (!middleware) throw new Error("Middleware is required when name is provided");
|
|
84
|
+
const named = middleware;
|
|
85
|
+
named.middlewareName = middlewareOrName;
|
|
86
|
+
this.middlewares.push(named);
|
|
87
|
+
} else this.middlewares.push(middlewareOrName);
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
headers(h) {
|
|
91
|
+
this.defaultHeaders = {
|
|
92
|
+
...this.defaultHeaders,
|
|
93
|
+
...h
|
|
94
|
+
};
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
timeout(ms) {
|
|
98
|
+
this.defaultTimeout = ms;
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
async request(method, path, body, config) {
|
|
102
|
+
const url = new URL(path.startsWith("/") ? path : `/${path}`, this.baseURL);
|
|
103
|
+
const headers = new Headers({
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
...this.defaultHeaders,
|
|
106
|
+
...config?.headers
|
|
107
|
+
});
|
|
108
|
+
const meta = /* @__PURE__ */ new Map();
|
|
109
|
+
if (config?.meta) for (const [key, value] of Object.entries(config.meta)) meta.set(key, value);
|
|
110
|
+
const ctx = {
|
|
111
|
+
method: method.toUpperCase(),
|
|
112
|
+
path,
|
|
113
|
+
url,
|
|
114
|
+
headers,
|
|
115
|
+
body,
|
|
116
|
+
config,
|
|
117
|
+
meta,
|
|
118
|
+
retryCount: 0
|
|
119
|
+
};
|
|
120
|
+
const finalHandler = async () => {
|
|
121
|
+
return this.executeFetch(ctx);
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
const response = await compose(this.middlewares)(ctx, finalHandler);
|
|
125
|
+
return {
|
|
126
|
+
data: response.data,
|
|
127
|
+
error: response.error
|
|
128
|
+
};
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return {
|
|
131
|
+
data: null,
|
|
132
|
+
error: {
|
|
133
|
+
code: 0,
|
|
134
|
+
message: (err instanceof Error ? err : new Error(String(err))).message || "请求失败"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* 执行实际的 fetch 请求
|
|
141
|
+
*/
|
|
142
|
+
async executeFetch(ctx) {
|
|
143
|
+
const { method, url, headers, body, config } = ctx;
|
|
144
|
+
const controller = new AbortController();
|
|
145
|
+
const timeoutMs = config?.timeout ?? this.defaultTimeout;
|
|
146
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
147
|
+
if (config?.signal) config.signal.addEventListener("abort", () => controller.abort());
|
|
148
|
+
const fetchOptions = {
|
|
149
|
+
method,
|
|
150
|
+
headers,
|
|
151
|
+
signal: controller.signal
|
|
152
|
+
};
|
|
153
|
+
if (body && method !== "GET" && method !== "HEAD") fetchOptions.body = JSON.stringify(body);
|
|
154
|
+
if (method === "GET" && body && typeof body === "object") {
|
|
155
|
+
for (const [key, value] of Object.entries(body)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const request = new Request(url.toString(), fetchOptions);
|
|
159
|
+
const response = await fetch(request);
|
|
160
|
+
clearTimeout(timeoutId);
|
|
161
|
+
const contentType = response.headers.get("content-type");
|
|
162
|
+
let data = null;
|
|
163
|
+
if (contentType?.includes("application/json")) data = await response.json();
|
|
164
|
+
else if (contentType?.includes("text/")) data = await response.text();
|
|
165
|
+
if (response.ok) return createSuccessResponse(data, ctx, response);
|
|
166
|
+
const errorData = data;
|
|
167
|
+
return createErrorResponse(errorData?.code ?? response.status, errorData?.message ?? `HTTP ${response.status}`, ctx, response);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
clearTimeout(timeoutId);
|
|
170
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
171
|
+
if (error.name === "AbortError") return createErrorResponse(408, "请求超时", ctx);
|
|
172
|
+
return createErrorResponse(0, error.message || "网络错误", ctx);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* 创建 HTTP 客户端
|
|
178
|
+
*
|
|
179
|
+
* @param baseURL 基础 URL
|
|
180
|
+
* @returns 客户端实例
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* const client = createClient('http://localhost:3000')
|
|
185
|
+
* .use(authMiddleware)
|
|
186
|
+
* .use('retry', retryMiddleware)
|
|
187
|
+
* .timeout(30000)
|
|
188
|
+
*
|
|
189
|
+
* const api = eden<Api>(client)
|
|
190
|
+
* ```
|
|
191
|
+
*/
|
|
192
|
+
function createClient(baseURL) {
|
|
193
|
+
return new ClientImpl(baseURL);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
1
197
|
//#region src/core/eden.ts
|
|
2
198
|
async function* parseSSEStream(reader) {
|
|
3
199
|
const decoder = new TextDecoder();
|
|
@@ -29,70 +225,27 @@ async function* parseSSEStream(reader) {
|
|
|
29
225
|
}
|
|
30
226
|
/**
|
|
31
227
|
* 创建 Eden 风格的类型安全 API 客户端
|
|
228
|
+
*
|
|
229
|
+
* @param client - Client 实例
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* import { createClient, eden } from '@vafast/api-client'
|
|
234
|
+
*
|
|
235
|
+
* const client = createClient('http://localhost:3000')
|
|
236
|
+
* .use(authMiddleware)
|
|
237
|
+
* .use(retryMiddleware)
|
|
238
|
+
*
|
|
239
|
+
* const api = eden<Api>(client)
|
|
240
|
+
*
|
|
241
|
+
* const { data, error } = await api.users.find.post({ page: 1 })
|
|
242
|
+
* ```
|
|
32
243
|
*/
|
|
33
|
-
function eden(
|
|
34
|
-
const
|
|
244
|
+
function eden(client) {
|
|
245
|
+
const baseURL = client.baseURL;
|
|
246
|
+
const defaultHeaders = {};
|
|
35
247
|
async function request(method, path, data, requestConfig) {
|
|
36
|
-
|
|
37
|
-
if (method === "GET" && data && typeof data === "object") {
|
|
38
|
-
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
39
|
-
}
|
|
40
|
-
const headers = {
|
|
41
|
-
"Content-Type": "application/json",
|
|
42
|
-
...defaultHeaders,
|
|
43
|
-
...requestConfig?.headers
|
|
44
|
-
};
|
|
45
|
-
const controller = new AbortController();
|
|
46
|
-
let timeoutId;
|
|
47
|
-
const userSignal = requestConfig?.signal;
|
|
48
|
-
const requestTimeout = requestConfig?.timeout ?? timeout;
|
|
49
|
-
if (userSignal) if (userSignal.aborted) controller.abort();
|
|
50
|
-
else userSignal.addEventListener("abort", () => controller.abort());
|
|
51
|
-
if (requestTimeout) timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
52
|
-
const fetchOptions = {
|
|
53
|
-
method,
|
|
54
|
-
headers,
|
|
55
|
-
signal: controller.signal
|
|
56
|
-
};
|
|
57
|
-
if (method !== "GET" && method !== "HEAD" && data) fetchOptions.body = JSON.stringify(data);
|
|
58
|
-
let req = new Request(url.toString(), fetchOptions);
|
|
59
|
-
if (onRequest) req = await onRequest(req);
|
|
60
|
-
try {
|
|
61
|
-
const response = await fetch(req);
|
|
62
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
63
|
-
const contentType = response.headers.get("content-type");
|
|
64
|
-
let responseData = null;
|
|
65
|
-
if (contentType?.includes("application/json")) responseData = await response.json();
|
|
66
|
-
else if (contentType?.includes("text/")) responseData = await response.text();
|
|
67
|
-
let result;
|
|
68
|
-
if (response.ok) result = {
|
|
69
|
-
data: responseData,
|
|
70
|
-
error: null
|
|
71
|
-
};
|
|
72
|
-
else {
|
|
73
|
-
const errorBody = responseData;
|
|
74
|
-
result = {
|
|
75
|
-
data: null,
|
|
76
|
-
error: {
|
|
77
|
-
code: errorBody?.code ?? response.status,
|
|
78
|
-
message: errorBody?.message ?? `HTTP ${response.status}`
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (onResponse) result = await onResponse(result);
|
|
83
|
-
if (result.error && onError) onError(result.error);
|
|
84
|
-
return result;
|
|
85
|
-
} catch (error) {
|
|
86
|
-
const apiError = {
|
|
87
|
-
code: 0,
|
|
88
|
-
message: (error instanceof Error ? error : new Error(String(error))).message || "网络错误"
|
|
89
|
-
};
|
|
90
|
-
if (onError) onError(apiError);
|
|
91
|
-
return {
|
|
92
|
-
data: null,
|
|
93
|
-
error: apiError
|
|
94
|
-
};
|
|
95
|
-
}
|
|
248
|
+
return client.request(method, path, data, requestConfig);
|
|
96
249
|
}
|
|
97
250
|
function subscribe(path, query, callbacks, options) {
|
|
98
251
|
const url = new URL(path, baseURL);
|
|
@@ -206,5 +359,139 @@ function eden(baseURL, config) {
|
|
|
206
359
|
}
|
|
207
360
|
|
|
208
361
|
//#endregion
|
|
209
|
-
|
|
362
|
+
//#region src/middlewares/timeout.ts
|
|
363
|
+
/**
|
|
364
|
+
* 创建超时中间件
|
|
365
|
+
*
|
|
366
|
+
* @param ms 超时时间(毫秒)
|
|
367
|
+
* @returns 超时中间件
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```typescript
|
|
371
|
+
* const client = createClient(BASE_URL)
|
|
372
|
+
* .use(timeoutMiddleware(30000)) // 30 秒超时
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
function timeoutMiddleware(ms) {
|
|
376
|
+
const middleware = async (ctx, next) => {
|
|
377
|
+
const timeout = ctx.config?.timeout ?? ms;
|
|
378
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
reject(/* @__PURE__ */ new Error(`请求超时 (${timeout}ms)`));
|
|
381
|
+
}, timeout);
|
|
382
|
+
});
|
|
383
|
+
try {
|
|
384
|
+
return await Promise.race([next(), timeoutPromise]);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
return {
|
|
387
|
+
request: ctx,
|
|
388
|
+
raw: null,
|
|
389
|
+
data: null,
|
|
390
|
+
error: {
|
|
391
|
+
code: 408,
|
|
392
|
+
message: (error instanceof Error ? error : new Error(String(error))).message
|
|
393
|
+
},
|
|
394
|
+
status: 408
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
const named = middleware;
|
|
399
|
+
named.middlewareName = "timeout";
|
|
400
|
+
return named;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/middlewares/retry.ts
|
|
405
|
+
/** 默认重试状态码 */
|
|
406
|
+
const DEFAULT_RETRY_STATUS = [
|
|
407
|
+
408,
|
|
408
|
+
429,
|
|
409
|
+
500,
|
|
410
|
+
502,
|
|
411
|
+
503,
|
|
412
|
+
504
|
|
413
|
+
];
|
|
414
|
+
/**
|
|
415
|
+
* 延迟执行
|
|
416
|
+
*/
|
|
417
|
+
function sleep(ms) {
|
|
418
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* 创建重试中间件
|
|
422
|
+
*
|
|
423
|
+
* @param options 重试配置
|
|
424
|
+
* @returns 重试中间件
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* ```typescript
|
|
428
|
+
* const client = createClient(BASE_URL)
|
|
429
|
+
* .use(retryMiddleware({ count: 3, delay: 1000 }))
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
function retryMiddleware(options) {
|
|
433
|
+
const { count = 3, delay = 1e3, backoff = true, on = DEFAULT_RETRY_STATUS, shouldRetry } = options ?? {};
|
|
434
|
+
const middleware = async (ctx, next) => {
|
|
435
|
+
let lastResponse = null;
|
|
436
|
+
let attempt = 0;
|
|
437
|
+
while (attempt <= count) {
|
|
438
|
+
ctx.retryCount = attempt;
|
|
439
|
+
const response = await next();
|
|
440
|
+
lastResponse = response;
|
|
441
|
+
if (!response.error) return response;
|
|
442
|
+
if (!(shouldRetry ? shouldRetry(response) : on.includes(response.status)) || attempt >= count) return response;
|
|
443
|
+
await sleep(backoff ? delay * Math.pow(2, attempt) : delay);
|
|
444
|
+
attempt++;
|
|
445
|
+
}
|
|
446
|
+
return lastResponse;
|
|
447
|
+
};
|
|
448
|
+
const named = middleware;
|
|
449
|
+
named.middlewareName = "retry";
|
|
450
|
+
return named;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/middlewares/logger.ts
|
|
455
|
+
/**
|
|
456
|
+
* 创建日志中间件
|
|
457
|
+
*
|
|
458
|
+
* @param options 日志配置
|
|
459
|
+
* @returns 日志中间件
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* // 使用默认控制台日志
|
|
464
|
+
* const client = createClient(BASE_URL)
|
|
465
|
+
* .use(loggerMiddleware())
|
|
466
|
+
*
|
|
467
|
+
* // 自定义日志
|
|
468
|
+
* const client = createClient(BASE_URL)
|
|
469
|
+
* .use(loggerMiddleware({
|
|
470
|
+
* onRequest: (ctx) => console.log(`[REQ] ${ctx.method} ${ctx.path}`),
|
|
471
|
+
* onResponse: (ctx) => console.log(`[RES] ${ctx.status} ${ctx.request.path}`),
|
|
472
|
+
* }))
|
|
473
|
+
* ```
|
|
474
|
+
*/
|
|
475
|
+
function loggerMiddleware(options) {
|
|
476
|
+
const { onRequest, onResponse, console: useConsole = true, prefix = "[API]" } = options ?? {};
|
|
477
|
+
const middleware = async (ctx, next) => {
|
|
478
|
+
const startTime = Date.now();
|
|
479
|
+
if (onRequest) onRequest(ctx);
|
|
480
|
+
if (useConsole) console.log(`${prefix} → ${ctx.method} ${ctx.path}`);
|
|
481
|
+
const response = await next();
|
|
482
|
+
const duration = Date.now() - startTime;
|
|
483
|
+
if (onResponse) onResponse(response);
|
|
484
|
+
if (useConsole) {
|
|
485
|
+
const status = response.error ? `ERR ${response.error.code}` : `${response.status}`;
|
|
486
|
+
console.log(`${prefix} ← ${status} ${ctx.path} (${duration}ms)`);
|
|
487
|
+
}
|
|
488
|
+
return response;
|
|
489
|
+
};
|
|
490
|
+
const named = middleware;
|
|
491
|
+
named.middlewareName = "logger";
|
|
492
|
+
return named;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
//#endregion
|
|
496
|
+
export { createClient, defineMiddleware, eden, loggerMiddleware, retryMiddleware, timeoutMiddleware };
|
|
210
497
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core/eden.ts"],"sourcesContent":["/**\n * Eden 风格 API 客户端\n * \n * 最自然的链式调用:\n * - api.users.get() // GET /users\n * - api.users.post({ name }) // POST /users\n * - api.users({ id }).get() // GET /users/:id\n * - api.users({ id }).delete() // DELETE /users/:id\n * - api.chat.stream.subscribe() // SSE 流式响应\n * \n * @example\n * ```typescript\n * import { defineRoute } from 'vafast'\n * import { eden, InferEden } from '@vafast/api-client'\n * \n * // 定义路由(保留类型)\n * const routeDefinitions = [\n * defineRoute({\n * method: 'GET',\n * path: '/users',\n * schema: { query: Type.Object({ page: Type.Number() }) },\n * handler: ({ query }) => ({ users: [], page: query.page })\n * })\n * ] as const\n * \n * // 客户端推断类型\n * type Api = InferEden<typeof routeDefinitions>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 类型安全调用\n * const { data } = await api.users.get({ page: 1 })\n * ```\n */\n\nimport type { ApiResponse, ApiError, RequestConfig } from '../types'\n\n// ============= 配置 =============\n\nexport interface EdenConfig {\n headers?: Record<string, string>\n onRequest?: (request: Request) => Request | Promise<Request>\n onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>\n onError?: (error: ApiError) => void\n timeout?: number\n}\n\n// ============= SSE 类型 =============\n\nexport interface SSEEvent<T = unknown> {\n event?: string\n data: T\n id?: string\n retry?: number\n}\n\nexport interface SSESubscribeOptions {\n headers?: Record<string, string>\n reconnectInterval?: number\n maxReconnects?: number\n timeout?: number\n}\n\nexport interface SSESubscription<T = unknown> {\n unsubscribe: () => void\n readonly connected: boolean\n}\n\n// ============= 基础类型工具 =============\n\n/** 从 TypeBox Schema 提取静态类型 */\ntype InferStatic<T> = T extends { static: infer S } ? S : T\n\n/** 检查是否是 SSE Handler */\ntype IsSSEHandler<T> = T extends { __sse: { readonly __brand: 'SSE' } } ? true : false\n\n/** 从 Schema 对象提取各部分类型 */\ntype GetSchemaQuery<S> = S extends { query: infer Q } ? InferStatic<Q> : undefined\ntype GetSchemaBody<S> = S extends { body: infer B } ? InferStatic<B> : undefined\ntype GetSchemaParams<S> = S extends { params: infer P } ? InferStatic<P> : undefined\n\n/** \n * 从 handler 函数推断返回类型\n * handler: (ctx) => TReturn | Promise<TReturn>\n */\ntype InferHandlerReturn<H> = H extends (...args: never[]) => infer R\n ? R extends Promise<infer T> ? T : R\n : unknown\n\n// ============= 路径处理 =============\n\n/** 移除开头斜杠:/users → users */\ntype TrimSlash<P extends string> = P extends `/${infer R}` ? R : P\n\n/** 获取第一段:users/posts → users */\ntype Head<P extends string> = P extends `${infer H}/${string}` ? H : P\n\n/** 获取剩余段:users/posts → posts */\ntype Tail<P extends string> = P extends `${string}/${infer T}` ? T : never\n\n/** 检查是否是动态参数段::id → true */\ntype IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false\n\n// ============= 清理 undefined 字段 =============\n\ntype Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }\n\n// ============= SSE 标记类型 =============\n\ntype SSEBrand = { readonly __brand: 'SSE' }\n\n// ============= 核心类型推断(适配新的 defineRoute) =============\n\n/**\n * 从 defineRoute 返回的路由配置构建方法定义\n * \n * defineRoute 返回的 LeafRouteConfig 结构:\n * {\n * method: TMethod,\n * path: TPath,\n * schema?: TSchema,\n * handler: (ctx) => TReturn | Promise<TReturn>\n * }\n */\ntype BuildMethodDef<R> = R extends {\n readonly schema?: infer TSchema\n readonly handler: infer THandler\n}\n ? Clean<{\n query: GetSchemaQuery<TSchema>\n body: GetSchemaBody<TSchema>\n params: GetSchemaParams<TSchema>\n return: InferHandlerReturn<THandler>\n }>\n : Clean<{\n return: R extends { readonly handler: infer H } ? InferHandlerReturn<H> : unknown\n }>\n\n/**\n * 递归构建嵌套路径结构\n * \n * 处理动态参数:/users/:id → { users: { ':id': { ... } } }\n */\ntype BuildPath<Path extends string, Method extends string, Def> =\n Path extends `${infer First}/${infer Rest}`\n ? IsDynamicSegment<First> extends true\n ? { ':id': BuildPath<Rest, Method, Def> }\n : { [K in First]: BuildPath<Rest, Method, Def> }\n : IsDynamicSegment<Path> extends true\n ? { ':id': { [M in Method]: Def } }\n : Path extends ''\n ? { [M in Method]: Def }\n : { [K in Path]: { [M in Method]: Def } }\n\n/**\n * 从单个路由生成嵌套类型结构\n */\ntype RouteToTree<R> = R extends {\n readonly method: infer M extends string\n readonly path: infer P extends string\n}\n ? BuildPath<TrimSlash<P>, Lowercase<M>, BuildMethodDef<R>>\n : {}\n\n// ============= 深度合并多个路由 =============\n\ntype DeepMerge<A, B> = {\n [K in keyof A | keyof B]: \n K extends keyof A & keyof B\n ? A[K] extends object\n ? B[K] extends object\n ? DeepMerge<A[K], B[K]>\n : A[K] & B[K]\n : A[K] & B[K]\n : K extends keyof A\n ? A[K]\n : K extends keyof B\n ? B[K]\n : never\n}\n\n/** 递归合并路由数组为单一类型结构 */\ntype MergeRoutes<T extends readonly unknown[]> = \n T extends readonly [infer First]\n ? RouteToTree<First>\n : T extends readonly [infer First, ...infer Rest]\n ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>>\n : {}\n\n/**\n * 从 defineRoute 数组自动推断 Eden 契约\n * \n * @example\n * ```typescript\n * import { defineRoute, defineRoutes, Type } from 'vafast'\n * import { eden, InferEden } from '@vafast/api-client'\n * \n * // 定义路由(使用 as const 保留字面量类型)\n * const routeDefinitions = [\n * defineRoute({\n * method: 'GET',\n * path: '/users',\n * schema: { query: Type.Object({ page: Type.Number() }) },\n * handler: ({ query }) => ({ users: [], total: 0 })\n * }),\n * defineRoute({\n * method: 'POST',\n * path: '/users',\n * schema: { body: Type.Object({ name: Type.String() }) },\n * handler: ({ body }) => ({ id: '1', name: body.name })\n * })\n * ] as const\n * \n * // 服务端\n * const routes = defineRoutes(routeDefinitions)\n * const server = new Server(routes)\n * \n * // 客户端类型推断\n * type Api = InferEden<typeof routeDefinitions>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 类型安全的调用\n * const { data } = await api.users.get({ page: 1 }) // ✅ query 类型推断\n * const { data: user } = await api.users.post({ name: 'John' }) // ✅ body 类型推断\n * ```\n */\nexport type InferEden<T extends readonly unknown[]> = MergeRoutes<T>\n\n// ============= 契约类型(手动定义时使用) =============\n\ninterface MethodDef {\n query?: unknown\n body?: unknown\n params?: unknown\n return: unknown\n sse?: SSEBrand\n}\n\ntype RouteNode = {\n get?: MethodDef\n post?: MethodDef\n put?: MethodDef\n patch?: MethodDef\n delete?: MethodDef\n ':id'?: RouteNode\n [key: string]: MethodDef | RouteNode | undefined\n}\n\n// ============= 客户端类型 =============\n\ninterface SSECallbacks<T> {\n onMessage: (data: T) => void\n onError?: (error: ApiError) => void\n onOpen?: () => void\n onClose?: () => void\n onReconnect?: (attempt: number, maxAttempts: number) => void\n onMaxReconnects?: () => void\n}\n\ntype MethodCall<M extends MethodDef, HasParams extends boolean = false> = \n M extends { sse: SSEBrand }\n ? M extends { query: infer Q }\n ? (query: Q, callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : (callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : HasParams extends true\n ? M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { query: infer Q }\n ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n\ntype IsSSEEndpoint<M> = M extends { sse: { readonly __brand: 'SSE' } } ? true : false\n\ntype Endpoint<T, HasParams extends boolean = false> = \n {\n [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: \n T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never\n } \n & (T extends { get: infer M extends MethodDef }\n ? IsSSEEndpoint<M> extends true \n ? { subscribe: MethodCall<M, HasParams> }\n : {}\n : {})\n\ntype HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'\n\nexport type EdenClient<T, HasParams extends boolean = false> = {\n [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: \n T[K] extends { ':id': infer Child }\n ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false>\n : EdenClient<T[K], false>\n} & Endpoint<T, HasParams>\n\n// ============= SSE 解析器 =============\n\nasync function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<SSEEvent, void, unknown> {\n const decoder = new TextDecoder();\n let buffer = '';\n \n while (true) {\n const { done, value } = await reader.read();\n \n if (done) break;\n \n buffer += decoder.decode(value, { stream: true });\n \n const events = buffer.split('\\n\\n');\n buffer = events.pop() || '';\n \n for (const eventStr of events) {\n if (!eventStr.trim()) continue;\n \n const event: SSEEvent = { data: '' };\n const lines = eventStr.split('\\n');\n let dataLines: string[] = [];\n \n for (const line of lines) {\n if (line.startsWith('event:')) {\n event.event = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trim());\n } else if (line.startsWith('id:')) {\n event.id = line.slice(3).trim();\n } else if (line.startsWith('retry:')) {\n event.retry = parseInt(line.slice(6).trim(), 10);\n }\n }\n \n const dataStr = dataLines.join('\\n');\n \n try {\n event.data = JSON.parse(dataStr);\n } catch {\n event.data = dataStr;\n }\n \n yield event;\n }\n }\n}\n\n// ============= 实现 =============\n\n/**\n * 创建 Eden 风格的类型安全 API 客户端\n */\nexport function eden<T>(\n baseURL: string,\n config?: EdenConfig\n): EdenClient<T> {\n const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {}\n\n async function request<TReturn>(\n method: string,\n path: string,\n data?: unknown,\n requestConfig?: RequestConfig\n ): Promise<ApiResponse<TReturn>> {\n const url = new URL(path, baseURL)\n \n if (method === 'GET' && data && typeof data === 'object') {\n for (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...defaultHeaders,\n ...requestConfig?.headers,\n }\n\n const controller = new AbortController()\n let timeoutId: ReturnType<typeof setTimeout> | undefined\n \n const userSignal = requestConfig?.signal\n const requestTimeout = requestConfig?.timeout ?? timeout\n \n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort()\n } else {\n userSignal.addEventListener('abort', () => controller.abort())\n }\n }\n \n if (requestTimeout) {\n timeoutId = setTimeout(() => controller.abort(), requestTimeout)\n }\n\n const fetchOptions: RequestInit = { \n method, \n headers,\n signal: controller.signal \n }\n\n if (method !== 'GET' && method !== 'HEAD' && data) {\n fetchOptions.body = JSON.stringify(data)\n }\n\n let req = new Request(url.toString(), fetchOptions)\n \n if (onRequest) {\n req = await onRequest(req)\n }\n\n try {\n const response = await fetch(req)\n \n if (timeoutId) clearTimeout(timeoutId)\n \n const contentType = response.headers.get('content-type')\n let responseData: TReturn | null = null\n \n if (contentType?.includes('application/json')) {\n responseData = await response.json()\n } else if (contentType?.includes('text/')) {\n responseData = await response.text() as unknown as TReturn\n }\n\n let result: ApiResponse<TReturn>\n \n if (response.ok) {\n result = { data: responseData, error: null }\n } else {\n const errorBody = responseData as { code?: number; message?: string } | null\n result = {\n data: null,\n error: {\n code: errorBody?.code ?? response.status,\n message: errorBody?.message ?? `HTTP ${response.status}`\n }\n }\n }\n\n if (onResponse) {\n result = await onResponse(result)\n }\n\n if (result.error && onError) {\n onError(result.error)\n }\n\n return result\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n const apiError: ApiError = { code: 0, message: err.message || '网络错误' }\n if (onError) onError(apiError)\n return {\n data: null,\n error: apiError,\n }\n }\n }\n\n function subscribe<TData>(\n path: string,\n query: Record<string, unknown> | undefined,\n callbacks: SSECallbacks<TData>,\n options?: SSESubscribeOptions\n ): SSESubscription<TData> {\n const url = new URL(path, baseURL)\n \n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n let abortController: AbortController | null = new AbortController()\n let connected = false\n let reconnectCount = 0\n let isUnsubscribed = false\n let lastEventId: string | undefined\n \n const reconnectInterval = options?.reconnectInterval ?? 3000\n const maxReconnects = options?.maxReconnects ?? 5\n\n const connect = async () => {\n if (isUnsubscribed) return\n \n try {\n abortController = new AbortController()\n \n const headers: Record<string, string> = {\n 'Accept': 'text/event-stream',\n ...defaultHeaders,\n ...options?.headers,\n }\n \n if (lastEventId) {\n headers['Last-Event-ID'] = lastEventId\n }\n \n const response = await fetch(url.toString(), {\n method: 'GET',\n headers,\n signal: abortController.signal,\n })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`)\n }\n\n if (!response.body) {\n throw new Error('No response body')\n }\n\n connected = true\n reconnectCount = 0\n callbacks.onOpen?.()\n\n const reader = response.body.getReader()\n \n for await (const event of parseSSEStream(reader)) {\n if (event.id) {\n lastEventId = event.id\n }\n \n if (event.event === 'error') {\n callbacks.onError?.({ code: -1, message: String(event.data) })\n } else {\n callbacks.onMessage(event.data as TData)\n }\n }\n\n connected = false\n callbacks.onClose?.()\n \n } catch (error) {\n connected = false\n \n if ((error as Error).name === 'AbortError' || isUnsubscribed) {\n return\n }\n \n const err = error instanceof Error ? error : new Error(String(error))\n callbacks.onError?.({ code: 0, message: err.message || 'SSE 连接错误' })\n \n if (reconnectCount < maxReconnects) {\n reconnectCount++\n callbacks.onReconnect?.(reconnectCount, maxReconnects)\n \n setTimeout(() => {\n if (!isUnsubscribed) {\n connect()\n }\n }, reconnectInterval)\n } else {\n callbacks.onMaxReconnects?.()\n }\n }\n }\n\n connect()\n\n return {\n unsubscribe: () => {\n isUnsubscribed = true\n abortController?.abort()\n abortController = null\n connected = false\n },\n get connected() {\n return connected\n }\n }\n }\n\n function createEndpoint(basePath: string): unknown {\n const methods = ['get', 'post', 'put', 'patch', 'delete']\n \n const handler = (params: Record<string, string>) => {\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n\n return new Proxy(handler as unknown as object, {\n get(_, prop: string) {\n if (methods.includes(prop)) {\n const httpMethod = prop.toUpperCase()\n return (data?: unknown, cfg?: RequestConfig) => {\n return request(httpMethod, basePath, data, cfg)\n }\n }\n \n if (prop === 'subscribe') {\n return <TData>(\n queryOrCallbacks: Record<string, unknown> | SSECallbacks<TData>,\n callbacksOrOptions?: SSECallbacks<TData> | SSESubscribeOptions,\n options?: SSESubscribeOptions\n ) => {\n if (typeof queryOrCallbacks === 'object' && 'onMessage' in queryOrCallbacks) {\n return subscribe<TData>(\n basePath, \n undefined, \n queryOrCallbacks as SSECallbacks<TData>,\n callbacksOrOptions as SSESubscribeOptions\n )\n } else {\n return subscribe<TData>(\n basePath,\n queryOrCallbacks as Record<string, unknown>,\n callbacksOrOptions as SSECallbacks<TData>,\n options\n )\n }\n }\n }\n \n const childPath = `${basePath}/${prop}`\n return createEndpoint(childPath)\n },\n apply(_, __, args) {\n const params = args[0] as Record<string, string>\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n })\n }\n\n return new Proxy({} as EdenClient<T>, {\n get(_, prop: string) {\n return createEndpoint(`/${prop}`)\n }\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core/compose.ts","../src/core/client.ts","../src/core/eden.ts","../src/middlewares/timeout.ts","../src/middlewares/retry.ts","../src/middlewares/logger.ts"],"sourcesContent":["/**\n * 中间件组合函数\n * \n * 将多个中间件组合成一个执行链,类似 Koa 的洋葱模型\n */\n\nimport type { Middleware, RequestContext, ResponseContext } from '../types'\n\n/**\n * 组合多个中间件为单一函数\n * \n * @param middlewares 中间件数组\n * @returns 组合后的中间件函数\n * \n * @example\n * ```typescript\n * const chain = compose([m1, m2, m3])\n * const response = await chain(ctx, finalHandler)\n * ```\n */\nexport function compose(\n middlewares: Middleware[]\n): (ctx: RequestContext, final: () => Promise<ResponseContext>) => Promise<ResponseContext> {\n // 验证中间件\n for (const fn of middlewares) {\n if (typeof fn !== 'function') {\n throw new TypeError('Middleware must be a function')\n }\n }\n\n return function composedMiddleware(\n ctx: RequestContext,\n final: () => Promise<ResponseContext>\n ): Promise<ResponseContext> {\n let index = -1\n\n function dispatch(i: number): Promise<ResponseContext> {\n // 防止多次调用 next\n if (i <= index) {\n return Promise.reject(new Error('next() called multiple times'))\n }\n index = i\n\n // 获取当前中间件\n const fn = i < middlewares.length ? middlewares[i] : final\n\n // 如果没有更多中间件,执行最终处理器\n if (!fn) {\n return final()\n }\n\n try {\n // 执行中间件,传入 next 函数\n return Promise.resolve(\n fn(ctx, () => dispatch(i + 1))\n )\n } catch (err) {\n return Promise.reject(err)\n }\n }\n\n return dispatch(0)\n }\n}\n","/**\n * HTTP 客户端实现\n * \n * 基于中间件模式的可扩展 HTTP 客户端\n */\n\nimport type {\n ApiError,\n ApiResponse,\n Client,\n Middleware,\n NamedMiddleware,\n MiddlewareOptions,\n RequestConfig,\n RequestContext,\n ResponseContext,\n} from '../types'\nimport { compose } from './compose'\n\n// ==================== 辅助函数 ====================\n\n/**\n * 定义带名称的中间件\n */\nexport function defineMiddleware(\n fn: Middleware,\n options?: MiddlewareOptions\n): NamedMiddleware {\n const named = fn as NamedMiddleware\n if (options?.name) {\n named.middlewareName = options.name\n }\n return named\n}\n\n/**\n * 创建成功响应\n */\nfunction createSuccessResponse<T>(data: T, ctx: RequestContext, raw: Response): ResponseContext<T> {\n return {\n request: ctx,\n raw,\n data,\n error: null,\n status: raw.status,\n }\n}\n\n/**\n * 创建错误响应\n */\nfunction createErrorResponse(\n code: number,\n message: string,\n ctx: RequestContext,\n raw: Response | null = null\n): ResponseContext {\n return {\n request: ctx,\n raw,\n data: null,\n error: { code, message },\n status: raw?.status ?? 0,\n }\n}\n\n// ==================== 客户端实现 ====================\n\n/**\n * 内部客户端实现类\n */\nclass ClientImpl implements Client {\n readonly baseURL: string\n private middlewares: NamedMiddleware[] = []\n private defaultHeaders: Record<string, string> = {}\n private defaultTimeout: number = 30000\n\n constructor(baseURL: string) {\n // 移除末尾斜杠\n this.baseURL = baseURL.replace(/\\/+$/, '')\n }\n\n use(middlewareOrName: Middleware | string, middleware?: Middleware): Client {\n if (typeof middlewareOrName === 'string') {\n // use(name, middleware) 形式\n if (!middleware) {\n throw new Error('Middleware is required when name is provided')\n }\n const named = middleware as NamedMiddleware\n named.middlewareName = middlewareOrName\n this.middlewares.push(named)\n } else {\n // use(middleware) 形式\n this.middlewares.push(middlewareOrName as NamedMiddleware)\n }\n return this\n }\n\n headers(h: Record<string, string>): Client {\n this.defaultHeaders = { ...this.defaultHeaders, ...h }\n return this\n }\n\n timeout(ms: number): Client {\n this.defaultTimeout = ms\n return this\n }\n\n async request<T = unknown>(\n method: string,\n path: string,\n body?: unknown,\n config?: RequestConfig\n ): Promise<ApiResponse<T>> {\n // 构建 URL\n const url = new URL(path.startsWith('/') ? path : `/${path}`, this.baseURL)\n\n // 构建请求头\n const headers = new Headers({\n 'Content-Type': 'application/json',\n ...this.defaultHeaders,\n ...config?.headers,\n })\n\n // 构建元数据\n const meta = new Map<string, unknown>()\n if (config?.meta) {\n for (const [key, value] of Object.entries(config.meta)) {\n meta.set(key, value)\n }\n }\n\n // 构建请求上下文\n const ctx: RequestContext = {\n method: method.toUpperCase(),\n path,\n url,\n headers,\n body,\n config,\n meta,\n retryCount: 0,\n }\n\n // 最终的 fetch 处理器\n const finalHandler = async (): Promise<ResponseContext<T>> => {\n return this.executeFetch<T>(ctx)\n }\n\n try {\n // 执行中间件链\n const response = await compose(this.middlewares)(ctx, finalHandler)\n\n // 转换为 ApiResponse\n return {\n data: response.data as T | null,\n error: response.error,\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n return {\n data: null,\n error: { code: 0, message: error.message || '请求失败' },\n }\n }\n }\n\n /**\n * 执行实际的 fetch 请求\n */\n private async executeFetch<T>(ctx: RequestContext): Promise<ResponseContext<T>> {\n const { method, url, headers, body, config } = ctx\n\n // 超时控制\n const controller = new AbortController()\n const timeoutMs = config?.timeout ?? this.defaultTimeout\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs)\n\n // 合并信号\n if (config?.signal) {\n config.signal.addEventListener('abort', () => controller.abort())\n }\n\n // 构建 fetch 选项\n const fetchOptions: RequestInit = {\n method,\n headers,\n signal: controller.signal,\n }\n\n // 添加请求体(GET/HEAD 不需要)\n if (body && method !== 'GET' && method !== 'HEAD') {\n fetchOptions.body = JSON.stringify(body)\n }\n\n // GET 请求的查询参数\n if (method === 'GET' && body && typeof body === 'object') {\n for (const [key, value] of Object.entries(body as Record<string, unknown>)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n try {\n // 创建 Request 对象(便于测试和拦截)\n const request = new Request(url.toString(), fetchOptions)\n const response = await fetch(request)\n clearTimeout(timeoutId)\n\n // 解析响应\n const contentType = response.headers.get('content-type')\n let data: T | null = null\n\n if (contentType?.includes('application/json')) {\n data = await response.json()\n } else if (contentType?.includes('text/')) {\n data = await response.text() as unknown as T\n }\n\n // 成功响应\n if (response.ok) {\n return createSuccessResponse(data, ctx, response)\n }\n\n // 错误响应\n const errorData = data as { code?: number; message?: string } | null\n return createErrorResponse(\n errorData?.code ?? response.status,\n errorData?.message ?? `HTTP ${response.status}`,\n ctx,\n response\n )\n } catch (err) {\n clearTimeout(timeoutId)\n\n const error = err instanceof Error ? err : new Error(String(err))\n\n // 超时错误\n if (error.name === 'AbortError') {\n return createErrorResponse(408, '请求超时', ctx)\n }\n\n // 网络错误\n return createErrorResponse(0, error.message || '网络错误', ctx)\n }\n }\n}\n\n// ==================== 导出 ====================\n\n/**\n * 创建 HTTP 客户端\n * \n * @param baseURL 基础 URL\n * @returns 客户端实例\n * \n * @example\n * ```typescript\n * const client = createClient('http://localhost:3000')\n * .use(authMiddleware)\n * .use('retry', retryMiddleware)\n * .timeout(30000)\n * \n * const api = eden<Api>(client)\n * ```\n */\nexport function createClient(baseURL: string): Client {\n return new ClientImpl(baseURL)\n}\n","/**\n * Eden 风格 API 客户端\n * \n * 最自然的链式调用:\n * - api.users.get() // GET /users\n * - api.users.post({ name }) // POST /users\n * - api.users({ id }).get() // GET /users/:id\n * - api.users({ id }).delete() // DELETE /users/:id\n * - api.chat.stream.subscribe() // SSE 流式响应\n * \n * @example\n * ```typescript\n * import { defineRoute } from 'vafast'\n * import { eden, InferEden } from '@vafast/api-client'\n * \n * // 定义路由(保留类型)\n * const routeDefinitions = [\n * defineRoute({\n * method: 'GET',\n * path: '/users',\n * schema: { query: Type.Object({ page: Type.Number() }) },\n * handler: ({ query }) => ({ users: [], page: query.page })\n * })\n * ] as const\n * \n * // 客户端推断类型\n * type Api = InferEden<typeof routeDefinitions>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 类型安全调用\n * const { data } = await api.users.get({ page: 1 })\n * ```\n */\n\nimport type { ApiResponse, ApiError, RequestConfig } from '../types'\n\n// ============= SSE 类型 =============\n\nexport interface SSEEvent<T = unknown> {\n event?: string\n data: T\n id?: string\n retry?: number\n}\n\nexport interface SSESubscribeOptions {\n headers?: Record<string, string>\n reconnectInterval?: number\n maxReconnects?: number\n timeout?: number\n}\n\nexport interface SSESubscription<T = unknown> {\n unsubscribe: () => void\n readonly connected: boolean\n}\n\n// ============= 基础类型工具 =============\n\n/** 从 TypeBox Schema 提取静态类型 */\ntype InferStatic<T> = T extends { static: infer S } ? S : T\n\n/** 检查是否是 SSE Handler */\ntype IsSSEHandler<T> = T extends { __sse: { readonly __brand: 'SSE' } } ? true : false\n\n/** 从 Schema 对象提取各部分类型 */\ntype GetSchemaQuery<S> = S extends { query: infer Q } ? InferStatic<Q> : undefined\ntype GetSchemaBody<S> = S extends { body: infer B } ? InferStatic<B> : undefined\ntype GetSchemaParams<S> = S extends { params: infer P } ? InferStatic<P> : undefined\n\n/** \n * 从 handler 函数推断返回类型\n * handler: (ctx) => TReturn | Promise<TReturn>\n */\ntype InferHandlerReturn<H> = H extends (...args: never[]) => infer R\n ? R extends Promise<infer T> ? T : R\n : unknown\n\n// ============= 路径处理 =============\n\n/** 移除开头斜杠:/users → users */\ntype TrimSlash<P extends string> = P extends `/${infer R}` ? R : P\n\n/** 获取第一段:users/posts → users */\ntype Head<P extends string> = P extends `${infer H}/${string}` ? H : P\n\n/** 获取剩余段:users/posts → posts */\ntype Tail<P extends string> = P extends `${string}/${infer T}` ? T : never\n\n/** 检查是否是动态参数段::id → true */\ntype IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false\n\n// ============= 清理 undefined 字段 =============\n\ntype Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }\n\n// ============= SSE 标记类型 =============\n\ntype SSEBrand = { readonly __brand: 'SSE' }\n\n// ============= 核心类型推断(适配新的 defineRoute) =============\n\n/**\n * 从 defineRoute 返回的路由配置构建方法定义\n * \n * defineRoute 返回的 LeafRouteConfig 结构:\n * {\n * method: TMethod,\n * path: TPath,\n * schema?: TSchema,\n * handler: (ctx) => TReturn | Promise<TReturn>\n * }\n */\ntype BuildMethodDef<R> = R extends {\n readonly schema?: infer TSchema\n readonly handler: infer THandler\n}\n ? Clean<{\n query: GetSchemaQuery<TSchema>\n body: GetSchemaBody<TSchema>\n params: GetSchemaParams<TSchema>\n return: InferHandlerReturn<THandler>\n }>\n : Clean<{\n return: R extends { readonly handler: infer H } ? InferHandlerReturn<H> : unknown\n }>\n\n/**\n * 递归构建嵌套路径结构\n * \n * 处理动态参数:/users/:id → { users: { ':id': { ... } } }\n */\ntype BuildPath<Path extends string, Method extends string, Def> =\n Path extends `${infer First}/${infer Rest}`\n ? IsDynamicSegment<First> extends true\n ? { ':id': BuildPath<Rest, Method, Def> }\n : { [K in First]: BuildPath<Rest, Method, Def> }\n : IsDynamicSegment<Path> extends true\n ? { ':id': { [M in Method]: Def } }\n : Path extends ''\n ? { [M in Method]: Def }\n : { [K in Path]: { [M in Method]: Def } }\n\n/**\n * 从单个路由生成嵌套类型结构\n */\ntype RouteToTree<R> = R extends {\n readonly method: infer M extends string\n readonly path: infer P extends string\n}\n ? BuildPath<TrimSlash<P>, Lowercase<M>, BuildMethodDef<R>>\n : {}\n\n// ============= 深度合并多个路由 =============\n\ntype DeepMerge<A, B> = {\n [K in keyof A | keyof B]: \n K extends keyof A & keyof B\n ? A[K] extends object\n ? B[K] extends object\n ? DeepMerge<A[K], B[K]>\n : A[K] & B[K]\n : A[K] & B[K]\n : K extends keyof A\n ? A[K]\n : K extends keyof B\n ? B[K]\n : never\n}\n\n/** 递归合并路由数组为单一类型结构 */\ntype MergeRoutes<T extends readonly unknown[]> = \n T extends readonly [infer First]\n ? RouteToTree<First>\n : T extends readonly [infer First, ...infer Rest]\n ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>>\n : {}\n\n/**\n * 从 defineRoutes 结果自动推断 Eden 契约\n * \n * 支持两种用法:\n * 1. 直接从 defineRoutes 结果推断(推荐,无需 as const)\n * 2. 从原始路由定义数组推断(需要 as const)\n * \n * @example\n * ```typescript\n * import { defineRoute, defineRoutes, Type } from 'vafast'\n * import { eden, InferEden } from '@vafast/api-client'\n * \n * // 方式1:直接从 defineRoutes 结果推断(推荐)\n * const routes = defineRoutes([\n * defineRoute({\n * method: 'GET',\n * path: '/users',\n * schema: { query: Type.Object({ page: Type.Number() }) },\n * handler: ({ query }) => ({ users: [], total: 0 })\n * }),\n * defineRoute({\n * method: 'POST',\n * path: '/users',\n * schema: { body: Type.Object({ name: Type.String() }) },\n * handler: ({ body }) => ({ id: '1', name: body.name })\n * })\n * ])\n * \n * const server = new Server(routes)\n * \n * // ✅ 类型推断自动工作,无需 as const!\n * type Api = InferEden<typeof routes>\n * const api = eden<Api>('http://localhost:3000')\n * \n * // 类型安全的调用\n * const { data } = await api.users.get({ page: 1 }) // ✅ query 类型推断\n * const { data: user } = await api.users.post({ name: 'John' }) // ✅ body 类型推断\n * ```\n */\nexport type InferEden<T> = \n // 优先从 __source 提取类型(defineRoutes 返回的结果)\n T extends { __source: infer S extends readonly unknown[] }\n ? MergeRoutes<S>\n // 否则直接作为路由数组处理(需要 as const)\n : T extends readonly unknown[]\n ? MergeRoutes<T>\n : {}\n\n// ============= 契约类型(手动定义时使用) =============\n\ninterface MethodDef {\n query?: unknown\n body?: unknown\n params?: unknown\n return: unknown\n sse?: SSEBrand\n}\n\ntype RouteNode = {\n get?: MethodDef\n post?: MethodDef\n put?: MethodDef\n patch?: MethodDef\n delete?: MethodDef\n ':id'?: RouteNode\n [key: string]: MethodDef | RouteNode | undefined\n}\n\n// ============= 客户端类型 =============\n\ninterface SSECallbacks<T> {\n onMessage: (data: T) => void\n onError?: (error: ApiError) => void\n onOpen?: () => void\n onClose?: () => void\n onReconnect?: (attempt: number, maxAttempts: number) => void\n onMaxReconnects?: () => void\n}\n\ntype MethodCall<M extends MethodDef, HasParams extends boolean = false> = \n M extends { sse: SSEBrand }\n ? M extends { query: infer Q }\n ? (query: Q, callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : (callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>\n : HasParams extends true\n ? M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { query: infer Q }\n ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : M extends { body: infer B }\n ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n : (config?: RequestConfig) => Promise<ApiResponse<M['return']>>\n\ntype IsSSEEndpoint<M> = M extends { sse: { readonly __brand: 'SSE' } } ? true : false\n\ntype Endpoint<T, HasParams extends boolean = false> = \n {\n [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: \n T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never\n } \n & (T extends { get: infer M extends MethodDef }\n ? IsSSEEndpoint<M> extends true \n ? { subscribe: MethodCall<M, HasParams> }\n : {}\n : {})\n\ntype HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'\n\nexport type EdenClient<T, HasParams extends boolean = false> = {\n [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: \n T[K] extends { ':id': infer Child }\n ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false>\n : EdenClient<T[K], false>\n} & Endpoint<T, HasParams>\n\n// ============= SSE 解析器 =============\n\nasync function* parseSSEStream(\n reader: ReadableStreamDefaultReader<Uint8Array>\n): AsyncGenerator<SSEEvent, void, unknown> {\n const decoder = new TextDecoder();\n let buffer = '';\n \n while (true) {\n const { done, value } = await reader.read();\n \n if (done) break;\n \n buffer += decoder.decode(value, { stream: true });\n \n const events = buffer.split('\\n\\n');\n buffer = events.pop() || '';\n \n for (const eventStr of events) {\n if (!eventStr.trim()) continue;\n \n const event: SSEEvent = { data: '' };\n const lines = eventStr.split('\\n');\n let dataLines: string[] = [];\n \n for (const line of lines) {\n if (line.startsWith('event:')) {\n event.event = line.slice(6).trim();\n } else if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).trim());\n } else if (line.startsWith('id:')) {\n event.id = line.slice(3).trim();\n } else if (line.startsWith('retry:')) {\n event.retry = parseInt(line.slice(6).trim(), 10);\n }\n }\n \n const dataStr = dataLines.join('\\n');\n \n try {\n event.data = JSON.parse(dataStr);\n } catch {\n event.data = dataStr;\n }\n \n yield event;\n }\n }\n}\n\n// ============= Client 类型导入 ==============\n\nimport type { Client } from '../types'\n\n// ============= 实现 =============\n\n/**\n * 创建 Eden 风格的类型安全 API 客户端\n * \n * @param client - Client 实例\n * \n * @example\n * ```typescript\n * import { createClient, eden } from '@vafast/api-client'\n * \n * const client = createClient('http://localhost:3000')\n * .use(authMiddleware)\n * .use(retryMiddleware)\n * \n * const api = eden<Api>(client)\n * \n * const { data, error } = await api.users.find.post({ page: 1 })\n * ```\n */\nexport function eden<T>(client: Client): EdenClient<T> {\n // 获取 baseURL 用于 SSE\n const baseURL = client.baseURL\n \n // SSE 默认 headers(空对象,用户通过中间件添加)\n const defaultHeaders: Record<string, string> = {}\n\n // 请求函数:委托给 client\n async function request<TReturn>(\n method: string,\n path: string,\n data?: unknown,\n requestConfig?: RequestConfig\n ): Promise<ApiResponse<TReturn>> {\n return client.request<TReturn>(method, path, data, requestConfig)\n }\n\n function subscribe<TData>(\n path: string,\n query: Record<string, unknown> | undefined,\n callbacks: SSECallbacks<TData>,\n options?: SSESubscribeOptions\n ): SSESubscription<TData> {\n const url = new URL(path, baseURL)\n \n if (query) {\n for (const [key, value] of Object.entries(query)) {\n if (value !== undefined && value !== null) {\n url.searchParams.set(key, String(value))\n }\n }\n }\n\n let abortController: AbortController | null = new AbortController()\n let connected = false\n let reconnectCount = 0\n let isUnsubscribed = false\n let lastEventId: string | undefined\n \n const reconnectInterval = options?.reconnectInterval ?? 3000\n const maxReconnects = options?.maxReconnects ?? 5\n\n const connect = async () => {\n if (isUnsubscribed) return\n \n try {\n abortController = new AbortController()\n \n const headers: Record<string, string> = {\n 'Accept': 'text/event-stream',\n ...defaultHeaders,\n ...options?.headers,\n }\n \n if (lastEventId) {\n headers['Last-Event-ID'] = lastEventId\n }\n \n const response = await fetch(url.toString(), {\n method: 'GET',\n headers,\n signal: abortController.signal,\n })\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`)\n }\n\n if (!response.body) {\n throw new Error('No response body')\n }\n\n connected = true\n reconnectCount = 0\n callbacks.onOpen?.()\n\n const reader = response.body.getReader()\n \n for await (const event of parseSSEStream(reader)) {\n if (event.id) {\n lastEventId = event.id\n }\n \n if (event.event === 'error') {\n callbacks.onError?.({ code: -1, message: String(event.data) })\n } else {\n callbacks.onMessage(event.data as TData)\n }\n }\n\n connected = false\n callbacks.onClose?.()\n \n } catch (error) {\n connected = false\n \n if ((error as Error).name === 'AbortError' || isUnsubscribed) {\n return\n }\n \n const err = error instanceof Error ? error : new Error(String(error))\n callbacks.onError?.({ code: 0, message: err.message || 'SSE 连接错误' })\n \n if (reconnectCount < maxReconnects) {\n reconnectCount++\n callbacks.onReconnect?.(reconnectCount, maxReconnects)\n \n setTimeout(() => {\n if (!isUnsubscribed) {\n connect()\n }\n }, reconnectInterval)\n } else {\n callbacks.onMaxReconnects?.()\n }\n }\n }\n\n connect()\n\n return {\n unsubscribe: () => {\n isUnsubscribed = true\n abortController?.abort()\n abortController = null\n connected = false\n },\n get connected() {\n return connected\n }\n }\n }\n\n function createEndpoint(basePath: string): unknown {\n const methods = ['get', 'post', 'put', 'patch', 'delete']\n \n const handler = (params: Record<string, string>) => {\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n\n return new Proxy(handler as unknown as object, {\n get(_, prop: string) {\n if (methods.includes(prop)) {\n const httpMethod = prop.toUpperCase()\n return (data?: unknown, cfg?: RequestConfig) => {\n return request(httpMethod, basePath, data, cfg)\n }\n }\n \n if (prop === 'subscribe') {\n return <TData>(\n queryOrCallbacks: Record<string, unknown> | SSECallbacks<TData>,\n callbacksOrOptions?: SSECallbacks<TData> | SSESubscribeOptions,\n options?: SSESubscribeOptions\n ) => {\n if (typeof queryOrCallbacks === 'object' && 'onMessage' in queryOrCallbacks) {\n return subscribe<TData>(\n basePath, \n undefined, \n queryOrCallbacks as SSECallbacks<TData>,\n callbacksOrOptions as SSESubscribeOptions\n )\n } else {\n return subscribe<TData>(\n basePath,\n queryOrCallbacks as Record<string, unknown>,\n callbacksOrOptions as SSECallbacks<TData>,\n options\n )\n }\n }\n }\n \n const childPath = `${basePath}/${prop}`\n return createEndpoint(childPath)\n },\n apply(_, __, args) {\n const params = args[0] as Record<string, string>\n const paramValue = Object.values(params)[0]\n const newPath = `${basePath}/${encodeURIComponent(paramValue)}`\n return createEndpoint(newPath)\n }\n })\n }\n\n return new Proxy({} as EdenClient<T>, {\n get(_, prop: string) {\n return createEndpoint(`/${prop}`)\n }\n })\n}\n","/**\n * 超时中间件\n * \n * 为请求设置超时限制\n */\n\nimport type { Middleware, NamedMiddleware } from '../types'\n\n/**\n * 创建超时中间件\n * \n * @param ms 超时时间(毫秒)\n * @returns 超时中间件\n * \n * @example\n * ```typescript\n * const client = createClient(BASE_URL)\n * .use(timeoutMiddleware(30000)) // 30 秒超时\n * ```\n */\nexport function timeoutMiddleware(ms: number): NamedMiddleware {\n const middleware: Middleware = async (ctx, next) => {\n // 如果请求配置中有超时,优先使用\n const timeout = ctx.config?.timeout ?? ms\n \n // 创建超时 Promise\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(new Error(`请求超时 (${timeout}ms)`))\n }, timeout)\n })\n \n // 竞争:请求 vs 超时\n try {\n const response = await Promise.race([\n next(),\n timeoutPromise,\n ])\n return response\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n return {\n request: ctx,\n raw: null,\n data: null,\n error: { code: 408, message: err.message },\n status: 408,\n }\n }\n }\n \n // 添加名称\n const named = middleware as NamedMiddleware\n named.middlewareName = 'timeout'\n return named\n}\n","/**\n * 重试中间件\n * \n * 自动重试失败的请求\n */\n\nimport type { Middleware, NamedMiddleware, ResponseContext } from '../types'\n\n/**\n * 重试配置\n */\nexport interface RetryOptions {\n /** 重试次数,默认 3 */\n count?: number\n /** 重试延迟(毫秒),默认 1000 */\n delay?: number\n /** 指数退避,默认 true */\n backoff?: boolean\n /** 触发重试的状态码,默认 [408, 429, 500, 502, 503, 504] */\n on?: number[]\n /** 自定义重试判断 */\n shouldRetry?: (ctx: ResponseContext) => boolean\n}\n\n/** 默认重试状态码 */\nconst DEFAULT_RETRY_STATUS = [408, 429, 500, 502, 503, 504]\n\n/**\n * 延迟执行\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\n/**\n * 创建重试中间件\n * \n * @param options 重试配置\n * @returns 重试中间件\n * \n * @example\n * ```typescript\n * const client = createClient(BASE_URL)\n * .use(retryMiddleware({ count: 3, delay: 1000 }))\n * ```\n */\nexport function retryMiddleware(options?: RetryOptions): NamedMiddleware {\n const {\n count = 3,\n delay = 1000,\n backoff = true,\n on = DEFAULT_RETRY_STATUS,\n shouldRetry,\n } = options ?? {}\n\n const middleware: Middleware = async (ctx, next) => {\n let lastResponse: ResponseContext | null = null\n let attempt = 0\n\n while (attempt <= count) {\n // 更新重试次数\n ctx.retryCount = attempt\n\n // 执行请求\n const response = await next()\n lastResponse = response\n\n // 成功或不需要重试\n if (!response.error) {\n return response\n }\n\n // 检查是否需要重试\n const needRetry = shouldRetry\n ? shouldRetry(response)\n : on.includes(response.status)\n\n if (!needRetry || attempt >= count) {\n return response\n }\n\n // 计算延迟(支持指数退避)\n const waitTime = backoff ? delay * Math.pow(2, attempt) : delay\n await sleep(waitTime)\n\n attempt++\n }\n\n // 返回最后一次响应\n return lastResponse!\n }\n\n // 添加名称\n const named = middleware as NamedMiddleware\n named.middlewareName = 'retry'\n return named\n}\n","/**\n * 日志中间件\n * \n * 记录请求和响应信息\n */\n\nimport type { Middleware, NamedMiddleware, RequestContext, ResponseContext } from '../types'\n\n/**\n * 日志配置\n */\nexport interface LoggerOptions {\n /** 请求日志回调 */\n onRequest?: (ctx: RequestContext) => void\n /** 响应日志回调 */\n onResponse?: (ctx: ResponseContext) => void\n /** 是否启用控制台日志,默认 true */\n console?: boolean\n /** 日志前缀 */\n prefix?: string\n}\n\n/**\n * 创建日志中间件\n * \n * @param options 日志配置\n * @returns 日志中间件\n * \n * @example\n * ```typescript\n * // 使用默认控制台日志\n * const client = createClient(BASE_URL)\n * .use(loggerMiddleware())\n * \n * // 自定义日志\n * const client = createClient(BASE_URL)\n * .use(loggerMiddleware({\n * onRequest: (ctx) => console.log(`[REQ] ${ctx.method} ${ctx.path}`),\n * onResponse: (ctx) => console.log(`[RES] ${ctx.status} ${ctx.request.path}`),\n * }))\n * ```\n */\nexport function loggerMiddleware(options?: LoggerOptions): NamedMiddleware {\n const {\n onRequest,\n onResponse,\n console: useConsole = true,\n prefix = '[API]',\n } = options ?? {}\n\n const middleware: Middleware = async (ctx, next) => {\n const startTime = Date.now()\n\n // 请求日志\n if (onRequest) {\n onRequest(ctx)\n }\n if (useConsole) {\n console.log(`${prefix} → ${ctx.method} ${ctx.path}`)\n }\n\n // 执行请求\n const response = await next()\n\n // 响应日志\n const duration = Date.now() - startTime\n if (onResponse) {\n onResponse(response)\n }\n if (useConsole) {\n const status = response.error ? `ERR ${response.error.code}` : `${response.status}`\n console.log(`${prefix} ← ${status} ${ctx.path} (${duration}ms)`)\n }\n\n return response\n }\n\n // 添加名称\n const named = middleware as NamedMiddleware\n named.middlewareName = 'logger'\n return named\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vafast/api-client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Type-safe API client for Vafast framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
"vafast": "^0.5.1"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"vafast": "^0.5.1",
|
|
21
20
|
"tsdown": "^0.19.0-beta.4",
|
|
22
21
|
"typescript": "^5.5.3",
|
|
22
|
+
"vafast": "^0.5.6",
|
|
23
23
|
"vitest": "^2.1.8"
|
|
24
24
|
},
|
|
25
25
|
"main": "./dist/index.mjs",
|