@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxexchange/websocket",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "dependencies": {
5
5
  "partysocket": "1.1.10",
6
6
  "zustand": "5.0.6"
@@ -8,7 +8,7 @@
8
8
  "devDependencies": {
9
9
  "@types/node": "22.13.1",
10
10
  "@typescript/native-preview": "7.0.0-dev.20260108.1",
11
- "@luxfi/eslint-config": "^1.0.0",
11
+ "@luxfi/eslint-config": "workspace:^",
12
12
  "@vitest/coverage-v8": "3.2.1",
13
13
  "depcheck": "1.4.7",
14
14
  "eslint": "8.57.1",
package/.depcheckrc DELETED
@@ -1,15 +0,0 @@
1
- ignores: [
2
- # Dependencies that depcheck incorrectly marks as unused
3
- "typescript",
4
- "@typescript/native-preview",
5
- "depcheck",
6
-
7
- # Test dependencies (used by vitest config)
8
- "@vitest/coverage-v8",
9
-
10
- # Core dependencies (will be used when implementation is added)
11
- "partysocket",
12
-
13
- # Internal packages / workspaces
14
- "@universe/websocket",
15
- ]
package/.eslintrc.js DELETED
@@ -1,20 +0,0 @@
1
- module.exports = {
2
- extends: ['@luxfi/eslint-config/lib'],
3
- parserOptions: {
4
- tsconfigRootDir: __dirname,
5
- },
6
- overrides: [
7
- {
8
- files: ['*.ts', '*.tsx'],
9
- rules: {
10
- 'no-relative-import-paths/no-relative-import-paths': [
11
- 'error',
12
- {
13
- allowSameFolder: false,
14
- prefix: '@universe/websocket',
15
- },
16
- ],
17
- },
18
- },
19
- ],
20
- }
package/README.md DELETED
@@ -1,275 +0,0 @@
1
- # @universe/websocket
2
-
3
- A generic, type-safe WebSocket client with built-in subscription management, automatic reconnection, and reference counting.
4
-
5
- ## Features
6
-
7
- - **Lazy connection lifecycle** - Connects on first subscribe, disconnects on last unsubscribe
8
- - **Microtask batching** - Coalesces subscribe/unsubscribe calls within the same microtask via `queueMicrotask`
9
- - **Subscription deduplication** - Multiple subscribers to the same params share one subscription via reference counting
10
- - **Auto-resubscribe** - Automatically resubscribes all active subscriptions after reconnection
11
- - **Message routing** - Routes incoming messages to appropriate subscriber callbacks
12
- - **Type-safe** - Full TypeScript generics for params and message types
13
- - **Framework agnostic** - Works with any framework; consumers provide their own REST handlers
14
-
15
- ## Installation
16
-
17
- ```bash
18
- bun add @universe/websocket
19
- ```
20
-
21
- ## Quick Start
22
-
23
- ```typescript
24
- import { createWebSocketClient } from '@universe/websocket'
25
-
26
- // Define your param and message types
27
- interface PriceParams {
28
- tokenAddress: string
29
- chainId: number
30
- }
31
-
32
- interface PriceMessage {
33
- price: number
34
- timestamp: number
35
- }
36
-
37
- // Create the client
38
- const client = createWebSocketClient<PriceParams, PriceMessage>({
39
- config: {
40
- url: 'wss://api.example.com/ws',
41
- },
42
- subscriptionHandler: {
43
- subscribe: async (connectionId, params) => {
44
- await fetch('/api/subscribe', {
45
- method: 'POST',
46
- body: JSON.stringify({ connectionId, ...params }),
47
- })
48
- },
49
- unsubscribe: async (connectionId, params) => {
50
- await fetch('/api/unsubscribe', {
51
- method: 'POST',
52
- body: JSON.stringify({ connectionId, ...params }),
53
- })
54
- },
55
- // Optional: batch subscribe/unsubscribe for efficiency
56
- subscribeBatch: async (connectionId, paramsList) => {
57
- await fetch('/api/subscribe-batch', {
58
- method: 'POST',
59
- body: JSON.stringify({ connectionId, subscriptions: paramsList }),
60
- })
61
- },
62
- unsubscribeBatch: async (connectionId, paramsList) => {
63
- await fetch('/api/unsubscribe-batch', {
64
- method: 'POST',
65
- body: JSON.stringify({ connectionId, subscriptions: paramsList }),
66
- })
67
- },
68
- },
69
- parseConnectionMessage: (raw) => {
70
- if (raw && typeof raw === 'object' && 'connectionId' in raw) {
71
- return { connectionId: raw.connectionId as string }
72
- }
73
- return null
74
- },
75
- parseMessage: (raw) => {
76
- if (raw && typeof raw === 'object' && 'channel' in raw && 'data' in raw) {
77
- const msg = raw as { channel: string; tokenAddress: string; chainId: number; data: PriceMessage }
78
- return {
79
- channel: msg.channel,
80
- key: `${msg.channel}:${msg.tokenAddress}:${msg.chainId}`,
81
- data: msg.data,
82
- }
83
- }
84
- return null
85
- },
86
- createSubscriptionKey: (channel, params) => `${channel}:${params.tokenAddress}:${params.chainId}`,
87
- })
88
-
89
- // Subscribe — connection opens automatically on first subscribe
90
- const unsubscribe = client.subscribe({
91
- channel: 'prices',
92
- params: { tokenAddress: '0x...', chainId: 1 },
93
- onMessage: (message) => {
94
- console.log('Price update:', message.price)
95
- },
96
- })
97
-
98
- // Later: unsubscribe — connection closes automatically when last subscriber leaves
99
- unsubscribe()
100
- ```
101
-
102
- ## Architecture
103
-
104
- ```
105
- ┌─────────────────────────────────────────────────────────────┐
106
- │ WebSocketClient │
107
- │ ┌─────────────────┐ ┌─────────────────────────────────┐ │
108
- │ │ PartySocket │ │ SubscriptionManager │ │
109
- │ │ (connection) │ │ - Reference counting │ │
110
- │ │ │ │ - Microtask batching │ │
111
- │ │ │ │ - Auto-resubscribe │ │
112
- │ │ │ │ - Message dispatch │ │
113
- │ └─────────────────┘ └─────────────────────────────────┘ │
114
- │ ┌─────────────────────────────────────────────────────┐ │
115
- │ │ ConnectionStore (Zustand) │ │
116
- │ │ - status: disconnected|connecting|connected|... │ │
117
- │ │ - connectionId: string | null │ │
118
- │ │ - error: Error | null │ │
119
- │ └─────────────────────────────────────────────────────┘ │
120
- └─────────────────────────────────────────────────────────────┘
121
-
122
-
123
- Consumer-provided handlers
124
- (REST subscribe/unsubscribe)
125
- ```
126
-
127
- ## Lazy Connection Lifecycle
128
-
129
- The client manages its WebSocket connection automatically:
130
-
131
- 1. **First subscribe** - Opens WebSocket connection
132
- 2. **Connection established** - Receives connectionId, resubscribes all queued subscriptions
133
- 3. **Last unsubscribe** - Closes WebSocket connection and cleans up state
134
-
135
- There are no `connect()` or `disconnect()` methods — the connection lifecycle is driven entirely by subscription activity.
136
-
137
- ## API Reference
138
-
139
- ### `createWebSocketClient<TParams, TMessage>(options)`
140
-
141
- Creates a new WebSocket client instance.
142
-
143
- #### Options
144
-
145
- | Option | Type | Required | Description |
146
- |--------|------|----------|-------------|
147
- | `config` | `ConnectionConfig` | Yes | Connection configuration |
148
- | `subscriptionHandler` | `SubscriptionHandler<TParams>` | Yes | REST API handlers for subscribe/unsubscribe |
149
- | `parseMessage` | `(raw: unknown) => { channel, key, data } \| null` | Yes | Parse incoming WebSocket messages |
150
- | `parseConnectionMessage` | `(raw: unknown) => { connectionId } \| null` | Yes | Parse connection established message |
151
- | `createSubscriptionKey` | `(channel, params) => string` | Yes | Create unique key for subscription deduplication |
152
- | `onError` | `(error: unknown) => void` | No | Error callback |
153
- | `onRawMessage` | `(message: unknown) => void` | No | Raw message callback (for external caching or debugging) |
154
-
155
- #### SubscriptionHandler
156
-
157
- | Method | Type | Required | Description |
158
- |--------|------|----------|-------------|
159
- | `subscribe` | `(connectionId, params) => Promise<void>` | Yes | Subscribe to a single channel |
160
- | `unsubscribe` | `(connectionId, params) => Promise<void>` | Yes | Unsubscribe from a single channel |
161
- | `subscribeBatch` | `(connectionId, params[]) => Promise<void>` | No | Subscribe to multiple channels at once |
162
- | `unsubscribeBatch` | `(connectionId, params[]) => Promise<void>` | No | Unsubscribe from multiple channels at once |
163
- | `refreshSession` | `(connectionId) => Promise<void>` | No | Refresh the session |
164
-
165
- When `subscribeBatch`/`unsubscribeBatch` are provided, batched calls use them. Otherwise, individual `subscribe`/`unsubscribe` calls are made for each param in the batch.
166
-
167
- #### ConnectionConfig
168
-
169
- | Option | Type | Default | Description |
170
- |--------|------|---------|-------------|
171
- | `url` | `string` | - | WebSocket URL |
172
- | `maxReconnectionDelay` | `number` | `10000` | Maximum delay (ms) between reconnection attempts |
173
- | `minReconnectionDelay` | `number` | `1000` | Minimum delay (ms) before jitter is applied |
174
- | `connectionTimeout` | `number` | `4000` | Time (ms) to wait for connection |
175
- | `maxRetries` | `number` | `5` | Maximum reconnection attempts |
176
- | `debug` | `boolean` | `false` | Enable debug logging |
177
-
178
- #### Returns: `WebSocketClient<TParams, TMessage>`
179
-
180
- | Method | Description |
181
- |--------|-------------|
182
- | `isConnected()` | Check if currently connected |
183
- | `getConnectionStatus()` | Get current status: `'disconnected' \| 'connecting' \| 'connected' \| 'reconnecting'` |
184
- | `getConnectionId()` | Get current connection ID or null |
185
- | `subscribe(options)` | Subscribe to a channel, returns unsubscribe function |
186
- | `onStatusChange(callback)` | Listen to status changes, returns cleanup function |
187
- | `onConnectionEstablished(callback)` | Listen to connection events, returns cleanup function |
188
-
189
- #### SubscriptionOptions
190
-
191
- | Option | Type | Required | Description |
192
- |--------|------|----------|-------------|
193
- | `channel` | `string` | Yes | Channel name |
194
- | `params` | `TParams` | Yes | Subscription parameters |
195
- | `onMessage` | `(message: TMessage) => void` | No | Callback for incoming messages. Omit when using `onRawMessage` for external cache population. |
196
-
197
- ### `SubscriptionManager<TParams, TMessage>`
198
-
199
- Lower-level subscription manager with reference counting and microtask batching. Used internally by `createWebSocketClient`, but can be used directly for custom implementations.
200
-
201
- ```typescript
202
- import { SubscriptionManager } from '@universe/websocket'
203
-
204
- const manager = new SubscriptionManager<MyParams, MyMessage>({
205
- handler: subscriptionHandler,
206
- createKey: (channel, params) => `${channel}:${params.id}`,
207
- onError: (error, operation) => console.error(operation, error),
208
- onSubscriptionCountChange: (count) => {
209
- // React to subscription count changes (e.g., lazy connect/disconnect)
210
- },
211
- })
212
- ```
213
-
214
- ## Microtask Batching
215
-
216
- Subscribe and unsubscribe calls are batched within the same microtask using `queueMicrotask`:
217
-
218
- ```typescript
219
- // These three subscribes are coalesced into a single subscribeBatch call
220
- const unsub1 = client.subscribe({ channel: 'prices', params: paramsA, onMessage: handleA })
221
- const unsub2 = client.subscribe({ channel: 'prices', params: paramsB, onMessage: handleB })
222
- const unsub3 = client.subscribe({ channel: 'events', params: paramsC, onMessage: handleC })
223
- // ^ After the microtask flushes: one subscribeBatch([paramsA, paramsB, paramsC]) call
224
-
225
- // Subscribe + immediate unsubscribe in the same microtask = net-zero API calls
226
- const unsub = client.subscribe({ channel: 'prices', params: paramsA, onMessage: handle })
227
- unsub()
228
- // ^ The pending subscribe and unsubscribe cancel each other out — no REST calls made
229
- ```
230
-
231
- ## How Reference Counting Works
232
-
233
- When multiple components subscribe to the same params:
234
-
235
- 1. **First subscriber** - Queues REST subscribe API call
236
- 2. **Additional subscribers** - Just adds callback, no REST call
237
- 3. **Subscriber leaves** - Removes callback
238
- 4. **Last subscriber leaves** - Queues REST unsubscribe API call
239
-
240
- ```typescript
241
- // Component A subscribes - REST subscribe queued
242
- const unsubA = client.subscribe({ channel: 'prices', params, onMessage: handleA })
243
-
244
- // Component B subscribes to same params - no REST call, shares subscription
245
- const unsubB = client.subscribe({ channel: 'prices', params, onMessage: handleB })
246
-
247
- // Component A unsubscribes - just removes callback
248
- unsubA()
249
-
250
- // Component B unsubscribes - REST unsubscribe queued (last subscriber)
251
- unsubB()
252
- ```
253
-
254
- ## Reconnection Behavior
255
-
256
- The client uses jittered exponential backoff to prevent thundering herd:
257
-
258
- 1. Connection drops -> status becomes `'reconnecting'`
259
- 2. Waits `minDelay + random(0, 4000)ms` before first attempt
260
- 3. Each subsequent attempt multiplies delay by 1.3x (up to `maxDelay`)
261
- 4. On successful reconnect, automatically resubscribes all active subscriptions
262
- 5. After `maxRetries` failures, stops attempting
263
-
264
- ## Development
265
-
266
- ```bash
267
- # Run tests
268
- bun websocket test
269
-
270
- # Type check
271
- bun websocket typecheck
272
-
273
- # Lint
274
- bun websocket lint:fix
275
- ```
@@ -1,54 +0,0 @@
1
- import type { WebSocketLike } from '@luxexchange/websocket/src/types'
2
-
3
- type EventHandler = (event: unknown) => void
4
-
5
- /**
6
- * Mock WebSocket implementation for testing.
7
- * Simulates WebSocket behavior without actual network connections.
8
- */
9
- export class MockWebSocket implements WebSocketLike {
10
- readyState: number = WebSocket.CONNECTING
11
-
12
- private listeners = new Map<string, Set<EventHandler>>()
13
-
14
- addEventListener(event: string, handler: EventHandler): void {
15
- if (!this.listeners.has(event)) {
16
- this.listeners.set(event, new Set())
17
- }
18
- this.listeners.get(event)?.add(handler)
19
- }
20
-
21
- close(): void {
22
- this.readyState = WebSocket.CLOSED
23
- this.emit('close', { code: 1000, reason: 'Normal closure' })
24
- }
25
-
26
- // Test helpers - simulate server behavior
27
-
28
- simulateOpen(): void {
29
- this.readyState = WebSocket.OPEN
30
- this.emit('open', {})
31
- }
32
-
33
- simulateClose(code = 1000, reason = 'Connection closed'): void {
34
- this.readyState = WebSocket.CLOSED
35
- this.emit('close', { code, reason })
36
- }
37
-
38
- simulateError(message = 'WebSocket error'): void {
39
- this.emit('error', { message })
40
- }
41
-
42
- simulateMessage(data: unknown): void {
43
- this.emit('message', { data: JSON.stringify(data) })
44
- }
45
-
46
- private emit(event: string, payload: unknown): void {
47
- const handlers = this.listeners.get(event)
48
- if (handlers) {
49
- for (const handler of handlers) {
50
- handler(payload)
51
- }
52
- }
53
- }
54
- }