@vafast/api-client 0.1.2 → 0.1.4
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 +43 -34
- package/dist/index.d.mts +296 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +226 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +13 -9
- package/TODO.md +0 -83
- package/build.ts +0 -37
- package/bun.lock +0 -569
- package/example/auto-infer.ts +0 -202
- package/example/test-sse.ts +0 -192
- package/src/core/eden.ts +0 -697
- package/src/index.ts +0 -42
- package/src/types/index.ts +0 -34
- package/test/eden.test.ts +0 -425
- package/tsconfig.dts.json +0 -20
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -23
- package/vitest.config.ts +0 -8
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## ✨ 特性
|
|
6
6
|
|
|
7
7
|
- 🔒 **完整类型推断** - 从路由定义自动推断 API 类型,无需手动定义接口
|
|
8
|
-
- 🎯 **无需 `as const`** -
|
|
8
|
+
- 🎯 **无需 `as const`** - `defineRoutes()` 自动保留字面量类型
|
|
9
9
|
- 🌊 **SSE 流式响应** - 内置 Server-Sent Events 支持,包含自动重连
|
|
10
10
|
- ⏹️ **请求取消** - 支持 AbortController 取消进行中的请求
|
|
11
11
|
- 🔗 **链式调用** - 优雅的 `api.users({ id }).posts.get()` 语法
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install @vafast/api-client
|
|
18
|
-
# 或
|
|
19
|
-
bun add @vafast/api-client
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
## 🚀 快速开始
|
|
@@ -25,39 +23,52 @@ bun add @vafast/api-client
|
|
|
25
23
|
|
|
26
24
|
```typescript
|
|
27
25
|
// server.ts
|
|
28
|
-
import { defineRoutes,
|
|
26
|
+
import { defineRoutes, createHandler, createSSEHandler, Type } from 'vafast'
|
|
29
27
|
|
|
30
28
|
export const routes = defineRoutes([
|
|
31
|
-
// ✨
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
29
|
+
// ✨ defineRoutes() 自动保留字面量类型,无需 as const
|
|
30
|
+
{
|
|
31
|
+
method: 'GET',
|
|
32
|
+
path: '/users',
|
|
33
|
+
handler: createHandler(
|
|
34
|
+
{ query: Type.Object({ page: Type.Optional(Type.Number()) }) },
|
|
35
|
+
async ({ query }) => ({ users: [], total: 0, page: query.page ?? 1 })
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
method: 'POST',
|
|
40
|
+
path: '/users',
|
|
41
|
+
handler: createHandler(
|
|
42
|
+
{ body: Type.Object({ name: Type.String(), email: Type.String() }) },
|
|
43
|
+
async ({ body }) => ({ id: crypto.randomUUID(), ...body })
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
method: 'GET',
|
|
48
|
+
path: '/users/:id',
|
|
49
|
+
handler: createHandler(
|
|
50
|
+
{ params: Type.Object({ id: Type.String() }) },
|
|
51
|
+
async ({ params }) => ({ id: params.id, name: 'User' })
|
|
52
|
+
)
|
|
53
|
+
},
|
|
47
54
|
// 🌊 SSE 流式响应
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
yield { data: {
|
|
55
|
-
|
|
55
|
+
{
|
|
56
|
+
method: 'GET',
|
|
57
|
+
path: '/chat/stream',
|
|
58
|
+
handler: createSSEHandler(
|
|
59
|
+
{ query: Type.Object({ prompt: Type.String() }) },
|
|
60
|
+
async function* ({ query }) {
|
|
61
|
+
yield { event: 'start', data: { message: 'Starting...' } }
|
|
62
|
+
|
|
63
|
+
for (const word of query.prompt.split(' ')) {
|
|
64
|
+
yield { data: { text: word } }
|
|
65
|
+
await new Promise(r => setTimeout(r, 100))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
yield { event: 'end', data: { message: 'Done!' } }
|
|
56
69
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
))
|
|
70
|
+
)
|
|
71
|
+
}
|
|
61
72
|
])
|
|
62
73
|
|
|
63
74
|
// 导出类型供客户端使用
|
|
@@ -280,8 +291,6 @@ interface RequestConfig {
|
|
|
280
291
|
|
|
281
292
|
```bash
|
|
282
293
|
npm test
|
|
283
|
-
# 或
|
|
284
|
-
bun test
|
|
285
294
|
```
|
|
286
295
|
|
|
287
296
|
## 📄 许可证
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
//#region src/types/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 类型定义
|
|
4
|
+
*/
|
|
5
|
+
/** HTTP 方法 */
|
|
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
|
+
/**
|
|
19
|
+
* API 响应
|
|
20
|
+
*/
|
|
21
|
+
interface ApiResponse<T = unknown> {
|
|
22
|
+
/** 响应数据 */
|
|
23
|
+
data: T | null;
|
|
24
|
+
/** 错误信息 */
|
|
25
|
+
error: Error | null;
|
|
26
|
+
/** HTTP 状态码 */
|
|
27
|
+
status: number;
|
|
28
|
+
/** 响应头 */
|
|
29
|
+
headers: Headers;
|
|
30
|
+
/** 原始响应对象 */
|
|
31
|
+
response: Response;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/core/eden.d.ts
|
|
35
|
+
interface EdenConfig {
|
|
36
|
+
headers?: Record<string, string>;
|
|
37
|
+
onRequest?: (request: Request) => Request | Promise<Request>;
|
|
38
|
+
onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;
|
|
39
|
+
onError?: (error: Error) => void;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* SSE 事件
|
|
44
|
+
*/
|
|
45
|
+
interface SSEEvent<T = unknown> {
|
|
46
|
+
event?: string;
|
|
47
|
+
data: T;
|
|
48
|
+
id?: string;
|
|
49
|
+
retry?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* SSE 订阅选项
|
|
53
|
+
*/
|
|
54
|
+
interface SSESubscribeOptions {
|
|
55
|
+
/** 自定义请求头 */
|
|
56
|
+
headers?: Record<string, string>;
|
|
57
|
+
/** 重连间隔(毫秒) */
|
|
58
|
+
reconnectInterval?: number;
|
|
59
|
+
/** 最大重连次数 */
|
|
60
|
+
maxReconnects?: number;
|
|
61
|
+
/** 连接超时(毫秒) */
|
|
62
|
+
timeout?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* SSE 订阅结果
|
|
66
|
+
*/
|
|
67
|
+
interface SSESubscription<T = unknown> {
|
|
68
|
+
/** 取消订阅 */
|
|
69
|
+
unsubscribe: () => void;
|
|
70
|
+
/** 是否已连接 */
|
|
71
|
+
readonly connected: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 从 TypeBox Schema 提取静态类型
|
|
75
|
+
* 使用 TypeBox 内部的 static 属性提取类型
|
|
76
|
+
*/
|
|
77
|
+
type InferStatic<T> = T extends {
|
|
78
|
+
static: infer S;
|
|
79
|
+
} ? S : T;
|
|
80
|
+
/** 从 InferableHandler 提取返回类型 */
|
|
81
|
+
type ExtractReturn<T> = T extends {
|
|
82
|
+
__returnType: infer R;
|
|
83
|
+
} ? R : unknown;
|
|
84
|
+
/** 从 InferableHandler 提取 Schema */
|
|
85
|
+
type ExtractSchema<T> = T extends {
|
|
86
|
+
__schema: infer S;
|
|
87
|
+
} ? S : {};
|
|
88
|
+
/** 检查是否是 SSE Handler(使用品牌类型检测) */
|
|
89
|
+
type IsSSEHandler<T> = T extends {
|
|
90
|
+
__sse: {
|
|
91
|
+
readonly __brand: 'SSE';
|
|
92
|
+
};
|
|
93
|
+
} ? true : false;
|
|
94
|
+
/** 从 Schema 提取各部分类型 */
|
|
95
|
+
type GetQuery<S> = S extends {
|
|
96
|
+
query: infer Q;
|
|
97
|
+
} ? InferStatic<Q> : undefined;
|
|
98
|
+
type GetBody<S> = S extends {
|
|
99
|
+
body: infer B;
|
|
100
|
+
} ? InferStatic<B> : undefined;
|
|
101
|
+
type GetParams<S> = S extends {
|
|
102
|
+
params: infer P;
|
|
103
|
+
} ? InferStatic<P> : undefined;
|
|
104
|
+
/** 移除开头斜杠:/users → users */
|
|
105
|
+
type TrimSlash<P$1 extends string> = P$1 extends `/${infer R}` ? R : P$1;
|
|
106
|
+
/** 检查是否是动态参数段::id → true */
|
|
107
|
+
type IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false;
|
|
108
|
+
/** 清理 undefined 字段 */
|
|
109
|
+
type Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] };
|
|
110
|
+
/** SSE 标记类型 */
|
|
111
|
+
type SSEBrand = {
|
|
112
|
+
readonly __brand: 'SSE';
|
|
113
|
+
};
|
|
114
|
+
/** 从路由构建方法定义 */
|
|
115
|
+
type BuildMethodDef<R extends {
|
|
116
|
+
readonly handler: unknown;
|
|
117
|
+
}> = IsSSEHandler<R['handler']> extends true ? Clean<{
|
|
118
|
+
query: GetQuery<ExtractSchema<R['handler']>>;
|
|
119
|
+
params: GetParams<ExtractSchema<R['handler']>>;
|
|
120
|
+
return: ExtractReturn<R['handler']>;
|
|
121
|
+
sse: SSEBrand;
|
|
122
|
+
}> : Clean<{
|
|
123
|
+
query: GetQuery<ExtractSchema<R['handler']>>;
|
|
124
|
+
body: GetBody<ExtractSchema<R['handler']>>;
|
|
125
|
+
params: GetParams<ExtractSchema<R['handler']>>;
|
|
126
|
+
return: ExtractReturn<R['handler']>;
|
|
127
|
+
}>;
|
|
128
|
+
/**
|
|
129
|
+
* 递归构建嵌套路径结构
|
|
130
|
+
*
|
|
131
|
+
* 处理动态参数:/users/:id → { users: { ':id': { ... } } }
|
|
132
|
+
*/
|
|
133
|
+
type BuildPath<Path extends string, Method extends string, Def> = Path extends `${infer First}/${infer Rest}` ? IsDynamicSegment<First> extends true ? {
|
|
134
|
+
':id': BuildPath<Rest, Method, Def>;
|
|
135
|
+
} : { [K in First]: BuildPath<Rest, Method, Def> } : IsDynamicSegment<Path> extends true ? {
|
|
136
|
+
':id': { [M in Method]: Def };
|
|
137
|
+
} : { [K in Path]: { [M in Method]: Def } };
|
|
138
|
+
/**
|
|
139
|
+
* 从单个路由生成嵌套类型结构
|
|
140
|
+
*/
|
|
141
|
+
type RouteToTree<R extends {
|
|
142
|
+
readonly method: string;
|
|
143
|
+
readonly path: string;
|
|
144
|
+
readonly handler: unknown;
|
|
145
|
+
}> = BuildPath<TrimSlash<R['path']>, Lowercase<R['method']>, BuildMethodDef<R>>;
|
|
146
|
+
type DeepMerge<A, B$1> = { [K in keyof A | keyof B$1]: K extends keyof A & keyof B$1 ? A[K] extends object ? B$1[K] extends object ? DeepMerge<A[K], B$1[K]> : A[K] & B$1[K] : A[K] & B$1[K] : K extends keyof A ? A[K] : K extends keyof B$1 ? B$1[K] : never };
|
|
147
|
+
/** 递归合并路由数组为单一类型结构 */
|
|
148
|
+
type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First extends {
|
|
149
|
+
readonly method: string;
|
|
150
|
+
readonly path: string;
|
|
151
|
+
readonly handler: unknown;
|
|
152
|
+
}] ? RouteToTree<First> : T extends readonly [infer First extends {
|
|
153
|
+
readonly method: string;
|
|
154
|
+
readonly path: string;
|
|
155
|
+
readonly handler: unknown;
|
|
156
|
+
}, ...infer Rest extends readonly {
|
|
157
|
+
readonly method: string;
|
|
158
|
+
readonly path: string;
|
|
159
|
+
readonly handler: unknown;
|
|
160
|
+
}[]] ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>> : {};
|
|
161
|
+
/**
|
|
162
|
+
* 从 vafast 路由数组自动推断 Eden 契约
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```typescript
|
|
166
|
+
* import { defineRoutes, createHandler, Type } from 'vafast'
|
|
167
|
+
* import { eden, InferEden } from 'vafast-api-client'
|
|
168
|
+
*
|
|
169
|
+
* // ✨ defineRoutes() 自动保留字面量类型,无需 as const
|
|
170
|
+
* const routes = defineRoutes([
|
|
171
|
+
* {
|
|
172
|
+
* method: 'GET',
|
|
173
|
+
* path: '/users',
|
|
174
|
+
* handler: createHandler(
|
|
175
|
+
* { query: Type.Object({ page: Type.Number() }) },
|
|
176
|
+
* async ({ query }) => ({ users: [], total: 0 })
|
|
177
|
+
* )
|
|
178
|
+
* },
|
|
179
|
+
* {
|
|
180
|
+
* method: 'GET',
|
|
181
|
+
* path: '/chat/stream',
|
|
182
|
+
* handler: createSSEHandler(
|
|
183
|
+
* { query: Type.Object({ prompt: Type.String() }) },
|
|
184
|
+
* async function* ({ query }) {
|
|
185
|
+
* yield { data: { text: 'Hello' } }
|
|
186
|
+
* }
|
|
187
|
+
* )
|
|
188
|
+
* }
|
|
189
|
+
* ])
|
|
190
|
+
*
|
|
191
|
+
* type Api = InferEden<typeof routes>
|
|
192
|
+
* const api = eden<Api>('http://localhost:3000')
|
|
193
|
+
*
|
|
194
|
+
* // 普通请求
|
|
195
|
+
* const { data } = await api.users.get({ page: 1 })
|
|
196
|
+
*
|
|
197
|
+
* // SSE 流式请求
|
|
198
|
+
* api.chat.stream.subscribe({ prompt: 'Hi' }, {
|
|
199
|
+
* onMessage: (data) => console.log(data),
|
|
200
|
+
* onError: (err) => console.error(err)
|
|
201
|
+
* })
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
type InferEden<T extends readonly {
|
|
205
|
+
readonly method: string;
|
|
206
|
+
readonly path: string;
|
|
207
|
+
readonly handler: unknown;
|
|
208
|
+
}[]> = MergeRoutes<T>;
|
|
209
|
+
/** HTTP 方法定义 */
|
|
210
|
+
interface MethodDef {
|
|
211
|
+
query?: unknown;
|
|
212
|
+
body?: unknown;
|
|
213
|
+
params?: unknown;
|
|
214
|
+
return: unknown;
|
|
215
|
+
sse?: SSEBrand;
|
|
216
|
+
}
|
|
217
|
+
/** SSE 订阅回调 */
|
|
218
|
+
interface SSECallbacks<T> {
|
|
219
|
+
/** 收到消息 */
|
|
220
|
+
onMessage: (data: T) => void;
|
|
221
|
+
/** 发生错误 */
|
|
222
|
+
onError?: (error: Error) => void;
|
|
223
|
+
/** 连接打开 */
|
|
224
|
+
onOpen?: () => void;
|
|
225
|
+
/** 连接关闭 */
|
|
226
|
+
onClose?: () => void;
|
|
227
|
+
/** 正在重连 */
|
|
228
|
+
onReconnect?: (attempt: number, maxAttempts: number) => void;
|
|
229
|
+
/** 达到最大重连次数 */
|
|
230
|
+
onMaxReconnects?: () => void;
|
|
231
|
+
}
|
|
232
|
+
/** 从方法定义提取调用签名 */
|
|
233
|
+
type MethodCall<M$1 extends MethodDef, HasParams extends boolean = false> = M$1 extends {
|
|
234
|
+
sse: SSEBrand;
|
|
235
|
+
} ? M$1 extends {
|
|
236
|
+
query: infer Q;
|
|
237
|
+
} ? (query: Q, callbacks: SSECallbacks<M$1['return']>, options?: SSESubscribeOptions) => SSESubscription<M$1['return']> : (callbacks: SSECallbacks<M$1['return']>, options?: SSESubscribeOptions) => SSESubscription<M$1['return']> : HasParams extends true ? M$1 extends {
|
|
238
|
+
body: infer B;
|
|
239
|
+
} ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : (config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : M$1 extends {
|
|
240
|
+
query: infer Q;
|
|
241
|
+
} ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : M$1 extends {
|
|
242
|
+
body: infer B;
|
|
243
|
+
} ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : (config?: RequestConfig) => Promise<ApiResponse<M$1['return']>>;
|
|
244
|
+
/** 检查是否是 SSE 端点(检测品牌类型标记) */
|
|
245
|
+
type IsSSEEndpoint<M$1> = M$1 extends {
|
|
246
|
+
sse: {
|
|
247
|
+
readonly __brand: 'SSE';
|
|
248
|
+
};
|
|
249
|
+
} ? true : false;
|
|
250
|
+
/** 端点类型 - 包含 subscribe 方法用于 SSE */
|
|
251
|
+
type Endpoint<T, HasParams extends boolean = false> = { [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never } & (T extends {
|
|
252
|
+
get: infer M extends MethodDef;
|
|
253
|
+
} ? IsSSEEndpoint<M> extends true ? {
|
|
254
|
+
subscribe: MethodCall<M, HasParams>;
|
|
255
|
+
} : {} : {});
|
|
256
|
+
/** HTTP 方法名 */
|
|
257
|
+
type HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
258
|
+
/** 递归构建客户端类型 */
|
|
259
|
+
type EdenClient<T, HasParams extends boolean = false> = { [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: T[K] extends {
|
|
260
|
+
':id': infer Child;
|
|
261
|
+
} ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false> : EdenClient<T[K], false> } & Endpoint<T, HasParams>;
|
|
262
|
+
/**
|
|
263
|
+
* 创建 Eden 风格的类型安全 API 客户端
|
|
264
|
+
*
|
|
265
|
+
* @param baseURL - API 基础 URL
|
|
266
|
+
* @param config - 可选配置
|
|
267
|
+
* @returns 类型安全的 API 客户端
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* // 使用自动推断的契约(无需 as const)
|
|
272
|
+
* const routes = defineRoutes([
|
|
273
|
+
* route('GET', '/users', createHandler(...)),
|
|
274
|
+
* route('GET', '/chat/stream', createSSEHandler(...))
|
|
275
|
+
* ])
|
|
276
|
+
*
|
|
277
|
+
* type Api = InferEden<typeof routes>
|
|
278
|
+
* const api = eden<Api>('http://localhost:3000')
|
|
279
|
+
*
|
|
280
|
+
* // 普通请求
|
|
281
|
+
* const { data } = await api.users.get({ page: 1 })
|
|
282
|
+
*
|
|
283
|
+
* // SSE 流式请求
|
|
284
|
+
* const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
|
|
285
|
+
* onMessage: (data) => console.log(data),
|
|
286
|
+
* onError: (err) => console.error(err)
|
|
287
|
+
* })
|
|
288
|
+
*
|
|
289
|
+
* // 取消订阅
|
|
290
|
+
* sub.unsubscribe()
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
declare function eden<T>(baseURL: string, config?: EdenConfig): EdenClient<T>;
|
|
294
|
+
//#endregion
|
|
295
|
+
export { type ApiResponse, type EdenClient, type EdenConfig, type HTTPMethod, type InferEden, type RequestConfig, type SSEEvent, type SSESubscribeOptions, type SSESubscription, eden };
|
|
296
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/index.ts","../src/core/eden.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//#region src/core/eden.ts
|
|
2
|
+
/**
|
|
3
|
+
* 解析 SSE 事件流
|
|
4
|
+
*/
|
|
5
|
+
async function* parseSSEStream(reader) {
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
let buffer = "";
|
|
8
|
+
while (true) {
|
|
9
|
+
const { done, value } = await reader.read();
|
|
10
|
+
if (done) break;
|
|
11
|
+
buffer += decoder.decode(value, { stream: true });
|
|
12
|
+
const events = buffer.split("\n\n");
|
|
13
|
+
buffer = events.pop() || "";
|
|
14
|
+
for (const eventStr of events) {
|
|
15
|
+
if (!eventStr.trim()) continue;
|
|
16
|
+
const event = { data: "" };
|
|
17
|
+
const lines = eventStr.split("\n");
|
|
18
|
+
let dataLines = [];
|
|
19
|
+
for (const line of lines) if (line.startsWith("event:")) event.event = line.slice(6).trim();
|
|
20
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
|
|
21
|
+
else if (line.startsWith("id:")) event.id = line.slice(3).trim();
|
|
22
|
+
else if (line.startsWith("retry:")) event.retry = parseInt(line.slice(6).trim(), 10);
|
|
23
|
+
const dataStr = dataLines.join("\n");
|
|
24
|
+
try {
|
|
25
|
+
event.data = JSON.parse(dataStr);
|
|
26
|
+
} catch {
|
|
27
|
+
event.data = dataStr;
|
|
28
|
+
}
|
|
29
|
+
yield event;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 创建 Eden 风格的类型安全 API 客户端
|
|
35
|
+
*
|
|
36
|
+
* @param baseURL - API 基础 URL
|
|
37
|
+
* @param config - 可选配置
|
|
38
|
+
* @returns 类型安全的 API 客户端
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // 使用自动推断的契约(无需 as const)
|
|
43
|
+
* const routes = defineRoutes([
|
|
44
|
+
* route('GET', '/users', createHandler(...)),
|
|
45
|
+
* route('GET', '/chat/stream', createSSEHandler(...))
|
|
46
|
+
* ])
|
|
47
|
+
*
|
|
48
|
+
* type Api = InferEden<typeof routes>
|
|
49
|
+
* const api = eden<Api>('http://localhost:3000')
|
|
50
|
+
*
|
|
51
|
+
* // 普通请求
|
|
52
|
+
* const { data } = await api.users.get({ page: 1 })
|
|
53
|
+
*
|
|
54
|
+
* // SSE 流式请求
|
|
55
|
+
* const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
|
|
56
|
+
* onMessage: (data) => console.log(data),
|
|
57
|
+
* onError: (err) => console.error(err)
|
|
58
|
+
* })
|
|
59
|
+
*
|
|
60
|
+
* // 取消订阅
|
|
61
|
+
* sub.unsubscribe()
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function eden(baseURL, config) {
|
|
65
|
+
const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {};
|
|
66
|
+
async function request(method, path, data, requestConfig) {
|
|
67
|
+
const url = new URL(path, baseURL);
|
|
68
|
+
if (method === "GET" && data && typeof data === "object") {
|
|
69
|
+
for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
70
|
+
}
|
|
71
|
+
const headers = {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
...defaultHeaders,
|
|
74
|
+
...requestConfig?.headers
|
|
75
|
+
};
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
let timeoutId;
|
|
78
|
+
const userSignal = requestConfig?.signal;
|
|
79
|
+
const requestTimeout = requestConfig?.timeout ?? timeout;
|
|
80
|
+
if (userSignal) if (userSignal.aborted) controller.abort();
|
|
81
|
+
else userSignal.addEventListener("abort", () => controller.abort());
|
|
82
|
+
if (requestTimeout) timeoutId = setTimeout(() => controller.abort(), requestTimeout);
|
|
83
|
+
const fetchOptions = {
|
|
84
|
+
method,
|
|
85
|
+
headers,
|
|
86
|
+
signal: controller.signal
|
|
87
|
+
};
|
|
88
|
+
if (method !== "GET" && method !== "HEAD" && data) fetchOptions.body = JSON.stringify(data);
|
|
89
|
+
let req = new Request(url.toString(), fetchOptions);
|
|
90
|
+
if (onRequest) req = await onRequest(req);
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(req);
|
|
93
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
94
|
+
const contentType = response.headers.get("content-type");
|
|
95
|
+
let responseData = null;
|
|
96
|
+
if (contentType?.includes("application/json")) responseData = await response.json();
|
|
97
|
+
else if (contentType?.includes("text/")) responseData = await response.text();
|
|
98
|
+
let result = {
|
|
99
|
+
data: responseData,
|
|
100
|
+
error: response.ok ? null : /* @__PURE__ */ new Error(`HTTP ${response.status}`),
|
|
101
|
+
status: response.status,
|
|
102
|
+
headers: response.headers,
|
|
103
|
+
response
|
|
104
|
+
};
|
|
105
|
+
if (onResponse) result = await onResponse(result);
|
|
106
|
+
if (!response.ok && onError) onError(result.error);
|
|
107
|
+
return result;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
110
|
+
if (onError) onError(err);
|
|
111
|
+
return {
|
|
112
|
+
data: null,
|
|
113
|
+
error: err,
|
|
114
|
+
status: 0,
|
|
115
|
+
headers: new Headers(),
|
|
116
|
+
response: new Response()
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function subscribe(path, query, callbacks, options) {
|
|
121
|
+
const url = new URL(path, baseURL);
|
|
122
|
+
if (query) {
|
|
123
|
+
for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
|
|
124
|
+
}
|
|
125
|
+
let abortController = new AbortController();
|
|
126
|
+
let connected = false;
|
|
127
|
+
let reconnectCount = 0;
|
|
128
|
+
let isUnsubscribed = false;
|
|
129
|
+
let lastEventId;
|
|
130
|
+
const reconnectInterval = options?.reconnectInterval ?? 3e3;
|
|
131
|
+
const maxReconnects = options?.maxReconnects ?? 5;
|
|
132
|
+
const connect = async () => {
|
|
133
|
+
if (isUnsubscribed) return;
|
|
134
|
+
try {
|
|
135
|
+
abortController = new AbortController();
|
|
136
|
+
const headers = {
|
|
137
|
+
"Accept": "text/event-stream",
|
|
138
|
+
...defaultHeaders,
|
|
139
|
+
...options?.headers
|
|
140
|
+
};
|
|
141
|
+
if (lastEventId) headers["Last-Event-ID"] = lastEventId;
|
|
142
|
+
const response = await fetch(url.toString(), {
|
|
143
|
+
method: "GET",
|
|
144
|
+
headers,
|
|
145
|
+
signal: abortController.signal
|
|
146
|
+
});
|
|
147
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
148
|
+
if (!response.body) throw new Error("No response body");
|
|
149
|
+
connected = true;
|
|
150
|
+
reconnectCount = 0;
|
|
151
|
+
callbacks.onOpen?.();
|
|
152
|
+
const reader = response.body.getReader();
|
|
153
|
+
for await (const event of parseSSEStream(reader)) {
|
|
154
|
+
if (event.id) lastEventId = event.id;
|
|
155
|
+
if (event.event === "error") callbacks.onError?.(new Error(String(event.data)));
|
|
156
|
+
else callbacks.onMessage(event.data);
|
|
157
|
+
}
|
|
158
|
+
connected = false;
|
|
159
|
+
callbacks.onClose?.();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
connected = false;
|
|
162
|
+
if (error.name === "AbortError" || isUnsubscribed) return;
|
|
163
|
+
callbacks.onError?.(error);
|
|
164
|
+
if (reconnectCount < maxReconnects) {
|
|
165
|
+
reconnectCount++;
|
|
166
|
+
callbacks.onReconnect?.(reconnectCount, maxReconnects);
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if (!isUnsubscribed) connect();
|
|
169
|
+
}, reconnectInterval);
|
|
170
|
+
} else callbacks.onMaxReconnects?.();
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
connect();
|
|
174
|
+
return {
|
|
175
|
+
unsubscribe: () => {
|
|
176
|
+
isUnsubscribed = true;
|
|
177
|
+
abortController?.abort();
|
|
178
|
+
abortController = null;
|
|
179
|
+
connected = false;
|
|
180
|
+
},
|
|
181
|
+
get connected() {
|
|
182
|
+
return connected;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function createEndpoint(basePath) {
|
|
187
|
+
const methods = [
|
|
188
|
+
"get",
|
|
189
|
+
"post",
|
|
190
|
+
"put",
|
|
191
|
+
"patch",
|
|
192
|
+
"delete"
|
|
193
|
+
];
|
|
194
|
+
const handler = (params) => {
|
|
195
|
+
const paramValue = Object.values(params)[0];
|
|
196
|
+
return createEndpoint(`${basePath}/${encodeURIComponent(paramValue)}`);
|
|
197
|
+
};
|
|
198
|
+
return new Proxy(handler, {
|
|
199
|
+
get(_, prop) {
|
|
200
|
+
if (methods.includes(prop)) {
|
|
201
|
+
const httpMethod = prop.toUpperCase();
|
|
202
|
+
return (data, cfg) => {
|
|
203
|
+
return request(httpMethod, basePath, data, cfg);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (prop === "subscribe") return (queryOrCallbacks, callbacksOrOptions, options) => {
|
|
207
|
+
if (typeof queryOrCallbacks === "object" && "onMessage" in queryOrCallbacks) return subscribe(basePath, void 0, queryOrCallbacks, callbacksOrOptions);
|
|
208
|
+
else return subscribe(basePath, queryOrCallbacks, callbacksOrOptions, options);
|
|
209
|
+
};
|
|
210
|
+
return createEndpoint(`${basePath}/${prop}`);
|
|
211
|
+
},
|
|
212
|
+
apply(_, __, args) {
|
|
213
|
+
const params = args[0];
|
|
214
|
+
const paramValue = Object.values(params)[0];
|
|
215
|
+
return createEndpoint(`${basePath}/${encodeURIComponent(paramValue)}`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return new Proxy({}, { get(_, prop) {
|
|
220
|
+
return createEndpoint(`/${prop}`);
|
|
221
|
+
} });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
export { eden };
|
|
226
|
+
//# sourceMappingURL=index.mjs.map
|