@luxexchange/websocket 1.0.1 → 1.0.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.
@@ -0,0 +1,113 @@
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
+ }
@@ -0,0 +1,281 @@
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
+ let disconnectTimer: ReturnType<typeof setTimeout> | null = null
77
+
78
+ function startSessionRefreshTimer(): void {
79
+ stopSessionRefreshTimer()
80
+ if (sessionRefreshIntervalMs && subscriptionHandler.refreshSession) {
81
+ sessionRefreshTimer = setInterval(() => {
82
+ subscriptionManager.refreshSession().catch((error) => onError?.(error))
83
+ }, sessionRefreshIntervalMs)
84
+ }
85
+ }
86
+
87
+ function stopSessionRefreshTimer(): void {
88
+ if (sessionRefreshTimer !== null) {
89
+ clearInterval(sessionRefreshTimer)
90
+ sessionRefreshTimer = null
91
+ }
92
+ }
93
+
94
+ // Subscription manager handles all subscription logic
95
+ const subscriptionManager = new SubscriptionManager<TParams, TMessage>({
96
+ handler: subscriptionHandler,
97
+ createKey: createSubscriptionKey,
98
+ onError: (error) => onError?.(error),
99
+ onSubscriptionCountChange: (count): void => {
100
+ if (count > 0) {
101
+ // Cancel any pending disconnect — new subscriptions arrived
102
+ if (disconnectTimer !== null) {
103
+ clearTimeout(disconnectTimer)
104
+ disconnectTimer = null
105
+ }
106
+ if (!socket) {
107
+ connect()
108
+ }
109
+ } else if (count === 0 && socket) {
110
+ // Debounce disconnect to bridge React cleanup→setup gaps during navigation
111
+ disconnectTimer = setTimeout(() => {
112
+ disconnectTimer = null
113
+ disconnect()
114
+ }, 2000)
115
+ }
116
+ },
117
+ })
118
+
119
+ function notifyStatusChange(status: ConnectionStatus): void {
120
+ connectionStore.setStatus(status)
121
+ }
122
+
123
+ function notifyConnectionEstablished(connectionId: string): void {
124
+ connectionStore.setConnectionId(connectionId)
125
+ for (const callback of connectionCallbacks) {
126
+ callback(connectionId)
127
+ }
128
+ }
129
+
130
+ function connect(): void {
131
+ if (socket) {
132
+ return
133
+ }
134
+
135
+ notifyStatusChange('connecting')
136
+
137
+ // Add jitter to prevent thundering herd on reconnect
138
+ const jitteredMinDelay = getDefaultJitteredDelay()
139
+
140
+ socket = socketFactory(url, {
141
+ maxReconnectionDelay,
142
+ minReconnectionDelay: jitteredMinDelay,
143
+ reconnectionDelayGrowFactor: 1.3,
144
+ connectionTimeout,
145
+ maxRetries,
146
+ debug,
147
+ })
148
+
149
+ // Capture a reference to this socket so event handlers can detect
150
+ // stale events from a previous socket (e.g. during React Strict Mode
151
+ // cleanup/remount cycles where disconnect + reconnect race).
152
+ const thisSocket = socket
153
+
154
+ socket.addEventListener('open', () => {
155
+ if (socket !== thisSocket) {
156
+ return
157
+ }
158
+ wasConnected = true
159
+ startSessionRefreshTimer()
160
+ notifyStatusChange('connected')
161
+ })
162
+
163
+ socket.addEventListener('close', () => {
164
+ if (socket !== thisSocket) {
165
+ return
166
+ }
167
+ stopSessionRefreshTimer()
168
+ // Ignore close events after intentional disconnect (socket already nulled)
169
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
170
+ if (!socket) {
171
+ return
172
+ }
173
+ if (wasConnected) {
174
+ notifyStatusChange('reconnecting')
175
+ } else {
176
+ notifyStatusChange('disconnected')
177
+ }
178
+ })
179
+
180
+ socket.addEventListener('error', () => {
181
+ if (socket !== thisSocket) {
182
+ return
183
+ }
184
+ onError?.(new Error('WebSocket error - check Network tab for details'))
185
+ connectionStore.setError(new Error('WebSocket error'))
186
+ })
187
+
188
+ socket.addEventListener('message', (event) => {
189
+ if (socket !== thisSocket) {
190
+ return
191
+ }
192
+ try {
193
+ const message: unknown = JSON.parse((event as { data: string }).data)
194
+ onRawMessage?.(message)
195
+
196
+ // Check for connection established message
197
+ const connectionInfo = parseConnectionMessage(message)
198
+ if (connectionInfo) {
199
+ const { connectionId } = connectionInfo
200
+ subscriptionManager.setConnectionId(connectionId)
201
+ notifyConnectionEstablished(connectionId)
202
+
203
+ // Always resubscribe if there are active subscriptions
204
+ // Covers both reconnect and initial connect with queued subs
205
+ if (subscriptionManager.hasActiveSubscriptions()) {
206
+ subscriptionManager.resubscribeAll(connectionId).catch((error) => onError?.(error))
207
+ }
208
+ return
209
+ }
210
+
211
+ // Try to parse as a subscription message
212
+ const parsed = parseMessage(message)
213
+ if (parsed) {
214
+ subscriptionManager.dispatch(parsed.key, parsed.data)
215
+ }
216
+ } catch (error) {
217
+ onError?.(error)
218
+ }
219
+ })
220
+ }
221
+
222
+ function disconnect(): void {
223
+ // Clear pending debounce timer first to prevent the timeout callback
224
+ // from re-entering disconnect() after we've already torn down.
225
+ const pendingTimer = disconnectTimer
226
+ disconnectTimer = null
227
+ if (pendingTimer !== null) {
228
+ clearTimeout(pendingTimer)
229
+ }
230
+ if (socket) {
231
+ stopSessionRefreshTimer()
232
+ const s = socket
233
+ wasConnected = false
234
+ socket = null
235
+ s.close()
236
+ subscriptionManager.clear()
237
+ connectionStore.reset()
238
+ notifyStatusChange('disconnected')
239
+ }
240
+ }
241
+
242
+ function isConnected(): boolean {
243
+ return socket?.readyState === WebSocket.OPEN
244
+ }
245
+
246
+ function getConnectionStatus(): ConnectionStatus {
247
+ return connectionStore.getStatus()
248
+ }
249
+
250
+ function getConnectionId(): string | null {
251
+ return connectionStore.getConnectionId()
252
+ }
253
+
254
+ function subscribe(opts: SubscriptionOptions<TParams, TMessage>): () => void {
255
+ return subscriptionManager.subscribe({
256
+ channel: opts.channel,
257
+ params: opts.params,
258
+ callback: opts.onMessage,
259
+ })
260
+ }
261
+
262
+ function onStatusChange(callback: (status: ConnectionStatus) => void): () => void {
263
+ return connectionStore.onStatusChange(callback)
264
+ }
265
+
266
+ function onConnectionEstablished(callback: (connectionId: string) => void): () => void {
267
+ connectionCallbacks.add(callback)
268
+ return () => {
269
+ connectionCallbacks.delete(callback)
270
+ }
271
+ }
272
+
273
+ return {
274
+ isConnected,
275
+ getConnectionStatus,
276
+ getConnectionId,
277
+ subscribe,
278
+ onStatusChange,
279
+ onConnectionEstablished,
280
+ }
281
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
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'
@@ -0,0 +1,162 @@
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
+ })
@@ -0,0 +1,74 @@
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
+ }
@@ -0,0 +1,17 @@
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
+ }