@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 CHANGED
@@ -1,282 +1,298 @@
1
- # Vafast API Client
1
+ # @vafast/api-client
2
2
 
3
- 一个专门为 [Vafast](https://github.com/vafastjs/vafast) 框架打造的现代化、类型安全的 API 客户端插件。
3
+ 🚀 类型安全的 Eden 风格 API 客户端,专为 [Vafast](https://github.com/user/vafast) 框架设计。
4
4
 
5
5
  ## ✨ 特性
6
6
 
7
- - 🚀 **专为 Vafast 设计**: 完全兼容 Vafast 框架架构
8
- - 🔒 **类型安全**: 完整的 TypeScript 类型支持
9
- - 🎯 **智能路由**: 自动推断路由类型和方法
10
- - 🔄 **自动重试**: 内置指数退避重试机制
11
- - 📡 **WebSocket 支持**: 完整的 WebSocket 客户端
12
- - 🧩 **中间件系统**: 灵活的请求/响应处理
13
- - 🎛️ **拦截器**: 强大的请求/响应拦截能力
14
- - 📁 **文件上传**: 支持文件和 FormData 上传
15
- - 💾 **缓存系统**: 智能的响应缓存机制
16
- - 📊 **监控统计**: 详细的请求统计和性能监控
7
+ - 🔒 **完整类型推断** - 从路由定义自动推断 API 类型,无需手动定义接口
8
+ - 🎯 **无需 `as const`** - `defineRoutes()` 自动保留字面量类型
9
+ - 🌊 **SSE 流式响应** - 内置 Server-Sent Events 支持,包含自动重连
10
+ - ⏹️ **请求取消** - 支持 AbortController 取消进行中的请求
11
+ - 🔗 **链式调用** - 优雅的 `api.users({ id }).posts.get()` 语法
12
+ - 📦 **轻量** - 仅 8KB (gzip)
17
13
 
18
14
  ## 📦 安装
19
15
 
20
16
  ```bash
21
- bun add @vafast/api-client
17
+ npm install @vafast/api-client
22
18
  ```
23
19
 
24
20
  ## 🚀 快速开始
25
21
 
26
- ### 基础用法
22
+ ### 1. 定义服务端路由
27
23
 
28
24
  ```typescript
29
- import { VafastApiClient } from '@vafast/api-client'
30
-
31
- // 创建客户端
32
- const client = new VafastApiClient({
33
- baseURL: 'https://api.example.com',
34
- timeout: 10000,
35
- retries: 3
36
- })
37
-
38
- // 发送请求
39
- const response = await client.get('/users', { page: 1, limit: 10 })
40
- if (response.error) {
41
- console.error('Error:', response.error)
42
- } else {
43
- console.log('Users:', response.data)
44
- }
45
- ```
46
-
47
- ### 类型安全客户端
48
-
49
- ```typescript
50
- import { createTypedClient } from '@vafast/api-client'
51
- import type { Server } from 'vafast'
52
-
53
- // 从 Vafast 服务器创建类型安全客户端
54
- const typedClient = createTypedClient<Server>(server, {
55
- baseURL: 'https://api.example.com'
56
- })
25
+ // server.ts
26
+ import { defineRoutes, createHandler, createSSEHandler, Type } from 'vafast'
27
+
28
+ export const routes = defineRoutes([
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
+ },
54
+ // 🌊 SSE 流式响应
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!' } }
69
+ }
70
+ )
71
+ }
72
+ ])
57
73
 
58
- // 现在有完整的类型检查
59
- const users = await typedClient.get('/users', { page: 1, limit: 10 })
60
- const user = await typedClient.post('/users', { name: 'John', email: 'john@example.com' })
74
+ // 导出类型供客户端使用
75
+ export type AppRoutes = typeof routes
61
76
  ```
62
77
 
63
- ### WebSocket 客户端
78
+ ### 2. 创建类型安全客户端
64
79
 
65
80
  ```typescript
66
- import { createWebSocketClient } from '@vafast/api-client'
67
-
68
- const wsClient = createWebSocketClient('wss://ws.example.com', {
69
- autoReconnect: true,
70
- maxReconnectAttempts: 5
71
- })
81
+ // client.ts
82
+ import { eden, InferEden } from '@vafast/api-client'
83
+ import type { AppRoutes } from './server'
72
84
 
73
- await wsClient.connect()
85
+ // 自动推断 API 类型
86
+ type Api = InferEden<AppRoutes>
74
87
 
75
- wsClient.on('message', (data) => {
76
- console.log('Received:', data)
88
+ // 创建客户端
89
+ const api = eden<Api>('http://localhost:3000', {
90
+ headers: { 'Authorization': 'Bearer token' },
91
+ timeout: 5000
77
92
  })
78
93
 
79
- wsClient.send({ type: 'chat', message: 'Hello!' })
80
- ```
81
-
82
- ## 📚 API 参考
83
-
84
- ### VafastApiClient
85
-
86
- 主要的 API 客户端类。
87
-
88
- #### 构造函数
89
-
90
- ```typescript
91
- new VafastApiClient(config?: ApiClientConfig)
92
- ```
93
-
94
- #### 配置选项
95
-
96
- ```typescript
97
- interface ApiClientConfig {
98
- baseURL?: string // 基础 URL
99
- defaultHeaders?: Record<string, string> // 默认请求头
100
- timeout?: number // 请求超时时间(毫秒)
101
- retries?: number // 重试次数
102
- retryDelay?: number // 重试延迟(毫秒)
103
- validateStatus?: (status: number) => boolean // 状态码验证函数
94
+ // 完全类型安全的调用
95
+ async function main() {
96
+ // GET /users?page=1
97
+ const { data: users } = await api.users.get({ page: 1 })
98
+ console.log(users?.total) // ✅ 类型推断
99
+
100
+ // POST /users
101
+ const { data: newUser } = await api.users.post({
102
+ name: 'John',
103
+ email: 'john@example.com'
104
+ })
105
+ console.log(newUser?.id) // ✅ 类型推断
106
+
107
+ // GET /users/:id
108
+ const { data: user } = await api.users({ id: '123' }).get()
109
+ console.log(user?.name) // ✅ 类型推断
104
110
  }
105
111
  ```
106
112
 
107
- #### 方法
113
+ ## 📖 API 文档
108
114
 
109
- - `get(path, query?, config?)` - GET 请求
110
- - `post(path, body?, config?)` - POST 请求
111
- - `put(path, body?, config?)` - PUT 请求
112
- - `delete(path, config?)` - DELETE 请求
113
- - `patch(path, body?, config?)` - PATCH 请求
114
- - `head(path, config?)` - HEAD 请求
115
- - `options(path, config?)` - OPTIONS 请求
115
+ ### `eden<T>(baseURL, config?)`
116
116
 
117
- ### 中间件系统
117
+ 创建 Eden 风格的 API 客户端。
118
118
 
119
119
  ```typescript
120
- client.addMiddleware({
121
- name: 'logging',
122
- onRequest: async (request, config) => {
123
- console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
120
+ const api = eden<Api>('http://localhost:3000', {
121
+ // 默认请求头
122
+ headers: { 'Authorization': 'Bearer token' },
123
+
124
+ // 全局超时(毫秒)
125
+ timeout: 5000,
126
+
127
+ // 请求拦截器
128
+ onRequest: (request) => {
129
+ console.log('Request:', request.url)
124
130
  return request
125
131
  },
126
- onResponse: async (response, config) => {
127
- console.log(`Response: ${response.status}`)
132
+
133
+ // 响应拦截器
134
+ onResponse: (response) => {
135
+ console.log('Response:', response.status)
128
136
  return response
129
137
  },
130
- onError: async (error, config) => {
138
+
139
+ // 错误处理
140
+ onError: (error) => {
131
141
  console.error('Error:', error.message)
132
142
  }
133
143
  })
134
144
  ```
135
145
 
136
- ### 拦截器系统
146
+ ### HTTP 方法
137
147
 
138
148
  ```typescript
139
- client.addInterceptor({
140
- request: async (config) => {
141
- // 添加认证头
142
- config.headers = { ...config.headers, 'Authorization': 'Bearer token' }
143
- return config
144
- },
145
- response: async (response) => {
146
- // 处理响应
147
- return response
148
- },
149
- error: async (error) => {
150
- // 处理错误
151
- return error
152
- }
153
- })
149
+ // GET 请求(带 query 参数)
150
+ api.users.get({ page: 1, limit: 10 })
151
+
152
+ // POST 请求(带 body)
153
+ api.users.post({ name: 'John', email: 'john@example.com' })
154
+
155
+ // PUT 请求
156
+ api.users({ id: '123' }).put({ name: 'Jane' })
157
+
158
+ // DELETE 请求
159
+ api.users({ id: '123' }).delete()
160
+
161
+ // PATCH 请求
162
+ api.users({ id: '123' }).patch({ name: 'Updated' })
154
163
  ```
155
164
 
156
- ### WebSocket 客户端
165
+ ### 路径参数
157
166
 
158
167
  ```typescript
159
- const wsClient = createWebSocketClient(url, options)
168
+ // 使用函数调用传递参数
169
+ api.users({ id: '123' }).get() // GET /users/123
170
+ api.users({ id: '123' }).posts.get() // GET /users/123/posts
171
+ api.users({ id: '123' }).posts({ postId: '456' }).get() // GET /users/123/posts/456
172
+ ```
160
173
 
161
- // 连接
162
- await wsClient.connect()
174
+ ### 请求取消
163
175
 
164
- // 监听事件
165
- wsClient.on('message', (data) => console.log(data))
166
- wsClient.on('open', () => console.log('Connected'))
167
- wsClient.on('close', () => console.log('Disconnected'))
176
+ ```typescript
177
+ const controller = new AbortController()
168
178
 
169
- // 发送数据
170
- wsClient.send({ type: 'chat', message: 'Hello' })
179
+ // 发起请求
180
+ const promise = api.users.get({ page: 1 }, { signal: controller.signal })
171
181
 
172
- // 断开连接
173
- wsClient.disconnect()
174
- ```
182
+ // 取消请求
183
+ controller.abort()
175
184
 
176
- ## 🔧 高级用法
185
+ const result = await promise
186
+ if (result.error) {
187
+ console.log('请求已取消')
188
+ }
189
+ ```
177
190
 
178
- ### 文件上传
191
+ ### 单次请求配置
179
192
 
180
193
  ```typescript
181
- // 单个文件
182
- const response = await client.post('/upload', {
183
- file: fileInput.files[0],
184
- description: 'User avatar'
194
+ // 覆盖全局配置
195
+ const result = await api.users.get({ page: 1 }, {
196
+ headers: { 'X-Custom-Header': 'value' },
197
+ timeout: 10000,
198
+ signal: abortController.signal
185
199
  })
200
+ ```
186
201
 
187
- // 多个文件
188
- const response = await client.post('/upload', {
189
- files: [file1, file2, file3],
190
- category: 'images'
191
- })
202
+ ## 🌊 SSE 流式响应
203
+
204
+ ### 基本用法
192
205
 
193
- // 混合数据
194
- const response = await client.post('/upload', {
195
- file: fileInput.files[0],
196
- metadata: {
197
- name: 'avatar.jpg',
198
- size: fileInput.files[0].size,
199
- type: fileInput.files[0].type
206
+ ```typescript
207
+ const subscription = api.chat.stream.subscribe(
208
+ { prompt: 'Hello AI!' }, // query 参数
209
+ {
210
+ onOpen: () => console.log('连接已建立'),
211
+ onMessage: (data) => console.log('收到:', data),
212
+ onError: (err) => console.error('错误:', err),
213
+ onClose: () => console.log('连接已关闭'),
214
+ onReconnect: (attempt, max) => console.log(`重连中 ${attempt}/${max}`),
215
+ onMaxReconnects: () => console.log('达到最大重连次数')
216
+ },
217
+ {
218
+ reconnectInterval: 3000, // 重连间隔(毫秒)
219
+ maxReconnects: 5 // 最大重连次数
200
220
  }
201
- })
221
+ )
222
+
223
+ // 取消订阅
224
+ subscription.unsubscribe()
202
225
  ```
203
226
 
204
- ### 路径参数
227
+ ### SSE 特性
205
228
 
206
- ```typescript
207
- // 使用工具函数替换路径参数
208
- import { replacePathParams } from '@vafast/api-client'
229
+ - ✅ **自动重连** - 网络断开后自动重连
230
+ - ✅ **断点续传** - 使用 `Last-Event-ID` 从断点继续
231
+ - **可配置重连策略** - 自定义重连间隔和最大次数
232
+ - ✅ **事件类型支持** - 支持自定义事件名称
209
233
 
210
- const path = '/users/:id/posts/:postId'
211
- const params = { id: '123', postId: '456' }
212
- const resolvedPath = replacePathParams(path, params)
213
- // 结果: '/users/123/posts/456'
234
+ ## 🔧 类型定义
214
235
 
215
- const response = await client.get(resolvedPath)
216
- ```
236
+ ### `InferEden<T>`
217
237
 
218
- ### 查询参数构建
238
+ vafast 路由数组推断 API 契约类型。
219
239
 
220
240
  ```typescript
221
- import { buildQueryString } from '@vafast/api-client'
222
-
223
- const query = { page: 1, limit: 10, search: 'john' }
224
- const queryString = buildQueryString(query)
225
- // 结果: '?page=1&limit=10&search=john'
241
+ import { InferEden } from '@vafast/api-client'
226
242
 
227
- const response = await client.get(`/users${queryString}`)
243
+ const routes = defineRoutes([...])
244
+ type Api = InferEden<typeof routes>
228
245
  ```
229
246
 
230
- ### 缓存配置
247
+ ### `EdenClient<T>`
248
+
249
+ Eden 客户端类型。
231
250
 
232
251
  ```typescript
233
- client.setCacheConfig({
234
- enabled: true,
235
- ttl: 300000, // 5分钟
236
- maxSize: 100,
237
- strategy: 'memory'
238
- })
252
+ import { EdenClient } from '@vafast/api-client'
253
+
254
+ type MyClient = EdenClient<Api>
239
255
  ```
240
256
 
241
- ### 重试配置
257
+ ### `ApiResponse<T>`
258
+
259
+ API 响应类型。
242
260
 
243
261
  ```typescript
244
- client.setRetryConfig({
245
- enabled: true,
246
- maxRetries: 5,
247
- retryDelay: 1000,
248
- backoffMultiplier: 2,
249
- retryableStatuses: [408, 429, 500, 502, 503, 504]
250
- })
262
+ interface ApiResponse<T> {
263
+ data: T | null // 响应数据
264
+ error: Error | null // 错误信息
265
+ status: number // HTTP 状态码
266
+ headers: Headers // 响应头
267
+ response: Response // 原始 Response
268
+ }
251
269
  ```
252
270
 
253
- ## 🧪 测试
271
+ ### `RequestConfig`
254
272
 
255
- ```bash
256
- bun test
273
+ 请求配置类型。
274
+
275
+ ```typescript
276
+ interface RequestConfig {
277
+ headers?: Record<string, string> // 请求头
278
+ timeout?: number // 超时(毫秒)
279
+ signal?: AbortSignal // 取消信号
280
+ }
257
281
  ```
258
282
 
259
- ## 📖 示例
283
+ ## 📁 示例
260
284
 
261
- 查看 `example/` 目录中的完整示例:
285
+ 查看 `example/` 目录获取完整示例:
262
286
 
263
- - `index.ts` - 主要使用示例
264
- - 基础 HTTP 请求
265
- - 类型安全客户端
266
- - WebSocket 客户端
267
- - 中间件和拦截器
268
- - 文件上传
287
+ - `auto-infer.ts` - 自动类型推断示例
288
+ - `test-sse.ts` - SSE 流式响应测试
269
289
 
270
- ## 🤝 贡献
290
+ ## 🧪 测试
271
291
 
272
- 欢迎提交 Issue 和 Pull Request!
292
+ ```bash
293
+ npm test
294
+ ```
273
295
 
274
296
  ## 📄 许可证
275
297
 
276
- MIT License
277
-
278
- ## 🔗 相关链接
279
-
280
- - [Vafast 官方文档](https://vafast.dev)
281
- - [GitHub 仓库](https://github.com/vafastjs/vafast-api-client)
282
- - [问题反馈](https://github.com/vafastjs/vafast-api-client/issues)
298
+ MIT
package/TODO.md ADDED
@@ -0,0 +1,83 @@
1
+ # TODO: 类型同步功能
2
+
3
+ ## 背景
4
+
5
+ 前后端分离项目中,共享类型需要发布 npm 包,流程繁琐且需要私有 npm 服务。
6
+
7
+ ## 目标
8
+
9
+ 实现一条命令同步 API 类型,无需 npm 发包。
10
+
11
+ ## 方案设计
12
+
13
+ ### 1. 服务端:暴露类型端点
14
+
15
+ ```typescript
16
+ // vafast 自动注册端点
17
+ // GET /__vafast__/types
18
+
19
+ import { serve } from 'vafast'
20
+
21
+ serve({
22
+ routes,
23
+ exposeTypes: true // 开启类型导出端点
24
+ })
25
+ ```
26
+
27
+ 返回内容:
28
+ ```typescript
29
+ // 自动生成的类型定义
30
+ export const routes = [...] as const
31
+ export type AppRoutes = typeof routes
32
+ ```
33
+
34
+ ### 2. 客户端 CLI
35
+
36
+ ```bash
37
+ # 安装
38
+ npm install -D @vafast/cli
39
+
40
+ # 同步类型
41
+ npx vafast sync --url http://localhost:3000
42
+
43
+ # 或指定输出路径
44
+ npx vafast sync --url http://localhost:3000 --out src/api.d.ts
45
+ ```
46
+
47
+ ### 3. 自动化(可选)
48
+
49
+ ```json
50
+ // package.json
51
+ {
52
+ "scripts": {
53
+ "dev": "vafast sync --url $API_URL && vite",
54
+ "build": "vafast sync --url $API_URL && vite build"
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## 实现步骤
60
+
61
+ - [ ] vafast 核心:添加 `exposeTypes` 选项
62
+ - [ ] vafast 核心:实现 `/__vafast__/types` 端点
63
+ - [ ] vafast 核心:实现类型序列化(保留字面量类型)
64
+ - [ ] @vafast/cli:创建 CLI 包
65
+ - [ ] @vafast/cli:实现 `sync` 命令
66
+ - [ ] @vafast/cli:支持配置文件 `.vafastrc`
67
+
68
+ ## 技术难点
69
+
70
+ 1. **类型序列化**:如何将运行时的路由定义导出为 TypeScript 类型字符串
71
+ 2. **TypeBox Schema 转换**:将 TypeBox schema 转为 TypeScript 类型
72
+ 3. **字面量保留**:确保 `'GET'`、`'/users'` 等字面量类型不被扩展
73
+
74
+ ## 参考
75
+
76
+ - tRPC:需要 monorepo 或 npm 包共享类型
77
+ - Elysia Eden:同样需要共享代码
78
+ - OpenAPI Generator:从 JSON Schema 生成类型(可参考)
79
+
80
+ ## 优先级
81
+
82
+ 中等 - 当前可用方案(npm link / monorepo)能满足需求,此功能为优化体验。
83
+