@gwakko/shared-websocket 0.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +381 -0
  3. package/dist/MessageBus.d.ts +20 -0
  4. package/dist/SharedSocket.d.ts +37 -0
  5. package/dist/SharedWebSocket.d.ts +45 -0
  6. package/dist/SubscriptionManager.d.ts +14 -0
  7. package/dist/TabCoordinator.d.ts +36 -0
  8. package/dist/WorkerSocket.d.ts +42 -0
  9. package/dist/adapters/index.d.ts +0 -0
  10. package/dist/adapters/react.d.ts +79 -0
  11. package/dist/adapters/vue.d.ts +53 -0
  12. package/dist/chunk-SMH3X34N.cjs +737 -0
  13. package/dist/chunk-SMH3X34N.cjs.map +1 -0
  14. package/dist/chunk-TNEMKPGP.js +737 -0
  15. package/dist/chunk-TNEMKPGP.js.map +1 -0
  16. package/dist/index.cjs +46 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.ts +8 -0
  19. package/dist/index.js +46 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/react.cjs +100 -0
  22. package/dist/react.cjs.map +1 -0
  23. package/dist/react.js +100 -0
  24. package/dist/react.js.map +1 -0
  25. package/dist/types.d.ts +27 -0
  26. package/dist/utils/backoff.d.ts +2 -0
  27. package/dist/utils/disposable.d.ts +0 -0
  28. package/dist/utils/id.d.ts +1 -0
  29. package/dist/vue.cjs +93 -0
  30. package/dist/vue.cjs.map +1 -0
  31. package/dist/vue.js +93 -0
  32. package/dist/vue.js.map +1 -0
  33. package/dist/withSocket.d.ts +51 -0
  34. package/dist/worker/socket.worker.d.ts +51 -0
  35. package/package.json +74 -0
  36. package/src/MessageBus.ts +112 -0
  37. package/src/SharedSocket.ts +183 -0
  38. package/src/SharedWebSocket.ts +225 -0
  39. package/src/SubscriptionManager.ts +86 -0
  40. package/src/TabCoordinator.ts +162 -0
  41. package/src/WorkerSocket.ts +149 -0
  42. package/src/adapters/index.ts +3 -0
  43. package/src/adapters/react.ts +189 -0
  44. package/src/adapters/vue.ts +149 -0
  45. package/src/index.ts +8 -0
  46. package/src/types.ts +29 -0
  47. package/src/utils/backoff.ts +9 -0
  48. package/src/utils/disposable.ts +4 -0
  49. package/src/utils/id.ts +6 -0
  50. package/src/withSocket.ts +89 -0
  51. package/src/worker/socket.worker.ts +205 -0
package/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gwakko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # Shared WebSocket
2
+
3
+ Share ONE WebSocket connection across multiple browser tabs. Zero dependencies. React and Vue adapters included.
4
+
5
+ ## Problem
6
+
7
+ 5 tabs open = 5 WebSocket connections = 5x server resources for the same user.
8
+
9
+ ## Solution
10
+
11
+ One tab becomes the **leader** (holds the WebSocket). Other tabs are **followers** (receive data via BroadcastChannel). If the leader closes — automatic election picks a new leader. Zero downtime.
12
+
13
+ ```
14
+ Tab 1 (Leader) Tab 2 (Follower) Tab 3 (Follower)
15
+ ┌──────────┐ ┌──────────┐ ┌──────────┐
16
+ │ WebSocket│ │ │ │ │
17
+ │ ↕ │ │ │ │ │
18
+ │ Server │ │ │ │ │
19
+ └────┬─────┘ └─────┬────┘ └─────┬────┘
20
+ └────── BroadcastChannel ──────────────────────┘
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ ### From npm
26
+
27
+ ```bash
28
+ npm install shared-websocket
29
+ ```
30
+
31
+ ### From GitHub (latest source)
32
+
33
+ ```bash
34
+ npm install github:Gwakko/shared-websocket
35
+ ```
36
+
37
+ ### Manual (copy into your project)
38
+
39
+ ```bash
40
+ # Clone and copy src/ into your project
41
+ git clone https://github.com/Gwakko/shared-websocket.git
42
+ cp -r shared-websocket/src ./your-project/shared-websocket
43
+ ```
44
+
45
+ ### Build from source
46
+
47
+ ```bash
48
+ git clone https://github.com/Gwakko/shared-websocket.git
49
+ cd shared-websocket
50
+ npm install
51
+ npm run build # outputs ESM + CJS + types to dist/
52
+ ```
53
+
54
+ ## Usage — Vanilla TypeScript
55
+
56
+ ```typescript
57
+ import { SharedWebSocket } from 'shared-websocket';
58
+
59
+ const ws = new SharedWebSocket('wss://api.example.com/ws', {
60
+ auth: () => localStorage.getItem('token')!,
61
+ useWorker: true, // optional: run WebSocket in Web Worker (offloads main thread)
62
+ });
63
+
64
+ await ws.connect();
65
+
66
+ // Subscribe to events (works in ALL tabs)
67
+ ws.on('order.created', (order) => {
68
+ console.log('New order:', order);
69
+ });
70
+
71
+ // Send message (auto-routed through leader tab)
72
+ ws.send('chat.message', { text: 'Hello!' });
73
+
74
+ // Generator streaming
75
+ for await (const msg of ws.stream('chat.messages')) {
76
+ console.log(msg);
77
+ }
78
+
79
+ // Sync state across tabs (no server roundtrip)
80
+ ws.sync('cart', { items: [1, 2, 3] });
81
+ ws.onSync('cart', (cart) => console.log('Cart updated:', cart));
82
+
83
+ // Cleanup
84
+ ws.disconnect();
85
+ ```
86
+
87
+ ### Scoped Lifecycle — `withSocket()`
88
+
89
+ Auto-creates, connects, and disposes. Guarantees cleanup even on errors.
90
+
91
+ ```typescript
92
+ import { withSocket } from 'shared-websocket';
93
+
94
+ // Basic
95
+ await withSocket('wss://api.example.com/ws', async ({ ws }) => {
96
+ ws.on('order.created', (order) => console.log(order));
97
+ await longRunningWork();
98
+ }); // auto-disposed here
99
+
100
+ // With auth
101
+ await withSocket('wss://api.example.com/ws', {
102
+ auth: () => localStorage.getItem('token')!,
103
+ }, async ({ ws, signal }) => {
104
+ for await (const msg of ws.stream('chat.messages', signal)) {
105
+ renderMessage(msg);
106
+ }
107
+ });
108
+
109
+ // Tab-to-tab sync via BroadcastChannel (no server roundtrip)
110
+ await withSocket('wss://api.example.com/ws', async ({ ws }) => {
111
+ // Send state to ALL tabs instantly
112
+ ws.sync('cart', { items: [1, 2, 3] });
113
+ ws.sync('theme', 'dark');
114
+ ws.sync('locale', 'en');
115
+
116
+ // Read synced state from other tabs
117
+ const cart = ws.getSync<Cart>('cart'); // { items: [1, 2, 3] }
118
+
119
+ // React to changes from other tabs
120
+ ws.onSync('cart', (cart) => {
121
+ updateCartBadge(cart.items.length);
122
+ });
123
+
124
+ ws.onSync('theme', (theme) => {
125
+ document.documentElement.setAttribute('data-theme', theme);
126
+ });
127
+ });
128
+
129
+ // Combine: server events + tab sync
130
+ await withSocket('wss://api.example.com/ws', {
131
+ auth: () => localStorage.getItem('token')!,
132
+ }, async ({ ws, signal }) => {
133
+ // Server events → update state → sync to all tabs
134
+ ws.on('order.status', (order) => {
135
+ ws.sync('activeOrder', order); // all tabs see the update
136
+ });
137
+
138
+ // One tab adds to cart → all tabs update
139
+ ws.onSync('cart', (cart) => renderCart(cart));
140
+
141
+ // Stream server messages with auto-cleanup
142
+ for await (const msg of ws.stream('chat.messages', signal)) {
143
+ renderMessage(msg);
144
+ }
145
+ });
146
+
147
+ // With external cancellation (AbortController)
148
+ const controller = new AbortController();
149
+ setTimeout(() => controller.abort(), 30_000);
150
+
151
+ await withSocket('wss://api.example.com/ws', {
152
+ signal: controller.signal,
153
+ }, async ({ ws, signal }) => {
154
+ ws.on('notifications', showToast);
155
+ await new Promise((_, reject) => signal.addEventListener('abort', reject));
156
+ });
157
+ ```
158
+
159
+ ## Usage — React
160
+
161
+ ```tsx
162
+ import {
163
+ SharedWebSocketProvider,
164
+ useSharedWebSocket,
165
+ useSocketEvent,
166
+ useSocketStream,
167
+ useSocketSync,
168
+ useSocketStatus,
169
+ } from 'shared-websocket/adapters/react';
170
+
171
+ // Provider accepts url and options as props
172
+ function App() {
173
+ return (
174
+ <SharedWebSocketProvider
175
+ url="wss://api.example.com/ws"
176
+ options={{
177
+ auth: () => localStorage.getItem('token')!,
178
+ useWorker: true,
179
+ }}
180
+ >
181
+ <Dashboard />
182
+ </SharedWebSocketProvider>
183
+ );
184
+ }
185
+
186
+ function Dashboard() {
187
+ const ws = useSharedWebSocket();
188
+
189
+ // Latest event value (reactive) — no need to pass ws, uses context
190
+ const order = useSocketEvent<Order>('order.created');
191
+
192
+ // Accumulated stream
193
+ const messages = useSocketStream<Message>('chat.message');
194
+
195
+ // Synced across tabs (no server roundtrip)
196
+ const [cart, setCart] = useSocketSync('cart', { items: [] });
197
+
198
+ // Connection status
199
+ const { connected, tabRole } = useSocketStatus();
200
+
201
+ return (
202
+ <div>
203
+ <p>Status: {connected ? 'Online' : 'Offline'} ({tabRole})</p>
204
+ {order && <p>Latest order: #{order.id}</p>}
205
+ <button onClick={() => ws.send('ping', {})}>Ping</button>
206
+ <button onClick={() => setCart({ items: [...cart.items, Date.now()] })}>
207
+ Add to cart ({cart.items.length})
208
+ </button>
209
+ </div>
210
+ );
211
+ }
212
+ ```
213
+
214
+ ## Usage — Vue 3
215
+
216
+ ```vue
217
+ <!-- main.ts -->
218
+ <script setup>
219
+ import { createApp } from 'vue';
220
+ import { createSharedWebSocketPlugin } from 'shared-websocket/adapters/vue';
221
+ import App from './App.vue';
222
+
223
+ const app = createApp(App);
224
+ app.use(createSharedWebSocketPlugin('wss://api.example.com/ws'));
225
+ app.mount('#app');
226
+ </script>
227
+ ```
228
+
229
+ ```vue
230
+ <!-- Dashboard.vue -->
231
+ <script setup lang="ts">
232
+ import {
233
+ useSharedWebSocket,
234
+ useSocketEvent,
235
+ useSocketStream,
236
+ useSocketSync,
237
+ useSocketStatus,
238
+ } from 'shared-websocket/adapters/vue';
239
+
240
+ const ws = useSharedWebSocket();
241
+
242
+ const order = useSocketEvent<Order>('order.created');
243
+ const messages = useSocketStream<Message>('chat.message');
244
+ const cart = useSocketSync('cart', { items: [] });
245
+ const { connected, tabRole } = useSocketStatus();
246
+
247
+ function addToCart() {
248
+ cart.value = { items: [...cart.value.items, Date.now()] };
249
+ }
250
+ </script>
251
+
252
+ <template>
253
+ <p>Status: {{ connected ? 'Online' : 'Offline' }} ({{ tabRole }})</p>
254
+ <p v-if="order">Latest order: #{{ order.id }}</p>
255
+ <button @click="ws.send('ping', {})">Ping</button>
256
+ <button @click="addToCart">Add to cart ({{ cart.items.length }})</button>
257
+ </template>
258
+ ```
259
+
260
+ ## API Reference
261
+
262
+ ### SharedWebSocket
263
+
264
+ | Method | Description |
265
+ |--------|-------------|
266
+ | `connect()` | Start leader election and connect |
267
+ | `on(event, handler)` | Subscribe to server events (all tabs) |
268
+ | `once(event, handler)` | Subscribe once |
269
+ | `off(event, handler?)` | Unsubscribe |
270
+ | `stream(event, signal?)` | AsyncGenerator for consuming events |
271
+ | `send(event, data)` | Send to server (routed through leader) |
272
+ | `request(event, data, timeout?)` | Request/response via server |
273
+ | `sync(key, value)` | Sync state across tabs |
274
+ | `getSync(key)` | Get synced value |
275
+ | `onSync(key, fn)` | Listen for sync changes |
276
+ | `disconnect()` | Close connection and cleanup |
277
+ | `[Symbol.dispose]()` | Cleanup (also called by `disconnect`) |
278
+
279
+ ### withSocket()
280
+
281
+ | Signature | Description |
282
+ |-----------|-------------|
283
+ | `withSocket(url, callback)` | Scoped lifecycle, auto-dispose |
284
+ | `withSocket(url, options, callback)` | With auth, signal, etc. |
285
+
286
+ Callback receives `{ ws, signal }` — destructure what you need. Signal aborts when scope exits.
287
+
288
+ ### Options
289
+
290
+ | Option | Type | Default | Description |
291
+ |--------|------|---------|-------------|
292
+ | `protocols` | `string[]` | `[]` | WebSocket subprotocols |
293
+ | `reconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
294
+ | `reconnectMaxDelay` | `number` | `30000` | Max reconnect backoff (ms) |
295
+ | `heartbeatInterval` | `number` | `30000` | Ping interval (ms) |
296
+ | `sendBuffer` | `number` | `100` | Max buffered messages during reconnect |
297
+ | `auth` | `() => string` | — | JWT token provider |
298
+ | **`useWorker`** | **`boolean`** | **`false`** | **Run WebSocket in Web Worker** |
299
+ | `workerUrl` | `string \| URL` | — | Custom worker URL (if useWorker) |
300
+ | `electionTimeout` | `number` | `200` | Leader election timeout (ms) |
301
+ | `leaderHeartbeat` | `number` | `2000` | Leader heartbeat interval (ms) |
302
+ | `leaderTimeout` | `number` | `5000` | Leader absence timeout (ms) |
303
+
304
+ ### Properties
305
+
306
+ | Property | Type | Description |
307
+ |----------|------|-------------|
308
+ | `connected` | `boolean` | Connection status |
309
+ | `tabRole` | `'leader' \| 'follower'` | Current tab's role |
310
+
311
+ ### React Hooks (React 19, uses `useEffectEvent` for stable refs)
312
+
313
+ | Hook | Returns | Description |
314
+ |------|---------|-------------|
315
+ | `useSharedWebSocket()` | `SharedWebSocket` | Access instance from context |
316
+ | `useSocketEvent<T>(event)` | `T \| undefined` | Latest event value |
317
+ | `useSocketStream<T>(event)` | `T[]` | Accumulated events |
318
+ | `useSocketSync<T>(key, init)` | `[T, setter]` | Cross-tab synced state |
319
+ | `useSocketStatus()` | `{ connected, tabRole }` | Connection status |
320
+
321
+ All hooks use context internally — no need to pass `ws` as argument.
322
+
323
+ ### Vue Composables
324
+
325
+ | Composable | Returns | Description |
326
+ |-----------|---------|-------------|
327
+ | `useSocketEvent<T>(event)` | `Ref<T>` | Latest event value |
328
+ | `useSocketStream<T>(event)` | `Ref<T[]>` | Accumulated events |
329
+ | `useSocketSync<T>(key, init)` | `Ref<T>` | Cross-tab synced state (two-way) |
330
+ | `useSocketStatus()` | `{ connected, tabRole }` | Reactive connection status |
331
+
332
+ ## How It Works
333
+
334
+ 1. **Leader Election** — new tab broadcasts election request via BroadcastChannel. If no rejection in 200ms → becomes leader. Leader sends heartbeat every 2s. No heartbeat for 5s → new election.
335
+
336
+ 2. **Message Flow** — follower calls `send()` → message goes to BroadcastChannel → leader picks it up → forwards to WebSocket → server response → leader broadcasts to all tabs.
337
+
338
+ 3. **Failover** — leader tab closes → `beforeunload` fires `abdicate` → followers detect missing heartbeat → election → new leader connects WebSocket → zero data loss (buffered messages replayed).
339
+
340
+ 4. **Resource Safety** — `withSocket()` for scoped lifecycle, `Symbol.dispose` support. All timers, listeners, and channels properly cleaned up.
341
+
342
+ 5. **Worker Mode** (optional) — `useWorker: true` runs WebSocket inside a Web Worker. JSON parsing, heartbeat timers, and reconnection logic run off main thread. UI stays responsive even at high message rates.
343
+
344
+ ## When to Use `useWorker: true`
345
+
346
+ | Scenario | useWorker | Why |
347
+ |----------|-----------|-----|
348
+ | Chat (10-50 msgs/sec) | `false` | Low overhead, not worth Worker complexity |
349
+ | Simple notifications | `false` | Few messages, main thread handles fine |
350
+ | Live trading feed (100+ msgs/sec) | **`true`** | JSON parsing 100+ msgs/sec blocks rendering |
351
+ | Real-time dashboard (50+ metrics/sec) | **`true`** | Continuous data stream, UI must stay smooth |
352
+ | Heavy payload (>100KB per message) | **`true`** | Parsing large JSON blocks main thread |
353
+ | Complex UI (React with 10k+ rows) | **`true`** | Main thread already busy, any extra work causes jank |
354
+ | Mobile / low-end devices | **`true`** | Less CPU available, offloading helps |
355
+ | Simple landing page | `false` | Minimal UI, no rendering pressure |
356
+ | SSR / Node.js | `false` | Workers are browser-only |
357
+ | Debugging | `false` | Worker DevTools is less convenient |
358
+
359
+ **Rule of thumb:** If your app drops frames when WebSocket messages arrive — add `useWorker: true`.
360
+
361
+ ```typescript
362
+ // Without worker (default) — WebSocket in main thread
363
+ const ws = new SharedWebSocket(url);
364
+
365
+ // With worker — WebSocket in Web Worker
366
+ const ws = new SharedWebSocket(url, { useWorker: true });
367
+
368
+ // API is identical — only internal transport changes
369
+ ```
370
+
371
+ ## Browser Support
372
+
373
+ | API | Chrome | Firefox | Safari | Edge |
374
+ |-----|--------|---------|--------|------|
375
+ | BroadcastChannel | 54+ | 38+ | 15.4+ | 79+ |
376
+ | Web Worker | ✅ | ✅ | ✅ | ✅ |
377
+ | AsyncGenerator | 63+ | 57+ | 12+ | 79+ |
378
+
379
+ ## License
380
+
381
+ MIT
@@ -0,0 +1,20 @@
1
+ import './utils/disposable';
2
+ import type { Unsubscribe } from './types';
3
+ export declare class MessageBus implements Disposable {
4
+ private readonly tabId;
5
+ private channel;
6
+ private listeners;
7
+ private pendingRequests;
8
+ constructor(channelName: string, tabId: string);
9
+ subscribe<T>(topic: string, fn: (data: T) => void): Unsubscribe;
10
+ publish<T>(topic: string, data: T): void;
11
+ broadcast<T>(topic: string, data: T): void;
12
+ request<T, R>(topic: string, data: T, timeout?: number): Promise<R>;
13
+ respond<T, R>(topic: string, fn: (data: T) => R | Promise<R>): Unsubscribe;
14
+ private handleMessage;
15
+ private postMessage;
16
+ private createMessage;
17
+ private addListener;
18
+ private removeListener;
19
+ [Symbol.dispose](): void;
20
+ }
@@ -0,0 +1,37 @@
1
+ import './utils/disposable';
2
+ import type { SocketState, Unsubscribe, EventHandler } from './types';
3
+ interface SharedSocketOptions {
4
+ protocols?: string[];
5
+ reconnect?: boolean;
6
+ reconnectMaxDelay?: number;
7
+ heartbeatInterval?: number;
8
+ sendBuffer?: number;
9
+ auth?: () => string | Promise<string>;
10
+ }
11
+ export declare class SharedSocket implements Disposable {
12
+ private url;
13
+ private ws;
14
+ private _state;
15
+ private buffer;
16
+ private disposed;
17
+ private heartbeatTimer;
18
+ private reconnectTimer;
19
+ private onMessageFns;
20
+ private onStateChangeFns;
21
+ private readonly opts;
22
+ constructor(url: string, options?: SharedSocketOptions);
23
+ get state(): SocketState;
24
+ connect(): Promise<void>;
25
+ disconnect(): void;
26
+ send(data: unknown): void;
27
+ onMessage(fn: EventHandler): Unsubscribe;
28
+ onStateChange(fn: (state: SocketState) => void): Unsubscribe;
29
+ private reconnect;
30
+ private flushBuffer;
31
+ private startHeartbeat;
32
+ private stopHeartbeat;
33
+ private clearReconnect;
34
+ private setState;
35
+ [Symbol.dispose](): void;
36
+ }
37
+ export {};
@@ -0,0 +1,45 @@
1
+ import './utils/disposable';
2
+ import type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler } from './types';
3
+ /**
4
+ * SharedWebSocket — shares ONE WebSocket connection across browser tabs.
5
+ *
6
+ * One tab becomes the "leader" and holds the WebSocket.
7
+ * Other tabs are "followers" receiving data via BroadcastChannel.
8
+ * If the leader closes, a new leader is elected automatically.
9
+ */
10
+ export declare class SharedWebSocket implements Disposable {
11
+ private readonly url;
12
+ private readonly options;
13
+ private bus;
14
+ private coordinator;
15
+ private socket;
16
+ private subs;
17
+ private syncStore;
18
+ private tabId;
19
+ private cleanups;
20
+ private disposed;
21
+ constructor(url: string, options?: SharedWebSocketOptions);
22
+ get connected(): boolean;
23
+ get tabRole(): TabRole;
24
+ /** Start leader election and connect. */
25
+ connect(): Promise<void>;
26
+ /** Subscribe to server events (works in ALL tabs). */
27
+ on(event: string, handler: EventHandler): Unsubscribe;
28
+ once(event: string, handler: EventHandler): Unsubscribe;
29
+ off(event: string, handler?: EventHandler): void;
30
+ /** Async generator for consuming events. */
31
+ stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown>;
32
+ /** Send message to server (auto-routed through leader). */
33
+ send(event: string, data: unknown): void;
34
+ /** Request/response through server via leader. */
35
+ request<T>(event: string, data: unknown, timeout?: number): Promise<T>;
36
+ /** Sync state across tabs (no server roundtrip). */
37
+ sync<T>(key: string, value: T): void;
38
+ getSync<T>(key: string): T | undefined;
39
+ onSync<T>(key: string, fn: (value: T) => void): Unsubscribe;
40
+ disconnect(): void;
41
+ private createSocket;
42
+ private onBecomeLeader;
43
+ private onLoseLeadership;
44
+ [Symbol.dispose](): void;
45
+ }
@@ -0,0 +1,14 @@
1
+ import './utils/disposable';
2
+ import type { EventHandler, Unsubscribe } from './types';
3
+ export declare class SubscriptionManager implements Disposable {
4
+ private handlers;
5
+ private lastMessages;
6
+ on(event: string, handler: EventHandler): Unsubscribe;
7
+ once(event: string, handler: EventHandler): Unsubscribe;
8
+ off(event: string, handler?: EventHandler): void;
9
+ emit(event: string, data: unknown): void;
10
+ getLastMessage(event: string): unknown | undefined;
11
+ stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown>;
12
+ offAll(): void;
13
+ [Symbol.dispose](): void;
14
+ }
@@ -0,0 +1,36 @@
1
+ import './utils/disposable';
2
+ import { MessageBus } from './MessageBus';
3
+ import type { Unsubscribe } from './types';
4
+ interface CoordinatorOptions {
5
+ electionTimeout?: number;
6
+ heartbeatInterval?: number;
7
+ leaderTimeout?: number;
8
+ }
9
+ export declare class TabCoordinator implements Disposable {
10
+ private readonly bus;
11
+ private readonly tabId;
12
+ private _isLeader;
13
+ private heartbeatTimer;
14
+ private leaderCheckTimer;
15
+ private lastHeartbeat;
16
+ private disposed;
17
+ private onBecomeLeaderFns;
18
+ private onLoseLeadershipFns;
19
+ private cleanups;
20
+ private readonly electionTimeout;
21
+ private readonly heartbeatInterval;
22
+ private readonly leaderTimeout;
23
+ constructor(bus: MessageBus, tabId: string, options?: CoordinatorOptions);
24
+ get isLeader(): boolean;
25
+ elect(): Promise<void>;
26
+ abdicate(): void;
27
+ onBecomeLeader(fn: () => void): Unsubscribe;
28
+ onLoseLeadership(fn: () => void): Unsubscribe;
29
+ private becomeLeader;
30
+ private startHeartbeat;
31
+ private stopHeartbeat;
32
+ private startLeaderCheck;
33
+ private stopLeaderCheck;
34
+ [Symbol.dispose](): void;
35
+ }
36
+ export {};
@@ -0,0 +1,42 @@
1
+ import './utils/disposable';
2
+ import type { SocketState, Unsubscribe, EventHandler } from './types';
3
+ /**
4
+ * WorkerSocket — WebSocket running inside a Web Worker.
5
+ *
6
+ * Same interface as SharedSocket, but WebSocket lives off main thread.
7
+ * Benefits: heartbeat timers and JSON parsing don't block UI rendering.
8
+ *
9
+ * Use when:
10
+ * - High message rate (50+ msgs/sec)
11
+ * - Heavy JSON payloads
12
+ * - UI does complex rendering that could block main thread
13
+ *
14
+ * Don't use when:
15
+ * - Low message rate (simple chat, notifications)
16
+ * - Bundle size matters (adds worker file)
17
+ * - Debugging (Worker DevTools is less convenient)
18
+ */
19
+ export declare class WorkerSocket implements Disposable {
20
+ private url;
21
+ private options;
22
+ private worker;
23
+ private _state;
24
+ private onMessageFns;
25
+ private onStateChangeFns;
26
+ constructor(url: string, options?: {
27
+ protocols?: string[];
28
+ reconnect?: boolean;
29
+ reconnectMaxDelay?: number;
30
+ heartbeatInterval?: number;
31
+ sendBuffer?: number;
32
+ workerUrl?: string | URL;
33
+ });
34
+ get state(): SocketState;
35
+ connect(): void;
36
+ send(data: unknown): void;
37
+ disconnect(): void;
38
+ onMessage(fn: EventHandler): Unsubscribe;
39
+ onStateChange(fn: (state: SocketState) => void): Unsubscribe;
40
+ private createWorkerBlob;
41
+ [Symbol.dispose](): void;
42
+ }
File without changes
@@ -0,0 +1,79 @@
1
+ import { type ReactNode } from 'react';
2
+ import { SharedWebSocket } from '../SharedWebSocket';
3
+ import type { SharedWebSocketOptions, TabRole } from '../types';
4
+ /**
5
+ * Provider props — pass URL and options as props for flexibility.
6
+ *
7
+ * @example
8
+ * <SharedWebSocketProvider url="wss://api.example.com/ws" options={{ auth: getToken }}>
9
+ * <App />
10
+ * </SharedWebSocketProvider>
11
+ */
12
+ export interface SharedWebSocketProviderProps {
13
+ url: string;
14
+ options?: SharedWebSocketOptions;
15
+ children: ReactNode;
16
+ }
17
+ /**
18
+ * Provider component — creates SharedWebSocket from props, auto-disposes on unmount.
19
+ *
20
+ * @example
21
+ * function App() {
22
+ * return (
23
+ * <SharedWebSocketProvider
24
+ * url="wss://api.example.com/ws"
25
+ * options={{
26
+ * auth: () => localStorage.getItem('token')!,
27
+ * useWorker: true,
28
+ * }}
29
+ * >
30
+ * <Dashboard />
31
+ * </SharedWebSocketProvider>
32
+ * );
33
+ * }
34
+ */
35
+ export declare function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SharedWebSocket | null>>;
36
+ /**
37
+ * Access the SharedWebSocket instance from context.
38
+ *
39
+ * @example
40
+ * const ws = useSharedWebSocket();
41
+ * ws.send('chat.message', { text: 'Hello' });
42
+ */
43
+ export declare function useSharedWebSocket(): SharedWebSocket;
44
+ /**
45
+ * Subscribe to a WebSocket event. Returns the latest received value.
46
+ * Uses useEffectEvent for a stable callback ref — no stale closures.
47
+ *
48
+ * @example
49
+ * const order = useSocketEvent<Order>('order.created');
50
+ */
51
+ export declare function useSocketEvent<T>(event: string): T | undefined;
52
+ /**
53
+ * Accumulate WebSocket events into an array.
54
+ * Uses useEffectEvent — handler always sees latest state without re-subscribing.
55
+ *
56
+ * @example
57
+ * const messages = useSocketStream<ChatMessage>('chat.message');
58
+ */
59
+ export declare function useSocketStream<T>(event: string): T[];
60
+ /**
61
+ * Two-way state sync across browser tabs.
62
+ * Uses useEffectEvent for stable sync callback.
63
+ *
64
+ * @example
65
+ * const [cart, setCart] = useSocketSync<Cart>('cart', { items: [] });
66
+ * // setCart in one tab → updates all tabs instantly
67
+ */
68
+ export declare function useSocketSync<T>(key: string, initialValue: T): [T, (value: T) => void];
69
+ /**
70
+ * Reactive connection status.
71
+ * Uses useEffectEvent to avoid re-creating interval on state change.
72
+ *
73
+ * @example
74
+ * const { connected, tabRole } = useSocketStatus();
75
+ */
76
+ export declare function useSocketStatus(): {
77
+ connected: boolean;
78
+ tabRole: TabRole;
79
+ };