tab-bridge 0.2.0 → 0.4.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 +340 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/{instance-5LIItazN.d.cts → instance-hvEUHx6i.d.cts} +1 -91
- package/dist/{instance-5LIItazN.d.ts → instance-hvEUHx6i.d.ts} +1 -91
- package/dist/jotai/index.cjs +44 -0
- package/dist/jotai/index.d.cts +68 -0
- package/dist/jotai/index.d.ts +68 -0
- package/dist/jotai/index.js +42 -0
- package/dist/options-DmHyGTL0.d.cts +93 -0
- package/dist/options-iN7Rnvwj.d.ts +93 -0
- package/dist/react/index.cjs +332 -0
- package/dist/react/index.d.cts +25 -2
- package/dist/react/index.d.ts +25 -2
- package/dist/react/index.js +333 -2
- package/dist/redux/index.cjs +99 -0
- package/dist/redux/index.d.cts +83 -0
- package/dist/redux/index.d.ts +83 -0
- package/dist/redux/index.js +96 -0
- 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 +52 -3
package/README.md
CHANGED
|
@@ -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) · [**Live Demo**](https://serbi2012.github.io/tab-bridge/)
|
|
29
|
+
[**Getting Started**](#-getting-started) · [**API**](#-api-reference) · [**React**](#%EF%B8%8F-react) · [**Zustand**](#-zustand) · [**Jotai**](#-jotai) · [**Redux**](#-redux) · [**DevTools**](#-devtools) · [**Next.js**](#-nextjs) · [**Architecture**](#-architecture) · [**Examples**](#-examples) · [**Live Demo**](https://serbi2012.github.io/tab-bridge/)
|
|
30
30
|
|
|
31
31
|
</div>
|
|
32
32
|
|
|
@@ -76,6 +76,12 @@ Intercept, validate, and transform state changes before they're applied
|
|
|
76
76
|
#### 💾 State Persistence
|
|
77
77
|
Survive page reloads with key whitelisting and custom storage backends
|
|
78
78
|
|
|
79
|
+
#### 🐻 Zustand / Jotai / Redux
|
|
80
|
+
First-class integrations — `tabSync`, `atomWithTabSync`, `tabSyncEnhancer`
|
|
81
|
+
|
|
82
|
+
#### 🔧 DevTools Panel
|
|
83
|
+
Floating `<TabSyncDevTools />` with state inspection, tab list, and event log
|
|
84
|
+
|
|
79
85
|
#### 📦 Zero Dependencies
|
|
80
86
|
Native browser APIs only, ~4KB gzipped, fully tree-shakable
|
|
81
87
|
|
|
@@ -513,6 +519,326 @@ Components using only `useTabSyncActions` **never re-render** due to state chang
|
|
|
513
519
|
|
|
514
520
|
<br />
|
|
515
521
|
|
|
522
|
+
## 🐻 Zustand
|
|
523
|
+
|
|
524
|
+
One-line integration for [Zustand](https://github.com/pmndrs/zustand) stores — all tabs stay in sync automatically.
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
npm install zustand
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
import { create } from 'zustand';
|
|
532
|
+
import { tabSync } from 'tab-bridge/zustand';
|
|
533
|
+
|
|
534
|
+
const useStore = create(
|
|
535
|
+
tabSync(
|
|
536
|
+
(set) => ({
|
|
537
|
+
count: 0,
|
|
538
|
+
theme: 'light',
|
|
539
|
+
inc: () => set((s) => ({ count: s.count + 1 })),
|
|
540
|
+
setTheme: (t: string) => set({ theme: t }),
|
|
541
|
+
}),
|
|
542
|
+
{ channel: 'my-app' }
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// That's it — all tabs now share the same state.
|
|
547
|
+
// Functions (actions) are never synced, only data.
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
<details open>
|
|
551
|
+
<summary><b>📋 Middleware Options</b></summary>
|
|
552
|
+
|
|
553
|
+
<br />
|
|
554
|
+
|
|
555
|
+
| Option | Type | Default | Description |
|
|
556
|
+
|:-------|:-----|:--------|:------------|
|
|
557
|
+
| `channel` | `string` | `'tab-sync-zustand'` | Channel name for cross-tab communication |
|
|
558
|
+
| `include` | `string[]` | — | Only sync these keys (mutually exclusive with `exclude`) |
|
|
559
|
+
| `exclude` | `string[]` | — | Exclude these keys from syncing (mutually exclusive with `include`) |
|
|
560
|
+
| `merge` | `(local, remote, key) => value` | LWW | Custom conflict resolution |
|
|
561
|
+
| `transport` | `'broadcast-channel'` \| `'local-storage'` | auto | Force a specific transport |
|
|
562
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
563
|
+
| `onError` | `(error) => void` | — | Error callback |
|
|
564
|
+
| `onSyncReady` | `(instance) => void` | — | Access the underlying `TabSyncInstance` for RPC/leader features |
|
|
565
|
+
|
|
566
|
+
</details>
|
|
567
|
+
|
|
568
|
+
<details>
|
|
569
|
+
<summary><b>🔑 Selective Key Sync</b></summary>
|
|
570
|
+
|
|
571
|
+
<br />
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
const useStore = create(
|
|
575
|
+
tabSync(
|
|
576
|
+
(set) => ({
|
|
577
|
+
count: 0,
|
|
578
|
+
theme: 'light',
|
|
579
|
+
localDraft: '', // won't be synced
|
|
580
|
+
inc: () => set((s) => ({ count: s.count + 1 })),
|
|
581
|
+
}),
|
|
582
|
+
{
|
|
583
|
+
channel: 'my-app',
|
|
584
|
+
exclude: ['localDraft'], // keep this key local-only
|
|
585
|
+
}
|
|
586
|
+
)
|
|
587
|
+
);
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
</details>
|
|
591
|
+
|
|
592
|
+
<details>
|
|
593
|
+
<summary><b>🤝 Works with Zustand <code>persist</code></b></summary>
|
|
594
|
+
|
|
595
|
+
<br />
|
|
596
|
+
|
|
597
|
+
Compose with Zustand's `persist` middleware — order doesn't matter:
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
import { persist } from 'zustand/middleware';
|
|
601
|
+
|
|
602
|
+
const useStore = create(
|
|
603
|
+
persist(
|
|
604
|
+
tabSync(
|
|
605
|
+
(set) => ({
|
|
606
|
+
count: 0,
|
|
607
|
+
inc: () => set((s) => ({ count: s.count + 1 })),
|
|
608
|
+
}),
|
|
609
|
+
{ channel: 'my-app' }
|
|
610
|
+
),
|
|
611
|
+
{ name: 'my-store' }
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
</details>
|
|
617
|
+
|
|
618
|
+
<details>
|
|
619
|
+
<summary><b>🚀 Advanced: Access tab-bridge Instance</b></summary>
|
|
620
|
+
|
|
621
|
+
<br />
|
|
622
|
+
|
|
623
|
+
Use `onSyncReady` to access the underlying `TabSyncInstance` for RPC, leader election, and other advanced features:
|
|
624
|
+
|
|
625
|
+
```ts
|
|
626
|
+
let syncInstance: TabSyncInstance | null = null;
|
|
627
|
+
|
|
628
|
+
const useStore = create(
|
|
629
|
+
tabSync(
|
|
630
|
+
(set) => ({ count: 0 }),
|
|
631
|
+
{
|
|
632
|
+
channel: 'my-app',
|
|
633
|
+
onSyncReady: (instance) => {
|
|
634
|
+
syncInstance = instance;
|
|
635
|
+
|
|
636
|
+
instance.handle('getCount', () => useStore.getState().count);
|
|
637
|
+
|
|
638
|
+
instance.onLeader(() => {
|
|
639
|
+
console.log('This tab is now the leader');
|
|
640
|
+
return () => console.log('Leadership lost');
|
|
641
|
+
});
|
|
642
|
+
},
|
|
643
|
+
}
|
|
644
|
+
)
|
|
645
|
+
);
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
</details>
|
|
649
|
+
|
|
650
|
+
<br />
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
<br />
|
|
655
|
+
|
|
656
|
+
## 🧪 Jotai
|
|
657
|
+
|
|
658
|
+
Synchronize individual [Jotai](https://jotai.org) atoms across browser tabs with zero boilerplate.
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
npm install tab-bridge jotai
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### Quick Start
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
import { atomWithTabSync } from 'tab-bridge/jotai';
|
|
668
|
+
|
|
669
|
+
const countAtom = atomWithTabSync('count', 0, { channel: 'my-app' });
|
|
670
|
+
const themeAtom = atomWithTabSync('theme', 'light', { channel: 'my-app' });
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Use them like regular atoms — `useAtom(countAtom)` works as expected. Changes are automatically synced to all tabs.
|
|
674
|
+
|
|
675
|
+
### Options
|
|
676
|
+
|
|
677
|
+
| Option | Type | Default | Description |
|
|
678
|
+
|---|---|---|---|
|
|
679
|
+
| `channel` | `string` | `'tab-sync-jotai'` | Channel name for cross-tab communication |
|
|
680
|
+
| `transport` | `'broadcast-channel' \| 'local-storage'` | auto-detect | Force a specific transport |
|
|
681
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
682
|
+
| `onError` | `(error: Error) => void` | — | Error callback |
|
|
683
|
+
| `onSyncReady` | `(instance: TabSyncInstance) => void` | — | Access underlying instance |
|
|
684
|
+
|
|
685
|
+
### How It Works
|
|
686
|
+
|
|
687
|
+
Each atom creates its own `createTabSync` instance scoped to `${channel}:${key}`. The instance is created when the atom is first subscribed to and destroyed when the last subscriber unmounts.
|
|
688
|
+
|
|
689
|
+
### Derived Atoms
|
|
690
|
+
|
|
691
|
+
Derived atoms work out of the box:
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
import { atom } from 'jotai';
|
|
695
|
+
|
|
696
|
+
const doubledAtom = atom((get) => get(countAtom) * 2);
|
|
697
|
+
// Automatically updates when countAtom syncs from another tab
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
<br />
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
<br />
|
|
705
|
+
|
|
706
|
+
## 🏪 Redux
|
|
707
|
+
|
|
708
|
+
Synchronize your Redux (or Redux Toolkit) store across browser tabs via a **store enhancer**.
|
|
709
|
+
|
|
710
|
+
```bash
|
|
711
|
+
npm install tab-bridge redux # or @reduxjs/toolkit
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Quick Start
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
import { configureStore } from '@reduxjs/toolkit';
|
|
718
|
+
import { tabSyncEnhancer } from 'tab-bridge/redux';
|
|
719
|
+
|
|
720
|
+
const store = configureStore({
|
|
721
|
+
reducer: { counter: counterReducer, theme: themeReducer },
|
|
722
|
+
enhancers: (getDefault) =>
|
|
723
|
+
getDefault().concat(tabSyncEnhancer({ channel: 'my-app' })),
|
|
724
|
+
});
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
Every dispatch that changes state is automatically synced. Remote changes are merged via an internal `@@tab-bridge/MERGE` action — no reducer changes needed.
|
|
728
|
+
|
|
729
|
+
### Options
|
|
730
|
+
|
|
731
|
+
| Option | Type | Default | Description |
|
|
732
|
+
|---|---|---|---|
|
|
733
|
+
| `channel` | `string` | `'tab-sync-redux'` | Channel name |
|
|
734
|
+
| `include` | `string[]` | — | Only sync these top-level keys (slice names) |
|
|
735
|
+
| `exclude` | `string[]` | — | Exclude these top-level keys |
|
|
736
|
+
| `merge` | `(local, remote, key) => unknown` | LWW | Custom conflict resolution |
|
|
737
|
+
| `transport` | `'broadcast-channel' \| 'local-storage'` | auto-detect | Force transport |
|
|
738
|
+
| `debug` | `boolean` | `false` | Debug logging |
|
|
739
|
+
| `onError` | `(error: Error) => void` | — | Error callback |
|
|
740
|
+
| `onSyncReady` | `(instance: TabSyncInstance) => void` | — | Access underlying instance |
|
|
741
|
+
|
|
742
|
+
### Selective Sync
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
tabSyncEnhancer({
|
|
746
|
+
channel: 'my-app',
|
|
747
|
+
include: ['counter', 'settings'], // only sync these slices
|
|
748
|
+
// exclude: ['auth'], // or exclude specific slices
|
|
749
|
+
})
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### Design Decision: State-Based Sync
|
|
753
|
+
|
|
754
|
+
The enhancer diffs top-level state keys after each dispatch rather than replaying actions. This guarantees consistency regardless of reducer purity and works with any middleware stack.
|
|
755
|
+
|
|
756
|
+
<br />
|
|
757
|
+
|
|
758
|
+
---
|
|
759
|
+
|
|
760
|
+
<br />
|
|
761
|
+
|
|
762
|
+
## 🔧 DevTools
|
|
763
|
+
|
|
764
|
+
A floating development panel for inspecting tab-bridge state, tabs, and events in real time.
|
|
765
|
+
|
|
766
|
+
```tsx
|
|
767
|
+
import { TabSyncDevTools } from 'tab-bridge/react';
|
|
768
|
+
|
|
769
|
+
function App() {
|
|
770
|
+
return (
|
|
771
|
+
<TabSyncProvider options={{ initial: { count: 0 }, channel: 'app' }}>
|
|
772
|
+
<MyApp />
|
|
773
|
+
{process.env.NODE_ENV === 'development' && <TabSyncDevTools />}
|
|
774
|
+
</TabSyncProvider>
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
### Features
|
|
780
|
+
|
|
781
|
+
| Tab | Description |
|
|
782
|
+
|---|---|
|
|
783
|
+
| **State** | Live JSON view of current state + manual editing (textarea → Apply) |
|
|
784
|
+
| **Tabs** | Active tab list with leader badge and "you" indicator |
|
|
785
|
+
| **Log** | Real-time event stream — state changes, tab joins/leaves |
|
|
786
|
+
|
|
787
|
+
### Props
|
|
788
|
+
|
|
789
|
+
| Prop | Type | Default | Description |
|
|
790
|
+
|---|---|---|---|
|
|
791
|
+
| `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Panel position |
|
|
792
|
+
| `defaultOpen` | `boolean` | `false` | Start expanded |
|
|
793
|
+
|
|
794
|
+
**Tree-shakeable** — if you never import `TabSyncDevTools`, it won't appear in your production bundle.
|
|
795
|
+
|
|
796
|
+
<br />
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
<br />
|
|
801
|
+
|
|
802
|
+
## 📘 Next.js
|
|
803
|
+
|
|
804
|
+
Using tab-bridge with **Next.js App Router**? Since tab-bridge relies on browser APIs, all usage must be in Client Components.
|
|
805
|
+
|
|
806
|
+
```tsx
|
|
807
|
+
// app/providers/tab-sync-provider.tsx
|
|
808
|
+
'use client';
|
|
809
|
+
|
|
810
|
+
import { TabSyncProvider } from 'tab-bridge/react';
|
|
811
|
+
|
|
812
|
+
export function AppTabSyncProvider({ children }: { children: React.ReactNode }) {
|
|
813
|
+
return (
|
|
814
|
+
<TabSyncProvider options={{ initial: { count: 0 }, channel: 'my-app' }}>
|
|
815
|
+
{children}
|
|
816
|
+
</TabSyncProvider>
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
```tsx
|
|
822
|
+
// app/layout.tsx
|
|
823
|
+
import { AppTabSyncProvider } from './providers/tab-sync-provider';
|
|
824
|
+
|
|
825
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
826
|
+
return (
|
|
827
|
+
<html><body>
|
|
828
|
+
<AppTabSyncProvider>{children}</AppTabSyncProvider>
|
|
829
|
+
</body></html>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
> **Full guide**: See [`docs/NEXTJS.md`](./docs/NEXTJS.md) for SSR safety patterns, hydration mismatch prevention, `useEffect` initialization, and Zustand integration with Next.js.
|
|
835
|
+
|
|
836
|
+
<br />
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
<br />
|
|
841
|
+
|
|
516
842
|
## 🚨 Error Handling
|
|
517
843
|
|
|
518
844
|
Structured errors with error codes for precise `catch` handling:
|
|
@@ -641,6 +967,19 @@ import {
|
|
|
641
967
|
|
|
642
968
|
## 💡 Examples
|
|
643
969
|
|
|
970
|
+
### 🎯 Interactive Demos
|
|
971
|
+
|
|
972
|
+
Try these demos live — open multiple tabs to see real-time synchronization in action:
|
|
973
|
+
|
|
974
|
+
| Demo | Description | Features |
|
|
975
|
+
|:-----|:-----------|:---------|
|
|
976
|
+
| [**Collaborative Editor**](https://serbi2012.github.io/tab-bridge/collaborative-editor.html) | Multi-tab real-time text editing | State Sync, Typing Indicators |
|
|
977
|
+
| [**Shopping Cart**](https://serbi2012.github.io/tab-bridge/shopping-cart.html) | Cart synced across all tabs + persistent | State Sync, Persistence |
|
|
978
|
+
| [**Leader Dashboard**](https://serbi2012.github.io/tab-bridge/leader-dashboard.html) | Only leader fetches data, followers use RPC | Leader Election, RPC, callAll |
|
|
979
|
+
| [**Full Feature Demo**](https://serbi2012.github.io/tab-bridge/) | All features in one page | Everything |
|
|
980
|
+
|
|
981
|
+
### Code Examples
|
|
982
|
+
|
|
644
983
|
<details>
|
|
645
984
|
<summary><b>🔐 Shared Authentication State</b></summary>
|
|
646
985
|
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { R as RPCMap, T as
|
|
2
|
-
export {
|
|
1
|
+
import { R as RPCMap, T as TabSyncInstance, C as ChangeMeta, a as TabInfo, b as RPCCallAllResult } from './instance-hvEUHx6i.cjs';
|
|
2
|
+
export { c as RPCArgs, d as RPCResult } from './instance-hvEUHx6i.cjs';
|
|
3
|
+
import { T as TabSyncOptions, M as Middleware, a as MiddlewareContext, P as PersistOptions } from './options-DmHyGTL0.cjs';
|
|
4
|
+
export { L as LeaderOptions, b as MiddlewareResult } from './options-DmHyGTL0.cjs';
|
|
3
5
|
|
|
4
6
|
declare const PROTOCOL_VERSION = 1;
|
|
5
7
|
interface StateUpdatePayload {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { R as RPCMap, T as
|
|
2
|
-
export {
|
|
1
|
+
import { R as RPCMap, T as TabSyncInstance, C as ChangeMeta, a as TabInfo, b as RPCCallAllResult } from './instance-hvEUHx6i.js';
|
|
2
|
+
export { c as RPCArgs, d as RPCResult } from './instance-hvEUHx6i.js';
|
|
3
|
+
import { T as TabSyncOptions, M as Middleware, a as MiddlewareContext, P as PersistOptions } from './options-iN7Rnvwj.js';
|
|
4
|
+
export { L as LeaderOptions, b as MiddlewareResult } from './options-iN7Rnvwj.js';
|
|
3
5
|
|
|
4
6
|
declare const PROTOCOL_VERSION = 1;
|
|
5
7
|
interface StateUpdatePayload {
|
|
@@ -13,65 +13,6 @@ interface ChangeMeta {
|
|
|
13
13
|
readonly timestamp: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
interface MiddlewareContext<TState extends Record<string, unknown>> {
|
|
17
|
-
readonly key: keyof TState;
|
|
18
|
-
readonly value: unknown;
|
|
19
|
-
readonly previousValue: unknown;
|
|
20
|
-
readonly meta: ChangeMeta;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Return `false` to reject the change, `{ value }` to transform it,
|
|
24
|
-
* or `void`/`undefined` to pass through unchanged.
|
|
25
|
-
*/
|
|
26
|
-
type MiddlewareResult = {
|
|
27
|
-
value: unknown;
|
|
28
|
-
} | false;
|
|
29
|
-
interface Middleware<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
30
|
-
readonly name: string;
|
|
31
|
-
/** Intercept local `set` / `patch` calls before they are applied. */
|
|
32
|
-
onSet?: (ctx: MiddlewareContext<TState>) => MiddlewareResult | void;
|
|
33
|
-
/** Called after any state change (local or remote) has been committed. */
|
|
34
|
-
afterChange?: (key: keyof TState, value: unknown, meta: ChangeMeta) => void;
|
|
35
|
-
/** Cleanup when the instance is destroyed. */
|
|
36
|
-
onDestroy?: () => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface PersistOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
40
|
-
/** Storage key. Default: `'tab-sync:state'` */
|
|
41
|
-
key?: string;
|
|
42
|
-
/** Only persist these keys (whitelist). */
|
|
43
|
-
include?: (keyof TState)[];
|
|
44
|
-
/** Exclude these keys from persistence (blacklist). */
|
|
45
|
-
exclude?: (keyof TState)[];
|
|
46
|
-
/** Custom serializer. Default: `JSON.stringify` */
|
|
47
|
-
serialize?: (state: Partial<TState>) => string;
|
|
48
|
-
/** Custom deserializer. Default: `JSON.parse` */
|
|
49
|
-
deserialize?: (raw: string) => Partial<TState>;
|
|
50
|
-
/** Debounce persistence writes in ms. Default: `100` */
|
|
51
|
-
debounce?: number;
|
|
52
|
-
/** Custom storage backend. Default: `localStorage` */
|
|
53
|
-
storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
|
54
|
-
/**
|
|
55
|
-
* Schema version for state migration. When the persisted version
|
|
56
|
-
* differs from this value, `migrate` is called.
|
|
57
|
-
*/
|
|
58
|
-
version?: number;
|
|
59
|
-
/**
|
|
60
|
-
* Migration function called when persisted version differs from current.
|
|
61
|
-
*
|
|
62
|
-
* ```ts
|
|
63
|
-
* persist: {
|
|
64
|
-
* version: 2,
|
|
65
|
-
* migrate: (oldState, oldVersion) => ({
|
|
66
|
-
* ...oldState,
|
|
67
|
-
* newField: oldVersion < 2 ? 'default' : oldState.newField,
|
|
68
|
-
* }),
|
|
69
|
-
* }
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
migrate?: (oldState: Partial<TState>, oldVersion: number) => Partial<TState>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
16
|
/**
|
|
76
17
|
* Define your RPC contract for full type inference:
|
|
77
18
|
*
|
|
@@ -99,37 +40,6 @@ type RPCArgs<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMa
|
|
|
99
40
|
/** Resolve result type for a method. Falls back to `unknown` for unregistered methods. */
|
|
100
41
|
type RPCResult<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMap[M]['result'] : unknown;
|
|
101
42
|
|
|
102
|
-
interface LeaderOptions {
|
|
103
|
-
/** Heartbeat interval in ms. Default: `2000` */
|
|
104
|
-
heartbeatInterval?: number;
|
|
105
|
-
/** Leader timeout in ms. Default: `6000` */
|
|
106
|
-
leaderTimeout?: number;
|
|
107
|
-
}
|
|
108
|
-
interface TabSyncOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
109
|
-
/** Initial state used before sync completes. */
|
|
110
|
-
initial?: TState;
|
|
111
|
-
/** Channel name — only tabs sharing the same name communicate. Default: `'tab-sync'` */
|
|
112
|
-
channel?: string;
|
|
113
|
-
/** Force a specific transport layer. Default: auto-detect. */
|
|
114
|
-
transport?: 'broadcast-channel' | 'local-storage';
|
|
115
|
-
/** Custom merge function for LWW conflict resolution. */
|
|
116
|
-
merge?: (localValue: unknown, remoteValue: unknown, key: keyof TState) => unknown;
|
|
117
|
-
/** Enable leader election. Default: `true` */
|
|
118
|
-
leader?: boolean | LeaderOptions;
|
|
119
|
-
/** Heartbeat interval in ms. Default: `2000` */
|
|
120
|
-
heartbeatInterval?: number;
|
|
121
|
-
/** Leader timeout in ms. Default: `6000` */
|
|
122
|
-
leaderTimeout?: number;
|
|
123
|
-
/** Enable debug logging. Default: `false` */
|
|
124
|
-
debug?: boolean;
|
|
125
|
-
/** Persist state across page reloads. `true` uses defaults, or pass options. */
|
|
126
|
-
persist?: PersistOptions<TState> | boolean;
|
|
127
|
-
/** Middleware pipeline for intercepting state changes. */
|
|
128
|
-
middlewares?: Middleware<TState>[];
|
|
129
|
-
/** Error callback for non-fatal errors (storage, channel, etc.). */
|
|
130
|
-
onError?: (error: Error) => void;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
43
|
interface TabSyncInstance<TState extends Record<string, unknown>, TRPCMap extends RPCMap = RPCMap> {
|
|
134
44
|
/** Read a single value by key. */
|
|
135
45
|
get<K extends keyof TState>(key: K): TState[K];
|
|
@@ -281,4 +191,4 @@ interface TabSyncInstance<TState extends Record<string, unknown>, TRPCMap extend
|
|
|
281
191
|
readonly ready: boolean;
|
|
282
192
|
}
|
|
283
193
|
|
|
284
|
-
export type { ChangeMeta as C,
|
|
194
|
+
export type { ChangeMeta as C, RPCMap as R, TabSyncInstance as T, TabInfo as a, RPCCallAllResult as b, RPCArgs as c, RPCResult as d };
|
|
@@ -13,65 +13,6 @@ interface ChangeMeta {
|
|
|
13
13
|
readonly timestamp: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
interface MiddlewareContext<TState extends Record<string, unknown>> {
|
|
17
|
-
readonly key: keyof TState;
|
|
18
|
-
readonly value: unknown;
|
|
19
|
-
readonly previousValue: unknown;
|
|
20
|
-
readonly meta: ChangeMeta;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Return `false` to reject the change, `{ value }` to transform it,
|
|
24
|
-
* or `void`/`undefined` to pass through unchanged.
|
|
25
|
-
*/
|
|
26
|
-
type MiddlewareResult = {
|
|
27
|
-
value: unknown;
|
|
28
|
-
} | false;
|
|
29
|
-
interface Middleware<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
30
|
-
readonly name: string;
|
|
31
|
-
/** Intercept local `set` / `patch` calls before they are applied. */
|
|
32
|
-
onSet?: (ctx: MiddlewareContext<TState>) => MiddlewareResult | void;
|
|
33
|
-
/** Called after any state change (local or remote) has been committed. */
|
|
34
|
-
afterChange?: (key: keyof TState, value: unknown, meta: ChangeMeta) => void;
|
|
35
|
-
/** Cleanup when the instance is destroyed. */
|
|
36
|
-
onDestroy?: () => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface PersistOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
40
|
-
/** Storage key. Default: `'tab-sync:state'` */
|
|
41
|
-
key?: string;
|
|
42
|
-
/** Only persist these keys (whitelist). */
|
|
43
|
-
include?: (keyof TState)[];
|
|
44
|
-
/** Exclude these keys from persistence (blacklist). */
|
|
45
|
-
exclude?: (keyof TState)[];
|
|
46
|
-
/** Custom serializer. Default: `JSON.stringify` */
|
|
47
|
-
serialize?: (state: Partial<TState>) => string;
|
|
48
|
-
/** Custom deserializer. Default: `JSON.parse` */
|
|
49
|
-
deserialize?: (raw: string) => Partial<TState>;
|
|
50
|
-
/** Debounce persistence writes in ms. Default: `100` */
|
|
51
|
-
debounce?: number;
|
|
52
|
-
/** Custom storage backend. Default: `localStorage` */
|
|
53
|
-
storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
|
|
54
|
-
/**
|
|
55
|
-
* Schema version for state migration. When the persisted version
|
|
56
|
-
* differs from this value, `migrate` is called.
|
|
57
|
-
*/
|
|
58
|
-
version?: number;
|
|
59
|
-
/**
|
|
60
|
-
* Migration function called when persisted version differs from current.
|
|
61
|
-
*
|
|
62
|
-
* ```ts
|
|
63
|
-
* persist: {
|
|
64
|
-
* version: 2,
|
|
65
|
-
* migrate: (oldState, oldVersion) => ({
|
|
66
|
-
* ...oldState,
|
|
67
|
-
* newField: oldVersion < 2 ? 'default' : oldState.newField,
|
|
68
|
-
* }),
|
|
69
|
-
* }
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
migrate?: (oldState: Partial<TState>, oldVersion: number) => Partial<TState>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
16
|
/**
|
|
76
17
|
* Define your RPC contract for full type inference:
|
|
77
18
|
*
|
|
@@ -99,37 +40,6 @@ type RPCArgs<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMa
|
|
|
99
40
|
/** Resolve result type for a method. Falls back to `unknown` for unregistered methods. */
|
|
100
41
|
type RPCResult<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMap[M]['result'] : unknown;
|
|
101
42
|
|
|
102
|
-
interface LeaderOptions {
|
|
103
|
-
/** Heartbeat interval in ms. Default: `2000` */
|
|
104
|
-
heartbeatInterval?: number;
|
|
105
|
-
/** Leader timeout in ms. Default: `6000` */
|
|
106
|
-
leaderTimeout?: number;
|
|
107
|
-
}
|
|
108
|
-
interface TabSyncOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
|
|
109
|
-
/** Initial state used before sync completes. */
|
|
110
|
-
initial?: TState;
|
|
111
|
-
/** Channel name — only tabs sharing the same name communicate. Default: `'tab-sync'` */
|
|
112
|
-
channel?: string;
|
|
113
|
-
/** Force a specific transport layer. Default: auto-detect. */
|
|
114
|
-
transport?: 'broadcast-channel' | 'local-storage';
|
|
115
|
-
/** Custom merge function for LWW conflict resolution. */
|
|
116
|
-
merge?: (localValue: unknown, remoteValue: unknown, key: keyof TState) => unknown;
|
|
117
|
-
/** Enable leader election. Default: `true` */
|
|
118
|
-
leader?: boolean | LeaderOptions;
|
|
119
|
-
/** Heartbeat interval in ms. Default: `2000` */
|
|
120
|
-
heartbeatInterval?: number;
|
|
121
|
-
/** Leader timeout in ms. Default: `6000` */
|
|
122
|
-
leaderTimeout?: number;
|
|
123
|
-
/** Enable debug logging. Default: `false` */
|
|
124
|
-
debug?: boolean;
|
|
125
|
-
/** Persist state across page reloads. `true` uses defaults, or pass options. */
|
|
126
|
-
persist?: PersistOptions<TState> | boolean;
|
|
127
|
-
/** Middleware pipeline for intercepting state changes. */
|
|
128
|
-
middlewares?: Middleware<TState>[];
|
|
129
|
-
/** Error callback for non-fatal errors (storage, channel, etc.). */
|
|
130
|
-
onError?: (error: Error) => void;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
43
|
interface TabSyncInstance<TState extends Record<string, unknown>, TRPCMap extends RPCMap = RPCMap> {
|
|
134
44
|
/** Read a single value by key. */
|
|
135
45
|
get<K extends keyof TState>(key: K): TState[K];
|
|
@@ -281,4 +191,4 @@ interface TabSyncInstance<TState extends Record<string, unknown>, TRPCMap extend
|
|
|
281
191
|
readonly ready: boolean;
|
|
282
192
|
}
|
|
283
193
|
|
|
284
|
-
export type { ChangeMeta as C,
|
|
194
|
+
export type { ChangeMeta as C, RPCMap as R, TabSyncInstance as T, TabInfo as a, RPCCallAllResult as b, RPCArgs as c, RPCResult as d };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkTGEXRVAL_cjs = require('../chunk-TGEXRVAL.cjs');
|
|
4
|
+
var vanilla = require('jotai/vanilla');
|
|
5
|
+
|
|
6
|
+
function atomWithTabSync(key, initialValue, options) {
|
|
7
|
+
const base_channel = options?.channel ?? "tab-sync-jotai";
|
|
8
|
+
const instance_channel = `${base_channel}:${key}`;
|
|
9
|
+
let sync_instance = null;
|
|
10
|
+
const base_atom = vanilla.atom(initialValue);
|
|
11
|
+
base_atom.onMount = (setAtom) => {
|
|
12
|
+
if (!chunkTGEXRVAL_cjs.isBrowser) return;
|
|
13
|
+
const instance = chunkTGEXRVAL_cjs.createTabSync({
|
|
14
|
+
channel: instance_channel,
|
|
15
|
+
initial: { value: initialValue },
|
|
16
|
+
transport: options?.transport,
|
|
17
|
+
debug: options?.debug,
|
|
18
|
+
onError: options?.onError
|
|
19
|
+
});
|
|
20
|
+
sync_instance = instance;
|
|
21
|
+
options?.onSyncReady?.(instance);
|
|
22
|
+
const unsub = instance.on("value", (remote_value, meta) => {
|
|
23
|
+
if (!meta.isLocal) {
|
|
24
|
+
setAtom(remote_value);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return () => {
|
|
28
|
+
unsub();
|
|
29
|
+
instance.destroy();
|
|
30
|
+
sync_instance = null;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const sync_atom = vanilla.atom(
|
|
34
|
+
(get) => get(base_atom),
|
|
35
|
+
(get, set, update) => {
|
|
36
|
+
const next_value = typeof update === "function" ? update(get(base_atom)) : update;
|
|
37
|
+
set(base_atom, next_value);
|
|
38
|
+
sync_instance?.set("value", next_value);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
return sync_atom;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
exports.atomWithTabSync = atomWithTabSync;
|