tab-bridge 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.
package/README.md ADDED
@@ -0,0 +1,834 @@
1
+ <div align="center">
2
+
3
+ <br />
4
+
5
+ <h1>
6
+ <code>๐Ÿ”„ tab-bridge</code>
7
+ </h1>
8
+
9
+ <h3>Real-time State Synchronization Across Browser Tabs</h3>
10
+
11
+ <p>
12
+ <strong>One function call. Every tab in sync. Zero dependencies.</strong>
13
+ </p>
14
+
15
+ <br />
16
+
17
+ [![npm version](https://img.shields.io/npm/v/tab-bridge?style=for-the-badge&color=cb3837&label=npm&logo=npm&logoColor=white)](https://www.npmjs.com/package/tab-bridge)
18
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/tab-bridge?style=for-the-badge&color=6ead0a&label=size&logo=webpack&logoColor=white)](https://bundlephobia.com/package/tab-bridge)
19
+ [![TypeScript](https://img.shields.io/badge/TypeScript-first-3178c6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
20
+ [![license](https://img.shields.io/github/license/serbi2012/tab-sync?style=for-the-badge&color=blue&logo=open-source-initiative&logoColor=white)](./LICENSE)
21
+ [![GitHub stars](https://img.shields.io/github/stars/serbi2012/tab-sync?style=for-the-badge&color=yellow&logo=github&logoColor=white)](https://github.com/serbi2012/tab-sync)
22
+
23
+ <br />
24
+
25
+ ```mermaid
26
+ graph LR
27
+ A["<b>Tab A</b><br/>๐Ÿ‘‘ Leader"] <-->|"realtime sync"| B["<b>Tab B</b><br/>Follower"]
28
+ B <-->|"realtime sync"| C["<b>Tab C</b><br/>Follower"]
29
+ A <-->|"realtime sync"| C
30
+
31
+ style A fill:#4f46e5,stroke:#4338ca,color:#fff,stroke-width:2px
32
+ style B fill:#6366f1,stroke:#4f46e5,color:#fff,stroke-width:2px
33
+ style C fill:#6366f1,stroke:#4f46e5,color:#fff,stroke-width:2px
34
+ ```
35
+
36
+ <br />
37
+
38
+ [**Getting Started**](#-getting-started) ยท [**API**](#-api-reference) ยท [**React**](#%EF%B8%8F-react) ยท [**Architecture**](#-architecture) ยท [**Examples**](#-examples)
39
+
40
+ </div>
41
+
42
+ <br />
43
+
44
+ ## Why tab-bridge?
45
+
46
+ > When users open your app in multiple tabs, things break โ€” **stale data**, **duplicated WebSocket connections**, **conflicting writes**.
47
+
48
+ **tab-bridge** solves all of this with a single function call:
49
+
50
+ ```ts
51
+ const sync = createTabSync({ initial: { theme: 'light', count: 0 } });
52
+ ```
53
+
54
+ Every tab now shares the same state. One tab is automatically elected as leader. You can call functions across tabs like they're local. **No server needed.**
55
+
56
+ <br />
57
+
58
+ ### โœจ Feature Highlights
59
+
60
+ <table>
61
+ <tr>
62
+ <td width="50%" valign="top">
63
+
64
+ #### โšก State Sync
65
+ LWW conflict resolution with batched broadcasts and custom merge strategies
66
+
67
+ #### ๐Ÿ‘‘ Leader Election
68
+ Bully algorithm with heartbeat monitoring and automatic failover
69
+
70
+ #### ๐Ÿ“ก Cross-Tab RPC
71
+ Fully typed arguments, Promise-based calls with timeout handling
72
+
73
+ #### โš›๏ธ React Hooks
74
+ Built on `useSyncExternalStore` for zero-tear concurrent rendering
75
+
76
+ </td>
77
+ <td width="50%" valign="top">
78
+
79
+ #### ๐Ÿ›ก๏ธ Middleware Pipeline
80
+ Intercept, validate, and transform state changes before they're applied
81
+
82
+ #### ๐Ÿ’พ State Persistence
83
+ Survive page reloads with key whitelisting and custom storage backends
84
+
85
+ #### ๐Ÿ”’ End-to-End Type Safety
86
+ Discriminated unions, full type inference, and generic constraints
87
+
88
+ #### ๐Ÿ“ฆ Zero Dependencies
89
+ Native browser APIs only, ~4KB gzipped, fully tree-shakable
90
+
91
+ </td>
92
+ </tr>
93
+ </table>
94
+
95
+ <br />
96
+
97
+ ---
98
+
99
+ <br />
100
+
101
+ ## ๐Ÿ“ฆ Getting Started
102
+
103
+ ```bash
104
+ npm install tab-bridge
105
+ ```
106
+
107
+ ```ts
108
+ import { createTabSync } from 'tab-bridge';
109
+
110
+ const sync = createTabSync({
111
+ initial: { theme: 'light', count: 0 },
112
+ });
113
+
114
+ // Read & write โ€” synced to all tabs instantly
115
+ sync.get('theme'); // 'light'
116
+ sync.set('theme', 'dark'); // โ†’ every tab updates
117
+
118
+ // Subscribe to changes
119
+ const off = sync.on('count', (value, meta) => {
120
+ console.log(`count is now ${value} (${meta.isLocal ? 'local' : 'remote'})`);
121
+ });
122
+
123
+ // Leader election โ€” automatic
124
+ sync.onLeader(() => {
125
+ const ws = new WebSocket('wss://api.example.com');
126
+ return () => ws.close(); // cleanup when leadership is lost
127
+ });
128
+
129
+ // Cross-tab RPC
130
+ sync.handle('double', (n: number) => n * 2);
131
+ const result = await sync.call('leader', 'double', 21); // 42
132
+ ```
133
+
134
+ <br />
135
+
136
+ ---
137
+
138
+ <br />
139
+
140
+ ## ๐Ÿ“– API Reference
141
+
142
+ ### `createTabSync<TState, TRPCMap>(options?)`
143
+
144
+ The single entry point. Returns a fully-typed `TabSyncInstance`.
145
+
146
+ ```ts
147
+ const sync = createTabSync<MyState>({
148
+ initial: { theme: 'light', count: 0 },
149
+ channel: 'my-app',
150
+ debug: true,
151
+ });
152
+ ```
153
+
154
+ <details>
155
+ <summary><b>๐Ÿ“‹ Full Options Table</b></summary>
156
+
157
+ <br />
158
+
159
+ | Option | Type | Default | Description |
160
+ |:-------|:-----|:--------|:------------|
161
+ | `initial` | `TState` | `{}` | Initial state before first sync |
162
+ | `channel` | `string` | `'tab-sync'` | Channel name โ€” only matching tabs communicate |
163
+ | `transport` | `'broadcast-channel'` \| `'local-storage'` | auto | Force a specific transport layer |
164
+ | `merge` | `(local, remote, key) => value` | LWW | Custom conflict resolution |
165
+ | `leader` | `boolean` \| `LeaderOptions` | `true` | Leader election config |
166
+ | `debug` | `boolean` | `false` | Enable colored console logging |
167
+ | `persist` | `PersistOptions` \| `boolean` | `false` | State persistence config |
168
+ | `middlewares` | `Middleware[]` | `[]` | Middleware pipeline |
169
+ | `onError` | `(error: Error) => void` | noop | Global error callback |
170
+
171
+ </details>
172
+
173
+ <br />
174
+
175
+ ### Instance Methods
176
+
177
+ <details open>
178
+ <summary><b>๐Ÿ“Š State</b></summary>
179
+
180
+ <br />
181
+
182
+ ```ts
183
+ sync.get('theme') // Read single key
184
+ sync.getAll() // Read full state (stable reference)
185
+ sync.set('theme', 'dark') // Write single key โ†’ broadcasts to all tabs
186
+ sync.patch({ theme: 'dark', count: 5 }) // Write multiple keys in one broadcast
187
+ ```
188
+
189
+ </details>
190
+
191
+ <details open>
192
+ <summary><b>๐Ÿ”” Subscriptions</b></summary>
193
+
194
+ <br />
195
+
196
+ ```ts
197
+ const off = sync.on('count', (value, meta) => { /* ... */ });
198
+ off(); // unsubscribe
199
+
200
+ sync.once('theme', (value) => console.log('Theme changed:', value));
201
+
202
+ sync.onChange((state, changedKeys, meta) => { /* ... */ });
203
+
204
+ sync.select(
205
+ (state) => state.items.filter(i => i.done).length,
206
+ (doneCount) => updateBadge(doneCount),
207
+ );
208
+ ```
209
+
210
+ </details>
211
+
212
+ <details open>
213
+ <summary><b>๐Ÿ‘‘ Leader Election</b></summary>
214
+
215
+ <br />
216
+
217
+ ```ts
218
+ sync.isLeader() // โ†’ boolean
219
+ sync.getLeader() // โ†’ TabInfo | null
220
+
221
+ sync.onLeader(() => {
222
+ const ws = new WebSocket('wss://...');
223
+ return () => ws.close(); // Cleanup on resign
224
+ });
225
+
226
+ const leader = await sync.waitForLeader(); // Promise-based
227
+ ```
228
+
229
+ </details>
230
+
231
+ <details open>
232
+ <summary><b>๐Ÿ“‹ Tab Registry</b></summary>
233
+
234
+ <br />
235
+
236
+ ```ts
237
+ sync.id // This tab's UUID
238
+ sync.getTabs() // โ†’ TabInfo[]
239
+ sync.getTabCount() // โ†’ number
240
+
241
+ sync.onTabChange((tabs) => {
242
+ console.log(`${tabs.length} tabs open`);
243
+ });
244
+ ```
245
+
246
+ </details>
247
+
248
+ <details open>
249
+ <summary><b>๐Ÿ“ก Cross-Tab RPC</b></summary>
250
+
251
+ <br />
252
+
253
+ ```ts
254
+ sync.handle('getServerTime', () => ({
255
+ iso: new Date().toISOString(),
256
+ }));
257
+
258
+ const { iso } = await sync.call('leader', 'getServerTime');
259
+ const result = await sync.call(tabId, 'compute', payload, 10_000);
260
+ ```
261
+
262
+ </details>
263
+
264
+ <details open>
265
+ <summary><b>โ™ป๏ธ Lifecycle</b></summary>
266
+
267
+ <br />
268
+
269
+ ```ts
270
+ sync.ready // false after destroy
271
+ sync.destroy() // graceful shutdown, safe to call multiple times
272
+ ```
273
+
274
+ </details>
275
+
276
+ <br />
277
+
278
+ ---
279
+
280
+ <br />
281
+
282
+ ## ๐Ÿ”ท Typed RPC
283
+
284
+ Define an RPC contract and get **full end-to-end type inference** โ€” arguments, return types, and method names are all checked at compile time:
285
+
286
+ ```ts
287
+ interface MyRPC {
288
+ getTime: { args: void; result: { iso: string } };
289
+ add: { args: { a: number; b: number }; result: number };
290
+ search: { args: string; result: string[] };
291
+ }
292
+
293
+ const sync = createTabSync<MyState, MyRPC>({
294
+ initial: { count: 0 },
295
+ });
296
+
297
+ sync.handle('add', ({ a, b }) => a + b); // args are typed
298
+ const { iso } = await sync.call('leader', 'getTime'); // result is typed
299
+ const results = await sync.call(tabId, 'search', 'query'); // string[]
300
+ ```
301
+
302
+ <br />
303
+
304
+ ---
305
+
306
+ <br />
307
+
308
+ ## ๐Ÿ›ก๏ธ Middleware
309
+
310
+ Intercept, validate, and transform state changes before they're applied:
311
+
312
+ ```ts
313
+ const sync = createTabSync({
314
+ initial: { name: '', age: 0 },
315
+ middlewares: [
316
+ {
317
+ name: 'validator',
318
+ onSet({ key, value, previousValue, meta }) {
319
+ if (key === 'age' && (value as number) < 0) return false; // reject
320
+ if (key === 'name') return { value: String(value).trim() }; // transform
321
+ },
322
+ afterChange(key, value, meta) {
323
+ analytics.track('state_change', { key, source: meta.sourceTabId });
324
+ },
325
+ onDestroy() { /* cleanup */ },
326
+ },
327
+ ],
328
+ });
329
+ ```
330
+
331
+ ```mermaid
332
+ graph LR
333
+ A["set('age', -5)"] --> B{Middleware<br/>Pipeline}
334
+ B -->|"age < 0 โ†’ reject"| C["โŒ Blocked"]
335
+ D["set('name', ' Alice ')"] --> B
336
+ B -->|"trim()"| E["โœ… 'Alice'"]
337
+
338
+ style A fill:#f59e0b,stroke:#d97706,color:#fff
339
+ style D fill:#f59e0b,stroke:#d97706,color:#fff
340
+ style B fill:#6366f1,stroke:#4f46e5,color:#fff
341
+ style C fill:#ef4444,stroke:#dc2626,color:#fff
342
+ style E fill:#22c55e,stroke:#16a34a,color:#fff
343
+ ```
344
+
345
+ <br />
346
+
347
+ ---
348
+
349
+ <br />
350
+
351
+ ## ๐Ÿ’พ Persistence
352
+
353
+ State survives page reloads automatically:
354
+
355
+ ```ts
356
+ // Simple โ€” persist everything to localStorage
357
+ createTabSync({ initial: { ... }, persist: true });
358
+
359
+ // Advanced โ€” fine-grained control
360
+ createTabSync({
361
+ initial: { theme: 'light', tempData: null },
362
+ persist: {
363
+ key: 'my-app:state',
364
+ include: ['theme'], // only persist these keys
365
+ debounce: 200, // debounce writes (ms)
366
+ storage: sessionStorage, // custom storage backend
367
+ },
368
+ });
369
+ ```
370
+
371
+ <br />
372
+
373
+ ---
374
+
375
+ <br />
376
+
377
+ ## โš›๏ธ React
378
+
379
+ First-class React integration built on `useSyncExternalStore` for **zero-tear concurrent rendering**.
380
+
381
+ ```tsx
382
+ import {
383
+ TabSyncProvider, useTabSync, useTabSyncValue, useTabSyncSelector, useIsLeader,
384
+ } from 'tab-bridge/react';
385
+ ```
386
+
387
+ <br />
388
+
389
+ <details open>
390
+ <summary><b><code>TabSyncProvider</code> โ€” Context Provider</b></summary>
391
+
392
+ <br />
393
+
394
+ ```tsx
395
+ <TabSyncProvider options={{ initial: { count: 0 }, channel: 'app' }}>
396
+ <App />
397
+ </TabSyncProvider>
398
+ ```
399
+
400
+ </details>
401
+
402
+ <details open>
403
+ <summary><b><code>useTabSync()</code> โ€” All-in-one hook</b></summary>
404
+
405
+ <br />
406
+
407
+ ```tsx
408
+ function Counter() {
409
+ const { state, set, isLeader, tabs } = useTabSync<MyState>();
410
+
411
+ return (
412
+ <div>
413
+ <h2>Count: {state.count}</h2>
414
+ <button onClick={() => set('count', state.count + 1)}>+1</button>
415
+ <p>{isLeader ? '๐Ÿ‘‘ Leader' : 'Follower'} ยท {tabs.length} tabs</p>
416
+ </div>
417
+ );
418
+ }
419
+ ```
420
+
421
+ </details>
422
+
423
+ <details open>
424
+ <summary><b><code>useTabSyncValue(key)</code> โ€” Single key, minimal re-renders</b></summary>
425
+
426
+ <br />
427
+
428
+ ```tsx
429
+ function ThemeDisplay() {
430
+ const theme = useTabSyncValue<MyState, 'theme'>('theme');
431
+ return <div className={`app ${theme}`}>Current theme: {theme}</div>;
432
+ }
433
+ ```
434
+
435
+ </details>
436
+
437
+ <details open>
438
+ <summary><b><code>useTabSyncSelector(selector)</code> โ€” Derived state with memoization</b></summary>
439
+
440
+ <br />
441
+
442
+ ```tsx
443
+ function DoneCount() {
444
+ const count = useTabSyncSelector<MyState, number>(
445
+ (state) => state.todos.filter(t => t.done).length,
446
+ );
447
+ return <span className="badge">{count} done</span>;
448
+ }
449
+ ```
450
+
451
+ </details>
452
+
453
+ <details open>
454
+ <summary><b><code>useIsLeader()</code> โ€” Leadership status</b></summary>
455
+
456
+ <br />
457
+
458
+ ```tsx
459
+ function LeaderIndicator() {
460
+ const isLeader = useIsLeader();
461
+ if (!isLeader) return null;
462
+ return <span className="badge badge-leader">Leader Tab</span>;
463
+ }
464
+ ```
465
+
466
+ </details>
467
+
468
+ <br />
469
+
470
+ ---
471
+
472
+ <br />
473
+
474
+ ## ๐Ÿšจ Error Handling
475
+
476
+ Structured errors with error codes for precise `catch` handling:
477
+
478
+ ```ts
479
+ import { TabSyncError, ErrorCode } from 'tab-bridge';
480
+
481
+ try {
482
+ await sync.call('leader', 'getData');
483
+ } catch (err) {
484
+ if (err instanceof TabSyncError) {
485
+ switch (err.code) {
486
+ case ErrorCode.RPC_TIMEOUT: // call timed out
487
+ case ErrorCode.RPC_NO_LEADER: // no leader elected yet
488
+ case ErrorCode.RPC_NO_HANDLER: // method not registered on target
489
+ case ErrorCode.DESTROYED: // instance was destroyed
490
+ }
491
+ }
492
+ }
493
+
494
+ // Global error handler
495
+ createTabSync({ onError: (err) => Sentry.captureException(err) });
496
+ ```
497
+
498
+ <br />
499
+
500
+ ---
501
+
502
+ <br />
503
+
504
+ ## ๐Ÿ—๏ธ Architecture
505
+
506
+ <div align="center">
507
+
508
+ ```mermaid
509
+ graph TB
510
+ subgraph API["๐Ÿ”Œ Public API โ€” createTabSync()"]
511
+ SM["๐Ÿ“Š State Manager<br/><i>get / set / patch / subscribe</i>"]
512
+ LE["๐Ÿ‘‘ Leader Election<br/><i>heartbeat / failover / resign</i>"]
513
+ RPC["๐Ÿ“ก RPC Handler<br/><i>call / handle / timeout</i>"]
514
+ end
515
+
516
+ subgraph CORE["โš™๏ธ Core Layer"]
517
+ TR["๐Ÿ“‹ Tab Registry<br/><i>tracks all active tabs</i>"]
518
+ MW["๐Ÿ›ก๏ธ Middleware Pipeline<br/><i>intercept โ†’ validate โ†’ transform โ†’ apply</i>"]
519
+ end
520
+
521
+ subgraph TRANSPORT["๐Ÿ“ก Transport Layer โ€” auto-detect"]
522
+ BC["โšก BroadcastChannel<br/><i>primary, fast</i>"]
523
+ LS["๐Ÿ’พ localStorage<br/><i>fallback</i>"]
524
+ end
525
+
526
+ SM --> TR
527
+ LE --> TR
528
+ RPC --> TR
529
+ TR --> MW
530
+ MW --> BC
531
+ MW --> LS
532
+
533
+ style API fill:#4f46e5,stroke:#4338ca,color:#fff,stroke-width:2px
534
+ style CORE fill:#7c3aed,stroke:#6d28d9,color:#fff,stroke-width:2px
535
+ style TRANSPORT fill:#2563eb,stroke:#1d4ed8,color:#fff,stroke-width:2px
536
+ style SM fill:#6366f1,stroke:#4f46e5,color:#fff
537
+ style LE fill:#6366f1,stroke:#4f46e5,color:#fff
538
+ style RPC fill:#6366f1,stroke:#4f46e5,color:#fff
539
+ style TR fill:#8b5cf6,stroke:#7c3aed,color:#fff
540
+ style MW fill:#8b5cf6,stroke:#7c3aed,color:#fff
541
+ style BC fill:#3b82f6,stroke:#2563eb,color:#fff
542
+ style LS fill:#3b82f6,stroke:#2563eb,color:#fff
543
+ ```
544
+
545
+ </div>
546
+
547
+ <br />
548
+
549
+ ### How State Sync Works
550
+
551
+ ```mermaid
552
+ sequenceDiagram
553
+ participant A as Tab A (Leader ๐Ÿ‘‘)
554
+ participant BC as BroadcastChannel
555
+ participant B as Tab B
556
+ participant C as Tab C
557
+
558
+ A->>A: set('theme', 'dark')
559
+ Note over A: Local state updated instantly
560
+ A->>BC: STATE_UPDATE { theme: 'dark' }
561
+ BC-->>B:
562
+ BC-->>C:
563
+ B->>B: Apply state + notify subscribers
564
+ C->>C: Apply state + notify subscribers
565
+ ```
566
+
567
+ ### How Leader Election Works
568
+
569
+ ```mermaid
570
+ sequenceDiagram
571
+ participant A as Tab A (oldest)
572
+ participant B as Tab B
573
+ participant C as Tab C (newest)
574
+
575
+ Note over A,C: Leader (Tab A) closes...
576
+ A--xB: โŒ Heartbeat stops
577
+ B->>B: 3 missed heartbeats โ†’ leader dead
578
+ B->>C: LEADER_CLAIM
579
+ C->>C: Tab B is older โ†’ yield
580
+ Note over B: Waits 300ms for higher-priority claims
581
+ B->>C: LEADER_ACK
582
+ Note over B: ๐Ÿ‘‘ Tab B is now leader
583
+ B->>C: LEADER_HEARTBEAT (every 2s)
584
+ ```
585
+
586
+ <br />
587
+
588
+ ---
589
+
590
+ <br />
591
+
592
+ ## ๐Ÿ”ง Advanced
593
+
594
+ <details>
595
+ <summary><b>๐Ÿ”Œ Custom Transport Layer</b></summary>
596
+
597
+ <br />
598
+
599
+ ```ts
600
+ import { createChannel } from 'tab-bridge';
601
+
602
+ createTabSync({ transport: 'local-storage' });
603
+
604
+ const channel = createChannel('my-channel', 'broadcast-channel');
605
+ ```
606
+
607
+ </details>
608
+
609
+ <details>
610
+ <summary><b>๐Ÿ”ข Protocol Versioning</b></summary>
611
+
612
+ <br />
613
+
614
+ All messages include a `version` field. The library automatically ignores messages from incompatible protocol versions, enabling **safe rolling deployments** โ€” old and new tabs can coexist without errors.
615
+
616
+ ```ts
617
+ import { PROTOCOL_VERSION } from 'tab-bridge';
618
+ console.log(PROTOCOL_VERSION); // 1
619
+ ```
620
+
621
+ </details>
622
+
623
+ <details>
624
+ <summary><b>๐Ÿ› Debug Mode</b></summary>
625
+
626
+ <br />
627
+
628
+ ```ts
629
+ createTabSync({ debug: true });
630
+ ```
631
+
632
+ Outputs colored, structured logs:
633
+
634
+ ```
635
+ [tab-sync:a1b2c3d4] โ†’ STATE_UPDATE { theme: 'dark' }
636
+ [tab-sync:a1b2c3d4] โ† LEADER_CLAIM { createdAt: 1708900000 }
637
+ [tab-sync:a1b2c3d4] โ™› Became leader
638
+ ```
639
+
640
+ </details>
641
+
642
+ <details>
643
+ <summary><b>๐Ÿงฉ Exported Internals</b></summary>
644
+
645
+ <br />
646
+
647
+ For library authors or advanced use cases, all internal modules are exported:
648
+
649
+ ```ts
650
+ import {
651
+ StateManager, TabRegistry, LeaderElection, RPCHandler,
652
+ Emitter, createMessage, generateTabId, monotonic,
653
+ } from 'tab-bridge';
654
+ ```
655
+
656
+ </details>
657
+
658
+ <br />
659
+
660
+ ---
661
+
662
+ <br />
663
+
664
+ ## ๐Ÿ’ก Examples
665
+
666
+ <details>
667
+ <summary><b>๐Ÿ” Shared Authentication State</b></summary>
668
+
669
+ <br />
670
+
671
+ ```ts
672
+ const auth = createTabSync({
673
+ initial: { user: null, token: null },
674
+ channel: 'auth',
675
+ persist: { include: ['token'] },
676
+ });
677
+
678
+ auth.on('user', (user) => {
679
+ if (user) showDashboard(user);
680
+ else redirectToLogin();
681
+ });
682
+
683
+ function logout() {
684
+ auth.patch({ user: null, token: null }); // logout everywhere
685
+ }
686
+ ```
687
+
688
+ </details>
689
+
690
+ <details>
691
+ <summary><b>๐ŸŒ Single WebSocket Connection (Leader Pattern)</b></summary>
692
+
693
+ <br />
694
+
695
+ ```mermaid
696
+ graph LR
697
+ Server["๐Ÿ–ฅ๏ธ Server"] <-->|WebSocket| A["Tab A<br/>๐Ÿ‘‘ Leader"]
698
+ A -->|"state sync"| B["Tab B"]
699
+ A -->|"state sync"| C["Tab C"]
700
+
701
+ style Server fill:#059669,stroke:#047857,color:#fff
702
+ style A fill:#4f46e5,stroke:#4338ca,color:#fff
703
+ style B fill:#6366f1,stroke:#4f46e5,color:#fff
704
+ style C fill:#6366f1,stroke:#4f46e5,color:#fff
705
+ ```
706
+
707
+ ```ts
708
+ const sync = createTabSync({
709
+ initial: { messages: [] as Message[] },
710
+ });
711
+
712
+ sync.onLeader(() => {
713
+ const ws = new WebSocket('wss://chat.example.com');
714
+
715
+ ws.onmessage = (e) => {
716
+ const msg = JSON.parse(e.data);
717
+ sync.set('messages', [...sync.get('messages'), msg]);
718
+ };
719
+
720
+ return () => ws.close(); // cleanup on leadership loss
721
+ });
722
+ ```
723
+
724
+ </details>
725
+
726
+ <details>
727
+ <summary><b>๐Ÿ”” Cross-Tab Notifications</b></summary>
728
+
729
+ <br />
730
+
731
+ ```ts
732
+ interface NotifyRPC {
733
+ notify: {
734
+ args: { title: string; body: string };
735
+ result: void;
736
+ };
737
+ }
738
+
739
+ const sync = createTabSync<{}, NotifyRPC>({ channel: 'notifications' });
740
+
741
+ sync.onLeader(() => {
742
+ sync.handle('notify', ({ title, body }) => {
743
+ new Notification(title, { body });
744
+ });
745
+ return () => {};
746
+ });
747
+
748
+ await sync.call('leader', 'notify', {
749
+ title: 'New Message',
750
+ body: 'You have 3 unread messages',
751
+ });
752
+ ```
753
+
754
+ </details>
755
+
756
+ <details>
757
+ <summary><b>๐Ÿ›’ React โ€” Shopping Cart Sync</b></summary>
758
+
759
+ <br />
760
+
761
+ ```tsx
762
+ interface CartState {
763
+ items: Array<{ id: string; name: string; qty: number }>;
764
+ total: number;
765
+ }
766
+
767
+ function Cart() {
768
+ const { state, set } = useTabSync<CartState>();
769
+
770
+ const itemCount = useTabSyncSelector<CartState, number>(
771
+ (s) => s.items.reduce((sum, i) => sum + i.qty, 0),
772
+ );
773
+
774
+ return (
775
+ <div>
776
+ <h2>Cart ({itemCount} items)</h2>
777
+ {state.items.map(item => (
778
+ <div key={item.id}>
779
+ {item.name} ร— {item.qty}
780
+ </div>
781
+ ))}
782
+ </div>
783
+ );
784
+ }
785
+ ```
786
+
787
+ </details>
788
+
789
+ <br />
790
+
791
+ ---
792
+
793
+ <br />
794
+
795
+ ## ๐ŸŒ Browser Support
796
+
797
+ | | Browser | Version | Transport |
798
+ |:--|:--------|:--------|:----------|
799
+ | ๐ŸŸข | Chrome | 54+ | `BroadcastChannel` |
800
+ | ๐ŸŸ  | Firefox | 38+ | `BroadcastChannel` |
801
+ | ๐Ÿ”ต | Safari | 15.4+ | `BroadcastChannel` |
802
+ | ๐Ÿ”ท | Edge | 79+ | `BroadcastChannel` |
803
+ | โšช | Older browsers | โ€” | `localStorage` (auto-fallback) |
804
+
805
+ <br />
806
+
807
+ ---
808
+
809
+ <br />
810
+
811
+ <div align="center">
812
+
813
+ <h3>๐Ÿ“„ License</h3>
814
+
815
+ MIT ยฉ [serbi2012](https://github.com/serbi2012)
816
+
817
+ <br />
818
+
819
+ **If this library helped you, consider giving it a โญ**
820
+
821
+ <br />
822
+
823
+ <a href="https://github.com/serbi2012/tab-sync">
824
+ <img src="https://img.shields.io/badge/GitHub-tab--bridge-4f46e5?style=for-the-badge&logo=github&logoColor=white" alt="GitHub" />
825
+ </a>
826
+ &nbsp;
827
+ <a href="https://www.npmjs.com/package/tab-bridge">
828
+ <img src="https://img.shields.io/badge/npm-tab--bridge-cb3837?style=for-the-badge&logo=npm&logoColor=white" alt="npm" />
829
+ </a>
830
+
831
+ <br />
832
+ <br />
833
+
834
+ </div>