@luxexchange/websocket 1.0.0 → 1.0.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.
- package/package.json +2 -2
- package/.depcheckrc +0 -15
- package/.eslintrc.js +0 -20
- package/README.md +0 -275
- package/src/client/__tests__/MockWebSocket.ts +0 -54
- package/src/client/__tests__/createWebSocketClient.integration.test.ts +0 -831
- package/src/client/__tests__/testUtils.ts +0 -113
- package/src/client/createWebSocketClient.ts +0 -262
- package/src/index.ts +0 -22
- package/src/store/createZustandConnectionStore.test.ts +0 -162
- package/src/store/createZustandConnectionStore.ts +0 -74
- package/src/store/types.ts +0 -17
- package/src/subscriptions/SubscriptionManager.test.ts +0 -556
- package/src/subscriptions/SubscriptionManager.ts +0 -274
- package/src/subscriptions/types.ts +0 -20
- package/src/types.ts +0 -88
- package/src/utils/backoff.test.ts +0 -48
- package/src/utils/backoff.ts +0 -15
- package/tsconfig.lint.json +0 -8
- package/vitest.config.ts +0 -17
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { MockWebSocket } from '@luxexchange/websocket/src/client/__tests__/MockWebSocket'
|
|
2
|
-
import { createWebSocketClient } from '@luxexchange/websocket/src/client/createWebSocketClient'
|
|
3
|
-
import { createZustandConnectionStore } from '@luxexchange/websocket/src/store/createZustandConnectionStore'
|
|
4
|
-
import type { ConnectionStore } from '@luxexchange/websocket/src/store/types'
|
|
5
|
-
import type { CreateWebSocketClientOptions, WebSocketClient } from '@luxexchange/websocket/src/types'
|
|
6
|
-
import { vi } from 'vitest'
|
|
7
|
-
|
|
8
|
-
export interface TestParams {
|
|
9
|
-
channel: string
|
|
10
|
-
id: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface TestMessage {
|
|
14
|
-
data: string
|
|
15
|
-
price?: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface TestHandler {
|
|
19
|
-
subscribe: ReturnType<typeof vi.fn>
|
|
20
|
-
unsubscribe: ReturnType<typeof vi.fn>
|
|
21
|
-
subscribeBatch: ReturnType<typeof vi.fn>
|
|
22
|
-
unsubscribeBatch: ReturnType<typeof vi.fn>
|
|
23
|
-
refreshSession: ReturnType<typeof vi.fn>
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface TestClientResult {
|
|
27
|
-
client: WebSocketClient<TestParams, TestMessage>
|
|
28
|
-
mockSocket: MockWebSocket
|
|
29
|
-
handler: TestHandler
|
|
30
|
-
connectionStore: ConnectionStore
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Creates a test WebSocket client with mocked dependencies.
|
|
35
|
-
* Returns the client, mock socket, mock handler, and connection store for assertions.
|
|
36
|
-
*/
|
|
37
|
-
export function createTestClient(
|
|
38
|
-
overrides?: Partial<CreateWebSocketClientOptions<TestParams, TestMessage>>,
|
|
39
|
-
): TestClientResult {
|
|
40
|
-
const mockSocket = new MockWebSocket()
|
|
41
|
-
const connectionStore = createZustandConnectionStore()
|
|
42
|
-
|
|
43
|
-
const handler: TestHandler = {
|
|
44
|
-
subscribe: vi.fn().mockResolvedValue(undefined),
|
|
45
|
-
unsubscribe: vi.fn().mockResolvedValue(undefined),
|
|
46
|
-
subscribeBatch: vi.fn().mockResolvedValue(undefined),
|
|
47
|
-
unsubscribeBatch: vi.fn().mockResolvedValue(undefined),
|
|
48
|
-
refreshSession: vi.fn().mockResolvedValue(undefined),
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const client = createWebSocketClient<TestParams, TestMessage>({
|
|
52
|
-
config: { url: 'wss://test.example.com' },
|
|
53
|
-
connectionStore,
|
|
54
|
-
subscriptionHandler: overrides?.subscriptionHandler ?? handler,
|
|
55
|
-
parseMessage: (raw) => {
|
|
56
|
-
const msg = raw as { channel?: string; key?: string; data?: TestMessage }
|
|
57
|
-
if (msg.channel && msg.key && msg.data) {
|
|
58
|
-
return { channel: msg.channel, key: msg.key, data: msg.data }
|
|
59
|
-
}
|
|
60
|
-
return null
|
|
61
|
-
},
|
|
62
|
-
parseConnectionMessage: (raw) => {
|
|
63
|
-
const msg = raw as { type?: string; connectionId?: string }
|
|
64
|
-
if (msg.type === 'connected' && msg.connectionId) {
|
|
65
|
-
return { connectionId: msg.connectionId }
|
|
66
|
-
}
|
|
67
|
-
return null
|
|
68
|
-
},
|
|
69
|
-
createSubscriptionKey: (channel, params) => `${channel}:${params.id}`,
|
|
70
|
-
socketFactory: () => mockSocket,
|
|
71
|
-
...overrides,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
return { client, mockSocket, handler, connectionStore }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
interface ConnectViaSubscribeOptions {
|
|
78
|
-
client: WebSocketClient<TestParams, TestMessage>
|
|
79
|
-
mockSocket: MockWebSocket
|
|
80
|
-
connectionId?: string
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Triggers a connection by subscribing, then simulates socket open and connection message.
|
|
85
|
-
* Returns the unsubscribe function from the subscription that triggered the connection.
|
|
86
|
-
*/
|
|
87
|
-
export function connectViaSubscribe(options: ConnectViaSubscribeOptions): () => void {
|
|
88
|
-
const { client, mockSocket, connectionId = 'conn-123' } = options
|
|
89
|
-
const unsub = client.subscribe({
|
|
90
|
-
channel: '__connect',
|
|
91
|
-
params: { channel: '__connect', id: '__connect' },
|
|
92
|
-
onMessage: vi.fn(),
|
|
93
|
-
})
|
|
94
|
-
mockSocket.simulateOpen()
|
|
95
|
-
mockSocket.simulateMessage({ type: 'connected', connectionId })
|
|
96
|
-
return unsub
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Flush all pending microtasks and promises.
|
|
101
|
-
*/
|
|
102
|
-
export async function flushMicrotasks(): Promise<void> {
|
|
103
|
-
// queueMicrotask runs before setTimeout, but we need to also flush
|
|
104
|
-
// any promises that are scheduled from within those microtasks
|
|
105
|
-
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Flush all pending promises in the microtask queue.
|
|
110
|
-
*/
|
|
111
|
-
export function flushPromises(): Promise<void> {
|
|
112
|
-
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
113
|
-
}
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { SubscriptionManager } from '@luxexchange/websocket/src/subscriptions/SubscriptionManager'
|
|
2
|
-
import type {
|
|
3
|
-
ConnectionStatus,
|
|
4
|
-
CreateWebSocketClientOptions,
|
|
5
|
-
SocketFactoryOptions,
|
|
6
|
-
SubscriptionOptions,
|
|
7
|
-
WebSocketClient,
|
|
8
|
-
WebSocketLike,
|
|
9
|
-
} from '@luxexchange/websocket/src/types'
|
|
10
|
-
import { getDefaultJitteredDelay } from '@luxexchange/websocket/src/utils/backoff'
|
|
11
|
-
import { WebSocket as PartySocket } from 'partysocket'
|
|
12
|
-
|
|
13
|
-
/** Default socket factory using PartySocket */
|
|
14
|
-
function defaultSocketFactory(url: string, options: SocketFactoryOptions): WebSocketLike {
|
|
15
|
-
return new PartySocket(url, [], options)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Default configuration for WebSocket connection behavior */
|
|
19
|
-
const DEFAULT_CONFIG = {
|
|
20
|
-
// Maximum delay (ms) between reconnection attempts
|
|
21
|
-
maxReconnectionDelay: 10000,
|
|
22
|
-
// Minimum delay (ms) between reconnection attempts (before jitter is applied)
|
|
23
|
-
minReconnectionDelay: 1000,
|
|
24
|
-
// Time (ms) to wait for a connection to establish before timing out
|
|
25
|
-
connectionTimeout: 4000,
|
|
26
|
-
// Maximum number of reconnection attempts before giving up
|
|
27
|
-
maxRetries: 5,
|
|
28
|
-
// Enable debug logging for connection events
|
|
29
|
-
debug: false,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Creates a generic WebSocket client with subscription management.
|
|
34
|
-
*
|
|
35
|
-
* The client handles:
|
|
36
|
-
* - Lazy connection lifecycle (connects on first subscribe, disconnects on last unsubscribe)
|
|
37
|
-
* - Subscription management via SubscriptionManager (reference counting, microtask batching)
|
|
38
|
-
* - Message parsing and routing to appropriate subscribers
|
|
39
|
-
*
|
|
40
|
-
* Consumers provide:
|
|
41
|
-
* - connectionStore: Manages connection state (status, connectionId, errors)
|
|
42
|
-
* - subscriptionHandler: REST API for subscribe/unsubscribe calls
|
|
43
|
-
* - parseMessage: Convert raw WS messages to typed messages
|
|
44
|
-
* - parseConnectionMessage: Extract connectionId from initial message
|
|
45
|
-
* - createSubscriptionKey: Create unique keys for subscriptions
|
|
46
|
-
*/
|
|
47
|
-
export function createWebSocketClient<TParams, TMessage>(
|
|
48
|
-
options: CreateWebSocketClientOptions<TParams, TMessage>,
|
|
49
|
-
): WebSocketClient<TParams, TMessage> {
|
|
50
|
-
const {
|
|
51
|
-
config,
|
|
52
|
-
connectionStore,
|
|
53
|
-
subscriptionHandler,
|
|
54
|
-
parseMessage,
|
|
55
|
-
parseConnectionMessage,
|
|
56
|
-
createSubscriptionKey,
|
|
57
|
-
onError,
|
|
58
|
-
onRawMessage,
|
|
59
|
-
sessionRefreshIntervalMs,
|
|
60
|
-
socketFactory = defaultSocketFactory,
|
|
61
|
-
} = options
|
|
62
|
-
|
|
63
|
-
const {
|
|
64
|
-
url,
|
|
65
|
-
maxReconnectionDelay = DEFAULT_CONFIG.maxReconnectionDelay,
|
|
66
|
-
connectionTimeout = DEFAULT_CONFIG.connectionTimeout,
|
|
67
|
-
maxRetries = DEFAULT_CONFIG.maxRetries,
|
|
68
|
-
debug = DEFAULT_CONFIG.debug,
|
|
69
|
-
} = config
|
|
70
|
-
|
|
71
|
-
// Internal state
|
|
72
|
-
let socket: WebSocketLike | null = null
|
|
73
|
-
const connectionCallbacks = new Set<(connectionId: string) => void>()
|
|
74
|
-
let wasConnected = false
|
|
75
|
-
let sessionRefreshTimer: ReturnType<typeof setInterval> | null = null
|
|
76
|
-
|
|
77
|
-
function startSessionRefreshTimer(): void {
|
|
78
|
-
stopSessionRefreshTimer()
|
|
79
|
-
if (sessionRefreshIntervalMs && subscriptionHandler.refreshSession) {
|
|
80
|
-
sessionRefreshTimer = setInterval(() => {
|
|
81
|
-
subscriptionManager.refreshSession().catch((error) => onError?.(error))
|
|
82
|
-
}, sessionRefreshIntervalMs)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function stopSessionRefreshTimer(): void {
|
|
87
|
-
if (sessionRefreshTimer !== null) {
|
|
88
|
-
clearInterval(sessionRefreshTimer)
|
|
89
|
-
sessionRefreshTimer = null
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Subscription manager handles all subscription logic
|
|
94
|
-
const subscriptionManager = new SubscriptionManager<TParams, TMessage>({
|
|
95
|
-
handler: subscriptionHandler,
|
|
96
|
-
createKey: createSubscriptionKey,
|
|
97
|
-
onError: (error) => onError?.(error),
|
|
98
|
-
onSubscriptionCountChange: (count): void => {
|
|
99
|
-
if (count > 0 && !socket) {
|
|
100
|
-
connect()
|
|
101
|
-
} else if (count === 0 && socket) {
|
|
102
|
-
disconnect()
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
function notifyStatusChange(status: ConnectionStatus): void {
|
|
108
|
-
connectionStore.setStatus(status)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function notifyConnectionEstablished(connectionId: string): void {
|
|
112
|
-
connectionStore.setConnectionId(connectionId)
|
|
113
|
-
for (const callback of connectionCallbacks) {
|
|
114
|
-
callback(connectionId)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function connect(): void {
|
|
119
|
-
if (socket) {
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
notifyStatusChange('connecting')
|
|
124
|
-
|
|
125
|
-
// Add jitter to prevent thundering herd on reconnect
|
|
126
|
-
const jitteredMinDelay = getDefaultJitteredDelay()
|
|
127
|
-
|
|
128
|
-
socket = socketFactory(url, {
|
|
129
|
-
maxReconnectionDelay,
|
|
130
|
-
minReconnectionDelay: jitteredMinDelay,
|
|
131
|
-
reconnectionDelayGrowFactor: 1.3,
|
|
132
|
-
connectionTimeout,
|
|
133
|
-
maxRetries,
|
|
134
|
-
debug,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Capture a reference to this socket so event handlers can detect
|
|
138
|
-
// stale events from a previous socket (e.g. during React Strict Mode
|
|
139
|
-
// cleanup/remount cycles where disconnect + reconnect race).
|
|
140
|
-
const thisSocket = socket
|
|
141
|
-
|
|
142
|
-
socket.addEventListener('open', () => {
|
|
143
|
-
if (socket !== thisSocket) {
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
wasConnected = true
|
|
147
|
-
startSessionRefreshTimer()
|
|
148
|
-
notifyStatusChange('connected')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
socket.addEventListener('close', () => {
|
|
152
|
-
if (socket !== thisSocket) {
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
stopSessionRefreshTimer()
|
|
156
|
-
// Ignore close events after intentional disconnect (socket already nulled)
|
|
157
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
158
|
-
if (!socket) {
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
if (wasConnected) {
|
|
162
|
-
notifyStatusChange('reconnecting')
|
|
163
|
-
} else {
|
|
164
|
-
notifyStatusChange('disconnected')
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
socket.addEventListener('error', () => {
|
|
169
|
-
if (socket !== thisSocket) {
|
|
170
|
-
return
|
|
171
|
-
}
|
|
172
|
-
onError?.(new Error('WebSocket error - check Network tab for details'))
|
|
173
|
-
connectionStore.setError(new Error('WebSocket error'))
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
socket.addEventListener('message', (event) => {
|
|
177
|
-
if (socket !== thisSocket) {
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
try {
|
|
181
|
-
const message: unknown = JSON.parse((event as { data: string }).data)
|
|
182
|
-
onRawMessage?.(message)
|
|
183
|
-
|
|
184
|
-
// Check for connection established message
|
|
185
|
-
const connectionInfo = parseConnectionMessage(message)
|
|
186
|
-
if (connectionInfo) {
|
|
187
|
-
const { connectionId } = connectionInfo
|
|
188
|
-
subscriptionManager.setConnectionId(connectionId)
|
|
189
|
-
notifyConnectionEstablished(connectionId)
|
|
190
|
-
|
|
191
|
-
// Always resubscribe if there are active subscriptions
|
|
192
|
-
// Covers both reconnect and initial connect with queued subs
|
|
193
|
-
if (subscriptionManager.hasActiveSubscriptions()) {
|
|
194
|
-
subscriptionManager.resubscribeAll(connectionId).catch((error) => onError?.(error))
|
|
195
|
-
}
|
|
196
|
-
return
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Try to parse as a subscription message
|
|
200
|
-
const parsed = parseMessage(message)
|
|
201
|
-
if (parsed) {
|
|
202
|
-
subscriptionManager.dispatch(parsed.key, parsed.data)
|
|
203
|
-
}
|
|
204
|
-
} catch (error) {
|
|
205
|
-
onError?.(error)
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function disconnect(): void {
|
|
211
|
-
if (socket) {
|
|
212
|
-
stopSessionRefreshTimer()
|
|
213
|
-
const s = socket
|
|
214
|
-
wasConnected = false
|
|
215
|
-
socket = null
|
|
216
|
-
s.close()
|
|
217
|
-
subscriptionManager.clear()
|
|
218
|
-
connectionStore.reset()
|
|
219
|
-
notifyStatusChange('disconnected')
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function isConnected(): boolean {
|
|
224
|
-
return socket?.readyState === WebSocket.OPEN
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function getConnectionStatus(): ConnectionStatus {
|
|
228
|
-
return connectionStore.getStatus()
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function getConnectionId(): string | null {
|
|
232
|
-
return connectionStore.getConnectionId()
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function subscribe(opts: SubscriptionOptions<TParams, TMessage>): () => void {
|
|
236
|
-
return subscriptionManager.subscribe({
|
|
237
|
-
channel: opts.channel,
|
|
238
|
-
params: opts.params,
|
|
239
|
-
callback: opts.onMessage,
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
|
|
244
|
-
return connectionStore.onStatusChange(callback)
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function onConnectionEstablished(callback: (connectionId: string) => void): () => void {
|
|
248
|
-
connectionCallbacks.add(callback)
|
|
249
|
-
return () => {
|
|
250
|
-
connectionCallbacks.delete(callback)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
isConnected,
|
|
256
|
-
getConnectionStatus,
|
|
257
|
-
getConnectionId,
|
|
258
|
-
subscribe,
|
|
259
|
-
onStatusChange,
|
|
260
|
-
onConnectionEstablished,
|
|
261
|
-
}
|
|
262
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// Main factory function
|
|
2
|
-
export { createWebSocketClient } from './client/createWebSocketClient'
|
|
3
|
-
export { createZustandConnectionStore } from './store/createZustandConnectionStore'
|
|
4
|
-
// Store types (for consumers who need to access connection state)
|
|
5
|
-
export type { ConnectionStore, CreateZustandConnectionStoreOptions } from './store/types'
|
|
6
|
-
// Additional exports for advanced use cases
|
|
7
|
-
export { SubscriptionManager } from './subscriptions/SubscriptionManager'
|
|
8
|
-
// Subscription types (for consumers building custom subscription logic)
|
|
9
|
-
export type { SubscribeInput, SubscriptionEntry, SubscriptionManagerOptions } from './subscriptions/types'
|
|
10
|
-
export type {
|
|
11
|
-
ConnectionConfig,
|
|
12
|
-
ConnectionStatus,
|
|
13
|
-
CreateWebSocketClientOptions,
|
|
14
|
-
SocketFactoryOptions,
|
|
15
|
-
SubscriptionHandler,
|
|
16
|
-
SubscriptionOptions,
|
|
17
|
-
WebSocketClient,
|
|
18
|
-
// For testing/mocking
|
|
19
|
-
WebSocketLike,
|
|
20
|
-
} from './types'
|
|
21
|
-
// Utility functions
|
|
22
|
-
export { addJitter, getDefaultJitteredDelay } from './utils/backoff'
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { createZustandConnectionStore } from '@luxexchange/websocket/src/store/createZustandConnectionStore'
|
|
2
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
|
|
4
|
-
describe('createZustandConnectionStore', () => {
|
|
5
|
-
describe('initial state', () => {
|
|
6
|
-
it('starts with disconnected status', () => {
|
|
7
|
-
const store = createZustandConnectionStore()
|
|
8
|
-
expect(store.getStatus()).toBe('disconnected')
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
it('starts with null connectionId', () => {
|
|
12
|
-
const store = createZustandConnectionStore()
|
|
13
|
-
expect(store.getConnectionId()).toBe(null)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('starts with null error', () => {
|
|
17
|
-
const store = createZustandConnectionStore()
|
|
18
|
-
expect(store.getError()).toBe(null)
|
|
19
|
-
})
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
describe('setStatus', () => {
|
|
23
|
-
it('updates status to connecting', () => {
|
|
24
|
-
const store = createZustandConnectionStore()
|
|
25
|
-
store.setStatus('connecting')
|
|
26
|
-
expect(store.getStatus()).toBe('connecting')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('updates status to connected', () => {
|
|
30
|
-
const store = createZustandConnectionStore()
|
|
31
|
-
store.setStatus('connected')
|
|
32
|
-
expect(store.getStatus()).toBe('connected')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('updates status to reconnecting', () => {
|
|
36
|
-
const store = createZustandConnectionStore()
|
|
37
|
-
store.setStatus('reconnecting')
|
|
38
|
-
expect(store.getStatus()).toBe('reconnecting')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('updates status to disconnected', () => {
|
|
42
|
-
const store = createZustandConnectionStore()
|
|
43
|
-
store.setStatus('connected')
|
|
44
|
-
store.setStatus('disconnected')
|
|
45
|
-
expect(store.getStatus()).toBe('disconnected')
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
describe('setConnectionId', () => {
|
|
50
|
-
it('sets connectionId', () => {
|
|
51
|
-
const store = createZustandConnectionStore()
|
|
52
|
-
store.setConnectionId('test-connection-123')
|
|
53
|
-
expect(store.getConnectionId()).toBe('test-connection-123')
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it('clears connectionId with null', () => {
|
|
57
|
-
const store = createZustandConnectionStore()
|
|
58
|
-
store.setConnectionId('test-connection-123')
|
|
59
|
-
store.setConnectionId(null)
|
|
60
|
-
expect(store.getConnectionId()).toBe(null)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('setError', () => {
|
|
65
|
-
it('sets error', () => {
|
|
66
|
-
const store = createZustandConnectionStore()
|
|
67
|
-
const error = new Error('Connection failed')
|
|
68
|
-
store.setError(error)
|
|
69
|
-
expect(store.getError()).toBe(error)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('clears error with null', () => {
|
|
73
|
-
const store = createZustandConnectionStore()
|
|
74
|
-
store.setError(new Error('Connection failed'))
|
|
75
|
-
store.setError(null)
|
|
76
|
-
expect(store.getError()).toBe(null)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('reset', () => {
|
|
81
|
-
it('resets all state to initial values', () => {
|
|
82
|
-
const store = createZustandConnectionStore()
|
|
83
|
-
|
|
84
|
-
// Modify all state
|
|
85
|
-
store.setStatus('connected')
|
|
86
|
-
store.setConnectionId('conn-123')
|
|
87
|
-
store.setError(new Error('test'))
|
|
88
|
-
|
|
89
|
-
// Reset
|
|
90
|
-
store.reset()
|
|
91
|
-
|
|
92
|
-
// Verify all reset
|
|
93
|
-
expect(store.getStatus()).toBe('disconnected')
|
|
94
|
-
expect(store.getConnectionId()).toBe(null)
|
|
95
|
-
expect(store.getError()).toBe(null)
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
describe('onStatusChange', () => {
|
|
100
|
-
it('calls callback when status changes', () => {
|
|
101
|
-
const store = createZustandConnectionStore()
|
|
102
|
-
const callback = vi.fn()
|
|
103
|
-
|
|
104
|
-
store.onStatusChange(callback)
|
|
105
|
-
store.setStatus('connecting')
|
|
106
|
-
|
|
107
|
-
expect(callback).toHaveBeenCalledWith('connecting')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('does not call callback when other state changes', () => {
|
|
111
|
-
const store = createZustandConnectionStore()
|
|
112
|
-
const callback = vi.fn()
|
|
113
|
-
|
|
114
|
-
store.onStatusChange(callback)
|
|
115
|
-
store.setConnectionId('conn-123')
|
|
116
|
-
store.setError(new Error('test'))
|
|
117
|
-
|
|
118
|
-
expect(callback).not.toHaveBeenCalled()
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('returns unsubscribe function', () => {
|
|
122
|
-
const store = createZustandConnectionStore()
|
|
123
|
-
const callback = vi.fn()
|
|
124
|
-
|
|
125
|
-
const unsubscribe = store.onStatusChange(callback)
|
|
126
|
-
store.setStatus('connecting')
|
|
127
|
-
expect(callback).toHaveBeenCalledTimes(1)
|
|
128
|
-
|
|
129
|
-
unsubscribe()
|
|
130
|
-
store.setStatus('connected')
|
|
131
|
-
expect(callback).toHaveBeenCalledTimes(1)
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('tracks multiple status transitions', () => {
|
|
135
|
-
const store = createZustandConnectionStore()
|
|
136
|
-
const statuses: string[] = []
|
|
137
|
-
|
|
138
|
-
store.onStatusChange((status) => statuses.push(status))
|
|
139
|
-
store.setStatus('connecting')
|
|
140
|
-
store.setStatus('connected')
|
|
141
|
-
store.setStatus('reconnecting')
|
|
142
|
-
store.setStatus('disconnected')
|
|
143
|
-
|
|
144
|
-
expect(statuses).toEqual(['connecting', 'connected', 'reconnecting', 'disconnected'])
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
describe('options', () => {
|
|
149
|
-
it('creates store without devtools by default', () => {
|
|
150
|
-
const store = createZustandConnectionStore()
|
|
151
|
-
expect(store).toBeDefined()
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('creates store with custom devtools name', () => {
|
|
155
|
-
const store = createZustandConnectionStore({
|
|
156
|
-
enableDevtools: true,
|
|
157
|
-
devtoolsName: 'customStoreName',
|
|
158
|
-
})
|
|
159
|
-
expect(store).toBeDefined()
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
})
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import type { ConnectionStore, CreateZustandConnectionStoreOptions } from '@luxexchange/websocket/src/store/types'
|
|
2
|
-
import type { ConnectionStatus } from '@luxexchange/websocket/src/types'
|
|
3
|
-
import { devtools } from 'zustand/middleware'
|
|
4
|
-
import { createStore } from 'zustand/vanilla'
|
|
5
|
-
|
|
6
|
-
interface ConnectionStoreState {
|
|
7
|
-
status: ConnectionStatus
|
|
8
|
-
connectionId: string | null
|
|
9
|
-
error: Error | null
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Creates a Zustand-backed ConnectionStore.
|
|
14
|
-
* This is a convenience implementation — consumers can provide any object
|
|
15
|
-
* that satisfies the ConnectionStore interface.
|
|
16
|
-
*/
|
|
17
|
-
export function createZustandConnectionStore(options?: CreateZustandConnectionStoreOptions): ConnectionStore {
|
|
18
|
-
const { enableDevtools = false, devtoolsName = 'websocketConnectionStore' } = options ?? {}
|
|
19
|
-
|
|
20
|
-
const store = createStore<ConnectionStoreState>()(
|
|
21
|
-
devtools(
|
|
22
|
-
() => ({
|
|
23
|
-
status: 'disconnected' as ConnectionStatus,
|
|
24
|
-
connectionId: null,
|
|
25
|
-
error: null,
|
|
26
|
-
}),
|
|
27
|
-
{
|
|
28
|
-
name: devtoolsName,
|
|
29
|
-
enabled: enableDevtools,
|
|
30
|
-
},
|
|
31
|
-
),
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
getStatus(): ConnectionStatus {
|
|
36
|
-
return store.getState().status
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
setStatus(status: ConnectionStatus): void {
|
|
40
|
-
store.setState({ status }, false, 'setStatus')
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
getConnectionId(): string | null {
|
|
44
|
-
return store.getState().connectionId
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
setConnectionId(id: string | null): void {
|
|
48
|
-
store.setState({ connectionId: id }, false, 'setConnectionId')
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
getError(): Error | null {
|
|
52
|
-
return store.getState().error
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
setError(error: Error | null): void {
|
|
56
|
-
store.setState({ error }, false, 'setError')
|
|
57
|
-
},
|
|
58
|
-
|
|
59
|
-
reset(): void {
|
|
60
|
-
store.setState({ status: 'disconnected', connectionId: null, error: null }, false, 'reset')
|
|
61
|
-
},
|
|
62
|
-
|
|
63
|
-
onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
|
|
64
|
-
let previousStatus = store.getState().status
|
|
65
|
-
return store.subscribe(() => {
|
|
66
|
-
const currentStatus = store.getState().status
|
|
67
|
-
if (currentStatus !== previousStatus) {
|
|
68
|
-
previousStatus = currentStatus
|
|
69
|
-
callback(currentStatus)
|
|
70
|
-
}
|
|
71
|
-
})
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/store/types.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type { ConnectionStatus } from '@luxexchange/websocket/src/types'
|
|
2
|
-
|
|
3
|
-
export interface ConnectionStore {
|
|
4
|
-
getStatus(): ConnectionStatus
|
|
5
|
-
setStatus(status: ConnectionStatus): void
|
|
6
|
-
getConnectionId(): string | null
|
|
7
|
-
setConnectionId(id: string | null): void
|
|
8
|
-
getError(): Error | null
|
|
9
|
-
setError(error: Error | null): void
|
|
10
|
-
reset(): void
|
|
11
|
-
onStatusChange(callback: (status: ConnectionStatus) => void): () => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface CreateZustandConnectionStoreOptions {
|
|
15
|
-
enableDevtools?: boolean
|
|
16
|
-
devtoolsName?: string
|
|
17
|
-
}
|