@vafast/api-client 0.1.1 → 0.1.2

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.
@@ -1,347 +0,0 @@
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
- }
@@ -1,262 +0,0 @@
1
- import { describe, expect, it, beforeEach, mock } from 'bun:test'
2
- import { VafastApiClient } from '../src'
3
-
4
- // Mock fetch
5
- const mockFetch = mock(() =>
6
- Promise.resolve(new Response(JSON.stringify({ success: true }), {
7
- status: 200,
8
- headers: { 'Content-Type': 'application/json' }
9
- }))
10
- )
11
-
12
- // Mock global fetch
13
- global.fetch = mockFetch
14
-
15
- describe('VafastApiClient', () => {
16
- let client: VafastApiClient
17
-
18
- beforeEach(() => {
19
- client = new VafastApiClient({
20
- baseURL: 'https://api.example.com',
21
- timeout: 5000,
22
- retries: 2
23
- })
24
- mockFetch.mockClear()
25
- })
26
-
27
- describe('Constructor', () => {
28
- it('should create client with default config', () => {
29
- const defaultClient = new VafastApiClient()
30
- expect(defaultClient).toBeDefined()
31
- })
32
-
33
- it('should create client with custom config', () => {
34
- expect(client).toBeDefined()
35
- })
36
-
37
- it('should merge config correctly', () => {
38
- const customClient = new VafastApiClient({
39
- baseURL: 'https://custom.api.com',
40
- timeout: 10000
41
- })
42
- expect(customClient).toBeDefined()
43
- })
44
- })
45
-
46
- describe('Middleware Management', () => {
47
- it('should add middleware', () => {
48
- const middleware = {
49
- name: 'test',
50
- onRequest: async (req: Request) => req
51
- }
52
-
53
- client.addMiddleware(middleware)
54
- expect(client).toBeDefined()
55
- })
56
-
57
- it('should remove middleware', () => {
58
- const middleware = {
59
- name: 'test',
60
- onRequest: async (req: Request) => req
61
- }
62
-
63
- client.addMiddleware(middleware)
64
- client.removeMiddleware('test')
65
- expect(client).toBeDefined()
66
- })
67
-
68
- it('should handle non-existent middleware removal', () => {
69
- client.removeMiddleware('non-existent')
70
- expect(client).toBeDefined()
71
- })
72
- })
73
-
74
- describe('Interceptor Management', () => {
75
- it('should add interceptor', () => {
76
- const interceptor = {
77
- request: async (config: any) => config
78
- }
79
-
80
- client.addInterceptor(interceptor)
81
- expect(client).toBeDefined()
82
- })
83
-
84
- it('should remove interceptor', () => {
85
- const interceptor = {
86
- request: async (config: any) => config
87
- }
88
-
89
- client.addInterceptor(interceptor)
90
- client.removeInterceptor(0)
91
- expect(client).toBeDefined()
92
- })
93
-
94
- it('should handle invalid interceptor index', () => {
95
- client.removeInterceptor(-1)
96
- client.removeInterceptor(999)
97
- expect(client).toBeDefined()
98
- })
99
- })
100
-
101
- describe('Cache Management', () => {
102
- it('should clear cache', () => {
103
- client.clearCache()
104
- const stats = client.getCacheStats()
105
- expect(stats.size).toBe(0)
106
- expect(stats.keys).toEqual([])
107
- })
108
-
109
- it('should get cache stats', () => {
110
- const stats = client.getCacheStats()
111
- expect(stats).toHaveProperty('size')
112
- expect(stats).toHaveProperty('keys')
113
- expect(Array.isArray(stats.keys)).toBe(true)
114
- })
115
- })
116
-
117
- describe('Configuration Methods', () => {
118
- it('should set cache config', () => {
119
- const result = client.setCacheConfig({
120
- enabled: true,
121
- ttl: 300000,
122
- maxSize: 100,
123
- strategy: 'memory'
124
- })
125
- expect(result).toBe(client)
126
- })
127
-
128
- it('should set retry config', () => {
129
- const result = client.setRetryConfig({
130
- enabled: true,
131
- maxRetries: 5,
132
- retryDelay: 1000,
133
- backoffMultiplier: 2,
134
- retryableStatuses: [500, 502, 503]
135
- })
136
- expect(result).toBe(client)
137
- })
138
-
139
- it('should set log config', () => {
140
- const result = client.setLogConfig({
141
- enabled: true,
142
- level: 'info',
143
- format: 'json',
144
- includeHeaders: true,
145
- includeBody: false
146
- })
147
- expect(result).toBe(client)
148
- })
149
- })
150
-
151
- describe('HTTP Methods', () => {
152
- it('should make GET request', async () => {
153
- const response = await client.get('/users', { page: 1, limit: 10 })
154
- expect(response).toBeDefined()
155
- expect(mockFetch).toHaveBeenCalled()
156
- })
157
-
158
- it('should make POST request', async () => {
159
- const response = await client.post('/users', { name: 'John', email: 'john@example.com' })
160
- expect(response).toBeDefined()
161
- expect(mockFetch).toHaveBeenCalled()
162
- })
163
-
164
- it('should make PUT request', async () => {
165
- const response = await client.put('/users/123', { name: 'John Updated' })
166
- expect(response).toBeDefined()
167
- expect(mockFetch).toHaveBeenCalled()
168
- })
169
-
170
- it('should make DELETE request', async () => {
171
- const response = await client.delete('/users/123')
172
- expect(response).toBeDefined()
173
- expect(mockFetch).toHaveBeenCalled()
174
- })
175
-
176
- it('should make PATCH request', async () => {
177
- const response = await client.patch('/users/123', { name: 'John Patched' })
178
- expect(response).toBeDefined()
179
- expect(mockFetch).toHaveBeenCalled()
180
- })
181
-
182
- it('should make HEAD request', async () => {
183
- const response = await client.head('/users')
184
- expect(response).toBeDefined()
185
- expect(mockFetch).toHaveBeenCalled()
186
- })
187
-
188
- it('should make OPTIONS request', async () => {
189
- const response = await client.options('/users')
190
- expect(response).toBeDefined()
191
- expect(mockFetch).toHaveBeenCalled()
192
- })
193
- })
194
-
195
- describe('Request Configuration', () => {
196
- it('should handle custom headers', async () => {
197
- const response = await client.get('/users', undefined, {
198
- headers: { 'X-Custom-Header': 'test' }
199
- })
200
- expect(response).toBeDefined()
201
- })
202
-
203
- it('should handle timeout configuration', async () => {
204
- const response = await client.get('/users', undefined, {
205
- timeout: 10000
206
- })
207
- expect(response).toBeDefined()
208
- })
209
-
210
- it('should handle retry configuration', async () => {
211
- const response = await client.get('/users', undefined, {
212
- retries: 5,
213
- retryDelay: 2000
214
- })
215
- expect(response).toBeDefined()
216
- })
217
- })
218
-
219
- describe('Error Handling', () => {
220
- it('should handle network errors gracefully', async () => {
221
- // Mock fetch to throw error
222
- const errorFetch = mock(() => Promise.reject(new Error('Network error')))
223
- global.fetch = errorFetch
224
-
225
- const response = await client.get('/users')
226
- expect(response.error).toBeDefined()
227
- expect(response.data).toBeNull()
228
-
229
- // Restore original mock
230
- global.fetch = mockFetch
231
- })
232
-
233
- it('should handle HTTP error statuses', async () => {
234
- const errorFetch = mock(() =>
235
- Promise.resolve(new Response('Not Found', { status: 404 }))
236
- )
237
- global.fetch = errorFetch
238
-
239
- const response = await client.get('/users')
240
- expect(response.status).toBe(404)
241
-
242
- // Restore original mock
243
- global.fetch = mockFetch
244
- })
245
- })
246
-
247
- describe('URL Building', () => {
248
- it('should build URLs correctly with baseURL', async () => {
249
- const customClient = new VafastApiClient({
250
- baseURL: 'https://api.example.com'
251
- })
252
-
253
- await customClient.get('/users')
254
- expect(mockFetch).toHaveBeenCalled()
255
- })
256
-
257
- it('should handle absolute URLs', async () => {
258
- await client.get('https://external.api.com/users')
259
- expect(mockFetch).toHaveBeenCalled()
260
- })
261
- })
262
- })
@@ -1,55 +0,0 @@
1
- import { describe, expect, it, beforeEach } from 'bun:test'
2
- import { VafastApiClient } from '../src'
3
-
4
- describe('VafastApiClient', () => {
5
- let client: VafastApiClient
6
-
7
- beforeEach(() => {
8
- client = new VafastApiClient({
9
- baseURL: 'https://api.example.com',
10
- timeout: 5000,
11
- retries: 2
12
- })
13
- })
14
-
15
- it('should create client with default config', () => {
16
- const defaultClient = new VafastApiClient()
17
- expect(defaultClient).toBeDefined()
18
- })
19
-
20
- it('should create client with custom config', () => {
21
- expect(client).toBeDefined()
22
- })
23
-
24
- it('should add and remove middleware', () => {
25
- const middleware = {
26
- name: 'test',
27
- onRequest: async (req: Request) => req
28
- }
29
-
30
- client.addMiddleware(middleware)
31
- expect(client.getCacheStats()).toBeDefined()
32
-
33
- client.removeMiddleware('test')
34
- expect(client.getCacheStats()).toBeDefined()
35
- })
36
-
37
- it('should add and remove interceptor', () => {
38
- const interceptor = {
39
- request: async (config: any) => config
40
- }
41
-
42
- client.addInterceptor(interceptor)
43
- expect(client.getCacheStats()).toBeDefined()
44
-
45
- client.removeInterceptor(0)
46
- expect(client.getCacheStats()).toBeDefined()
47
- })
48
-
49
- it('should clear cache', () => {
50
- client.clearCache()
51
- const stats = client.getCacheStats()
52
- expect(stats.size).toBe(0)
53
- expect(stats.keys).toEqual([])
54
- })
55
- })