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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkBQCNBNBT_cjs = require('../chunk-BQCNBNBT.cjs');
3
+ var chunkTGEXRVAL_cjs = require('../chunk-TGEXRVAL.cjs');
4
4
  var react = require('react');
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
 
@@ -11,7 +11,7 @@ function TabSyncProvider({
11
11
  }) {
12
12
  const instanceRef = react.useRef(null);
13
13
  if (!instanceRef.current) {
14
- instanceRef.current = chunkBQCNBNBT_cjs.createTabSync(options);
14
+ instanceRef.current = chunkTGEXRVAL_cjs.createTabSync(options);
15
15
  }
16
16
  react.useEffect(() => {
17
17
  return () => {
@@ -26,7 +26,7 @@ function useInstance(options) {
26
26
  const contextInstance = react.useContext(TabSyncContext);
27
27
  const [ownInstance] = react.useState(() => {
28
28
  if (!contextInstance && options) {
29
- return chunkBQCNBNBT_cjs.createTabSync(options);
29
+ return chunkTGEXRVAL_cjs.createTabSync(options);
30
30
  }
31
31
  return null;
32
32
  });
@@ -127,14 +127,14 @@ function useTabSyncSelector(selector, isEqual) {
127
127
  initializedRef.current = true;
128
128
  }
129
129
  const subscribe = react.useCallback(
130
- (onStoreChange) => instance.onChange(() => {
131
- const next = selectorRef.current(instance.getAll());
132
- const equal = isEqualRef.current ?? Object.is;
133
- if (!equal(resultRef.current, next)) {
130
+ (onStoreChange) => instance.select(
131
+ (state) => selectorRef.current(state),
132
+ (next) => {
134
133
  resultRef.current = next;
135
134
  onStoreChange();
136
- }
137
- }),
135
+ },
136
+ { isEqual: isEqualRef.current }
137
+ ),
138
138
  [instance]
139
139
  );
140
140
  const getSnapshot = react.useCallback(() => resultRef.current, []);
@@ -165,10 +165,83 @@ function useIsLeader() {
165
165
  const getServerSnapshot = react.useCallback(() => SERVER_SNAPSHOT, []);
166
166
  return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
167
167
  }
168
+ var SERVER_SNAPSHOT2 = [];
169
+ function useTabs() {
170
+ const instance = react.useContext(TabSyncContext);
171
+ if (!instance) {
172
+ throw new Error("useTabs must be used within a <TabSyncProvider>");
173
+ }
174
+ const tabsRef = react.useRef(instance.getTabs());
175
+ const subscribe = react.useCallback(
176
+ (onStoreChange) => instance.onTabChange((tabs) => {
177
+ tabsRef.current = tabs;
178
+ onStoreChange();
179
+ }),
180
+ [instance]
181
+ );
182
+ const getSnapshot = react.useCallback(() => tabsRef.current, []);
183
+ const getServerSnapshot = react.useCallback(() => SERVER_SNAPSHOT2, []);
184
+ return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
185
+ }
186
+ function useLeaderInfo() {
187
+ const instance = react.useContext(TabSyncContext);
188
+ if (!instance) {
189
+ throw new Error("useLeaderInfo must be used within a <TabSyncProvider>");
190
+ }
191
+ const leaderRef = react.useRef(instance.getLeader());
192
+ const subscribe = react.useCallback(
193
+ (onStoreChange) => {
194
+ const unsubs = [];
195
+ unsubs.push(
196
+ instance.onTabChange(() => {
197
+ const next = instance.getLeader();
198
+ if (next?.id !== leaderRef.current?.id) {
199
+ leaderRef.current = next;
200
+ onStoreChange();
201
+ }
202
+ })
203
+ );
204
+ unsubs.push(
205
+ instance.onLeader(() => {
206
+ leaderRef.current = instance.getLeader();
207
+ onStoreChange();
208
+ return () => {
209
+ leaderRef.current = instance.getLeader();
210
+ onStoreChange();
211
+ };
212
+ })
213
+ );
214
+ return () => {
215
+ for (const u of unsubs) u();
216
+ };
217
+ },
218
+ [instance]
219
+ );
220
+ const getSnapshot = react.useCallback(() => leaderRef.current, []);
221
+ const getServerSnapshot = react.useCallback(() => null, []);
222
+ return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
223
+ }
224
+ function useTabSyncActions() {
225
+ const instance = react.useContext(TabSyncContext);
226
+ if (!instance) {
227
+ throw new Error("useTabSyncActions must be used within a <TabSyncProvider>");
228
+ }
229
+ return react.useMemo(
230
+ () => ({
231
+ set: instance.set.bind(instance),
232
+ patch: instance.patch.bind(instance),
233
+ transaction: instance.transaction.bind(instance)
234
+ }),
235
+ [instance]
236
+ );
237
+ }
168
238
 
169
239
  exports.TabSyncContext = TabSyncContext;
170
240
  exports.TabSyncProvider = TabSyncProvider;
171
241
  exports.useIsLeader = useIsLeader;
242
+ exports.useLeaderInfo = useLeaderInfo;
172
243
  exports.useTabSync = useTabSync;
244
+ exports.useTabSyncActions = useTabSyncActions;
173
245
  exports.useTabSyncSelector = useTabSyncSelector;
174
246
  exports.useTabSyncValue = useTabSyncValue;
247
+ exports.useTabs = useTabs;
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
- import { T as TabSyncOptions, a as TabSyncInstance, R as RPCMap, c as TabInfo } from '../types-BtK4ixKz.cjs';
4
+ import { T as TabSyncOptions } from '../options-DmHyGTL0.cjs';
5
+ import { T as TabSyncInstance, R as RPCMap, a as TabInfo } from '../instance-hvEUHx6i.cjs';
5
6
 
6
7
  interface TabSyncProviderProps<TState extends Record<string, unknown>> {
7
8
  options: TabSyncOptions<TState>;
@@ -35,6 +36,9 @@ declare function useTabSyncValue<TState extends Record<string, unknown>, K exten
35
36
  * Subscribe to a **derived value** from the synced state.
36
37
  * Only re-renders when the selector's output actually changes.
37
38
  *
39
+ * Uses `instance.select()` internally so only changed-key
40
+ * evaluations trigger the selector, reducing unnecessary work.
41
+ *
38
42
  * ```tsx
39
43
  * const doneCount = useTabSyncSelector(
40
44
  * (s) => s.todos.filter(t => t.done).length,
@@ -58,4 +62,46 @@ declare function useTabSyncSelector<TState extends Record<string, unknown>, TRes
58
62
  */
59
63
  declare function useIsLeader(): boolean;
60
64
 
61
- export { TabSyncContext, TabSyncProvider, type TabSyncProviderProps, useIsLeader, useTabSync, useTabSyncSelector, useTabSyncValue };
65
+ /**
66
+ * Subscribe to the list of active tabs. Re-renders when tabs join or leave.
67
+ *
68
+ * ```tsx
69
+ * const tabs = useTabs();
70
+ * // tabs: TabInfo[]
71
+ * ```
72
+ *
73
+ * Must be used within a `<TabSyncProvider>`.
74
+ */
75
+ declare function useTabs(): TabInfo[];
76
+
77
+ /**
78
+ * Subscribe to the current leader's full `TabInfo`.
79
+ * Returns `null` if no leader has been elected yet.
80
+ *
81
+ * ```tsx
82
+ * const leader = useLeaderInfo();
83
+ * // leader: TabInfo | null
84
+ * ```
85
+ *
86
+ * Must be used within a `<TabSyncProvider>`.
87
+ */
88
+ declare function useLeaderInfo(): TabInfo | null;
89
+
90
+ interface TabSyncActions<TState extends Record<string, unknown>> {
91
+ set: TabSyncInstance<TState>['set'];
92
+ patch: TabSyncInstance<TState>['patch'];
93
+ transaction: TabSyncInstance<TState>['transaction'];
94
+ }
95
+ /**
96
+ * Returns `set`, `patch`, and `transaction` without subscribing to state.
97
+ * Components using only this hook never re-render due to state changes.
98
+ *
99
+ * ```tsx
100
+ * const { set, patch, transaction } = useTabSyncActions();
101
+ * ```
102
+ *
103
+ * Must be used within a `<TabSyncProvider>`.
104
+ */
105
+ declare function useTabSyncActions<TState extends Record<string, unknown> = Record<string, unknown>>(): TabSyncActions<TState>;
106
+
107
+ export { type TabSyncActions, TabSyncContext, TabSyncProvider, type TabSyncProviderProps, useIsLeader, useLeaderInfo, useTabSync, useTabSyncActions, useTabSyncSelector, useTabSyncValue, useTabs };
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
- import { T as TabSyncOptions, a as TabSyncInstance, R as RPCMap, c as TabInfo } from '../types-BtK4ixKz.js';
4
+ import { T as TabSyncOptions } from '../options-iN7Rnvwj.js';
5
+ import { T as TabSyncInstance, R as RPCMap, a as TabInfo } from '../instance-hvEUHx6i.js';
5
6
 
6
7
  interface TabSyncProviderProps<TState extends Record<string, unknown>> {
7
8
  options: TabSyncOptions<TState>;
@@ -35,6 +36,9 @@ declare function useTabSyncValue<TState extends Record<string, unknown>, K exten
35
36
  * Subscribe to a **derived value** from the synced state.
36
37
  * Only re-renders when the selector's output actually changes.
37
38
  *
39
+ * Uses `instance.select()` internally so only changed-key
40
+ * evaluations trigger the selector, reducing unnecessary work.
41
+ *
38
42
  * ```tsx
39
43
  * const doneCount = useTabSyncSelector(
40
44
  * (s) => s.todos.filter(t => t.done).length,
@@ -58,4 +62,46 @@ declare function useTabSyncSelector<TState extends Record<string, unknown>, TRes
58
62
  */
59
63
  declare function useIsLeader(): boolean;
60
64
 
61
- export { TabSyncContext, TabSyncProvider, type TabSyncProviderProps, useIsLeader, useTabSync, useTabSyncSelector, useTabSyncValue };
65
+ /**
66
+ * Subscribe to the list of active tabs. Re-renders when tabs join or leave.
67
+ *
68
+ * ```tsx
69
+ * const tabs = useTabs();
70
+ * // tabs: TabInfo[]
71
+ * ```
72
+ *
73
+ * Must be used within a `<TabSyncProvider>`.
74
+ */
75
+ declare function useTabs(): TabInfo[];
76
+
77
+ /**
78
+ * Subscribe to the current leader's full `TabInfo`.
79
+ * Returns `null` if no leader has been elected yet.
80
+ *
81
+ * ```tsx
82
+ * const leader = useLeaderInfo();
83
+ * // leader: TabInfo | null
84
+ * ```
85
+ *
86
+ * Must be used within a `<TabSyncProvider>`.
87
+ */
88
+ declare function useLeaderInfo(): TabInfo | null;
89
+
90
+ interface TabSyncActions<TState extends Record<string, unknown>> {
91
+ set: TabSyncInstance<TState>['set'];
92
+ patch: TabSyncInstance<TState>['patch'];
93
+ transaction: TabSyncInstance<TState>['transaction'];
94
+ }
95
+ /**
96
+ * Returns `set`, `patch`, and `transaction` without subscribing to state.
97
+ * Components using only this hook never re-render due to state changes.
98
+ *
99
+ * ```tsx
100
+ * const { set, patch, transaction } = useTabSyncActions();
101
+ * ```
102
+ *
103
+ * Must be used within a `<TabSyncProvider>`.
104
+ */
105
+ declare function useTabSyncActions<TState extends Record<string, unknown> = Record<string, unknown>>(): TabSyncActions<TState>;
106
+
107
+ export { type TabSyncActions, TabSyncContext, TabSyncProvider, type TabSyncProviderProps, useIsLeader, useLeaderInfo, useTabSync, useTabSyncActions, useTabSyncSelector, useTabSyncValue, useTabs };
@@ -1,5 +1,5 @@
1
- import { createTabSync } from '../chunk-42VOZR6E.js';
2
- import { createContext, useRef, useEffect, useCallback, useSyncExternalStore, useContext, useState } from 'react';
1
+ import { createTabSync } from '../chunk-4JDWAUYM.js';
2
+ import { createContext, useRef, useEffect, useCallback, useSyncExternalStore, useContext, useMemo, useState } from 'react';
3
3
  import { jsx } from 'react/jsx-runtime';
4
4
 
5
5
  var TabSyncContext = createContext(null);
@@ -125,14 +125,14 @@ function useTabSyncSelector(selector, isEqual) {
125
125
  initializedRef.current = true;
126
126
  }
127
127
  const subscribe = useCallback(
128
- (onStoreChange) => instance.onChange(() => {
129
- const next = selectorRef.current(instance.getAll());
130
- const equal = isEqualRef.current ?? Object.is;
131
- if (!equal(resultRef.current, next)) {
128
+ (onStoreChange) => instance.select(
129
+ (state) => selectorRef.current(state),
130
+ (next) => {
132
131
  resultRef.current = next;
133
132
  onStoreChange();
134
- }
135
- }),
133
+ },
134
+ { isEqual: isEqualRef.current }
135
+ ),
136
136
  [instance]
137
137
  );
138
138
  const getSnapshot = useCallback(() => resultRef.current, []);
@@ -163,5 +163,75 @@ function useIsLeader() {
163
163
  const getServerSnapshot = useCallback(() => SERVER_SNAPSHOT, []);
164
164
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
165
165
  }
166
+ var SERVER_SNAPSHOT2 = [];
167
+ function useTabs() {
168
+ const instance = useContext(TabSyncContext);
169
+ if (!instance) {
170
+ throw new Error("useTabs must be used within a <TabSyncProvider>");
171
+ }
172
+ const tabsRef = useRef(instance.getTabs());
173
+ const subscribe = useCallback(
174
+ (onStoreChange) => instance.onTabChange((tabs) => {
175
+ tabsRef.current = tabs;
176
+ onStoreChange();
177
+ }),
178
+ [instance]
179
+ );
180
+ const getSnapshot = useCallback(() => tabsRef.current, []);
181
+ const getServerSnapshot = useCallback(() => SERVER_SNAPSHOT2, []);
182
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
183
+ }
184
+ function useLeaderInfo() {
185
+ const instance = useContext(TabSyncContext);
186
+ if (!instance) {
187
+ throw new Error("useLeaderInfo must be used within a <TabSyncProvider>");
188
+ }
189
+ const leaderRef = useRef(instance.getLeader());
190
+ const subscribe = useCallback(
191
+ (onStoreChange) => {
192
+ const unsubs = [];
193
+ unsubs.push(
194
+ instance.onTabChange(() => {
195
+ const next = instance.getLeader();
196
+ if (next?.id !== leaderRef.current?.id) {
197
+ leaderRef.current = next;
198
+ onStoreChange();
199
+ }
200
+ })
201
+ );
202
+ unsubs.push(
203
+ instance.onLeader(() => {
204
+ leaderRef.current = instance.getLeader();
205
+ onStoreChange();
206
+ return () => {
207
+ leaderRef.current = instance.getLeader();
208
+ onStoreChange();
209
+ };
210
+ })
211
+ );
212
+ return () => {
213
+ for (const u of unsubs) u();
214
+ };
215
+ },
216
+ [instance]
217
+ );
218
+ const getSnapshot = useCallback(() => leaderRef.current, []);
219
+ const getServerSnapshot = useCallback(() => null, []);
220
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
221
+ }
222
+ function useTabSyncActions() {
223
+ const instance = useContext(TabSyncContext);
224
+ if (!instance) {
225
+ throw new Error("useTabSyncActions must be used within a <TabSyncProvider>");
226
+ }
227
+ return useMemo(
228
+ () => ({
229
+ set: instance.set.bind(instance),
230
+ patch: instance.patch.bind(instance),
231
+ transaction: instance.transaction.bind(instance)
232
+ }),
233
+ [instance]
234
+ );
235
+ }
166
236
 
167
- export { TabSyncContext, TabSyncProvider, useIsLeader, useTabSync, useTabSyncSelector, useTabSyncValue };
237
+ export { TabSyncContext, TabSyncProvider, useIsLeader, useLeaderInfo, useTabSync, useTabSyncActions, useTabSyncSelector, useTabSyncValue, useTabs };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ var chunkTGEXRVAL_cjs = require('../chunk-TGEXRVAL.cjs');
4
+
5
+ // src/zustand/middleware.ts
6
+ function shouldSyncKey(key, value, options) {
7
+ if (typeof value === "function") return false;
8
+ if (options?.include) return options.include.includes(key);
9
+ if (options?.exclude) return !options.exclude.includes(key);
10
+ return true;
11
+ }
12
+ function extractSyncableState(state, options) {
13
+ const result = {};
14
+ for (const [key, value] of Object.entries(state)) {
15
+ if (shouldSyncKey(key, value, options)) {
16
+ result[key] = value;
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ function validateOptions(options) {
22
+ if (options?.include && options?.exclude) {
23
+ throw new Error(
24
+ "[tab-bridge/zustand] `include` and `exclude` are mutually exclusive"
25
+ );
26
+ }
27
+ }
28
+ var tabSyncImpl = (initializer, options) => (set, get, api) => {
29
+ validateOptions(options);
30
+ let sync_instance = null;
31
+ let is_remote_update = false;
32
+ const initial_state = initializer(set, get, api);
33
+ const syncable_initial = extractSyncableState(initial_state, options);
34
+ sync_instance = chunkTGEXRVAL_cjs.createTabSync({
35
+ initial: syncable_initial,
36
+ channel: options?.channel ?? "tab-sync-zustand",
37
+ debug: options?.debug,
38
+ transport: options?.transport,
39
+ merge: options?.merge,
40
+ onError: options?.onError
41
+ });
42
+ api.subscribe((next_state, prev_state) => {
43
+ if (is_remote_update || !sync_instance) return;
44
+ const next = next_state;
45
+ const prev = prev_state;
46
+ const diff = {};
47
+ let has_diff = false;
48
+ for (const key of Object.keys(next)) {
49
+ if (shouldSyncKey(key, next[key], options) && !Object.is(prev[key], next[key])) {
50
+ diff[key] = next[key];
51
+ has_diff = true;
52
+ }
53
+ }
54
+ if (has_diff) {
55
+ sync_instance.patch(diff);
56
+ }
57
+ });
58
+ sync_instance.onChange((remote_state, changed_keys, meta) => {
59
+ if (meta.isLocal) return;
60
+ is_remote_update = true;
61
+ try {
62
+ const patch = {};
63
+ let has_patch = false;
64
+ for (const key of changed_keys) {
65
+ patch[key] = remote_state[key];
66
+ has_patch = true;
67
+ }
68
+ if (has_patch) {
69
+ api.setState(patch);
70
+ }
71
+ } finally {
72
+ is_remote_update = false;
73
+ }
74
+ });
75
+ options?.onSyncReady?.(sync_instance);
76
+ return initial_state;
77
+ };
78
+ var tabSync = tabSyncImpl;
79
+
80
+ exports.tabSync = tabSync;
@@ -0,0 +1,87 @@
1
+ import { StoreMutatorIdentifier, StateCreator } from 'zustand/vanilla';
2
+ import { T as TabSyncInstance } from '../instance-hvEUHx6i.cjs';
3
+
4
+ /**
5
+ * Options for the `tabSync` Zustand middleware.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { create } from 'zustand';
10
+ * import { tabSync } from 'tab-bridge/zustand';
11
+ *
12
+ * const useStore = create(
13
+ * tabSync(
14
+ * (set) => ({
15
+ * count: 0,
16
+ * inc: () => set((s) => ({ count: s.count + 1 })),
17
+ * }),
18
+ * { channel: 'my-app', exclude: ['localOnly'] }
19
+ * )
20
+ * );
21
+ * ```
22
+ */
23
+ interface TabSyncZustandOptions<T = Record<string, unknown>> {
24
+ /** Channel name for cross-tab communication. @default 'tab-sync-zustand' */
25
+ channel?: string;
26
+ /**
27
+ * Only sync these keys. Functions are always excluded.
28
+ * Mutually exclusive with `exclude`.
29
+ */
30
+ include?: readonly (keyof T & string)[];
31
+ /**
32
+ * Exclude these keys from syncing. Functions are always excluded.
33
+ * Mutually exclusive with `include`.
34
+ */
35
+ exclude?: readonly (keyof T & string)[];
36
+ /**
37
+ * Custom conflict resolution.
38
+ * Called when the same key is updated on two tabs simultaneously.
39
+ * @default Last-write-wins (LWW)
40
+ */
41
+ merge?: (localValue: unknown, remoteValue: unknown, key: string) => unknown;
42
+ /** Force a specific transport layer. @default auto-detect */
43
+ transport?: 'broadcast-channel' | 'local-storage';
44
+ /** Enable debug logging. @default false */
45
+ debug?: boolean;
46
+ /** Error callback for non-fatal errors (channel failures, etc.). */
47
+ onError?: (error: Error) => void;
48
+ /**
49
+ * Callback invoked when the underlying `TabSyncInstance` is ready.
50
+ * Use this to access advanced features (RPC, leader election, etc.)
51
+ * or to store a reference for manual cleanup via `instance.destroy()`.
52
+ */
53
+ onSyncReady?: (instance: TabSyncInstance<Record<string, unknown>>) => void;
54
+ }
55
+
56
+ type TabSyncMiddleware = <T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, Mps, Mcs>, options?: TabSyncZustandOptions<T>) => StateCreator<T, Mps, Mcs>;
57
+ /**
58
+ * Zustand middleware that synchronizes store state across browser tabs
59
+ * via tab-bridge's BroadcastChannel/localStorage transport.
60
+ *
61
+ * Functions (actions) are automatically excluded from synchronization.
62
+ * Use `include` or `exclude` options to further filter which keys are synced.
63
+ *
64
+ * @param initializer - Zustand StateCreator function.
65
+ * @param options - Synchronization options.
66
+ * @returns A wrapped StateCreator that synchronizes state across tabs.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import { create } from 'zustand';
71
+ * import { tabSync } from 'tab-bridge/zustand';
72
+ *
73
+ * const useStore = create(
74
+ * tabSync(
75
+ * (set) => ({
76
+ * count: 0,
77
+ * inc: () => set((s) => ({ count: s.count + 1 })),
78
+ * }),
79
+ * { channel: 'my-app' }
80
+ * )
81
+ * );
82
+ * // All tabs sharing channel 'my-app' will have synchronized state.
83
+ * ```
84
+ */
85
+ declare const tabSync: TabSyncMiddleware;
86
+
87
+ export { type TabSyncZustandOptions, tabSync };
@@ -0,0 +1,87 @@
1
+ import { StoreMutatorIdentifier, StateCreator } from 'zustand/vanilla';
2
+ import { T as TabSyncInstance } from '../instance-hvEUHx6i.js';
3
+
4
+ /**
5
+ * Options for the `tabSync` Zustand middleware.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { create } from 'zustand';
10
+ * import { tabSync } from 'tab-bridge/zustand';
11
+ *
12
+ * const useStore = create(
13
+ * tabSync(
14
+ * (set) => ({
15
+ * count: 0,
16
+ * inc: () => set((s) => ({ count: s.count + 1 })),
17
+ * }),
18
+ * { channel: 'my-app', exclude: ['localOnly'] }
19
+ * )
20
+ * );
21
+ * ```
22
+ */
23
+ interface TabSyncZustandOptions<T = Record<string, unknown>> {
24
+ /** Channel name for cross-tab communication. @default 'tab-sync-zustand' */
25
+ channel?: string;
26
+ /**
27
+ * Only sync these keys. Functions are always excluded.
28
+ * Mutually exclusive with `exclude`.
29
+ */
30
+ include?: readonly (keyof T & string)[];
31
+ /**
32
+ * Exclude these keys from syncing. Functions are always excluded.
33
+ * Mutually exclusive with `include`.
34
+ */
35
+ exclude?: readonly (keyof T & string)[];
36
+ /**
37
+ * Custom conflict resolution.
38
+ * Called when the same key is updated on two tabs simultaneously.
39
+ * @default Last-write-wins (LWW)
40
+ */
41
+ merge?: (localValue: unknown, remoteValue: unknown, key: string) => unknown;
42
+ /** Force a specific transport layer. @default auto-detect */
43
+ transport?: 'broadcast-channel' | 'local-storage';
44
+ /** Enable debug logging. @default false */
45
+ debug?: boolean;
46
+ /** Error callback for non-fatal errors (channel failures, etc.). */
47
+ onError?: (error: Error) => void;
48
+ /**
49
+ * Callback invoked when the underlying `TabSyncInstance` is ready.
50
+ * Use this to access advanced features (RPC, leader election, etc.)
51
+ * or to store a reference for manual cleanup via `instance.destroy()`.
52
+ */
53
+ onSyncReady?: (instance: TabSyncInstance<Record<string, unknown>>) => void;
54
+ }
55
+
56
+ type TabSyncMiddleware = <T, Mps extends [StoreMutatorIdentifier, unknown][] = [], Mcs extends [StoreMutatorIdentifier, unknown][] = []>(initializer: StateCreator<T, Mps, Mcs>, options?: TabSyncZustandOptions<T>) => StateCreator<T, Mps, Mcs>;
57
+ /**
58
+ * Zustand middleware that synchronizes store state across browser tabs
59
+ * via tab-bridge's BroadcastChannel/localStorage transport.
60
+ *
61
+ * Functions (actions) are automatically excluded from synchronization.
62
+ * Use `include` or `exclude` options to further filter which keys are synced.
63
+ *
64
+ * @param initializer - Zustand StateCreator function.
65
+ * @param options - Synchronization options.
66
+ * @returns A wrapped StateCreator that synchronizes state across tabs.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * import { create } from 'zustand';
71
+ * import { tabSync } from 'tab-bridge/zustand';
72
+ *
73
+ * const useStore = create(
74
+ * tabSync(
75
+ * (set) => ({
76
+ * count: 0,
77
+ * inc: () => set((s) => ({ count: s.count + 1 })),
78
+ * }),
79
+ * { channel: 'my-app' }
80
+ * )
81
+ * );
82
+ * // All tabs sharing channel 'my-app' will have synchronized state.
83
+ * ```
84
+ */
85
+ declare const tabSync: TabSyncMiddleware;
86
+
87
+ export { type TabSyncZustandOptions, tabSync };