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/LICENSE +21 -0
- package/README.md +834 -0
- package/dist/chunk-42VOZR6E.js +1309 -0
- package/dist/chunk-BQCNBNBT.cjs +1332 -0
- package/dist/index.cjs +149 -0
- package/dist/index.d.cts +304 -0
- package/dist/index.d.ts +304 -0
- package/dist/index.js +59 -0
- package/dist/react/index.cjs +174 -0
- package/dist/react/index.d.cts +61 -0
- package/dist/react/index.d.ts +61 -0
- package/dist/react/index.js +167 -0
- package/dist/types-BtK4ixKz.d.cts +306 -0
- package/dist/types-BtK4ixKz.d.ts +306 -0
- package/package.json +90 -0
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
|
+
[](https://www.npmjs.com/package/tab-bridge)
|
|
18
|
+
[](https://bundlephobia.com/package/tab-bridge)
|
|
19
|
+
[](https://www.typescriptlang.org)
|
|
20
|
+
[](./LICENSE)
|
|
21
|
+
[](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
|
+
|
|
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>
|