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 CHANGED
@@ -17,8 +17,8 @@
17
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
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
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)
20
+ [![license](https://img.shields.io/github/license/serbi2012/tab-bridge?style=for-the-badge&color=blue&logo=open-source-initiative&logoColor=white)](./LICENSE)
21
+ [![GitHub stars](https://img.shields.io/github/stars/serbi2012/tab-bridge?style=for-the-badge&color=yellow&logo=github&logoColor=white)](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 timeout handling
62
+ Fully typed arguments, Promise-based calls with `callAll` broadcast support
63
63
 
64
- #### ⚛️ React Hooks
65
- Built on `useSyncExternalStore` for zero-tear concurrent rendering
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
- #### 🔒 End-to-End Type Safety
77
- Discriminated unions, full type inference, and generic constraints
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, useIsLeader,
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-sync">
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
  &nbsp;