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.
@@ -0,0 +1,194 @@
1
+ interface TabInfo {
2
+ id: string;
3
+ createdAt: number;
4
+ lastSeen: number;
5
+ isLeader: boolean;
6
+ isActive: boolean;
7
+ url: string;
8
+ title?: string;
9
+ }
10
+ interface ChangeMeta {
11
+ readonly sourceTabId: string;
12
+ readonly isLocal: boolean;
13
+ readonly timestamp: number;
14
+ }
15
+
16
+ /**
17
+ * Define your RPC contract for full type inference:
18
+ *
19
+ * ```ts
20
+ * interface MyRPC {
21
+ * getTime: { args: void; result: { iso: string } };
22
+ * add: { args: { a: number; b: number }; result: number };
23
+ * }
24
+ *
25
+ * const sync = createTabSync<State, MyRPC>({ ... });
26
+ * const { iso } = await sync.call('leader', 'getTime'); // fully typed
27
+ * ```
28
+ */
29
+ type RPCMap = Record<string, {
30
+ args: unknown;
31
+ result: unknown;
32
+ }>;
33
+ interface RPCCallAllResult<T = unknown> {
34
+ tabId: string;
35
+ result?: T;
36
+ error?: string;
37
+ }
38
+ /** Resolve args type for a method. Falls back to `unknown` for unregistered methods. */
39
+ type RPCArgs<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMap[M]['args'] : unknown;
40
+ /** Resolve result type for a method. Falls back to `unknown` for unregistered methods. */
41
+ type RPCResult<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMap[M]['result'] : unknown;
42
+
43
+ interface TabSyncInstance<TState extends Record<string, unknown>, TRPCMap extends RPCMap = RPCMap> {
44
+ /** Read a single value by key. */
45
+ get<K extends keyof TState>(key: K): TState[K];
46
+ /**
47
+ * Read the entire state as a frozen snapshot.
48
+ * Returns the same reference until state changes (safe for React).
49
+ */
50
+ getAll(): Readonly<TState>;
51
+ /** Update a single key. Synced to all tabs. */
52
+ set<K extends keyof TState>(key: K, value: TState[K]): void;
53
+ /** Update multiple keys in a single broadcast. */
54
+ patch(partial: Partial<TState>): void;
55
+ /**
56
+ * Atomically update multiple keys based on the current state.
57
+ * Return `null` to abort the transaction.
58
+ *
59
+ * ```ts
60
+ * sync.transaction((state) => {
61
+ * if (state.balance >= amount) {
62
+ * return { balance: state.balance - amount, history: [...state.history, tx] };
63
+ * }
64
+ * return null; // abort
65
+ * });
66
+ * ```
67
+ */
68
+ transaction(fn: (state: Readonly<TState>) => Partial<TState> | null): void;
69
+ /**
70
+ * Subscribe to changes for a specific key.
71
+ * @returns Unsubscribe function.
72
+ *
73
+ * ```ts
74
+ * const off = sync.on('theme', (value, meta) => {
75
+ * console.log(value, meta.isLocal ? 'local' : 'remote');
76
+ * });
77
+ * off(); // unsubscribe
78
+ * ```
79
+ */
80
+ on<K extends keyof TState>(key: K, callback: (value: TState[K], meta: ChangeMeta) => void): () => void;
81
+ /**
82
+ * Subscribe to a specific key, but fire only once then auto-unsubscribe.
83
+ *
84
+ * ```ts
85
+ * sync.once('theme', (value) => console.log('First change:', value));
86
+ * ```
87
+ */
88
+ once<K extends keyof TState>(key: K, callback: (value: TState[K], meta: ChangeMeta) => void): () => void;
89
+ /**
90
+ * Subscribe to all state changes.
91
+ * @returns Unsubscribe function.
92
+ */
93
+ onChange(callback: (state: Readonly<TState>, changedKeys: (keyof TState)[], meta: ChangeMeta) => void): () => void;
94
+ /**
95
+ * Subscribe to a **derived value**. The callback only fires when the
96
+ * selector's return value actually changes (compared via `isEqual`,
97
+ * default `Object.is`).
98
+ *
99
+ * ```ts
100
+ * sync.select(
101
+ * (s) => s.items.filter(i => i.done).length,
102
+ * (doneCount) => badge.textContent = doneCount,
103
+ * );
104
+ * ```
105
+ *
106
+ * @returns Unsubscribe function.
107
+ */
108
+ select<TResult>(selector: (state: Readonly<TState>) => TResult, callback: (result: TResult, meta: ChangeMeta) => void, options?: {
109
+ isEqual?: (a: TResult, b: TResult) => boolean;
110
+ debounce?: number;
111
+ }): () => void;
112
+ /** Whether this tab is currently the leader. */
113
+ isLeader(): boolean;
114
+ /**
115
+ * Register a callback that fires when this tab becomes leader.
116
+ * Optionally return a cleanup function that runs when leadership is lost.
117
+ *
118
+ * ```ts
119
+ * sync.onLeader(() => {
120
+ * const ws = new WebSocket('...');
121
+ * return () => ws.close(); // cleanup on resign
122
+ * });
123
+ * ```
124
+ */
125
+ onLeader(callback: () => void | (() => void)): () => void;
126
+ /** Get info about the current leader tab, or `null` if no leader yet. */
127
+ getLeader(): TabInfo | null;
128
+ /**
129
+ * Returns a promise that resolves with the leader's `TabInfo`
130
+ * as soon as a leader is elected. Resolves immediately if a leader
131
+ * already exists.
132
+ *
133
+ * ```ts
134
+ * const leader = await sync.waitForLeader();
135
+ * const result = await sync.call('leader', 'getData');
136
+ * ```
137
+ */
138
+ waitForLeader(): Promise<TabInfo>;
139
+ /** Unique ID of this tab (UUID v4). */
140
+ readonly id: string;
141
+ /** List of all currently active tabs. */
142
+ getTabs(): TabInfo[];
143
+ /** Number of currently active tabs. */
144
+ getTabCount(): number;
145
+ /**
146
+ * Subscribe to tab presence changes (join, leave, leader change).
147
+ * @returns Unsubscribe function.
148
+ */
149
+ onTabChange(callback: (tabs: TabInfo[]) => void): () => void;
150
+ /**
151
+ * Call a remote procedure on another tab.
152
+ *
153
+ * ```ts
154
+ * const result = await sync.call('leader', 'getTime');
155
+ * const sum = await sync.call(tabId, 'add', { a: 1, b: 2 });
156
+ * ```
157
+ *
158
+ * @param target - Tab ID or `'leader'` to auto-resolve.
159
+ * @param method - Method name (typed if `TRPCMap` is provided).
160
+ * @param args - Arguments to pass to the handler.
161
+ * @param timeout - Timeout in ms. Default: `5000`.
162
+ */
163
+ call<M extends string>(target: string | 'leader', method: M, args?: RPCArgs<TRPCMap, M>, timeout?: number): Promise<RPCResult<TRPCMap, M>>;
164
+ /**
165
+ * Call a method on **all other tabs** (fan-out).
166
+ * Returns an array of per-tab results, including errors.
167
+ *
168
+ * ```ts
169
+ * const results = await sync.callAll('getStatus');
170
+ * for (const { tabId, result, error } of results) { ... }
171
+ * ```
172
+ */
173
+ callAll<M extends string>(method: M, args?: RPCArgs<TRPCMap, M>, timeout?: number): Promise<RPCCallAllResult<RPCResult<TRPCMap, M>>[]>;
174
+ /**
175
+ * Register an RPC handler that other tabs can call.
176
+ *
177
+ * ```ts
178
+ * sync.handle('add', ({ a, b }) => a + b);
179
+ * ```
180
+ *
181
+ * @returns Unsubscribe function to remove the handler.
182
+ */
183
+ handle<M extends string>(method: M, handler: (args: RPCArgs<TRPCMap, M>, callerTabId: string) => RPCResult<TRPCMap, M> | Promise<RPCResult<TRPCMap, M>>): () => void;
184
+ /**
185
+ * Destroy this instance. Sends goodbye to other tabs,
186
+ * cancels all timers, and flushes pending state.
187
+ * Safe to call multiple times.
188
+ */
189
+ destroy(): void;
190
+ /** `false` after `destroy()` has been called. */
191
+ readonly ready: boolean;
192
+ }
193
+
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,194 @@
1
+ interface TabInfo {
2
+ id: string;
3
+ createdAt: number;
4
+ lastSeen: number;
5
+ isLeader: boolean;
6
+ isActive: boolean;
7
+ url: string;
8
+ title?: string;
9
+ }
10
+ interface ChangeMeta {
11
+ readonly sourceTabId: string;
12
+ readonly isLocal: boolean;
13
+ readonly timestamp: number;
14
+ }
15
+
16
+ /**
17
+ * Define your RPC contract for full type inference:
18
+ *
19
+ * ```ts
20
+ * interface MyRPC {
21
+ * getTime: { args: void; result: { iso: string } };
22
+ * add: { args: { a: number; b: number }; result: number };
23
+ * }
24
+ *
25
+ * const sync = createTabSync<State, MyRPC>({ ... });
26
+ * const { iso } = await sync.call('leader', 'getTime'); // fully typed
27
+ * ```
28
+ */
29
+ type RPCMap = Record<string, {
30
+ args: unknown;
31
+ result: unknown;
32
+ }>;
33
+ interface RPCCallAllResult<T = unknown> {
34
+ tabId: string;
35
+ result?: T;
36
+ error?: string;
37
+ }
38
+ /** Resolve args type for a method. Falls back to `unknown` for unregistered methods. */
39
+ type RPCArgs<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMap[M]['args'] : unknown;
40
+ /** Resolve result type for a method. Falls back to `unknown` for unregistered methods. */
41
+ type RPCResult<TMap extends RPCMap, M extends string> = M extends keyof TMap ? TMap[M]['result'] : unknown;
42
+
43
+ interface TabSyncInstance<TState extends Record<string, unknown>, TRPCMap extends RPCMap = RPCMap> {
44
+ /** Read a single value by key. */
45
+ get<K extends keyof TState>(key: K): TState[K];
46
+ /**
47
+ * Read the entire state as a frozen snapshot.
48
+ * Returns the same reference until state changes (safe for React).
49
+ */
50
+ getAll(): Readonly<TState>;
51
+ /** Update a single key. Synced to all tabs. */
52
+ set<K extends keyof TState>(key: K, value: TState[K]): void;
53
+ /** Update multiple keys in a single broadcast. */
54
+ patch(partial: Partial<TState>): void;
55
+ /**
56
+ * Atomically update multiple keys based on the current state.
57
+ * Return `null` to abort the transaction.
58
+ *
59
+ * ```ts
60
+ * sync.transaction((state) => {
61
+ * if (state.balance >= amount) {
62
+ * return { balance: state.balance - amount, history: [...state.history, tx] };
63
+ * }
64
+ * return null; // abort
65
+ * });
66
+ * ```
67
+ */
68
+ transaction(fn: (state: Readonly<TState>) => Partial<TState> | null): void;
69
+ /**
70
+ * Subscribe to changes for a specific key.
71
+ * @returns Unsubscribe function.
72
+ *
73
+ * ```ts
74
+ * const off = sync.on('theme', (value, meta) => {
75
+ * console.log(value, meta.isLocal ? 'local' : 'remote');
76
+ * });
77
+ * off(); // unsubscribe
78
+ * ```
79
+ */
80
+ on<K extends keyof TState>(key: K, callback: (value: TState[K], meta: ChangeMeta) => void): () => void;
81
+ /**
82
+ * Subscribe to a specific key, but fire only once then auto-unsubscribe.
83
+ *
84
+ * ```ts
85
+ * sync.once('theme', (value) => console.log('First change:', value));
86
+ * ```
87
+ */
88
+ once<K extends keyof TState>(key: K, callback: (value: TState[K], meta: ChangeMeta) => void): () => void;
89
+ /**
90
+ * Subscribe to all state changes.
91
+ * @returns Unsubscribe function.
92
+ */
93
+ onChange(callback: (state: Readonly<TState>, changedKeys: (keyof TState)[], meta: ChangeMeta) => void): () => void;
94
+ /**
95
+ * Subscribe to a **derived value**. The callback only fires when the
96
+ * selector's return value actually changes (compared via `isEqual`,
97
+ * default `Object.is`).
98
+ *
99
+ * ```ts
100
+ * sync.select(
101
+ * (s) => s.items.filter(i => i.done).length,
102
+ * (doneCount) => badge.textContent = doneCount,
103
+ * );
104
+ * ```
105
+ *
106
+ * @returns Unsubscribe function.
107
+ */
108
+ select<TResult>(selector: (state: Readonly<TState>) => TResult, callback: (result: TResult, meta: ChangeMeta) => void, options?: {
109
+ isEqual?: (a: TResult, b: TResult) => boolean;
110
+ debounce?: number;
111
+ }): () => void;
112
+ /** Whether this tab is currently the leader. */
113
+ isLeader(): boolean;
114
+ /**
115
+ * Register a callback that fires when this tab becomes leader.
116
+ * Optionally return a cleanup function that runs when leadership is lost.
117
+ *
118
+ * ```ts
119
+ * sync.onLeader(() => {
120
+ * const ws = new WebSocket('...');
121
+ * return () => ws.close(); // cleanup on resign
122
+ * });
123
+ * ```
124
+ */
125
+ onLeader(callback: () => void | (() => void)): () => void;
126
+ /** Get info about the current leader tab, or `null` if no leader yet. */
127
+ getLeader(): TabInfo | null;
128
+ /**
129
+ * Returns a promise that resolves with the leader's `TabInfo`
130
+ * as soon as a leader is elected. Resolves immediately if a leader
131
+ * already exists.
132
+ *
133
+ * ```ts
134
+ * const leader = await sync.waitForLeader();
135
+ * const result = await sync.call('leader', 'getData');
136
+ * ```
137
+ */
138
+ waitForLeader(): Promise<TabInfo>;
139
+ /** Unique ID of this tab (UUID v4). */
140
+ readonly id: string;
141
+ /** List of all currently active tabs. */
142
+ getTabs(): TabInfo[];
143
+ /** Number of currently active tabs. */
144
+ getTabCount(): number;
145
+ /**
146
+ * Subscribe to tab presence changes (join, leave, leader change).
147
+ * @returns Unsubscribe function.
148
+ */
149
+ onTabChange(callback: (tabs: TabInfo[]) => void): () => void;
150
+ /**
151
+ * Call a remote procedure on another tab.
152
+ *
153
+ * ```ts
154
+ * const result = await sync.call('leader', 'getTime');
155
+ * const sum = await sync.call(tabId, 'add', { a: 1, b: 2 });
156
+ * ```
157
+ *
158
+ * @param target - Tab ID or `'leader'` to auto-resolve.
159
+ * @param method - Method name (typed if `TRPCMap` is provided).
160
+ * @param args - Arguments to pass to the handler.
161
+ * @param timeout - Timeout in ms. Default: `5000`.
162
+ */
163
+ call<M extends string>(target: string | 'leader', method: M, args?: RPCArgs<TRPCMap, M>, timeout?: number): Promise<RPCResult<TRPCMap, M>>;
164
+ /**
165
+ * Call a method on **all other tabs** (fan-out).
166
+ * Returns an array of per-tab results, including errors.
167
+ *
168
+ * ```ts
169
+ * const results = await sync.callAll('getStatus');
170
+ * for (const { tabId, result, error } of results) { ... }
171
+ * ```
172
+ */
173
+ callAll<M extends string>(method: M, args?: RPCArgs<TRPCMap, M>, timeout?: number): Promise<RPCCallAllResult<RPCResult<TRPCMap, M>>[]>;
174
+ /**
175
+ * Register an RPC handler that other tabs can call.
176
+ *
177
+ * ```ts
178
+ * sync.handle('add', ({ a, b }) => a + b);
179
+ * ```
180
+ *
181
+ * @returns Unsubscribe function to remove the handler.
182
+ */
183
+ handle<M extends string>(method: M, handler: (args: RPCArgs<TRPCMap, M>, callerTabId: string) => RPCResult<TRPCMap, M> | Promise<RPCResult<TRPCMap, M>>): () => void;
184
+ /**
185
+ * Destroy this instance. Sends goodbye to other tabs,
186
+ * cancels all timers, and flushes pending state.
187
+ * Safe to call multiple times.
188
+ */
189
+ destroy(): void;
190
+ /** `false` after `destroy()` has been called. */
191
+ readonly ready: boolean;
192
+ }
193
+
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,93 @@
1
+ import { C as ChangeMeta } from './instance-hvEUHx6i.cjs';
2
+
3
+ interface MiddlewareContext<TState extends Record<string, unknown>> {
4
+ readonly key: keyof TState;
5
+ readonly value: unknown;
6
+ readonly previousValue: unknown;
7
+ readonly meta: ChangeMeta;
8
+ }
9
+ /**
10
+ * Return `false` to reject the change, `{ value }` to transform it,
11
+ * or `void`/`undefined` to pass through unchanged.
12
+ */
13
+ type MiddlewareResult = {
14
+ value: unknown;
15
+ } | false;
16
+ interface Middleware<TState extends Record<string, unknown> = Record<string, unknown>> {
17
+ readonly name: string;
18
+ /** Intercept local `set` / `patch` calls before they are applied. */
19
+ onSet?: (ctx: MiddlewareContext<TState>) => MiddlewareResult | void;
20
+ /** Called after any state change (local or remote) has been committed. */
21
+ afterChange?: (key: keyof TState, value: unknown, meta: ChangeMeta) => void;
22
+ /** Cleanup when the instance is destroyed. */
23
+ onDestroy?: () => void;
24
+ }
25
+
26
+ interface PersistOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
27
+ /** Storage key. Default: `'tab-sync:state'` */
28
+ key?: string;
29
+ /** Only persist these keys (whitelist). */
30
+ include?: (keyof TState)[];
31
+ /** Exclude these keys from persistence (blacklist). */
32
+ exclude?: (keyof TState)[];
33
+ /** Custom serializer. Default: `JSON.stringify` */
34
+ serialize?: (state: Partial<TState>) => string;
35
+ /** Custom deserializer. Default: `JSON.parse` */
36
+ deserialize?: (raw: string) => Partial<TState>;
37
+ /** Debounce persistence writes in ms. Default: `100` */
38
+ debounce?: number;
39
+ /** Custom storage backend. Default: `localStorage` */
40
+ storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
41
+ /**
42
+ * Schema version for state migration. When the persisted version
43
+ * differs from this value, `migrate` is called.
44
+ */
45
+ version?: number;
46
+ /**
47
+ * Migration function called when persisted version differs from current.
48
+ *
49
+ * ```ts
50
+ * persist: {
51
+ * version: 2,
52
+ * migrate: (oldState, oldVersion) => ({
53
+ * ...oldState,
54
+ * newField: oldVersion < 2 ? 'default' : oldState.newField,
55
+ * }),
56
+ * }
57
+ * ```
58
+ */
59
+ migrate?: (oldState: Partial<TState>, oldVersion: number) => Partial<TState>;
60
+ }
61
+
62
+ interface LeaderOptions {
63
+ /** Heartbeat interval in ms. Default: `2000` */
64
+ heartbeatInterval?: number;
65
+ /** Leader timeout in ms. Default: `6000` */
66
+ leaderTimeout?: number;
67
+ }
68
+ interface TabSyncOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
69
+ /** Initial state used before sync completes. */
70
+ initial?: TState;
71
+ /** Channel name — only tabs sharing the same name communicate. Default: `'tab-sync'` */
72
+ channel?: string;
73
+ /** Force a specific transport layer. Default: auto-detect. */
74
+ transport?: 'broadcast-channel' | 'local-storage';
75
+ /** Custom merge function for LWW conflict resolution. */
76
+ merge?: (localValue: unknown, remoteValue: unknown, key: keyof TState) => unknown;
77
+ /** Enable leader election. Default: `true` */
78
+ leader?: boolean | LeaderOptions;
79
+ /** Heartbeat interval in ms. Default: `2000` */
80
+ heartbeatInterval?: number;
81
+ /** Leader timeout in ms. Default: `6000` */
82
+ leaderTimeout?: number;
83
+ /** Enable debug logging. Default: `false` */
84
+ debug?: boolean;
85
+ /** Persist state across page reloads. `true` uses defaults, or pass options. */
86
+ persist?: PersistOptions<TState> | boolean;
87
+ /** Middleware pipeline for intercepting state changes. */
88
+ middlewares?: Middleware<TState>[];
89
+ /** Error callback for non-fatal errors (storage, channel, etc.). */
90
+ onError?: (error: Error) => void;
91
+ }
92
+
93
+ export type { LeaderOptions as L, Middleware as M, PersistOptions as P, TabSyncOptions as T, MiddlewareContext as a, MiddlewareResult as b };
@@ -0,0 +1,93 @@
1
+ import { C as ChangeMeta } from './instance-hvEUHx6i.js';
2
+
3
+ interface MiddlewareContext<TState extends Record<string, unknown>> {
4
+ readonly key: keyof TState;
5
+ readonly value: unknown;
6
+ readonly previousValue: unknown;
7
+ readonly meta: ChangeMeta;
8
+ }
9
+ /**
10
+ * Return `false` to reject the change, `{ value }` to transform it,
11
+ * or `void`/`undefined` to pass through unchanged.
12
+ */
13
+ type MiddlewareResult = {
14
+ value: unknown;
15
+ } | false;
16
+ interface Middleware<TState extends Record<string, unknown> = Record<string, unknown>> {
17
+ readonly name: string;
18
+ /** Intercept local `set` / `patch` calls before they are applied. */
19
+ onSet?: (ctx: MiddlewareContext<TState>) => MiddlewareResult | void;
20
+ /** Called after any state change (local or remote) has been committed. */
21
+ afterChange?: (key: keyof TState, value: unknown, meta: ChangeMeta) => void;
22
+ /** Cleanup when the instance is destroyed. */
23
+ onDestroy?: () => void;
24
+ }
25
+
26
+ interface PersistOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
27
+ /** Storage key. Default: `'tab-sync:state'` */
28
+ key?: string;
29
+ /** Only persist these keys (whitelist). */
30
+ include?: (keyof TState)[];
31
+ /** Exclude these keys from persistence (blacklist). */
32
+ exclude?: (keyof TState)[];
33
+ /** Custom serializer. Default: `JSON.stringify` */
34
+ serialize?: (state: Partial<TState>) => string;
35
+ /** Custom deserializer. Default: `JSON.parse` */
36
+ deserialize?: (raw: string) => Partial<TState>;
37
+ /** Debounce persistence writes in ms. Default: `100` */
38
+ debounce?: number;
39
+ /** Custom storage backend. Default: `localStorage` */
40
+ storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
41
+ /**
42
+ * Schema version for state migration. When the persisted version
43
+ * differs from this value, `migrate` is called.
44
+ */
45
+ version?: number;
46
+ /**
47
+ * Migration function called when persisted version differs from current.
48
+ *
49
+ * ```ts
50
+ * persist: {
51
+ * version: 2,
52
+ * migrate: (oldState, oldVersion) => ({
53
+ * ...oldState,
54
+ * newField: oldVersion < 2 ? 'default' : oldState.newField,
55
+ * }),
56
+ * }
57
+ * ```
58
+ */
59
+ migrate?: (oldState: Partial<TState>, oldVersion: number) => Partial<TState>;
60
+ }
61
+
62
+ interface LeaderOptions {
63
+ /** Heartbeat interval in ms. Default: `2000` */
64
+ heartbeatInterval?: number;
65
+ /** Leader timeout in ms. Default: `6000` */
66
+ leaderTimeout?: number;
67
+ }
68
+ interface TabSyncOptions<TState extends Record<string, unknown> = Record<string, unknown>> {
69
+ /** Initial state used before sync completes. */
70
+ initial?: TState;
71
+ /** Channel name — only tabs sharing the same name communicate. Default: `'tab-sync'` */
72
+ channel?: string;
73
+ /** Force a specific transport layer. Default: auto-detect. */
74
+ transport?: 'broadcast-channel' | 'local-storage';
75
+ /** Custom merge function for LWW conflict resolution. */
76
+ merge?: (localValue: unknown, remoteValue: unknown, key: keyof TState) => unknown;
77
+ /** Enable leader election. Default: `true` */
78
+ leader?: boolean | LeaderOptions;
79
+ /** Heartbeat interval in ms. Default: `2000` */
80
+ heartbeatInterval?: number;
81
+ /** Leader timeout in ms. Default: `6000` */
82
+ leaderTimeout?: number;
83
+ /** Enable debug logging. Default: `false` */
84
+ debug?: boolean;
85
+ /** Persist state across page reloads. `true` uses defaults, or pass options. */
86
+ persist?: PersistOptions<TState> | boolean;
87
+ /** Middleware pipeline for intercepting state changes. */
88
+ middlewares?: Middleware<TState>[];
89
+ /** Error callback for non-fatal errors (storage, channel, etc.). */
90
+ onError?: (error: Error) => void;
91
+ }
92
+
93
+ export type { LeaderOptions as L, Middleware as M, PersistOptions as P, TabSyncOptions as T, MiddlewareContext as a, MiddlewareResult as b };