tab-bridge 0.3.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 +152 -3
- 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/react/index.cjs +332 -0
- package/dist/react/index.d.cts +23 -1
- package/dist/react/index.d.ts +23 -1
- 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/package.json +35 -1
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) · [**Zustand**](#-zustand) · [**Next.js**](#-nextjs) · [**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,8 +76,11 @@ 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
|
|
80
|
-
|
|
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
|
|
81
84
|
|
|
82
85
|
#### 📦 Zero Dependencies
|
|
83
86
|
Native browser APIs only, ~4KB gzipped, fully tree-shakable
|
|
@@ -650,6 +653,152 @@ const useStore = create(
|
|
|
650
653
|
|
|
651
654
|
<br />
|
|
652
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
|
+
|
|
653
802
|
## 📘 Next.js
|
|
654
803
|
|
|
655
804
|
Using tab-bridge with **Next.js App Router**? Since tab-bridge relies on browser APIs, all usage must be in Client Components.
|
|
@@ -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;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { WritableAtom } from 'jotai/vanilla';
|
|
2
|
+
import { T as TabSyncInstance } from '../instance-hvEUHx6i.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for `atomWithTabSync`.
|
|
6
|
+
*
|
|
7
|
+
* All atoms sharing the same `channel` reuse a single `createTabSync`
|
|
8
|
+
* instance internally. Channel-level options (`transport`, `debug`,
|
|
9
|
+
* `onError`) are taken from the **first** atom that creates the
|
|
10
|
+
* instance on that channel.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { atomWithTabSync } from 'tab-bridge/jotai';
|
|
15
|
+
*
|
|
16
|
+
* const countAtom = atomWithTabSync('count', 0, {
|
|
17
|
+
* channel: 'my-app',
|
|
18
|
+
* debug: true,
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
interface AtomWithTabSyncOptions {
|
|
23
|
+
/** Channel name for cross-tab communication. @default 'tab-sync-jotai' */
|
|
24
|
+
channel?: string;
|
|
25
|
+
/** Force a specific transport layer. @default auto-detect */
|
|
26
|
+
transport?: 'broadcast-channel' | 'local-storage';
|
|
27
|
+
/** Enable debug logging. @default false */
|
|
28
|
+
debug?: boolean;
|
|
29
|
+
/** Error callback for non-fatal errors (channel failures, etc.). */
|
|
30
|
+
onError?: (error: Error) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Callback invoked when the underlying `TabSyncInstance` is ready.
|
|
33
|
+
* Useful for accessing advanced features (RPC, leader election, etc.).
|
|
34
|
+
* Called once per shared instance — only the first atom to trigger
|
|
35
|
+
* instance creation will have its callback invoked.
|
|
36
|
+
*/
|
|
37
|
+
onSyncReady?: (instance: TabSyncInstance<Record<string, unknown>>) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type SetStateAction<T> = T | ((prev: T) => T);
|
|
41
|
+
/**
|
|
42
|
+
* Creates a Jotai atom whose value is automatically synchronised across
|
|
43
|
+
* browser tabs via `tab-bridge`.
|
|
44
|
+
*
|
|
45
|
+
* Each atom creates its own `createTabSync` instance scoped to the channel
|
|
46
|
+
* `${channel}:${key}`. The instance is created when the atom is first
|
|
47
|
+
* subscribed to (React component mount / `store.sub`) and destroyed on
|
|
48
|
+
* the last unsubscription (unmount).
|
|
49
|
+
*
|
|
50
|
+
* The atom behaves like a normal writable atom — use `useAtom` in React
|
|
51
|
+
* or `store.get` / `store.set` with Jotai's vanilla store.
|
|
52
|
+
*
|
|
53
|
+
* @param key - Unique key identifying this piece of state within the channel.
|
|
54
|
+
* @param initialValue - Default value used when no synced state exists yet.
|
|
55
|
+
* @param options - Channel and transport configuration.
|
|
56
|
+
* @returns A writable Jotai atom.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { atomWithTabSync } from 'tab-bridge/jotai';
|
|
61
|
+
*
|
|
62
|
+
* const countAtom = atomWithTabSync('count', 0, { channel: 'my-app' });
|
|
63
|
+
* const themeAtom = atomWithTabSync('theme', 'light', { channel: 'my-app' });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function atomWithTabSync<T>(key: string, initialValue: T, options?: AtomWithTabSyncOptions): WritableAtom<T, [SetStateAction<T>], void>;
|
|
67
|
+
|
|
68
|
+
export { type AtomWithTabSyncOptions, atomWithTabSync };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { WritableAtom } from 'jotai/vanilla';
|
|
2
|
+
import { T as TabSyncInstance } from '../instance-hvEUHx6i.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for `atomWithTabSync`.
|
|
6
|
+
*
|
|
7
|
+
* All atoms sharing the same `channel` reuse a single `createTabSync`
|
|
8
|
+
* instance internally. Channel-level options (`transport`, `debug`,
|
|
9
|
+
* `onError`) are taken from the **first** atom that creates the
|
|
10
|
+
* instance on that channel.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { atomWithTabSync } from 'tab-bridge/jotai';
|
|
15
|
+
*
|
|
16
|
+
* const countAtom = atomWithTabSync('count', 0, {
|
|
17
|
+
* channel: 'my-app',
|
|
18
|
+
* debug: true,
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
interface AtomWithTabSyncOptions {
|
|
23
|
+
/** Channel name for cross-tab communication. @default 'tab-sync-jotai' */
|
|
24
|
+
channel?: string;
|
|
25
|
+
/** Force a specific transport layer. @default auto-detect */
|
|
26
|
+
transport?: 'broadcast-channel' | 'local-storage';
|
|
27
|
+
/** Enable debug logging. @default false */
|
|
28
|
+
debug?: boolean;
|
|
29
|
+
/** Error callback for non-fatal errors (channel failures, etc.). */
|
|
30
|
+
onError?: (error: Error) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Callback invoked when the underlying `TabSyncInstance` is ready.
|
|
33
|
+
* Useful for accessing advanced features (RPC, leader election, etc.).
|
|
34
|
+
* Called once per shared instance — only the first atom to trigger
|
|
35
|
+
* instance creation will have its callback invoked.
|
|
36
|
+
*/
|
|
37
|
+
onSyncReady?: (instance: TabSyncInstance<Record<string, unknown>>) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type SetStateAction<T> = T | ((prev: T) => T);
|
|
41
|
+
/**
|
|
42
|
+
* Creates a Jotai atom whose value is automatically synchronised across
|
|
43
|
+
* browser tabs via `tab-bridge`.
|
|
44
|
+
*
|
|
45
|
+
* Each atom creates its own `createTabSync` instance scoped to the channel
|
|
46
|
+
* `${channel}:${key}`. The instance is created when the atom is first
|
|
47
|
+
* subscribed to (React component mount / `store.sub`) and destroyed on
|
|
48
|
+
* the last unsubscription (unmount).
|
|
49
|
+
*
|
|
50
|
+
* The atom behaves like a normal writable atom — use `useAtom` in React
|
|
51
|
+
* or `store.get` / `store.set` with Jotai's vanilla store.
|
|
52
|
+
*
|
|
53
|
+
* @param key - Unique key identifying this piece of state within the channel.
|
|
54
|
+
* @param initialValue - Default value used when no synced state exists yet.
|
|
55
|
+
* @param options - Channel and transport configuration.
|
|
56
|
+
* @returns A writable Jotai atom.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { atomWithTabSync } from 'tab-bridge/jotai';
|
|
61
|
+
*
|
|
62
|
+
* const countAtom = atomWithTabSync('count', 0, { channel: 'my-app' });
|
|
63
|
+
* const themeAtom = atomWithTabSync('theme', 'light', { channel: 'my-app' });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
declare function atomWithTabSync<T>(key: string, initialValue: T, options?: AtomWithTabSyncOptions): WritableAtom<T, [SetStateAction<T>], void>;
|
|
67
|
+
|
|
68
|
+
export { type AtomWithTabSyncOptions, atomWithTabSync };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isBrowser, createTabSync } from '../chunk-4JDWAUYM.js';
|
|
2
|
+
import { atom } from 'jotai/vanilla';
|
|
3
|
+
|
|
4
|
+
function atomWithTabSync(key, initialValue, options) {
|
|
5
|
+
const base_channel = options?.channel ?? "tab-sync-jotai";
|
|
6
|
+
const instance_channel = `${base_channel}:${key}`;
|
|
7
|
+
let sync_instance = null;
|
|
8
|
+
const base_atom = atom(initialValue);
|
|
9
|
+
base_atom.onMount = (setAtom) => {
|
|
10
|
+
if (!isBrowser) return;
|
|
11
|
+
const instance = createTabSync({
|
|
12
|
+
channel: instance_channel,
|
|
13
|
+
initial: { value: initialValue },
|
|
14
|
+
transport: options?.transport,
|
|
15
|
+
debug: options?.debug,
|
|
16
|
+
onError: options?.onError
|
|
17
|
+
});
|
|
18
|
+
sync_instance = instance;
|
|
19
|
+
options?.onSyncReady?.(instance);
|
|
20
|
+
const unsub = instance.on("value", (remote_value, meta) => {
|
|
21
|
+
if (!meta.isLocal) {
|
|
22
|
+
setAtom(remote_value);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return () => {
|
|
26
|
+
unsub();
|
|
27
|
+
instance.destroy();
|
|
28
|
+
sync_instance = null;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
const sync_atom = atom(
|
|
32
|
+
(get) => get(base_atom),
|
|
33
|
+
(get, set, update) => {
|
|
34
|
+
const next_value = typeof update === "function" ? update(get(base_atom)) : update;
|
|
35
|
+
set(base_atom, next_value);
|
|
36
|
+
sync_instance?.set("value", next_value);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
return sync_atom;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { atomWithTabSync };
|