@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
package/example/index.ts
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
VafastApiClient,
|
|
3
|
-
createTypedClient,
|
|
4
|
-
createWebSocketClient,
|
|
5
|
-
createTypedWebSocketClient
|
|
6
|
-
} from '../src'
|
|
7
|
-
|
|
8
|
-
// 创建基础 API 客户端
|
|
9
|
-
const apiClient = new VafastApiClient({
|
|
10
|
-
baseURL: 'https://api.example.com',
|
|
11
|
-
timeout: 10000,
|
|
12
|
-
retries: 3,
|
|
13
|
-
defaultHeaders: {
|
|
14
|
-
'Authorization': 'Bearer your-token-here'
|
|
15
|
-
}
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
// 添加请求拦截器
|
|
19
|
-
apiClient.addInterceptor({
|
|
20
|
-
request: async (config) => {
|
|
21
|
-
console.log('Request interceptor:', config)
|
|
22
|
-
// 可以在这里添加认证头、日志等
|
|
23
|
-
return config
|
|
24
|
-
},
|
|
25
|
-
response: async (response) => {
|
|
26
|
-
console.log('Response interceptor:', response.status)
|
|
27
|
-
return response
|
|
28
|
-
},
|
|
29
|
-
error: async (error) => {
|
|
30
|
-
console.error('Error interceptor:', error)
|
|
31
|
-
return error
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
// 添加中间件
|
|
36
|
-
apiClient.addMiddleware({
|
|
37
|
-
name: 'logging',
|
|
38
|
-
onRequest: async (request, config) => {
|
|
39
|
-
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`)
|
|
40
|
-
return request
|
|
41
|
-
},
|
|
42
|
-
onResponse: async (response, config) => {
|
|
43
|
-
console.log(`[${new Date().toISOString()}] Response: ${response.status}`)
|
|
44
|
-
return response
|
|
45
|
-
},
|
|
46
|
-
onError: async (error, config) => {
|
|
47
|
-
console.error(`[${new Date().toISOString()}] Error:`, error.message)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
// 使用示例
|
|
52
|
-
async function example() {
|
|
53
|
-
try {
|
|
54
|
-
// GET 请求
|
|
55
|
-
const usersResponse = await apiClient.get('/users', { page: 1, limit: 10 })
|
|
56
|
-
if (usersResponse.error) {
|
|
57
|
-
console.error('Failed to fetch users:', usersResponse.error)
|
|
58
|
-
} else {
|
|
59
|
-
console.log('Users:', usersResponse.data)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// POST 请求
|
|
63
|
-
const createUserResponse = await apiClient.post('/users', {
|
|
64
|
-
name: 'John Doe',
|
|
65
|
-
email: 'john@example.com'
|
|
66
|
-
})
|
|
67
|
-
if (createUserResponse.error) {
|
|
68
|
-
console.error('Failed to create user:', createUserResponse.error)
|
|
69
|
-
} else {
|
|
70
|
-
console.log('Created user:', createUserResponse.data)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// PUT 请求
|
|
74
|
-
const updateUserResponse = await apiClient.put('/users/123', {
|
|
75
|
-
name: 'John Updated',
|
|
76
|
-
email: 'john.updated@example.com'
|
|
77
|
-
})
|
|
78
|
-
if (updateUserResponse.error) {
|
|
79
|
-
console.error('Failed to update user:', updateUserResponse.error)
|
|
80
|
-
} else {
|
|
81
|
-
console.log('Updated user:', updateUserResponse.data)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// DELETE 请求
|
|
85
|
-
const deleteUserResponse = await apiClient.delete('/users/123')
|
|
86
|
-
if (deleteUserResponse.error) {
|
|
87
|
-
console.error('Failed to delete user:', deleteUserResponse.error)
|
|
88
|
-
} else {
|
|
89
|
-
console.log('Deleted user successfully')
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 文件上传
|
|
93
|
-
const fileInput = document.createElement('input')
|
|
94
|
-
fileInput.type = 'file'
|
|
95
|
-
const file = fileInput.files?.[0]
|
|
96
|
-
|
|
97
|
-
if (file) {
|
|
98
|
-
const uploadResponse = await apiClient.post('/upload', {
|
|
99
|
-
file: file,
|
|
100
|
-
description: 'User avatar'
|
|
101
|
-
})
|
|
102
|
-
if (uploadResponse.error) {
|
|
103
|
-
console.error('Failed to upload file:', uploadResponse.error)
|
|
104
|
-
} else {
|
|
105
|
-
console.log('File uploaded:', uploadResponse.data)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.error('Example error:', error)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 类型安全客户端示例
|
|
115
|
-
interface User {
|
|
116
|
-
id: number
|
|
117
|
-
name: string
|
|
118
|
-
email: string
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
interface CreateUserRequest {
|
|
122
|
-
name: string
|
|
123
|
-
email: string
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
interface ApiResponse<T> {
|
|
127
|
-
data: T
|
|
128
|
-
message: string
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 模拟服务器类型
|
|
132
|
-
type MockServer = {
|
|
133
|
-
routes: {
|
|
134
|
-
'/users': {
|
|
135
|
-
GET: { query: { page?: number; limit?: number } }
|
|
136
|
-
POST: { body: CreateUserRequest }
|
|
137
|
-
}
|
|
138
|
-
'/users/:id': {
|
|
139
|
-
GET: { params: { id: string } }
|
|
140
|
-
PUT: { params: { id: string }; body: Partial<CreateUserRequest> }
|
|
141
|
-
DELETE: { params: { id: string } }
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 创建类型安全客户端
|
|
147
|
-
const typedClient = createTypedClient<MockServer>({} as MockServer, {
|
|
148
|
-
baseURL: 'https://api.example.com'
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
// 使用类型安全客户端
|
|
152
|
-
async function typedExample() {
|
|
153
|
-
try {
|
|
154
|
-
// 这些调用现在有类型检查
|
|
155
|
-
const users = await typedClient.get('/users', { page: 1, limit: 10 })
|
|
156
|
-
const user = await typedClient.post('/users', { name: 'Jane', email: 'jane@example.com' })
|
|
157
|
-
|
|
158
|
-
console.log('Typed client response:', users, user)
|
|
159
|
-
} catch (error) {
|
|
160
|
-
console.error('Typed example error:', error)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// WebSocket 示例
|
|
165
|
-
async function websocketExample() {
|
|
166
|
-
const wsClient = createWebSocketClient('wss://ws.example.com', {
|
|
167
|
-
autoReconnect: true,
|
|
168
|
-
maxReconnectAttempts: 5,
|
|
169
|
-
reconnectDelay: 1000
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
// 监听连接事件
|
|
173
|
-
wsClient.on('open', () => {
|
|
174
|
-
console.log('WebSocket connected')
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
wsClient.on('message', (data) => {
|
|
178
|
-
console.log('WebSocket message:', data)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
wsClient.on('close', () => {
|
|
182
|
-
console.log('WebSocket disconnected')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
await wsClient.connect()
|
|
187
|
-
|
|
188
|
-
// 发送消息
|
|
189
|
-
wsClient.send({ type: 'chat', message: 'Hello, WebSocket!' })
|
|
190
|
-
|
|
191
|
-
// 延迟后断开连接
|
|
192
|
-
setTimeout(() => {
|
|
193
|
-
wsClient.disconnect()
|
|
194
|
-
}, 5000)
|
|
195
|
-
} catch (error) {
|
|
196
|
-
console.error('WebSocket error:', error)
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 类型安全的 WebSocket 客户端
|
|
201
|
-
interface ChatEvents {
|
|
202
|
-
message: { text: string; userId: string }
|
|
203
|
-
join: { room: string; userId: string }
|
|
204
|
-
leave: { room: string; userId: string }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const typedWsClient = createTypedWebSocketClient<ChatEvents>('wss://chat.example.com')
|
|
208
|
-
|
|
209
|
-
async function typedWebSocketExample() {
|
|
210
|
-
try {
|
|
211
|
-
await typedWsClient.connect()
|
|
212
|
-
|
|
213
|
-
// 类型安全的事件监听
|
|
214
|
-
typedWsClient.on('message', (data) => {
|
|
215
|
-
console.log('Chat message:', data.text, 'from user:', data.userId)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
typedWsClient.on('join', (data) => {
|
|
219
|
-
console.log('User joined:', data.userId, 'room:', data.room)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// 类型安全的发送
|
|
223
|
-
typedWsClient.send('message', { text: 'Hello!', userId: 'user123' })
|
|
224
|
-
typedWsClient.send('join', { room: 'general', userId: 'user123' })
|
|
225
|
-
|
|
226
|
-
} catch (error) {
|
|
227
|
-
console.error('Typed WebSocket error:', error)
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// 运行示例
|
|
232
|
-
console.log('🚀 Vafast API Client Examples')
|
|
233
|
-
console.log('==============================')
|
|
234
|
-
|
|
235
|
-
// 运行基础示例
|
|
236
|
-
example().then(() => {
|
|
237
|
-
console.log('✅ Basic examples completed')
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
// 运行类型安全示例
|
|
241
|
-
typedExample().then(() => {
|
|
242
|
-
console.log('✅ Typed examples completed')
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
// 运行 WebSocket 示例
|
|
246
|
-
websocketExample().then(() => {
|
|
247
|
-
console.log('✅ WebSocket examples completed')
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
// 运行类型安全 WebSocket 示例
|
|
251
|
-
typedWebSocketExample().then(() => {
|
|
252
|
-
console.log('✅ Typed WebSocket examples completed')
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
export { apiClient, typedClient, wsClient: createWebSocketClient }
|
package/src/core/api-client.ts
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ApiClientConfig,
|
|
3
|
-
RequestConfig,
|
|
4
|
-
ApiResponse,
|
|
5
|
-
QueryParams,
|
|
6
|
-
PathParams,
|
|
7
|
-
RequestBody,
|
|
8
|
-
ApiMiddleware,
|
|
9
|
-
Interceptor,
|
|
10
|
-
CacheConfig,
|
|
11
|
-
RetryConfig,
|
|
12
|
-
LogConfig,
|
|
13
|
-
} from "../types";
|
|
14
|
-
import {
|
|
15
|
-
buildQueryString,
|
|
16
|
-
replacePathParams,
|
|
17
|
-
hasFiles,
|
|
18
|
-
createFormData,
|
|
19
|
-
deepMerge,
|
|
20
|
-
delay,
|
|
21
|
-
exponentialBackoff,
|
|
22
|
-
validateStatus,
|
|
23
|
-
parseResponse,
|
|
24
|
-
createError,
|
|
25
|
-
cloneRequest,
|
|
26
|
-
isRetryableError,
|
|
27
|
-
} from "../utils";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Vafast API 客户端
|
|
31
|
-
*/
|
|
32
|
-
export class VafastApiClient {
|
|
33
|
-
private config: ApiClientConfig;
|
|
34
|
-
private middlewares: ApiMiddleware[] = [];
|
|
35
|
-
private interceptors: Interceptor[] = [];
|
|
36
|
-
private cache = new Map<string, { data: unknown; timestamp: number; ttl: number }>();
|
|
37
|
-
|
|
38
|
-
constructor(config: ApiClientConfig = {}) {
|
|
39
|
-
this.config = {
|
|
40
|
-
baseURL: "",
|
|
41
|
-
defaultHeaders: {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
},
|
|
44
|
-
timeout: 30000,
|
|
45
|
-
retries: 3,
|
|
46
|
-
retryDelay: 1000,
|
|
47
|
-
validateStatus,
|
|
48
|
-
...config,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 添加中间件
|
|
54
|
-
*/
|
|
55
|
-
addMiddleware(middleware: ApiMiddleware): this {
|
|
56
|
-
this.middlewares.push(middleware);
|
|
57
|
-
return this;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 移除中间件
|
|
62
|
-
*/
|
|
63
|
-
removeMiddleware(name: string): this {
|
|
64
|
-
const index = this.middlewares.findIndex((m) => m.name === name);
|
|
65
|
-
if (index !== -1) {
|
|
66
|
-
this.middlewares.splice(index, 1);
|
|
67
|
-
}
|
|
68
|
-
return this;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 添加拦截器
|
|
73
|
-
*/
|
|
74
|
-
addInterceptor(interceptor: Interceptor): this {
|
|
75
|
-
this.interceptors.push(interceptor);
|
|
76
|
-
return this;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 移除拦截器
|
|
81
|
-
*/
|
|
82
|
-
removeInterceptor(index: number): this {
|
|
83
|
-
if (index >= 0 && index < this.interceptors.length) {
|
|
84
|
-
this.interceptors.splice(index, 1);
|
|
85
|
-
}
|
|
86
|
-
return this;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 设置缓存配置
|
|
91
|
-
*/
|
|
92
|
-
setCacheConfig(config: CacheConfig): this {
|
|
93
|
-
// 实现缓存逻辑
|
|
94
|
-
return this;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 设置重试配置
|
|
99
|
-
*/
|
|
100
|
-
setRetryConfig(config: RetryConfig): this {
|
|
101
|
-
// 实现重试逻辑
|
|
102
|
-
return this;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 设置日志配置
|
|
107
|
-
*/
|
|
108
|
-
setLogConfig(config: LogConfig): this {
|
|
109
|
-
// 实现日志逻辑
|
|
110
|
-
return this;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 发送请求
|
|
115
|
-
*/
|
|
116
|
-
async request<T = any>(
|
|
117
|
-
method: string,
|
|
118
|
-
path: string,
|
|
119
|
-
config: RequestConfig = {}
|
|
120
|
-
): Promise<ApiResponse<T>> {
|
|
121
|
-
const requestConfig = deepMerge(this.config, config);
|
|
122
|
-
const url = this.buildUrl(path);
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
// 应用请求拦截器
|
|
126
|
-
let finalConfig = requestConfig;
|
|
127
|
-
for (const interceptor of this.interceptors) {
|
|
128
|
-
if (interceptor.request) {
|
|
129
|
-
const result = await interceptor.request(finalConfig);
|
|
130
|
-
if (result) finalConfig = result;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 应用中间件
|
|
135
|
-
let request = this.createRequest(method, url, finalConfig);
|
|
136
|
-
for (const middleware of this.middlewares) {
|
|
137
|
-
if (middleware.onRequest) {
|
|
138
|
-
const result = await middleware.onRequest(request, finalConfig);
|
|
139
|
-
if (result) request = result;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 发送请求
|
|
144
|
-
const response = await this.sendRequest(request, finalConfig);
|
|
145
|
-
|
|
146
|
-
// 应用响应拦截器
|
|
147
|
-
let finalResponse = response;
|
|
148
|
-
for (const interceptor of this.interceptors) {
|
|
149
|
-
if (interceptor.response) {
|
|
150
|
-
const result = await interceptor.response(finalResponse);
|
|
151
|
-
if (result) finalResponse = result;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// 应用响应中间件
|
|
156
|
-
for (const middleware of this.middlewares) {
|
|
157
|
-
if (middleware.onResponse) {
|
|
158
|
-
const result = await middleware.onResponse(finalResponse, finalConfig);
|
|
159
|
-
if (result) finalResponse = result;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 解析响应
|
|
164
|
-
const data = await parseResponse(finalResponse);
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
data,
|
|
168
|
-
error: null,
|
|
169
|
-
status: finalResponse.status,
|
|
170
|
-
headers: finalResponse.headers,
|
|
171
|
-
response: finalResponse,
|
|
172
|
-
};
|
|
173
|
-
} catch (error) {
|
|
174
|
-
// 应用错误拦截器
|
|
175
|
-
let finalError = error as Error;
|
|
176
|
-
for (const interceptor of this.interceptors) {
|
|
177
|
-
if (interceptor.error) {
|
|
178
|
-
const result = await interceptor.error(finalError);
|
|
179
|
-
if (result) finalError = result;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 应用错误中间件
|
|
184
|
-
for (const middleware of this.middlewares) {
|
|
185
|
-
if (middleware.onError) {
|
|
186
|
-
middleware.onError(finalError, requestConfig);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
data: null,
|
|
192
|
-
error: finalError,
|
|
193
|
-
status: 0,
|
|
194
|
-
headers: new Headers(),
|
|
195
|
-
response: new Response(),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* GET 请求
|
|
202
|
-
*/
|
|
203
|
-
async get<T = any>(
|
|
204
|
-
path: string,
|
|
205
|
-
query?: QueryParams,
|
|
206
|
-
config?: RequestConfig
|
|
207
|
-
): Promise<ApiResponse<T>> {
|
|
208
|
-
const url = path + buildQueryString(query || {});
|
|
209
|
-
return this.request<T>("GET", url, config);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* POST 请求
|
|
214
|
-
*/
|
|
215
|
-
async post<T = any>(
|
|
216
|
-
path: string,
|
|
217
|
-
body?: RequestBody,
|
|
218
|
-
config?: RequestConfig
|
|
219
|
-
): Promise<ApiResponse<T>> {
|
|
220
|
-
return this.request<T>("POST", path, { ...config, body });
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* PUT 请求
|
|
225
|
-
*/
|
|
226
|
-
async put<T = any>(
|
|
227
|
-
path: string,
|
|
228
|
-
body?: RequestBody,
|
|
229
|
-
config?: RequestConfig
|
|
230
|
-
): Promise<ApiResponse<T>> {
|
|
231
|
-
return this.request<T>("PUT", path, { ...config, body });
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* DELETE 请求
|
|
236
|
-
*/
|
|
237
|
-
async delete<T = any>(path: string, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
238
|
-
return this.request<T>("DELETE", path, config);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* PATCH 请求
|
|
243
|
-
*/
|
|
244
|
-
async patch<T = any>(
|
|
245
|
-
path: string,
|
|
246
|
-
body?: RequestBody,
|
|
247
|
-
config?: RequestConfig
|
|
248
|
-
): Promise<ApiResponse<T>> {
|
|
249
|
-
return this.request<T>("PATCH", path, { ...config, body });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* HEAD 请求
|
|
254
|
-
*/
|
|
255
|
-
async head<T = any>(path: string, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
256
|
-
return this.request<T>("HEAD", path, config);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* OPTIONS 请求
|
|
261
|
-
*/
|
|
262
|
-
async options<T = any>(path: string, config?: RequestConfig): Promise<ApiResponse<T>> {
|
|
263
|
-
return this.request<T>("OPTIONS", path, config);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* 构建完整 URL
|
|
268
|
-
*/
|
|
269
|
-
private buildUrl(path: string): string {
|
|
270
|
-
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
271
|
-
return path;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const baseURL = this.config.baseURL || "";
|
|
275
|
-
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
276
|
-
|
|
277
|
-
return `${baseURL}${cleanPath}`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* 创建请求对象
|
|
282
|
-
*/
|
|
283
|
-
private createRequest(method: string, url: string, config: RequestConfig): Request {
|
|
284
|
-
const headers = new Headers(this.config.defaultHeaders);
|
|
285
|
-
|
|
286
|
-
if (config.headers) {
|
|
287
|
-
for (const [key, value] of Object.entries(config.headers)) {
|
|
288
|
-
headers.set(key, value);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
let body: string | FormData | undefined = undefined;
|
|
293
|
-
|
|
294
|
-
if (config.body !== undefined && config.body !== null) {
|
|
295
|
-
if (hasFiles(config.body as Record<string, unknown>)) {
|
|
296
|
-
body = createFormData(config.body as Record<string, unknown>);
|
|
297
|
-
// 不设置 Content-Type,让浏览器自动设置
|
|
298
|
-
headers.delete("Content-Type");
|
|
299
|
-
} else if (typeof config.body === "object") {
|
|
300
|
-
body = JSON.stringify(config.body);
|
|
301
|
-
headers.set("Content-Type", "application/json");
|
|
302
|
-
} else {
|
|
303
|
-
body = String(config.body);
|
|
304
|
-
headers.set("Content-Type", "text/plain");
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return new Request(url, {
|
|
309
|
-
method: method.toUpperCase(),
|
|
310
|
-
headers,
|
|
311
|
-
body,
|
|
312
|
-
mode: "cors",
|
|
313
|
-
credentials: "same-origin",
|
|
314
|
-
cache: "default",
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* 发送请求(带重试逻辑)
|
|
320
|
-
*/
|
|
321
|
-
private async sendRequest(request: Request, config: RequestConfig): Promise<Response> {
|
|
322
|
-
let lastError: Error | null = null;
|
|
323
|
-
|
|
324
|
-
for (let attempt = 0; attempt <= (config.retries || this.config.retries || 0); attempt++) {
|
|
325
|
-
try {
|
|
326
|
-
const controller = new AbortController();
|
|
327
|
-
const timeoutId = setTimeout(
|
|
328
|
-
() => controller.abort(),
|
|
329
|
-
config.timeout || this.config.timeout
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
const response = await fetch(cloneRequest(request), {
|
|
333
|
-
signal: controller.signal,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
clearTimeout(timeoutId);
|
|
337
|
-
|
|
338
|
-
if (this.config.validateStatus!(response.status)) {
|
|
339
|
-
return response;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// 状态码错误,检查是否可重试
|
|
343
|
-
if (!isRetryableError(new Error(), response.status)) {
|
|
344
|
-
return response;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
lastError = createError(response.status, `HTTP ${response.status}`, response);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
lastError = error as Error;
|
|
350
|
-
|
|
351
|
-
// 检查是否可重试
|
|
352
|
-
if (
|
|
353
|
-
!isRetryableError(error as Error) ||
|
|
354
|
-
attempt === (config.retries || this.config.retries || 0)
|
|
355
|
-
) {
|
|
356
|
-
throw error;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// 等待后重试
|
|
360
|
-
const delayMs = exponentialBackoff(
|
|
361
|
-
attempt,
|
|
362
|
-
config.retryDelay || this.config.retryDelay || 1000,
|
|
363
|
-
10000
|
|
364
|
-
);
|
|
365
|
-
await delay(delayMs);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
throw lastError || new Error("Request failed after all retries");
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* 清除缓存
|
|
374
|
-
*/
|
|
375
|
-
clearCache(): this {
|
|
376
|
-
this.cache.clear();
|
|
377
|
-
return this;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* 获取缓存统计
|
|
382
|
-
*/
|
|
383
|
-
getCacheStats(): { size: number; keys: string[] } {
|
|
384
|
-
return {
|
|
385
|
-
size: this.cache.size,
|
|
386
|
-
keys: Array.from(this.cache.keys()),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
}
|