@vafast/api-client 0.1.3 → 0.1.5
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 +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 +11 -8
- package/TODO.md +0 -83
- package/build.ts +0 -37
- package/example/auto-infer.ts +0 -223
- package/example/test-sse.ts +0 -192
- package/src/core/eden.ts +0 -705
- 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/tsdown.config.ts +0 -10
- package/vitest.config.ts +0 -8
package/src/core/eden.ts
DELETED
|
@@ -1,705 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Eden 风格 API 客户端
|
|
3
|
-
*
|
|
4
|
-
* 最自然的链式调用:
|
|
5
|
-
* - api.users.get() // GET /users
|
|
6
|
-
* - api.users.post({ name }) // POST /users
|
|
7
|
-
* - api.users({ id }).get() // GET /users/:id
|
|
8
|
-
* - api.users({ id }).delete() // DELETE /users/:id
|
|
9
|
-
* - api.chat.stream.subscribe() // SSE 流式响应
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { ApiResponse, RequestConfig } from '../types'
|
|
13
|
-
|
|
14
|
-
// ============= 配置 =============
|
|
15
|
-
|
|
16
|
-
export interface EdenConfig {
|
|
17
|
-
headers?: Record<string, string>
|
|
18
|
-
onRequest?: (request: Request) => Request | Promise<Request>
|
|
19
|
-
onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>
|
|
20
|
-
onError?: (error: Error) => void
|
|
21
|
-
timeout?: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ============= SSE 类型 =============
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* SSE 事件
|
|
28
|
-
*/
|
|
29
|
-
export interface SSEEvent<T = unknown> {
|
|
30
|
-
event?: string
|
|
31
|
-
data: T
|
|
32
|
-
id?: string
|
|
33
|
-
retry?: number
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* SSE 订阅选项
|
|
38
|
-
*/
|
|
39
|
-
export interface SSESubscribeOptions {
|
|
40
|
-
/** 自定义请求头 */
|
|
41
|
-
headers?: Record<string, string>
|
|
42
|
-
/** 重连间隔(毫秒) */
|
|
43
|
-
reconnectInterval?: number
|
|
44
|
-
/** 最大重连次数 */
|
|
45
|
-
maxReconnects?: number
|
|
46
|
-
/** 连接超时(毫秒) */
|
|
47
|
-
timeout?: number
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* SSE 订阅结果
|
|
52
|
-
*/
|
|
53
|
-
export interface SSESubscription<T = unknown> {
|
|
54
|
-
/** 取消订阅 */
|
|
55
|
-
unsubscribe: () => void
|
|
56
|
-
/** 是否已连接 */
|
|
57
|
-
readonly connected: boolean
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ============= 基础类型工具 =============
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 从 TypeBox Schema 提取静态类型
|
|
64
|
-
* 使用 TypeBox 内部的 static 属性提取类型
|
|
65
|
-
*/
|
|
66
|
-
type InferStatic<T> = T extends { static: infer S } ? S : T
|
|
67
|
-
|
|
68
|
-
/** 从 InferableHandler 提取返回类型 */
|
|
69
|
-
type ExtractReturn<T> = T extends { __returnType: infer R } ? R : unknown
|
|
70
|
-
|
|
71
|
-
/** 从 InferableHandler 提取 Schema */
|
|
72
|
-
type ExtractSchema<T> = T extends { __schema: infer S } ? S : {}
|
|
73
|
-
|
|
74
|
-
/** 检查是否是 SSE Handler(使用品牌类型检测) */
|
|
75
|
-
type IsSSEHandler<T> = T extends { __sse: { readonly __brand: 'SSE' } } ? true : false
|
|
76
|
-
|
|
77
|
-
/** 从 Schema 提取各部分类型 */
|
|
78
|
-
type GetQuery<S> = S extends { query: infer Q } ? InferStatic<Q> : undefined
|
|
79
|
-
type GetBody<S> = S extends { body: infer B } ? InferStatic<B> : undefined
|
|
80
|
-
type GetParams<S> = S extends { params: infer P } ? InferStatic<P> : undefined
|
|
81
|
-
|
|
82
|
-
// ============= 路径处理 =============
|
|
83
|
-
|
|
84
|
-
/** 移除开头斜杠:/users → users */
|
|
85
|
-
type TrimSlash<P extends string> = P extends `/${infer R}` ? R : P
|
|
86
|
-
|
|
87
|
-
/** 获取第一段:users/posts → users */
|
|
88
|
-
type Head<P extends string> = P extends `${infer H}/${string}` ? H : P
|
|
89
|
-
|
|
90
|
-
/** 获取剩余段:users/posts → posts */
|
|
91
|
-
type Tail<P extends string> = P extends `${string}/${infer T}` ? T : never
|
|
92
|
-
|
|
93
|
-
/** 检查是否是动态参数段::id → true */
|
|
94
|
-
type IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false
|
|
95
|
-
|
|
96
|
-
// ============= 核心类型推断 =============
|
|
97
|
-
|
|
98
|
-
/** 清理 undefined 字段 */
|
|
99
|
-
type Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] }
|
|
100
|
-
|
|
101
|
-
/** SSE 标记类型 */
|
|
102
|
-
type SSEBrand = { readonly __brand: 'SSE' }
|
|
103
|
-
|
|
104
|
-
/** 从路由构建方法定义 */
|
|
105
|
-
type BuildMethodDef<R extends { readonly handler: unknown }> =
|
|
106
|
-
IsSSEHandler<R['handler']> extends true
|
|
107
|
-
? Clean<{
|
|
108
|
-
query: GetQuery<ExtractSchema<R['handler']>>
|
|
109
|
-
params: GetParams<ExtractSchema<R['handler']>>
|
|
110
|
-
return: ExtractReturn<R['handler']>
|
|
111
|
-
sse: SSEBrand
|
|
112
|
-
}>
|
|
113
|
-
: Clean<{
|
|
114
|
-
query: GetQuery<ExtractSchema<R['handler']>>
|
|
115
|
-
body: GetBody<ExtractSchema<R['handler']>>
|
|
116
|
-
params: GetParams<ExtractSchema<R['handler']>>
|
|
117
|
-
return: ExtractReturn<R['handler']>
|
|
118
|
-
}>
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 递归构建嵌套路径结构
|
|
122
|
-
*
|
|
123
|
-
* 处理动态参数:/users/:id → { users: { ':id': { ... } } }
|
|
124
|
-
*/
|
|
125
|
-
type BuildPath<Path extends string, Method extends string, Def> =
|
|
126
|
-
Path extends `${infer First}/${infer Rest}`
|
|
127
|
-
? IsDynamicSegment<First> extends true
|
|
128
|
-
? { ':id': BuildPath<Rest, Method, Def> }
|
|
129
|
-
: { [K in First]: BuildPath<Rest, Method, Def> }
|
|
130
|
-
: IsDynamicSegment<Path> extends true
|
|
131
|
-
? { ':id': { [M in Method]: Def } }
|
|
132
|
-
: { [K in Path]: { [M in Method]: Def } }
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* 从单个路由生成嵌套类型结构
|
|
136
|
-
*/
|
|
137
|
-
type RouteToTree<R extends { readonly method: string; readonly path: string; readonly handler: unknown }> =
|
|
138
|
-
BuildPath<TrimSlash<R['path']>, Lowercase<R['method']>, BuildMethodDef<R>>
|
|
139
|
-
|
|
140
|
-
// ============= 深度合并多个路由 =============
|
|
141
|
-
|
|
142
|
-
type DeepMerge<A, B> = {
|
|
143
|
-
[K in keyof A | keyof B]:
|
|
144
|
-
K extends keyof A & keyof B
|
|
145
|
-
? A[K] extends object
|
|
146
|
-
? B[K] extends object
|
|
147
|
-
? DeepMerge<A[K], B[K]>
|
|
148
|
-
: A[K] & B[K]
|
|
149
|
-
: A[K] & B[K]
|
|
150
|
-
: K extends keyof A
|
|
151
|
-
? A[K]
|
|
152
|
-
: K extends keyof B
|
|
153
|
-
? B[K]
|
|
154
|
-
: never
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** 递归合并路由数组为单一类型结构 */
|
|
158
|
-
type MergeRoutes<T extends readonly unknown[]> =
|
|
159
|
-
T extends readonly [infer First extends { readonly method: string; readonly path: string; readonly handler: unknown }]
|
|
160
|
-
? RouteToTree<First>
|
|
161
|
-
: T extends readonly [
|
|
162
|
-
infer First extends { readonly method: string; readonly path: string; readonly handler: unknown },
|
|
163
|
-
...infer Rest extends readonly { readonly method: string; readonly path: string; readonly handler: unknown }[]
|
|
164
|
-
]
|
|
165
|
-
? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>>
|
|
166
|
-
: {}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 从 vafast 路由数组自动推断 Eden 契约
|
|
170
|
-
*
|
|
171
|
-
* @example
|
|
172
|
-
* ```typescript
|
|
173
|
-
* import { defineRoutes, createHandler, Type } from 'vafast'
|
|
174
|
-
* import { eden, InferEden } from 'vafast-api-client'
|
|
175
|
-
*
|
|
176
|
-
* // ✨ defineRoutes() 自动保留字面量类型,无需 as const
|
|
177
|
-
* const routes = defineRoutes([
|
|
178
|
-
* {
|
|
179
|
-
* method: 'GET',
|
|
180
|
-
* path: '/users',
|
|
181
|
-
* handler: createHandler(
|
|
182
|
-
* { query: Type.Object({ page: Type.Number() }) },
|
|
183
|
-
* async ({ query }) => ({ users: [], total: 0 })
|
|
184
|
-
* )
|
|
185
|
-
* },
|
|
186
|
-
* {
|
|
187
|
-
* method: 'GET',
|
|
188
|
-
* path: '/chat/stream',
|
|
189
|
-
* handler: createSSEHandler(
|
|
190
|
-
* { query: Type.Object({ prompt: Type.String() }) },
|
|
191
|
-
* async function* ({ query }) {
|
|
192
|
-
* yield { data: { text: 'Hello' } }
|
|
193
|
-
* }
|
|
194
|
-
* )
|
|
195
|
-
* }
|
|
196
|
-
* ])
|
|
197
|
-
*
|
|
198
|
-
* type Api = InferEden<typeof routes>
|
|
199
|
-
* const api = eden<Api>('http://localhost:3000')
|
|
200
|
-
*
|
|
201
|
-
* // 普通请求
|
|
202
|
-
* const { data } = await api.users.get({ page: 1 })
|
|
203
|
-
*
|
|
204
|
-
* // SSE 流式请求
|
|
205
|
-
* api.chat.stream.subscribe({ prompt: 'Hi' }, {
|
|
206
|
-
* onMessage: (data) => console.log(data),
|
|
207
|
-
* onError: (err) => console.error(err)
|
|
208
|
-
* })
|
|
209
|
-
* ```
|
|
210
|
-
*/
|
|
211
|
-
export type InferEden<T extends readonly { readonly method: string; readonly path: string; readonly handler: unknown }[]> =
|
|
212
|
-
MergeRoutes<T>
|
|
213
|
-
|
|
214
|
-
// ============= 契约类型(手动定义时使用) =============
|
|
215
|
-
|
|
216
|
-
/** HTTP 方法定义 */
|
|
217
|
-
interface MethodDef {
|
|
218
|
-
query?: unknown
|
|
219
|
-
body?: unknown
|
|
220
|
-
params?: unknown
|
|
221
|
-
return: unknown
|
|
222
|
-
sse?: SSEBrand
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** 路由节点 */
|
|
226
|
-
type RouteNode = {
|
|
227
|
-
get?: MethodDef
|
|
228
|
-
post?: MethodDef
|
|
229
|
-
put?: MethodDef
|
|
230
|
-
patch?: MethodDef
|
|
231
|
-
delete?: MethodDef
|
|
232
|
-
':id'?: RouteNode
|
|
233
|
-
[key: string]: MethodDef | RouteNode | undefined
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// ============= 客户端类型 =============
|
|
237
|
-
|
|
238
|
-
/** SSE 订阅回调 */
|
|
239
|
-
interface SSECallbacks<T> {
|
|
240
|
-
/** 收到消息 */
|
|
241
|
-
onMessage: (data: T) => void
|
|
242
|
-
/** 发生错误 */
|
|
243
|
-
onError?: (error: Error) => void
|
|
244
|
-
/** 连接打开 */
|
|
245
|
-
onOpen?: () => void
|
|
246
|
-
/** 连接关闭 */
|
|
247
|
-
onClose?: () => void
|
|
248
|
-
/** 正在重连 */
|
|
249
|
-
onReconnect?: (attempt: number, maxAttempts: number) => void
|
|
250
|
-
/** 达到最大重连次数 */
|
|
251
|
-
onMaxReconnects?: () => void
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** 从方法定义提取调用签名 */
|
|
255
|
-
type MethodCall<M extends MethodDef, HasParams extends boolean = false> =
|
|
256
|
-
M extends { sse: SSEBrand }
|
|
257
|
-
? M extends { query: infer Q }
|
|
258
|
-
? (query: Q, callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>
|
|
259
|
-
: (callbacks: SSECallbacks<M['return']>, options?: SSESubscribeOptions) => SSESubscription<M['return']>
|
|
260
|
-
: HasParams extends true
|
|
261
|
-
? M extends { body: infer B }
|
|
262
|
-
? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>
|
|
263
|
-
: (config?: RequestConfig) => Promise<ApiResponse<M['return']>>
|
|
264
|
-
: M extends { query: infer Q }
|
|
265
|
-
? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M['return']>>
|
|
266
|
-
: M extends { body: infer B }
|
|
267
|
-
? (body: B, config?: RequestConfig) => Promise<ApiResponse<M['return']>>
|
|
268
|
-
: (config?: RequestConfig) => Promise<ApiResponse<M['return']>>
|
|
269
|
-
|
|
270
|
-
/** 检查是否是 SSE 端点(检测品牌类型标记) */
|
|
271
|
-
type IsSSEEndpoint<M> = M extends { sse: { readonly __brand: 'SSE' } } ? true : false
|
|
272
|
-
|
|
273
|
-
/** 端点类型 - 包含 subscribe 方法用于 SSE */
|
|
274
|
-
type Endpoint<T, HasParams extends boolean = false> =
|
|
275
|
-
// HTTP 方法
|
|
276
|
-
{
|
|
277
|
-
[K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]:
|
|
278
|
-
T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never
|
|
279
|
-
}
|
|
280
|
-
// SSE subscribe 方法(如果 GET 是 SSE)
|
|
281
|
-
& (T extends { get: infer M extends MethodDef }
|
|
282
|
-
? IsSSEEndpoint<M> extends true
|
|
283
|
-
? { subscribe: MethodCall<M, HasParams> }
|
|
284
|
-
: {}
|
|
285
|
-
: {})
|
|
286
|
-
|
|
287
|
-
/** 检查节点是否有动态参数子路由 */
|
|
288
|
-
type HasDynamicChild<T> = T extends { ':id': unknown } ? true : false
|
|
289
|
-
|
|
290
|
-
/** HTTP 方法名 */
|
|
291
|
-
type HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete'
|
|
292
|
-
|
|
293
|
-
/** 递归构建客户端类型 */
|
|
294
|
-
export type EdenClient<T, HasParams extends boolean = false> = {
|
|
295
|
-
// 嵌套路径(排除 HTTP 方法和动态参数)
|
|
296
|
-
[K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]:
|
|
297
|
-
T[K] extends { ':id': infer Child }
|
|
298
|
-
// 有动态参数子路由
|
|
299
|
-
? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false>
|
|
300
|
-
// 普通嵌套路由
|
|
301
|
-
: EdenClient<T[K], false>
|
|
302
|
-
} & Endpoint<T, HasParams>
|
|
303
|
-
|
|
304
|
-
// ============= SSE 解析器 =============
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* 解析 SSE 事件流
|
|
308
|
-
*/
|
|
309
|
-
async function* parseSSEStream(
|
|
310
|
-
reader: ReadableStreamDefaultReader<Uint8Array>
|
|
311
|
-
): AsyncGenerator<SSEEvent, void, unknown> {
|
|
312
|
-
const decoder = new TextDecoder();
|
|
313
|
-
let buffer = '';
|
|
314
|
-
|
|
315
|
-
while (true) {
|
|
316
|
-
const { done, value } = await reader.read();
|
|
317
|
-
|
|
318
|
-
if (done) break;
|
|
319
|
-
|
|
320
|
-
buffer += decoder.decode(value, { stream: true });
|
|
321
|
-
|
|
322
|
-
// 按双换行分割事件
|
|
323
|
-
const events = buffer.split('\n\n');
|
|
324
|
-
buffer = events.pop() || ''; // 保留未完成的部分
|
|
325
|
-
|
|
326
|
-
for (const eventStr of events) {
|
|
327
|
-
if (!eventStr.trim()) continue;
|
|
328
|
-
|
|
329
|
-
const event: SSEEvent = { data: '' };
|
|
330
|
-
const lines = eventStr.split('\n');
|
|
331
|
-
let dataLines: string[] = [];
|
|
332
|
-
|
|
333
|
-
for (const line of lines) {
|
|
334
|
-
if (line.startsWith('event:')) {
|
|
335
|
-
event.event = line.slice(6).trim();
|
|
336
|
-
} else if (line.startsWith('data:')) {
|
|
337
|
-
dataLines.push(line.slice(5).trim());
|
|
338
|
-
} else if (line.startsWith('id:')) {
|
|
339
|
-
event.id = line.slice(3).trim();
|
|
340
|
-
} else if (line.startsWith('retry:')) {
|
|
341
|
-
event.retry = parseInt(line.slice(6).trim(), 10);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 合并多行 data
|
|
346
|
-
const dataStr = dataLines.join('\n');
|
|
347
|
-
|
|
348
|
-
// 尝试解析 JSON
|
|
349
|
-
try {
|
|
350
|
-
event.data = JSON.parse(dataStr);
|
|
351
|
-
} catch {
|
|
352
|
-
event.data = dataStr;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
yield event;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// ============= 实现 =============
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* 创建 Eden 风格的类型安全 API 客户端
|
|
364
|
-
*
|
|
365
|
-
* @param baseURL - API 基础 URL
|
|
366
|
-
* @param config - 可选配置
|
|
367
|
-
* @returns 类型安全的 API 客户端
|
|
368
|
-
*
|
|
369
|
-
* @example
|
|
370
|
-
* ```typescript
|
|
371
|
-
* // 使用自动推断的契约(无需 as const)
|
|
372
|
-
* const routes = defineRoutes([
|
|
373
|
-
* route('GET', '/users', createHandler(...)),
|
|
374
|
-
* route('GET', '/chat/stream', createSSEHandler(...))
|
|
375
|
-
* ])
|
|
376
|
-
*
|
|
377
|
-
* type Api = InferEden<typeof routes>
|
|
378
|
-
* const api = eden<Api>('http://localhost:3000')
|
|
379
|
-
*
|
|
380
|
-
* // 普通请求
|
|
381
|
-
* const { data } = await api.users.get({ page: 1 })
|
|
382
|
-
*
|
|
383
|
-
* // SSE 流式请求
|
|
384
|
-
* const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
|
|
385
|
-
* onMessage: (data) => console.log(data),
|
|
386
|
-
* onError: (err) => console.error(err)
|
|
387
|
-
* })
|
|
388
|
-
*
|
|
389
|
-
* // 取消订阅
|
|
390
|
-
* sub.unsubscribe()
|
|
391
|
-
* ```
|
|
392
|
-
*/
|
|
393
|
-
export function eden<T>(
|
|
394
|
-
baseURL: string,
|
|
395
|
-
config?: EdenConfig
|
|
396
|
-
): EdenClient<T> {
|
|
397
|
-
const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {}
|
|
398
|
-
|
|
399
|
-
// 发送普通请求
|
|
400
|
-
async function request<TReturn>(
|
|
401
|
-
method: string,
|
|
402
|
-
path: string,
|
|
403
|
-
data?: unknown,
|
|
404
|
-
requestConfig?: RequestConfig
|
|
405
|
-
): Promise<ApiResponse<TReturn>> {
|
|
406
|
-
const url = new URL(path, baseURL)
|
|
407
|
-
|
|
408
|
-
// Query 参数
|
|
409
|
-
if (method === 'GET' && data && typeof data === 'object') {
|
|
410
|
-
for (const [key, value] of Object.entries(data as Record<string, unknown>)) {
|
|
411
|
-
if (value !== undefined && value !== null) {
|
|
412
|
-
url.searchParams.set(key, String(value))
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const headers: Record<string, string> = {
|
|
418
|
-
'Content-Type': 'application/json',
|
|
419
|
-
...defaultHeaders,
|
|
420
|
-
...requestConfig?.headers,
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// 支持用户传入的取消信号,或内部超时取消
|
|
424
|
-
const controller = new AbortController()
|
|
425
|
-
let timeoutId: ReturnType<typeof setTimeout> | undefined
|
|
426
|
-
|
|
427
|
-
// 合并用户的 signal 和内部的超时 signal
|
|
428
|
-
const userSignal = requestConfig?.signal
|
|
429
|
-
const requestTimeout = requestConfig?.timeout ?? timeout
|
|
430
|
-
|
|
431
|
-
if (userSignal) {
|
|
432
|
-
// 如果用户已取消,直接中止
|
|
433
|
-
if (userSignal.aborted) {
|
|
434
|
-
controller.abort()
|
|
435
|
-
} else {
|
|
436
|
-
// 监听用户取消
|
|
437
|
-
userSignal.addEventListener('abort', () => controller.abort())
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (requestTimeout) {
|
|
442
|
-
timeoutId = setTimeout(() => controller.abort(), requestTimeout)
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const fetchOptions: RequestInit = {
|
|
446
|
-
method,
|
|
447
|
-
headers,
|
|
448
|
-
signal: controller.signal
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Body
|
|
452
|
-
if (method !== 'GET' && method !== 'HEAD' && data) {
|
|
453
|
-
fetchOptions.body = JSON.stringify(data)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
let req = new Request(url.toString(), fetchOptions)
|
|
457
|
-
|
|
458
|
-
if (onRequest) {
|
|
459
|
-
req = await onRequest(req)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
const response = await fetch(req)
|
|
464
|
-
|
|
465
|
-
if (timeoutId) clearTimeout(timeoutId)
|
|
466
|
-
|
|
467
|
-
const contentType = response.headers.get('content-type')
|
|
468
|
-
let responseData: TReturn | null = null
|
|
469
|
-
|
|
470
|
-
if (contentType?.includes('application/json')) {
|
|
471
|
-
responseData = await response.json()
|
|
472
|
-
} else if (contentType?.includes('text/')) {
|
|
473
|
-
responseData = await response.text() as unknown as TReturn
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
let result: ApiResponse<TReturn> = {
|
|
477
|
-
data: responseData,
|
|
478
|
-
error: response.ok ? null : new Error(`HTTP ${response.status}`),
|
|
479
|
-
status: response.status,
|
|
480
|
-
headers: response.headers,
|
|
481
|
-
response,
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (onResponse) {
|
|
485
|
-
result = await onResponse(result)
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
if (!response.ok && onError) {
|
|
489
|
-
onError(result.error!)
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return result
|
|
493
|
-
} catch (error) {
|
|
494
|
-
const err = error instanceof Error ? error : new Error(String(error))
|
|
495
|
-
if (onError) onError(err)
|
|
496
|
-
return {
|
|
497
|
-
data: null,
|
|
498
|
-
error: err,
|
|
499
|
-
status: 0,
|
|
500
|
-
headers: new Headers(),
|
|
501
|
-
response: new Response(),
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// SSE 订阅(支持自动重连)
|
|
507
|
-
function subscribe<TData>(
|
|
508
|
-
path: string,
|
|
509
|
-
query: Record<string, unknown> | undefined,
|
|
510
|
-
callbacks: SSECallbacks<TData>,
|
|
511
|
-
options?: SSESubscribeOptions
|
|
512
|
-
): SSESubscription<TData> {
|
|
513
|
-
const url = new URL(path, baseURL)
|
|
514
|
-
|
|
515
|
-
// 添加 query 参数
|
|
516
|
-
if (query) {
|
|
517
|
-
for (const [key, value] of Object.entries(query)) {
|
|
518
|
-
if (value !== undefined && value !== null) {
|
|
519
|
-
url.searchParams.set(key, String(value))
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
let abortController: AbortController | null = new AbortController()
|
|
525
|
-
let connected = false
|
|
526
|
-
let reconnectCount = 0
|
|
527
|
-
let isUnsubscribed = false
|
|
528
|
-
let lastEventId: string | undefined
|
|
529
|
-
|
|
530
|
-
// 重连配置
|
|
531
|
-
const reconnectInterval = options?.reconnectInterval ?? 3000
|
|
532
|
-
const maxReconnects = options?.maxReconnects ?? 5
|
|
533
|
-
|
|
534
|
-
const connect = async () => {
|
|
535
|
-
if (isUnsubscribed) return
|
|
536
|
-
|
|
537
|
-
try {
|
|
538
|
-
// 重新创建 AbortController(重连时需要新的)
|
|
539
|
-
abortController = new AbortController()
|
|
540
|
-
|
|
541
|
-
const headers: Record<string, string> = {
|
|
542
|
-
'Accept': 'text/event-stream',
|
|
543
|
-
...defaultHeaders,
|
|
544
|
-
...options?.headers,
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// SSE 规范:发送 Last-Event-ID 用于断点续传
|
|
548
|
-
if (lastEventId) {
|
|
549
|
-
headers['Last-Event-ID'] = lastEventId
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const response = await fetch(url.toString(), {
|
|
553
|
-
method: 'GET',
|
|
554
|
-
headers,
|
|
555
|
-
signal: abortController.signal,
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
if (!response.ok) {
|
|
559
|
-
throw new Error(`HTTP ${response.status}`)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (!response.body) {
|
|
563
|
-
throw new Error('No response body')
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// 连接成功,重置重连计数
|
|
567
|
-
connected = true
|
|
568
|
-
reconnectCount = 0
|
|
569
|
-
callbacks.onOpen?.()
|
|
570
|
-
|
|
571
|
-
const reader = response.body.getReader()
|
|
572
|
-
|
|
573
|
-
for await (const event of parseSSEStream(reader)) {
|
|
574
|
-
// 保存最后的事件 ID 用于重连
|
|
575
|
-
if (event.id) {
|
|
576
|
-
lastEventId = event.id
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// 服务端可以通过 retry 字段动态调整重连间隔
|
|
580
|
-
// 这里不改变配置,仅记录
|
|
581
|
-
|
|
582
|
-
if (event.event === 'error') {
|
|
583
|
-
callbacks.onError?.(new Error(String(event.data)))
|
|
584
|
-
} else {
|
|
585
|
-
callbacks.onMessage(event.data as TData)
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// 流正常结束
|
|
590
|
-
connected = false
|
|
591
|
-
callbacks.onClose?.()
|
|
592
|
-
|
|
593
|
-
} catch (error) {
|
|
594
|
-
connected = false
|
|
595
|
-
|
|
596
|
-
// 用户主动取消,不重连
|
|
597
|
-
if ((error as Error).name === 'AbortError' || isUnsubscribed) {
|
|
598
|
-
return
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
callbacks.onError?.(error as Error)
|
|
602
|
-
|
|
603
|
-
// 自动重连
|
|
604
|
-
if (reconnectCount < maxReconnects) {
|
|
605
|
-
reconnectCount++
|
|
606
|
-
callbacks.onReconnect?.(reconnectCount, maxReconnects)
|
|
607
|
-
|
|
608
|
-
// 延迟后重连
|
|
609
|
-
setTimeout(() => {
|
|
610
|
-
if (!isUnsubscribed) {
|
|
611
|
-
connect()
|
|
612
|
-
}
|
|
613
|
-
}, reconnectInterval)
|
|
614
|
-
} else {
|
|
615
|
-
// 达到最大重连次数
|
|
616
|
-
callbacks.onMaxReconnects?.()
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
connect()
|
|
622
|
-
|
|
623
|
-
return {
|
|
624
|
-
unsubscribe: () => {
|
|
625
|
-
isUnsubscribed = true
|
|
626
|
-
abortController?.abort()
|
|
627
|
-
abortController = null
|
|
628
|
-
connected = false
|
|
629
|
-
},
|
|
630
|
-
get connected() {
|
|
631
|
-
return connected
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
// 创建端点代理
|
|
637
|
-
function createEndpoint(basePath: string): unknown {
|
|
638
|
-
const methods = ['get', 'post', 'put', 'patch', 'delete']
|
|
639
|
-
|
|
640
|
-
// 创建可调用的代理(支持参数化路由 api.users({ id }) 调用)
|
|
641
|
-
const handler = (params: Record<string, string>) => {
|
|
642
|
-
const paramValue = Object.values(params)[0]
|
|
643
|
-
const newPath = `${basePath}/${encodeURIComponent(paramValue)}`
|
|
644
|
-
return createEndpoint(newPath)
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
return new Proxy(handler as unknown as object, {
|
|
648
|
-
get(_, prop: string) {
|
|
649
|
-
// HTTP 方法
|
|
650
|
-
if (methods.includes(prop)) {
|
|
651
|
-
const httpMethod = prop.toUpperCase()
|
|
652
|
-
return (data?: unknown, cfg?: RequestConfig) => {
|
|
653
|
-
return request(httpMethod, basePath, data, cfg)
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// SSE 订阅
|
|
658
|
-
if (prop === 'subscribe') {
|
|
659
|
-
return <TData>(
|
|
660
|
-
queryOrCallbacks: Record<string, unknown> | SSECallbacks<TData>,
|
|
661
|
-
callbacksOrOptions?: SSECallbacks<TData> | SSESubscribeOptions,
|
|
662
|
-
options?: SSESubscribeOptions
|
|
663
|
-
) => {
|
|
664
|
-
// 判断第一个参数是 query 还是 callbacks
|
|
665
|
-
if (typeof queryOrCallbacks === 'object' && 'onMessage' in queryOrCallbacks) {
|
|
666
|
-
// subscribe(callbacks, options)
|
|
667
|
-
return subscribe<TData>(
|
|
668
|
-
basePath,
|
|
669
|
-
undefined,
|
|
670
|
-
queryOrCallbacks as SSECallbacks<TData>,
|
|
671
|
-
callbacksOrOptions as SSESubscribeOptions
|
|
672
|
-
)
|
|
673
|
-
} else {
|
|
674
|
-
// subscribe(query, callbacks, options)
|
|
675
|
-
return subscribe<TData>(
|
|
676
|
-
basePath,
|
|
677
|
-
queryOrCallbacks as Record<string, unknown>,
|
|
678
|
-
callbacksOrOptions as SSECallbacks<TData>,
|
|
679
|
-
options
|
|
680
|
-
)
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// 嵌套路径
|
|
686
|
-
const childPath = `${basePath}/${prop}`
|
|
687
|
-
return createEndpoint(childPath)
|
|
688
|
-
},
|
|
689
|
-
apply(_, __, args) {
|
|
690
|
-
// 调用函数处理参数化路由
|
|
691
|
-
const params = args[0] as Record<string, string>
|
|
692
|
-
const paramValue = Object.values(params)[0]
|
|
693
|
-
const newPath = `${basePath}/${encodeURIComponent(paramValue)}`
|
|
694
|
-
return createEndpoint(newPath)
|
|
695
|
-
}
|
|
696
|
-
})
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// 根代理
|
|
700
|
-
return new Proxy({} as EdenClient<T>, {
|
|
701
|
-
get(_, prop: string) {
|
|
702
|
-
return createEndpoint(`/${prop}`)
|
|
703
|
-
}
|
|
704
|
-
})
|
|
705
|
-
}
|