tab-bridge 0.1.1 → 0.3.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 +263 -10
- package/dist/{chunk-42VOZR6E.js → chunk-4JDWAUYM.js} +216 -93
- package/dist/{chunk-BQCNBNBT.cjs → chunk-TGEXRVAL.cjs} +219 -92
- package/dist/index.cjs +41 -25
- package/dist/index.d.cts +112 -5
- package/dist/index.d.ts +112 -5
- package/dist/index.js +2 -2
- package/dist/instance-hvEUHx6i.d.cts +194 -0
- package/dist/instance-hvEUHx6i.d.ts +194 -0
- package/dist/options-DmHyGTL0.d.cts +93 -0
- package/dist/options-iN7Rnvwj.d.ts +93 -0
- package/dist/react/index.cjs +82 -9
- package/dist/react/index.d.cts +48 -2
- package/dist/react/index.d.ts +48 -2
- package/dist/react/index.js +79 -9
- package/dist/zustand/index.cjs +80 -0
- package/dist/zustand/index.d.cts +87 -0
- package/dist/zustand/index.d.ts +87 -0
- package/dist/zustand/index.js +78 -0
- package/package.json +22 -6
- package/dist/types-BtK4ixKz.d.cts +0 -306
- package/dist/types-BtK4ixKz.d.ts +0 -306
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
[](https://www.npmjs.com/package/tab-bridge)
|
|
18
18
|
[](https://bundlephobia.com/package/tab-bridge)
|
|
19
19
|
[](https://www.typescriptlang.org)
|
|
20
|
-
[](./LICENSE)
|
|
21
|
+
[](https://github.com/serbi2012/tab-bridge)
|
|
22
22
|
|
|
23
23
|
<br />
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
<br />
|
|
28
28
|
|
|
29
|
-
[**Getting Started**](#-getting-started) · [**API**](#-api-reference) · [**React**](#%EF%B8%8F-react) · [**Architecture**](#-architecture) · [**Examples**](#-examples)
|
|
29
|
+
[**Getting Started**](#-getting-started) · [**API**](#-api-reference) · [**React**](#%EF%B8%8F-react) · [**Zustand**](#-zustand) · [**Next.js**](#-nextjs) · [**Architecture**](#-architecture) · [**Examples**](#-examples) · [**Live Demo**](https://serbi2012.github.io/tab-bridge/)
|
|
30
30
|
|
|
31
31
|
</div>
|
|
32
32
|
|
|
@@ -59,22 +59,25 @@ LWW conflict resolution with batched broadcasts and custom merge strategies
|
|
|
59
59
|
Bully algorithm with heartbeat monitoring and automatic failover
|
|
60
60
|
|
|
61
61
|
#### 📡 Cross-Tab RPC
|
|
62
|
-
Fully typed arguments, Promise-based calls with
|
|
62
|
+
Fully typed arguments, Promise-based calls with `callAll` broadcast support
|
|
63
63
|
|
|
64
|
-
####
|
|
65
|
-
|
|
64
|
+
#### 🔄 Atomic Transactions
|
|
65
|
+
`transaction()` for safe multi-key updates with abort support
|
|
66
66
|
|
|
67
67
|
</td>
|
|
68
68
|
<td width="50%" valign="top">
|
|
69
69
|
|
|
70
|
+
#### ⚛️ React Hooks
|
|
71
|
+
7 hooks built on `useSyncExternalStore` — zero-tear concurrent rendering
|
|
72
|
+
|
|
70
73
|
#### 🛡️ Middleware Pipeline
|
|
71
74
|
Intercept, validate, and transform state changes before they're applied
|
|
72
75
|
|
|
73
76
|
#### 💾 State Persistence
|
|
74
77
|
Survive page reloads with key whitelisting and custom storage backends
|
|
75
78
|
|
|
76
|
-
####
|
|
77
|
-
|
|
79
|
+
#### 🐻 Zustand Middleware
|
|
80
|
+
One-line integration — `tabSync()` wraps any Zustand store for cross-tab sync
|
|
78
81
|
|
|
79
82
|
#### 📦 Zero Dependencies
|
|
80
83
|
Native browser APIs only, ~4KB gzipped, fully tree-shakable
|
|
@@ -175,6 +178,12 @@ sync.get('theme') // Read single key
|
|
|
175
178
|
sync.getAll() // Read full state (stable reference)
|
|
176
179
|
sync.set('theme', 'dark') // Write single key → broadcasts to all tabs
|
|
177
180
|
sync.patch({ theme: 'dark', count: 5 }) // Write multiple keys in one broadcast
|
|
181
|
+
|
|
182
|
+
// Atomic multi-key update — return null to abort
|
|
183
|
+
sync.transaction((state) => {
|
|
184
|
+
if (state.count >= 100) return null; // abort
|
|
185
|
+
return { count: state.count + 1, lastUpdated: Date.now() };
|
|
186
|
+
});
|
|
178
187
|
```
|
|
179
188
|
|
|
180
189
|
</details>
|
|
@@ -196,6 +205,13 @@ sync.select(
|
|
|
196
205
|
(state) => state.items.filter(i => i.done).length,
|
|
197
206
|
(doneCount) => updateBadge(doneCount),
|
|
198
207
|
);
|
|
208
|
+
|
|
209
|
+
// Debounced derived state — callback fires at most once per 200ms
|
|
210
|
+
sync.select(
|
|
211
|
+
(state) => state.items.length,
|
|
212
|
+
(count) => analytics.track('item_count', count),
|
|
213
|
+
{ debounce: 200 },
|
|
214
|
+
);
|
|
199
215
|
```
|
|
200
216
|
|
|
201
217
|
</details>
|
|
@@ -248,6 +264,10 @@ sync.handle('getServerTime', () => ({
|
|
|
248
264
|
|
|
249
265
|
const { iso } = await sync.call('leader', 'getServerTime');
|
|
250
266
|
const result = await sync.call(tabId, 'compute', payload, 10_000);
|
|
267
|
+
|
|
268
|
+
// Broadcast RPC to ALL other tabs and collect responses
|
|
269
|
+
const results = await sync.callAll('getStatus');
|
|
270
|
+
// results: Array<{ tabId: string; result?: T; error?: string }>
|
|
251
271
|
```
|
|
252
272
|
|
|
253
273
|
</details>
|
|
@@ -359,7 +379,8 @@ First-class React integration built on `useSyncExternalStore` for **zero-tear co
|
|
|
359
379
|
|
|
360
380
|
```tsx
|
|
361
381
|
import {
|
|
362
|
-
TabSyncProvider, useTabSync, useTabSyncValue, useTabSyncSelector,
|
|
382
|
+
TabSyncProvider, useTabSync, useTabSyncValue, useTabSyncSelector,
|
|
383
|
+
useIsLeader, useTabs, useLeaderInfo, useTabSyncActions,
|
|
363
384
|
} from 'tab-bridge/react';
|
|
364
385
|
```
|
|
365
386
|
|
|
@@ -444,6 +465,225 @@ function LeaderIndicator() {
|
|
|
444
465
|
|
|
445
466
|
</details>
|
|
446
467
|
|
|
468
|
+
<details open>
|
|
469
|
+
<summary><b><code>useTabs()</code> — Active tab list</b></summary>
|
|
470
|
+
|
|
471
|
+
<br />
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
function TabList() {
|
|
475
|
+
const tabs = useTabs();
|
|
476
|
+
return <p>{tabs.length} tab(s) open</p>;
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
</details>
|
|
481
|
+
|
|
482
|
+
<details open>
|
|
483
|
+
<summary><b><code>useLeaderInfo()</code> — Leader tab info</b></summary>
|
|
484
|
+
|
|
485
|
+
<br />
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
function LeaderDisplay() {
|
|
489
|
+
const leader = useLeaderInfo();
|
|
490
|
+
if (!leader) return <p>No leader yet</p>;
|
|
491
|
+
return <p>Leader: {leader.id}</p>;
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
</details>
|
|
496
|
+
|
|
497
|
+
<details open>
|
|
498
|
+
<summary><b><code>useTabSyncActions()</code> — Write-only (no re-renders)</b></summary>
|
|
499
|
+
|
|
500
|
+
<br />
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
function IncrementButton() {
|
|
504
|
+
const { set, patch, transaction } = useTabSyncActions<MyState>();
|
|
505
|
+
return <button onClick={() => set('count', prev => prev + 1)}>+1</button>;
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Components using only `useTabSyncActions` **never re-render** due to state changes — perfect for write-only controls.
|
|
510
|
+
|
|
511
|
+
</details>
|
|
512
|
+
|
|
513
|
+
<br />
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
<br />
|
|
518
|
+
|
|
519
|
+
## 🐻 Zustand
|
|
520
|
+
|
|
521
|
+
One-line integration for [Zustand](https://github.com/pmndrs/zustand) stores — all tabs stay in sync automatically.
|
|
522
|
+
|
|
523
|
+
```bash
|
|
524
|
+
npm install zustand
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```ts
|
|
528
|
+
import { create } from 'zustand';
|
|
529
|
+
import { tabSync } from 'tab-bridge/zustand';
|
|
530
|
+
|
|
531
|
+
const useStore = create(
|
|
532
|
+
tabSync(
|
|
533
|
+
(set) => ({
|
|
534
|
+
count: 0,
|
|
535
|
+
theme: 'light',
|
|
536
|
+
inc: () => set((s) => ({ count: s.count + 1 })),
|
|
537
|
+
setTheme: (t: string) => set({ theme: t }),
|
|
538
|
+
}),
|
|
539
|
+
{ channel: 'my-app' }
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// That's it — all tabs now share the same state.
|
|
544
|
+
// Functions (actions) are never synced, only data.
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
<details open>
|
|
548
|
+
<summary><b>📋 Middleware Options</b></summary>
|
|
549
|
+
|
|
550
|
+
<br />
|
|
551
|
+
|
|
552
|
+
| Option | Type | Default | Description |
|
|
553
|
+
|:-------|:-----|:--------|:------------|
|
|
554
|
+
| `channel` | `string` | `'tab-sync-zustand'` | Channel name for cross-tab communication |
|
|
555
|
+
| `include` | `string[]` | — | Only sync these keys (mutually exclusive with `exclude`) |
|
|
556
|
+
| `exclude` | `string[]` | — | Exclude these keys from syncing (mutually exclusive with `include`) |
|
|
557
|
+
| `merge` | `(local, remote, key) => value` | LWW | Custom conflict resolution |
|
|
558
|
+
| `transport` | `'broadcast-channel'` \| `'local-storage'` | auto | Force a specific transport |
|
|
559
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
560
|
+
| `onError` | `(error) => void` | — | Error callback |
|
|
561
|
+
| `onSyncReady` | `(instance) => void` | — | Access the underlying `TabSyncInstance` for RPC/leader features |
|
|
562
|
+
|
|
563
|
+
</details>
|
|
564
|
+
|
|
565
|
+
<details>
|
|
566
|
+
<summary><b>🔑 Selective Key Sync</b></summary>
|
|
567
|
+
|
|
568
|
+
<br />
|
|
569
|
+
|
|
570
|
+
```ts
|
|
571
|
+
const useStore = create(
|
|
572
|
+
tabSync(
|
|
573
|
+
(set) => ({
|
|
574
|
+
count: 0,
|
|
575
|
+
theme: 'light',
|
|
576
|
+
localDraft: '', // won't be synced
|
|
577
|
+
inc: () => set((s) => ({ count: s.count + 1 })),
|
|
578
|
+
}),
|
|
579
|
+
{
|
|
580
|
+
channel: 'my-app',
|
|
581
|
+
exclude: ['localDraft'], // keep this key local-only
|
|
582
|
+
}
|
|
583
|
+
)
|
|
584
|
+
);
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
</details>
|
|
588
|
+
|
|
589
|
+
<details>
|
|
590
|
+
<summary><b>🤝 Works with Zustand <code>persist</code></b></summary>
|
|
591
|
+
|
|
592
|
+
<br />
|
|
593
|
+
|
|
594
|
+
Compose with Zustand's `persist` middleware — order doesn't matter:
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
import { persist } from 'zustand/middleware';
|
|
598
|
+
|
|
599
|
+
const useStore = create(
|
|
600
|
+
persist(
|
|
601
|
+
tabSync(
|
|
602
|
+
(set) => ({
|
|
603
|
+
count: 0,
|
|
604
|
+
inc: () => set((s) => ({ count: s.count + 1 })),
|
|
605
|
+
}),
|
|
606
|
+
{ channel: 'my-app' }
|
|
607
|
+
),
|
|
608
|
+
{ name: 'my-store' }
|
|
609
|
+
)
|
|
610
|
+
);
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
</details>
|
|
614
|
+
|
|
615
|
+
<details>
|
|
616
|
+
<summary><b>🚀 Advanced: Access tab-bridge Instance</b></summary>
|
|
617
|
+
|
|
618
|
+
<br />
|
|
619
|
+
|
|
620
|
+
Use `onSyncReady` to access the underlying `TabSyncInstance` for RPC, leader election, and other advanced features:
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
let syncInstance: TabSyncInstance | null = null;
|
|
624
|
+
|
|
625
|
+
const useStore = create(
|
|
626
|
+
tabSync(
|
|
627
|
+
(set) => ({ count: 0 }),
|
|
628
|
+
{
|
|
629
|
+
channel: 'my-app',
|
|
630
|
+
onSyncReady: (instance) => {
|
|
631
|
+
syncInstance = instance;
|
|
632
|
+
|
|
633
|
+
instance.handle('getCount', () => useStore.getState().count);
|
|
634
|
+
|
|
635
|
+
instance.onLeader(() => {
|
|
636
|
+
console.log('This tab is now the leader');
|
|
637
|
+
return () => console.log('Leadership lost');
|
|
638
|
+
});
|
|
639
|
+
},
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
</details>
|
|
646
|
+
|
|
647
|
+
<br />
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
<br />
|
|
652
|
+
|
|
653
|
+
## 📘 Next.js
|
|
654
|
+
|
|
655
|
+
Using tab-bridge with **Next.js App Router**? Since tab-bridge relies on browser APIs, all usage must be in Client Components.
|
|
656
|
+
|
|
657
|
+
```tsx
|
|
658
|
+
// app/providers/tab-sync-provider.tsx
|
|
659
|
+
'use client';
|
|
660
|
+
|
|
661
|
+
import { TabSyncProvider } from 'tab-bridge/react';
|
|
662
|
+
|
|
663
|
+
export function AppTabSyncProvider({ children }: { children: React.ReactNode }) {
|
|
664
|
+
return (
|
|
665
|
+
<TabSyncProvider options={{ initial: { count: 0 }, channel: 'my-app' }}>
|
|
666
|
+
{children}
|
|
667
|
+
</TabSyncProvider>
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
```tsx
|
|
673
|
+
// app/layout.tsx
|
|
674
|
+
import { AppTabSyncProvider } from './providers/tab-sync-provider';
|
|
675
|
+
|
|
676
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
677
|
+
return (
|
|
678
|
+
<html><body>
|
|
679
|
+
<AppTabSyncProvider>{children}</AppTabSyncProvider>
|
|
680
|
+
</body></html>
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
> **Full guide**: See [`docs/NEXTJS.md`](./docs/NEXTJS.md) for SSR safety patterns, hydration mismatch prevention, `useEffect` initialization, and Zustand integration with Next.js.
|
|
686
|
+
|
|
447
687
|
<br />
|
|
448
688
|
|
|
449
689
|
---
|
|
@@ -578,6 +818,19 @@ import {
|
|
|
578
818
|
|
|
579
819
|
## 💡 Examples
|
|
580
820
|
|
|
821
|
+
### 🎯 Interactive Demos
|
|
822
|
+
|
|
823
|
+
Try these demos live — open multiple tabs to see real-time synchronization in action:
|
|
824
|
+
|
|
825
|
+
| Demo | Description | Features |
|
|
826
|
+
|:-----|:-----------|:---------|
|
|
827
|
+
| [**Collaborative Editor**](https://serbi2012.github.io/tab-bridge/collaborative-editor.html) | Multi-tab real-time text editing | State Sync, Typing Indicators |
|
|
828
|
+
| [**Shopping Cart**](https://serbi2012.github.io/tab-bridge/shopping-cart.html) | Cart synced across all tabs + persistent | State Sync, Persistence |
|
|
829
|
+
| [**Leader Dashboard**](https://serbi2012.github.io/tab-bridge/leader-dashboard.html) | Only leader fetches data, followers use RPC | Leader Election, RPC, callAll |
|
|
830
|
+
| [**Full Feature Demo**](https://serbi2012.github.io/tab-bridge/) | All features in one page | Everything |
|
|
831
|
+
|
|
832
|
+
### Code Examples
|
|
833
|
+
|
|
581
834
|
<details>
|
|
582
835
|
<summary><b>🔐 Shared Authentication State</b></summary>
|
|
583
836
|
|
|
@@ -725,7 +978,7 @@ MIT © [serbi2012](https://github.com/serbi2012)
|
|
|
725
978
|
|
|
726
979
|
<br />
|
|
727
980
|
|
|
728
|
-
<a href="https://github.com/serbi2012/tab-
|
|
981
|
+
<a href="https://github.com/serbi2012/tab-bridge">
|
|
729
982
|
<img src="https://img.shields.io/badge/GitHub-tab--bridge-4f46e5?style=for-the-badge&logo=github&logoColor=white" alt="GitHub" />
|
|
730
983
|
</a>
|
|
731
984
|
|