@vafast/api-client 0.1.1

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.
@@ -0,0 +1,232 @@
1
+ import type { QueryParams, PathParams, RequestBody, FileUpload, ApiFormData } from "../types";
2
+
3
+ /**
4
+ * 构建查询字符串
5
+ */
6
+ export function buildQueryString(params: QueryParams): string {
7
+ if (!params || Object.keys(params).length === 0) return "";
8
+
9
+ const searchParams = new URLSearchParams();
10
+
11
+ for (const [key, value] of Object.entries(params)) {
12
+ if (value !== undefined && value !== null) {
13
+ if (Array.isArray(value)) {
14
+ value.forEach((v) => searchParams.append(key, String(v)));
15
+ } else {
16
+ searchParams.append(key, String(value));
17
+ }
18
+ }
19
+ }
20
+
21
+ const queryString = searchParams.toString();
22
+ return queryString ? `?${queryString}` : "";
23
+ }
24
+
25
+ /**
26
+ * 替换路径参数
27
+ */
28
+ export function replacePathParams(path: string, params: PathParams): string {
29
+ let result = path;
30
+
31
+ for (const [key, value] of Object.entries(params)) {
32
+ result = result.replace(`:${key}`, String(value));
33
+ }
34
+
35
+ return result;
36
+ }
37
+
38
+ /**
39
+ * 检查是否为文件对象
40
+ */
41
+ export function isFile(value: unknown): value is File | Blob {
42
+ return value instanceof File || value instanceof Blob;
43
+ }
44
+
45
+ /**
46
+ * 检查是否为文件上传对象
47
+ */
48
+ export function isFileUpload(value: unknown): value is FileUpload {
49
+ if (value === null || value === undefined) return false;
50
+ return (
51
+ value && typeof value === "object" && "file" in value && isFile((value as FileUpload).file)
52
+ );
53
+ }
54
+
55
+ /**
56
+ * 检查对象是否包含文件
57
+ */
58
+ export function hasFiles(obj: unknown): boolean {
59
+ if (!obj || typeof obj !== "object") return false;
60
+
61
+ for (const value of Object.values(obj as Record<string, unknown>)) {
62
+ if (isFile(value) || isFileUpload(value)) return true;
63
+ if (Array.isArray(value) && value.some(isFile)) return true;
64
+ if (typeof value === "object" && hasFiles(value)) return true;
65
+ }
66
+
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * 创建 FormData
72
+ */
73
+ export function createFormData(data: ApiFormData): globalThis.FormData {
74
+ const formData = new globalThis.FormData();
75
+
76
+ for (const [key, value] of Object.entries(data)) {
77
+ if (value === undefined || value === null) continue;
78
+
79
+ if (isFileUpload(value)) {
80
+ formData.append(key, value.file, value.filename);
81
+ } else if (isFile(value)) {
82
+ formData.append(key, value);
83
+ } else if (Array.isArray(value)) {
84
+ value.forEach((v) => {
85
+ if (isFileUpload(v)) {
86
+ formData.append(key, v.file, v.filename);
87
+ } else if (isFile(v)) {
88
+ formData.append(key, v);
89
+ } else {
90
+ formData.append(key, String(v));
91
+ }
92
+ });
93
+ } else {
94
+ formData.append(key, String(value));
95
+ }
96
+ }
97
+
98
+ return formData;
99
+ }
100
+
101
+ /**
102
+ * 深度合并对象
103
+ */
104
+ export function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
105
+ const result = { ...target } as T;
106
+
107
+ for (const [key, value] of Object.entries(source)) {
108
+ if (value && typeof value === "object" && !Array.isArray(value)) {
109
+ const targetValue = result[key as keyof T];
110
+ if (targetValue && typeof targetValue === "object" && !Array.isArray(targetValue)) {
111
+ result[key as keyof T] = deepMerge(targetValue, value) as T[keyof T];
112
+ } else {
113
+ result[key as keyof T] = value as T[keyof T];
114
+ }
115
+ } else {
116
+ result[key as keyof T] = value as T[keyof T];
117
+ }
118
+ }
119
+
120
+ return result;
121
+ }
122
+
123
+ /**
124
+ * 延迟函数
125
+ */
126
+ export function delay(ms: number): Promise<void> {
127
+ return new Promise((resolve) => setTimeout(resolve, ms));
128
+ }
129
+
130
+ /**
131
+ * 指数退避重试延迟
132
+ */
133
+ export function exponentialBackoff(attempt: number, baseDelay: number, maxDelay: number): number {
134
+ const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
135
+ return delay + Math.random() * 1000; // 添加随机抖动
136
+ }
137
+
138
+ /**
139
+ * 验证状态码
140
+ */
141
+ export function validateStatus(status: number): boolean {
142
+ return status >= 200 && status < 300;
143
+ }
144
+
145
+ /**
146
+ * 解析响应内容类型
147
+ */
148
+ export function parseContentType(contentType: string | null): string {
149
+ if (!contentType) return "text/plain";
150
+ return contentType.split(";")[0].trim();
151
+ }
152
+
153
+ /**
154
+ * 解析响应数据
155
+ */
156
+ export async function parseResponse(response: Response): Promise<any> {
157
+ const contentType = parseContentType(response.headers.get("content-type"));
158
+
159
+ switch (contentType) {
160
+ case "application/json":
161
+ return response.json();
162
+ case "application/octet-stream":
163
+ return response.arrayBuffer();
164
+ case "multipart/form-data":
165
+ const formData = await response.formData();
166
+ const data: Record<string, any> = {};
167
+ formData.forEach((value, key) => {
168
+ data[key] = value;
169
+ });
170
+ return data;
171
+ case "text/event-stream":
172
+ return response.body;
173
+ default:
174
+ return response.text();
175
+ }
176
+ }
177
+
178
+ /**
179
+ * 创建错误对象
180
+ */
181
+ export function createError(status: number, message: string, data?: unknown): Error {
182
+ const error = new Error(message) as Error & { status: number; data?: unknown; name: string };
183
+ error.status = status;
184
+ error.data = data;
185
+ error.name = "ApiError";
186
+ return error;
187
+ }
188
+
189
+ /**
190
+ * 克隆请求对象
191
+ */
192
+ export function cloneRequest(request: Request): Request {
193
+ return new Request(request.url, {
194
+ method: request.method,
195
+ headers: request.headers,
196
+ body: request.body,
197
+ mode: request.mode,
198
+ credentials: request.credentials,
199
+ cache: request.cache,
200
+ redirect: request.redirect,
201
+ referrer: request.referrer,
202
+ referrerPolicy: request.referrerPolicy,
203
+ integrity: request.integrity,
204
+ keepalive: request.keepalive,
205
+ signal: request.signal,
206
+ });
207
+ }
208
+
209
+ /**
210
+ * 检查是否为可重试的错误
211
+ */
212
+ export function isRetryableError(error: Error, status?: number): boolean {
213
+ if (status) {
214
+ return [408, 429, 500, 502, 503, 504].includes(status);
215
+ }
216
+
217
+ // 网络错误通常是可重试的
218
+ if (error.name === "TypeError" && error.message.includes("fetch")) {
219
+ return true;
220
+ }
221
+
222
+ // 检查其他网络相关错误
223
+ if (
224
+ error.message.includes("fetch") ||
225
+ error.message.includes("network") ||
226
+ error.message.includes("connection")
227
+ ) {
228
+ return true;
229
+ }
230
+
231
+ return false;
232
+ }
@@ -0,0 +1,347 @@
1
+ import type { WebSocketClient, WebSocketEvent } from '../types'
2
+
3
+ /**
4
+ * Vafast WebSocket 客户端
5
+ */
6
+ export class VafastWebSocketClient implements WebSocketClient {
7
+ private ws: WebSocket | null = null
8
+ private url: string
9
+ private eventListeners = new Map<string, Set<(data: any) => void>>()
10
+ private reconnectAttempts = 0
11
+ private maxReconnectAttempts = 5
12
+ private reconnectDelay = 1000
13
+ private isReconnecting = false
14
+ private autoReconnect = true
15
+
16
+ constructor(url: string, options: {
17
+ autoReconnect?: boolean
18
+ maxReconnectAttempts?: number
19
+ reconnectDelay?: number
20
+ } = {}) {
21
+ this.url = url
22
+ this.autoReconnect = options.autoReconnect ?? true
23
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5
24
+ this.reconnectDelay = options.reconnectDelay ?? 1000
25
+ }
26
+
27
+ /**
28
+ * 连接到 WebSocket 服务器
29
+ */
30
+ async connect(): Promise<void> {
31
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
32
+ return
33
+ }
34
+
35
+ return new Promise((resolve, reject) => {
36
+ try {
37
+ this.ws = new WebSocket(this.url)
38
+
39
+ this.ws.onopen = () => {
40
+ this.reconnectAttempts = 0
41
+ this.isReconnecting = false
42
+ resolve()
43
+ }
44
+
45
+ this.ws.onclose = (event) => {
46
+ if (this.autoReconnect && !this.isReconnecting && this.reconnectAttempts < this.maxReconnectAttempts) {
47
+ this.scheduleReconnect()
48
+ }
49
+ }
50
+
51
+ this.ws.onerror = (error) => {
52
+ reject(error)
53
+ }
54
+
55
+ this.ws.onmessage = (event) => {
56
+ this.handleMessage(event)
57
+ }
58
+ } catch (error) {
59
+ reject(error)
60
+ }
61
+ })
62
+ }
63
+
64
+ /**
65
+ * 断开 WebSocket 连接
66
+ */
67
+ disconnect(): void {
68
+ this.autoReconnect = false
69
+ if (this.ws) {
70
+ this.ws.close()
71
+ this.ws = null
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 发送数据
77
+ */
78
+ send(data: any): void {
79
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
80
+ throw new Error('WebSocket is not connected')
81
+ }
82
+
83
+ if (typeof data === 'string') {
84
+ this.ws.send(data)
85
+ } else {
86
+ this.ws.send(JSON.stringify(data))
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 监听事件
92
+ */
93
+ on(event: string, callback: (data: any) => void): void {
94
+ if (!this.eventListeners.has(event)) {
95
+ this.eventListeners.set(event, new Set())
96
+ }
97
+ this.eventListeners.get(event)!.add(callback)
98
+ }
99
+
100
+ /**
101
+ * 移除事件监听器
102
+ */
103
+ off(event: string, callback: (data: any) => void): void {
104
+ const listeners = this.eventListeners.get(event)
105
+ if (listeners) {
106
+ listeners.delete(callback)
107
+ if (listeners.size === 0) {
108
+ this.eventListeners.delete(event)
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 检查是否已连接
115
+ */
116
+ isConnected(): boolean {
117
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN
118
+ }
119
+
120
+ /**
121
+ * 获取连接状态
122
+ */
123
+ getReadyState(): number {
124
+ return this.ws ? this.ws.readyState : WebSocket.CLOSED
125
+ }
126
+
127
+ /**
128
+ * 获取连接状态文本
129
+ */
130
+ getReadyStateText(): string {
131
+ const states: Record<number, string> = {
132
+ [WebSocket.CONNECTING]: 'CONNECTING',
133
+ [WebSocket.OPEN]: 'OPEN',
134
+ [WebSocket.CLOSING]: 'CLOSING',
135
+ [WebSocket.CLOSED]: 'CLOSED'
136
+ }
137
+ return states[this.getReadyState()] || 'UNKNOWN'
138
+ }
139
+
140
+ /**
141
+ * 设置自动重连
142
+ */
143
+ setAutoReconnect(enabled: boolean): void {
144
+ this.autoReconnect = enabled
145
+ }
146
+
147
+ /**
148
+ * 设置最大重连次数
149
+ */
150
+ setMaxReconnectAttempts(attempts: number): void {
151
+ this.maxReconnectAttempts = attempts
152
+ }
153
+
154
+ /**
155
+ * 设置重连延迟
156
+ */
157
+ setReconnectDelay(delay: number): void {
158
+ this.reconnectDelay = delay
159
+ }
160
+
161
+ /**
162
+ * 手动重连
163
+ */
164
+ async reconnect(): Promise<void> {
165
+ if (this.isReconnecting) {
166
+ return
167
+ }
168
+
169
+ this.isReconnecting = true
170
+ this.disconnect()
171
+
172
+ // 等待一段时间后重连
173
+ await new Promise(resolve => setTimeout(resolve, this.reconnectDelay))
174
+
175
+ try {
176
+ await this.connect()
177
+ } catch (error) {
178
+ this.isReconnecting = false
179
+ throw error
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 获取事件监听器数量
185
+ */
186
+ getEventListenerCount(event: string): number {
187
+ const listeners = this.eventListeners.get(event)
188
+ return listeners ? listeners.size : 0
189
+ }
190
+
191
+ /**
192
+ * 清除所有事件监听器
193
+ */
194
+ clearEventListeners(event?: string): void {
195
+ if (event) {
196
+ this.eventListeners.delete(event)
197
+ } else {
198
+ this.eventListeners.clear()
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 获取所有事件名称
204
+ */
205
+ getEventNames(): string[] {
206
+ return Array.from(this.eventListeners.keys())
207
+ }
208
+
209
+ /**
210
+ * 处理接收到的消息
211
+ */
212
+ private handleMessage(event: MessageEvent): void {
213
+ let data: any
214
+
215
+ try {
216
+ data = JSON.parse(event.data)
217
+ } catch {
218
+ data = event.data
219
+ }
220
+
221
+ const wsEvent: WebSocketEvent = {
222
+ type: 'message',
223
+ data,
224
+ timestamp: Date.now()
225
+ }
226
+
227
+ // 触发消息事件监听器
228
+ this.triggerEvent('message', wsEvent)
229
+
230
+ // 如果有特定类型的事件监听器,也触发它们
231
+ if (data && typeof data === 'object' && data.type) {
232
+ this.triggerEvent(data.type, data)
233
+ }
234
+ }
235
+
236
+ /**
237
+ * 触发事件
238
+ */
239
+ private triggerEvent(event: string, data: any): void {
240
+ const listeners = this.eventListeners.get(event)
241
+ if (listeners) {
242
+ listeners.forEach(callback => {
243
+ try {
244
+ callback(data)
245
+ } catch (error) {
246
+ console.error(`Error in WebSocket event listener for ${event}:`, error)
247
+ }
248
+ })
249
+ }
250
+ }
251
+
252
+ /**
253
+ * 安排重连
254
+ */
255
+ private scheduleReconnect(): void {
256
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
257
+ return
258
+ }
259
+
260
+ this.reconnectAttempts++
261
+ this.isReconnecting = true
262
+
263
+ setTimeout(async () => {
264
+ try {
265
+ await this.connect()
266
+ } catch (error) {
267
+ console.error('WebSocket reconnection failed:', error)
268
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
269
+ this.scheduleReconnect()
270
+ }
271
+ }
272
+ }, this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1))
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 创建 WebSocket 客户端
278
+ */
279
+ export function createWebSocketClient(
280
+ url: string,
281
+ options?: {
282
+ autoReconnect?: boolean
283
+ maxReconnectAttempts?: number
284
+ reconnectDelay?: number
285
+ }
286
+ ): VafastWebSocketClient {
287
+ return new VafastWebSocketClient(url, options)
288
+ }
289
+
290
+ /**
291
+ * 创建类型安全的 WebSocket 客户端
292
+ */
293
+ export function createTypedWebSocketClient<T = any>(
294
+ url: string,
295
+ options?: {
296
+ autoReconnect?: boolean
297
+ maxReconnectAttempts?: number
298
+ reconnectDelay?: number
299
+ }
300
+ ): VafastWebSocketClient & {
301
+ on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void
302
+ send<K extends keyof T>(event: K, data: T[K]): void
303
+ } {
304
+ const client = new VafastWebSocketClient(url, options)
305
+
306
+ // Create a new object that extends the client
307
+ const typedClient = Object.create(Object.getPrototypeOf(client))
308
+
309
+ // Copy all properties and methods from the client
310
+ for (const key of Object.getOwnPropertyNames(client)) {
311
+ const descriptor = Object.getOwnPropertyDescriptor(client, key)
312
+ if (descriptor) {
313
+ Object.defineProperty(typedClient, key, descriptor)
314
+ }
315
+ }
316
+
317
+ // Copy all symbol properties
318
+ for (const symbol of Object.getOwnPropertySymbols(client)) {
319
+ const descriptor = Object.getOwnPropertyDescriptor(client, symbol)
320
+ if (descriptor) {
321
+ Object.defineProperty(typedClient, symbol, descriptor)
322
+ }
323
+ }
324
+
325
+ // Override the on method for typed events
326
+ typedClient.on = (event: keyof T, callback: (data: any) => void): void => {
327
+ client.on(String(event), callback)
328
+ }
329
+
330
+ // Override the send method for typed events
331
+ typedClient.send = (event: keyof T, data: any): void => {
332
+ // Use the original client's send method directly
333
+ client.send({ type: event, data })
334
+ }
335
+
336
+ // Ensure the typed client has access to the original client's properties
337
+ Object.defineProperty(typedClient, 'ws', {
338
+ get() {
339
+ return (client as any).ws
340
+ },
341
+ set(value) {
342
+ (client as any).ws = value
343
+ }
344
+ })
345
+
346
+ return typedClient
347
+ }