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 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 Middleware
80
- One-line integration — `tabSync()` wraps any Zustand store for cross-tab sync
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 };