@vafast/api-client 0.1.1 → 0.1.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/README.md +212 -196
- package/TODO.md +83 -0
- package/example/auto-infer.ts +223 -0
- package/example/test-sse.ts +192 -0
- package/package.json +12 -13
- package/src/core/eden.ts +705 -0
- package/src/index.ts +34 -65
- package/src/types/index.ts +17 -116
- package/test/eden.test.ts +425 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +10 -0
- package/vitest.config.ts +8 -0
- package/bun.lock +0 -569
- package/example/index.ts +0 -255
- package/src/core/api-client.ts +0 -389
- package/src/core/typed-client.ts +0 -305
- package/src/utils/index.ts +0 -232
- package/src/websocket/websocket-client.ts +0 -347
- package/test/api-client.test.ts +0 -262
- package/test/basic.test.ts +0 -55
- package/test/typed-client.test.ts +0 -304
- package/test/utils.test.ts +0 -363
- package/test/websocket.test.ts +0 -434
- package/tsup.config.ts +0 -23
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ✨ 自动从 vafast 路由推断契约
|
|
3
|
+
*
|
|
4
|
+
* 特性:
|
|
5
|
+
* 1. 使用 defineRoutes() 自动保留字面量类型
|
|
6
|
+
* 2. 支持 SSE 流式响应
|
|
7
|
+
* 3. 完整的类型推断
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
defineRoutes,
|
|
12
|
+
createHandler,
|
|
13
|
+
createSSEHandler,
|
|
14
|
+
Type
|
|
15
|
+
} from 'vafast'
|
|
16
|
+
import { eden, InferEden } from '../src'
|
|
17
|
+
|
|
18
|
+
// ============= 业务类型定义 =============
|
|
19
|
+
|
|
20
|
+
interface User {
|
|
21
|
+
id: string
|
|
22
|
+
name: string
|
|
23
|
+
email: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ChatMessage {
|
|
27
|
+
text: string
|
|
28
|
+
timestamp?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============= 服务端:定义路由 =============
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* ✨ defineRoutes() 自动保留字面量类型,无需 as const!
|
|
35
|
+
*/
|
|
36
|
+
const routes = defineRoutes([
|
|
37
|
+
// GET /users - 获取用户列表
|
|
38
|
+
{
|
|
39
|
+
method: 'GET',
|
|
40
|
+
path: '/users',
|
|
41
|
+
handler: createHandler(
|
|
42
|
+
{
|
|
43
|
+
query: Type.Object({
|
|
44
|
+
page: Type.Optional(Type.Number({ default: 1 })),
|
|
45
|
+
limit: Type.Optional(Type.Number({ default: 10 }))
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
async ({ query }) => ({
|
|
49
|
+
users: [] as User[],
|
|
50
|
+
total: 0,
|
|
51
|
+
page: query.page ?? 1,
|
|
52
|
+
limit: query.limit ?? 10
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// POST /users - 创建用户
|
|
58
|
+
{
|
|
59
|
+
method: 'POST',
|
|
60
|
+
path: '/users',
|
|
61
|
+
handler: createHandler(
|
|
62
|
+
{ body: Type.Object({ name: Type.String(), email: Type.String() }) },
|
|
63
|
+
async ({ body }) => ({
|
|
64
|
+
id: crypto.randomUUID(),
|
|
65
|
+
name: body.name,
|
|
66
|
+
email: body.email
|
|
67
|
+
} as User)
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// GET /users/:id - 获取单个用户
|
|
72
|
+
{
|
|
73
|
+
method: 'GET',
|
|
74
|
+
path: '/users/:id',
|
|
75
|
+
handler: createHandler(
|
|
76
|
+
{ params: Type.Object({ id: Type.String() }) },
|
|
77
|
+
async ({ params }) => ({
|
|
78
|
+
id: params.id,
|
|
79
|
+
name: 'User',
|
|
80
|
+
email: 'user@example.com'
|
|
81
|
+
} as User | null)
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// PUT /users/:id - 更新用户
|
|
86
|
+
{
|
|
87
|
+
method: 'PUT',
|
|
88
|
+
path: '/users/:id',
|
|
89
|
+
handler: createHandler(
|
|
90
|
+
{
|
|
91
|
+
params: Type.Object({ id: Type.String() }),
|
|
92
|
+
body: Type.Object({
|
|
93
|
+
name: Type.Optional(Type.String()),
|
|
94
|
+
email: Type.Optional(Type.String())
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
async ({ params, body }) => ({
|
|
98
|
+
id: params.id,
|
|
99
|
+
name: body?.name ?? 'User',
|
|
100
|
+
email: body?.email ?? 'user@example.com'
|
|
101
|
+
} as User)
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// DELETE /users/:id - 删除用户
|
|
106
|
+
{
|
|
107
|
+
method: 'DELETE',
|
|
108
|
+
path: '/users/:id',
|
|
109
|
+
handler: createHandler(
|
|
110
|
+
{ params: Type.Object({ id: Type.String() }) },
|
|
111
|
+
async () => ({ success: true, deletedAt: new Date().toISOString() })
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// 🌊 GET /chat/stream - SSE 流式响应
|
|
116
|
+
{
|
|
117
|
+
method: 'GET',
|
|
118
|
+
path: '/chat/stream',
|
|
119
|
+
handler: createSSEHandler(
|
|
120
|
+
{ query: Type.Object({ prompt: Type.String() }) },
|
|
121
|
+
async function* ({ query }) {
|
|
122
|
+
// 模拟 AI 流式响应
|
|
123
|
+
yield { event: 'start', data: { message: 'Starting...' } }
|
|
124
|
+
|
|
125
|
+
const words = `Hello! You said: "${query.prompt}"`.split(' ')
|
|
126
|
+
for (const word of words) {
|
|
127
|
+
yield { data: { text: word + ' ' } as ChatMessage }
|
|
128
|
+
await new Promise(r => setTimeout(r, 100))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
yield { event: 'end', data: { message: 'Done!' } }
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
// ============= 🎉 自动推断契约类型!=============
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 从路由定义自动推断 API 契约
|
|
141
|
+
* 无需手动定义任何接口!无需 as const!
|
|
142
|
+
*/
|
|
143
|
+
type Api = InferEden<typeof routes>
|
|
144
|
+
|
|
145
|
+
// ============= 客户端:完全类型安全的调用 =============
|
|
146
|
+
|
|
147
|
+
const api = eden<Api>('http://localhost:3000', {
|
|
148
|
+
headers: {
|
|
149
|
+
'Authorization': 'Bearer your-token-here'
|
|
150
|
+
},
|
|
151
|
+
timeout: 5000,
|
|
152
|
+
onError: (error) => {
|
|
153
|
+
console.error('API Error:', error.message)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
async function main() {
|
|
158
|
+
console.log('=== 自动推断契约示例(无需 as const)===\n')
|
|
159
|
+
|
|
160
|
+
// ✅ GET /users?page=1&limit=10
|
|
161
|
+
const usersResult = await api.users.get({ page: 1, limit: 10 })
|
|
162
|
+
if (usersResult.data) {
|
|
163
|
+
console.log('📋 用户列表:', usersResult.data.users)
|
|
164
|
+
console.log(' 总数:', usersResult.data.total)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ✅ POST /users
|
|
168
|
+
const newUserResult = await api.users.post({
|
|
169
|
+
name: 'John Doe',
|
|
170
|
+
email: 'john@example.com'
|
|
171
|
+
})
|
|
172
|
+
if (newUserResult.data) {
|
|
173
|
+
console.log('\n✨ 新用户:', newUserResult.data.name)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ✅ GET /users/:id
|
|
177
|
+
const userResult = await api.users({ id: '123' }).get()
|
|
178
|
+
if (userResult.data) {
|
|
179
|
+
console.log('\n👤 用户详情:', userResult.data.name)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ✅ PUT /users/:id
|
|
183
|
+
const updateResult = await api.users({ id: '123' }).put({ name: 'Jane' })
|
|
184
|
+
if (updateResult.data) {
|
|
185
|
+
console.log('\n📝 更新后:', updateResult.data.name)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ✅ DELETE /users/:id
|
|
189
|
+
const deleteResult = await api.users({ id: '123' }).delete()
|
|
190
|
+
if (deleteResult.data) {
|
|
191
|
+
console.log('\n🗑️ 删除成功:', deleteResult.data.success)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 🌊 SSE 流式响应
|
|
195
|
+
console.log('\n=== SSE 流式响应 ===\n')
|
|
196
|
+
|
|
197
|
+
// SSE 返回类型目前是 unknown,需要手动断言
|
|
198
|
+
// 未来版本会改进 SSE 返回类型推断
|
|
199
|
+
const subscription = api.chat.stream.subscribe(
|
|
200
|
+
{ prompt: 'Hello AI!' },
|
|
201
|
+
{
|
|
202
|
+
onOpen: () => console.log('📡 连接已建立'),
|
|
203
|
+
onMessage: (data: unknown) => {
|
|
204
|
+
console.log('收到消息:', data)
|
|
205
|
+
},
|
|
206
|
+
onError: (err) => console.error('❌ 错误:', err.message),
|
|
207
|
+
onClose: () => console.log('📴 连接已关闭')
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// 5 秒后取消订阅
|
|
212
|
+
setTimeout(() => {
|
|
213
|
+
subscription.unsubscribe()
|
|
214
|
+
console.log('\n\n=== 示例完成 ===')
|
|
215
|
+
}, 5000)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main().catch(console.error)
|
|
219
|
+
|
|
220
|
+
// ============= 导出 =============
|
|
221
|
+
|
|
222
|
+
export { routes, api }
|
|
223
|
+
export type { Api }
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE 端到端测试
|
|
3
|
+
* 测试功能:
|
|
4
|
+
* 1. 请求取消 (AbortController)
|
|
5
|
+
* 2. SSE 自动重连
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
defineRoutes,
|
|
10
|
+
route,
|
|
11
|
+
createHandler,
|
|
12
|
+
createSSEHandler,
|
|
13
|
+
Type,
|
|
14
|
+
serve
|
|
15
|
+
} from 'vafast'
|
|
16
|
+
import { eden, InferEden } from '../src'
|
|
17
|
+
|
|
18
|
+
// 定义路由
|
|
19
|
+
const routes = defineRoutes([
|
|
20
|
+
// 普通 GET 请求
|
|
21
|
+
route('GET', '/hello', createHandler(
|
|
22
|
+
{ query: Type.Object({ name: Type.Optional(Type.String()) }) },
|
|
23
|
+
async ({ query }) => ({ message: `Hello, ${query.name || 'World'}!` })
|
|
24
|
+
)),
|
|
25
|
+
|
|
26
|
+
// 慢请求(用于测试取消)
|
|
27
|
+
route('GET', '/slow', createHandler(
|
|
28
|
+
{},
|
|
29
|
+
async () => {
|
|
30
|
+
await new Promise(r => setTimeout(r, 5000))
|
|
31
|
+
return { message: 'Slow response' }
|
|
32
|
+
}
|
|
33
|
+
)),
|
|
34
|
+
|
|
35
|
+
// SSE 流式响应
|
|
36
|
+
route('GET', '/stream', createSSEHandler(
|
|
37
|
+
{ query: Type.Object({ count: Type.Optional(Type.Number({ default: 5 })) }) },
|
|
38
|
+
async function* ({ query }) {
|
|
39
|
+
const count = query.count ?? 5
|
|
40
|
+
|
|
41
|
+
yield { event: 'start', data: { message: '开始流式传输...' } }
|
|
42
|
+
|
|
43
|
+
for (let i = 1; i <= count; i++) {
|
|
44
|
+
yield { id: String(i), data: { index: i, text: `消息 ${i}/${count}` } }
|
|
45
|
+
await new Promise(r => setTimeout(r, 200))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
yield { event: 'end', data: { message: '传输完成!' } }
|
|
49
|
+
}
|
|
50
|
+
))
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
type Api = InferEden<typeof routes>
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
// 启动服务器
|
|
57
|
+
console.log('🚀 启动服务器...')
|
|
58
|
+
const server = serve({
|
|
59
|
+
fetch: (req) => {
|
|
60
|
+
const url = new URL(req.url)
|
|
61
|
+
|
|
62
|
+
// 简单路由
|
|
63
|
+
if (url.pathname === '/hello') {
|
|
64
|
+
const name = url.searchParams.get('name') || 'World'
|
|
65
|
+
return new Response(JSON.stringify({ message: `Hello, ${name}!` }), {
|
|
66
|
+
headers: { 'Content-Type': 'application/json' }
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 慢请求
|
|
71
|
+
if (url.pathname === '/slow') {
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
resolve(new Response(JSON.stringify({ message: 'Slow response' }), {
|
|
75
|
+
headers: { 'Content-Type': 'application/json' }
|
|
76
|
+
}))
|
|
77
|
+
}, 5000)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (url.pathname === '/stream') {
|
|
82
|
+
const count = parseInt(url.searchParams.get('count') || '5')
|
|
83
|
+
|
|
84
|
+
const stream = new ReadableStream({
|
|
85
|
+
async start(controller) {
|
|
86
|
+
const encoder = new TextEncoder()
|
|
87
|
+
|
|
88
|
+
controller.enqueue(encoder.encode(`event: start\ndata: ${JSON.stringify({ message: '开始流式传输...' })}\n\n`))
|
|
89
|
+
|
|
90
|
+
for (let i = 1; i <= count; i++) {
|
|
91
|
+
controller.enqueue(encoder.encode(`id: ${i}\ndata: ${JSON.stringify({ index: i, text: `消息 ${i}/${count}` })}\n\n`))
|
|
92
|
+
await new Promise(r => setTimeout(r, 200))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
controller.enqueue(encoder.encode(`event: end\ndata: ${JSON.stringify({ message: '传输完成!' })}\n\n`))
|
|
96
|
+
controller.close()
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return new Response(stream, {
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'text/event-stream',
|
|
103
|
+
'Cache-Control': 'no-cache',
|
|
104
|
+
'Connection': 'keep-alive'
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return new Response('Not Found', { status: 404 })
|
|
110
|
+
},
|
|
111
|
+
port: 3456
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
console.log('✅ 服务器启动在 http://localhost:3456\n')
|
|
115
|
+
|
|
116
|
+
// 等待服务器启动
|
|
117
|
+
await new Promise(r => setTimeout(r, 500))
|
|
118
|
+
|
|
119
|
+
// 创建客户端
|
|
120
|
+
const api = eden<Api>('http://localhost:3456')
|
|
121
|
+
|
|
122
|
+
// ============= 测试 1: 请求取消 =============
|
|
123
|
+
console.log('🧪 测试 1: 请求取消')
|
|
124
|
+
|
|
125
|
+
const controller = new AbortController()
|
|
126
|
+
|
|
127
|
+
// 发起慢请求
|
|
128
|
+
const slowPromise = api.slow.get(undefined, { signal: controller.signal })
|
|
129
|
+
|
|
130
|
+
// 100ms 后取消
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
controller.abort()
|
|
133
|
+
console.log(' ⏹️ 请求已取消')
|
|
134
|
+
}, 100)
|
|
135
|
+
|
|
136
|
+
const result = await slowPromise
|
|
137
|
+
// 取消后 status 为 0,error 可能是 AbortError 或 "This operation was aborted"
|
|
138
|
+
if (result.status === 0 && result.error) {
|
|
139
|
+
console.log(' ✅ 请求取消成功 (error:', result.error.message || result.error.name, ')\n')
|
|
140
|
+
} else {
|
|
141
|
+
console.log(' ❌ 请求取消失败: status=', result.status, '\n')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============= 测试 2: 普通请求 =============
|
|
145
|
+
console.log('🧪 测试 2: 普通请求')
|
|
146
|
+
const helloResult = await api.hello.get({ name: 'TypeScript' })
|
|
147
|
+
console.log(' 响应:', helloResult.data)
|
|
148
|
+
console.log()
|
|
149
|
+
|
|
150
|
+
// ============= 测试 3: SSE 流式响应 =============
|
|
151
|
+
console.log('🧪 测试 3: SSE 流式响应')
|
|
152
|
+
|
|
153
|
+
await new Promise<void>((resolve) => {
|
|
154
|
+
const sub = api.stream.subscribe(
|
|
155
|
+
{ count: 3 },
|
|
156
|
+
{
|
|
157
|
+
onOpen: () => console.log(' 📡 连接已建立'),
|
|
158
|
+
onMessage: (data: unknown) => {
|
|
159
|
+
console.log(' 📨', data)
|
|
160
|
+
},
|
|
161
|
+
onError: (err) => console.log(' ❌ 错误:', err.message),
|
|
162
|
+
onClose: () => {
|
|
163
|
+
console.log(' 📴 连接已关闭')
|
|
164
|
+
resolve()
|
|
165
|
+
},
|
|
166
|
+
onReconnect: (attempt, max) => {
|
|
167
|
+
console.log(` 🔄 重连中 (${attempt}/${max})...`)
|
|
168
|
+
},
|
|
169
|
+
onMaxReconnects: () => {
|
|
170
|
+
console.log(' ⚠️ 达到最大重连次数')
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
reconnectInterval: 1000,
|
|
175
|
+
maxReconnects: 3
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
// 5 秒超时
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
sub.unsubscribe()
|
|
182
|
+
resolve()
|
|
183
|
+
}, 5000)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
console.log('\n✅ 所有测试完成!')
|
|
187
|
+
|
|
188
|
+
// 关闭服务器
|
|
189
|
+
server.stop()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch(console.error)
|
package/package.json
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vafast/api-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Type-safe API client for Vafast framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "
|
|
8
|
-
"dev": "npx tsx watch example/
|
|
7
|
+
"build": "tsdown",
|
|
8
|
+
"dev": "npx tsx watch example/auto-infer.ts",
|
|
9
9
|
"test": "npx vitest run",
|
|
10
10
|
"release": "npm run build && npm run test && npx bumpp && npm publish --access=public"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"vafast": ">=
|
|
13
|
+
"vafast": ">=0.3.11"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"typescript": "^5.5.3"
|
|
16
|
+
"vafast": ">=0.3.11",
|
|
17
|
+
"tsdown": "^0.19.0-beta.4",
|
|
18
|
+
"typescript": "^5.5.3",
|
|
19
|
+
"vitest": "^2.1.8"
|
|
21
20
|
},
|
|
22
21
|
"main": "./dist/cjs/index.js",
|
|
23
22
|
"module": "./dist/index.mjs",
|
|
@@ -41,13 +40,13 @@
|
|
|
41
40
|
],
|
|
42
41
|
"repository": {
|
|
43
42
|
"type": "git",
|
|
44
|
-
"url": "https://github.com/
|
|
43
|
+
"url": "https://github.com/vafast/vafast-api-client"
|
|
45
44
|
},
|
|
46
45
|
"author": {
|
|
47
46
|
"name": "Vafast Team",
|
|
48
|
-
"url": "https://github.com/
|
|
47
|
+
"url": "https://github.com/vafast",
|
|
49
48
|
"email": "team@vafast.dev"
|
|
50
49
|
},
|
|
51
|
-
"homepage": "https://github.com/
|
|
52
|
-
"bugs": "https://github.com/
|
|
50
|
+
"homepage": "https://github.com/vafast/vafast-api-client",
|
|
51
|
+
"bugs": "https://github.com/vafast/vafast-api-client/issues"
|
|
53
52
|
}
|