tab-bridge 0.1.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/LICENSE +21 -0
- package/README.md +834 -0
- package/dist/chunk-42VOZR6E.js +1309 -0
- package/dist/chunk-BQCNBNBT.cjs +1332 -0
- package/dist/index.cjs +149 -0
- package/dist/index.d.cts +304 -0
- package/dist/index.d.ts +304 -0
- package/dist/index.js +59 -0
- package/dist/react/index.cjs +174 -0
- package/dist/react/index.d.cts +61 -0
- package/dist/react/index.d.ts +61 -0
- package/dist/react/index.js +167 -0
- package/dist/types-BtK4ixKz.d.cts +306 -0
- package/dist/types-BtK4ixKz.d.ts +306 -0
- package/package.json +90 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { R as RPCMap, T as TabSyncOptions, a as TabSyncInstance, b as TabMessage, S as SendFn, C as ChangeMeta, c as TabInfo, M as Middleware, d as MiddlewareContext, e as MessageType, f as MessagePayloadMap, g as MessageOf } from './types-BtK4ixKz.js';
|
|
2
|
+
export { L as LeaderClaimPayload, h as LeaderOptions, i as MiddlewareResult, P as PROTOCOL_VERSION, j as PersistOptions, k as RPCArgs, l as RPCResult, m as RpcRequestPayload, n as RpcResponsePayload, o as StateSyncResponsePayload, p as StateUpdatePayload, q as TabAnnouncePayload } from './types-BtK4ixKz.js';
|
|
3
|
+
|
|
4
|
+
declare function createTabSync<TState extends Record<string, unknown> = Record<string, unknown>, TRPCMap extends RPCMap = RPCMap>(options?: TabSyncOptions<TState>): TabSyncInstance<TState, TRPCMap>;
|
|
5
|
+
|
|
6
|
+
interface Channel {
|
|
7
|
+
postMessage(message: TabMessage): void;
|
|
8
|
+
onMessage(callback: (message: TabMessage) => void): () => void;
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
declare function createChannel(channelName: string, transport?: 'broadcast-channel' | 'local-storage'): Channel;
|
|
12
|
+
|
|
13
|
+
declare class BroadcastChannelTransport implements Channel {
|
|
14
|
+
private bc;
|
|
15
|
+
private closed;
|
|
16
|
+
constructor(channelName: string);
|
|
17
|
+
postMessage(message: TabMessage): void;
|
|
18
|
+
onMessage(callback: (message: TabMessage) => void): () => void;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fallback transport using `localStorage` `storage` events.
|
|
24
|
+
*
|
|
25
|
+
* Limitations vs BroadcastChannel:
|
|
26
|
+
* - Values must be JSON-serializable
|
|
27
|
+
* - `storage` event only fires on OTHER tabs (same-tab is not needed since
|
|
28
|
+
* the originating tab already handles the change locally)
|
|
29
|
+
* - Slightly slower due to serialization overhead
|
|
30
|
+
*/
|
|
31
|
+
declare class StorageChannel implements Channel {
|
|
32
|
+
private readonly key;
|
|
33
|
+
private readonly listeners;
|
|
34
|
+
private closed;
|
|
35
|
+
private seq;
|
|
36
|
+
constructor(channelName: string);
|
|
37
|
+
postMessage(message: TabMessage): void;
|
|
38
|
+
onMessage(callback: (message: TabMessage) => void): () => void;
|
|
39
|
+
close(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface StateManagerOptions<TState extends Record<string, unknown>> {
|
|
43
|
+
send: SendFn;
|
|
44
|
+
tabId: string;
|
|
45
|
+
initial?: TState;
|
|
46
|
+
merge?: (localValue: unknown, remoteValue: unknown, key: keyof TState) => unknown;
|
|
47
|
+
/**
|
|
48
|
+
* Hook called before a remote value is applied.
|
|
49
|
+
* Return `false` to reject, `{ value }` to transform, or `void` to accept.
|
|
50
|
+
*/
|
|
51
|
+
interceptRemote?: (key: keyof TState, value: unknown, previousValue: unknown, meta: ChangeMeta) => {
|
|
52
|
+
value: unknown;
|
|
53
|
+
} | false | void;
|
|
54
|
+
/** Hook called after any remote change is committed. */
|
|
55
|
+
afterRemoteChange?: (key: keyof TState, value: unknown, meta: ChangeMeta) => void;
|
|
56
|
+
}
|
|
57
|
+
declare class StateManager<TState extends Record<string, unknown>> {
|
|
58
|
+
private readonly state;
|
|
59
|
+
private readonly keyListeners;
|
|
60
|
+
private readonly changeListeners;
|
|
61
|
+
private readonly send;
|
|
62
|
+
private readonly tabId;
|
|
63
|
+
private readonly mergeFn?;
|
|
64
|
+
private readonly interceptRemote?;
|
|
65
|
+
private readonly afterRemoteChange?;
|
|
66
|
+
private readonly batcher;
|
|
67
|
+
private snapshotCache;
|
|
68
|
+
constructor(options: StateManagerOptions<TState>);
|
|
69
|
+
get<K extends keyof TState>(key: K): TState[K];
|
|
70
|
+
/**
|
|
71
|
+
* Returns a cached snapshot. The same reference is returned until state
|
|
72
|
+
* changes, making this safe for React's `useSyncExternalStore`.
|
|
73
|
+
*/
|
|
74
|
+
getAll(): Readonly<TState>;
|
|
75
|
+
set<K extends keyof TState>(key: K, value: TState[K]): void;
|
|
76
|
+
patch(partial: Partial<TState>): void;
|
|
77
|
+
on<K extends keyof TState>(key: K, callback: (value: TState[K], meta: ChangeMeta) => void): () => void;
|
|
78
|
+
onChange(callback: (state: Readonly<TState>, changedKeys: (keyof TState)[], meta: ChangeMeta) => void): () => void;
|
|
79
|
+
handleMessage(message: TabMessage): void;
|
|
80
|
+
requestSync(): void;
|
|
81
|
+
respondToSync(targetId: string): void;
|
|
82
|
+
flush(): void;
|
|
83
|
+
destroy(): void;
|
|
84
|
+
private applyRemoteUpdate;
|
|
85
|
+
/** LWW: accept remote if newer timestamp, or same timestamp with higher senderId. */
|
|
86
|
+
private shouldAcceptRemote;
|
|
87
|
+
private applySyncResponse;
|
|
88
|
+
private notifyKey;
|
|
89
|
+
private notifyChange;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface TabRegistryOptions {
|
|
93
|
+
send: SendFn;
|
|
94
|
+
tabId: string;
|
|
95
|
+
tabCreatedAt: number;
|
|
96
|
+
/** Interval (ms) to check for dead tabs. Default: `2000` */
|
|
97
|
+
heartbeatInterval?: number;
|
|
98
|
+
/** How long (ms) before a tab is considered dead. Default: `6000` */
|
|
99
|
+
tabTimeout?: number;
|
|
100
|
+
}
|
|
101
|
+
declare class TabRegistry {
|
|
102
|
+
private readonly tabs;
|
|
103
|
+
private readonly tabChangeListeners;
|
|
104
|
+
private readonly send;
|
|
105
|
+
private readonly tabId;
|
|
106
|
+
private readonly tabCreatedAt;
|
|
107
|
+
private readonly heartbeatInterval;
|
|
108
|
+
private readonly tabTimeout;
|
|
109
|
+
private heartbeatTimer;
|
|
110
|
+
private pruneTimer;
|
|
111
|
+
private visibilityHandler;
|
|
112
|
+
private unloadHandler;
|
|
113
|
+
constructor(options: TabRegistryOptions);
|
|
114
|
+
getTabs(): TabInfo[];
|
|
115
|
+
getTabCount(): number;
|
|
116
|
+
getTab(id: string): TabInfo | undefined;
|
|
117
|
+
onTabChange(callback: (tabs: TabInfo[]) => void): () => void;
|
|
118
|
+
announce(): void;
|
|
119
|
+
handleMessage(message: TabMessage): void;
|
|
120
|
+
setLeader(tabId: string | null): void;
|
|
121
|
+
destroy(): void;
|
|
122
|
+
private registerSelf;
|
|
123
|
+
private buildAnnouncePayload;
|
|
124
|
+
private startHeartbeat;
|
|
125
|
+
private startPruning;
|
|
126
|
+
private listenVisibility;
|
|
127
|
+
private listenUnload;
|
|
128
|
+
private sendGoodbye;
|
|
129
|
+
private handleAnnounce;
|
|
130
|
+
private handleGoodbye;
|
|
131
|
+
private touchTab;
|
|
132
|
+
private touchSelf;
|
|
133
|
+
private pruneDeadTabs;
|
|
134
|
+
private notifyChange;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface LeaderElectionOptions {
|
|
138
|
+
send: SendFn;
|
|
139
|
+
tabId: string;
|
|
140
|
+
tabCreatedAt: number;
|
|
141
|
+
/** Time (ms) to wait for a higher-priority claim before becoming leader. Default: `300` */
|
|
142
|
+
electionTimeout?: number;
|
|
143
|
+
/** Heartbeat interval (ms). Default: `2000` */
|
|
144
|
+
heartbeatInterval?: number;
|
|
145
|
+
/** How many missed heartbeats before declaring leader dead. Default: `3` */
|
|
146
|
+
missedHeartbeatsLimit?: number;
|
|
147
|
+
}
|
|
148
|
+
declare class LeaderElection {
|
|
149
|
+
private readonly send;
|
|
150
|
+
private readonly tabId;
|
|
151
|
+
private readonly tabCreatedAt;
|
|
152
|
+
private readonly electionTimeout;
|
|
153
|
+
private readonly heartbeatInterval;
|
|
154
|
+
private readonly leaderTimeout;
|
|
155
|
+
private leaderId;
|
|
156
|
+
private electionTimer;
|
|
157
|
+
private heartbeatTimer;
|
|
158
|
+
private leaderWatchTimer;
|
|
159
|
+
private lastLeaderHeartbeat;
|
|
160
|
+
private electing;
|
|
161
|
+
private readonly leaderCallbacks;
|
|
162
|
+
private readonly leaderCleanups;
|
|
163
|
+
constructor(options: LeaderElectionOptions);
|
|
164
|
+
isLeader(): boolean;
|
|
165
|
+
getLeaderId(): string | null;
|
|
166
|
+
onLeader(callback: () => void | (() => void)): () => void;
|
|
167
|
+
start(): void;
|
|
168
|
+
handleMessage(message: TabMessage): void;
|
|
169
|
+
destroy(): void;
|
|
170
|
+
private startElection;
|
|
171
|
+
private handleClaim;
|
|
172
|
+
private handleAck;
|
|
173
|
+
private handleHeartbeat;
|
|
174
|
+
private handleResign;
|
|
175
|
+
private becomeLeader;
|
|
176
|
+
private setLeader;
|
|
177
|
+
/**
|
|
178
|
+
* Priority: oldest tab (smaller `createdAt`) wins.
|
|
179
|
+
* Tiebreak: lower `tabId` wins (deterministic).
|
|
180
|
+
*/
|
|
181
|
+
private hasPriority;
|
|
182
|
+
private startHeartbeat;
|
|
183
|
+
private clearHeartbeat;
|
|
184
|
+
private startLeaderWatch;
|
|
185
|
+
private clearLeaderWatch;
|
|
186
|
+
private clearElectionTimer;
|
|
187
|
+
private runCleanups;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
interface RPCHandlerOptions {
|
|
191
|
+
send: SendFn;
|
|
192
|
+
tabId: string;
|
|
193
|
+
resolveLeaderId?: () => string | null;
|
|
194
|
+
onError?: (error: Error) => void;
|
|
195
|
+
}
|
|
196
|
+
declare class RPCHandler {
|
|
197
|
+
private readonly send;
|
|
198
|
+
private readonly tabId;
|
|
199
|
+
private readonly resolveLeaderId;
|
|
200
|
+
private readonly onError;
|
|
201
|
+
private readonly handlers;
|
|
202
|
+
private readonly pending;
|
|
203
|
+
constructor(options: RPCHandlerOptions);
|
|
204
|
+
call<TResult>(targetTabId: string | 'leader', method: string, args?: unknown, timeout?: number): Promise<TResult>;
|
|
205
|
+
handle<TArgs = unknown, TResult = unknown>(method: string, handler: (args: TArgs, callerTabId: string) => TResult | Promise<TResult>): () => void;
|
|
206
|
+
handleMessage(message: TabMessage): void;
|
|
207
|
+
destroy(): void;
|
|
208
|
+
private handleRequest;
|
|
209
|
+
private handleResponse;
|
|
210
|
+
private sendResponse;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Runs the middleware pipeline for a given hook (`onSet`).
|
|
215
|
+
* Returns the (possibly transformed) value and whether the change was rejected.
|
|
216
|
+
*/
|
|
217
|
+
declare function runMiddleware<TState extends Record<string, unknown>>(middlewares: readonly Middleware<TState>[], ctx: MiddlewareContext<TState>): {
|
|
218
|
+
value: unknown;
|
|
219
|
+
rejected: boolean;
|
|
220
|
+
};
|
|
221
|
+
/** Notify all middlewares that a state change has been committed. */
|
|
222
|
+
declare function notifyMiddleware<TState extends Record<string, unknown>>(middlewares: readonly Middleware<TState>[], key: keyof TState, value: unknown, meta: ChangeMeta): void;
|
|
223
|
+
/** Destroy all middlewares. */
|
|
224
|
+
declare function destroyMiddleware<TState extends Record<string, unknown>>(middlewares: readonly Middleware<TState>[]): void;
|
|
225
|
+
|
|
226
|
+
declare function generateTabId(): string;
|
|
227
|
+
|
|
228
|
+
declare function monotonic(): number;
|
|
229
|
+
|
|
230
|
+
interface Batcher<T> {
|
|
231
|
+
/** Queue a key-value pair. Resets the flush timer if not already running. */
|
|
232
|
+
add(key: string, value: T): void;
|
|
233
|
+
/** Immediately flush all pending entries. */
|
|
234
|
+
flush(): void;
|
|
235
|
+
/** Cancel pending flush and discard all entries. */
|
|
236
|
+
destroy(): void;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Batches rapid writes into a single flush callback.
|
|
240
|
+
* Within the `delay` window, only the last value per key is kept.
|
|
241
|
+
*/
|
|
242
|
+
declare function createBatcher<T>(onFlush: (entries: Map<string, T>) => void, delay?: number): Batcher<T>;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Type-safe message factory. Guarantees every message has a correct
|
|
246
|
+
* structure, protocol version, and monotonically increasing timestamp.
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* const msg = createMessage('STATE_UPDATE', tabId, { entries: { ... } });
|
|
250
|
+
* // ^? MessageOf<'STATE_UPDATE'> — payload is StateUpdatePayload
|
|
251
|
+
* ```
|
|
252
|
+
*/
|
|
253
|
+
declare function createMessage<T extends MessageType>(type: T, senderId: string, payload: MessagePayloadMap[T], targetId?: string): MessageOf<T>;
|
|
254
|
+
|
|
255
|
+
type Handler<T = void> = T extends void ? () => void : (data: T) => void;
|
|
256
|
+
/**
|
|
257
|
+
* Minimal, fully typed event emitter.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* const bus = new Emitter<{ click: { x: number }; close: void }>();
|
|
261
|
+
* bus.on('click', ({ x }) => console.log(x));
|
|
262
|
+
* bus.emit('click', { x: 42 });
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
declare class Emitter<TEvents extends Record<string, unknown>> {
|
|
266
|
+
private readonly _handlers;
|
|
267
|
+
on<K extends keyof TEvents>(event: K, handler: Handler<TEvents[K]>): () => void;
|
|
268
|
+
once<K extends keyof TEvents>(event: K, handler: Handler<TEvents[K]>): () => void;
|
|
269
|
+
emit<K extends keyof TEvents>(...args: TEvents[K] extends void ? [event: K] : [event: K, data: TEvents[K]]): void;
|
|
270
|
+
listenerCount<K extends keyof TEvents>(event: K): number;
|
|
271
|
+
removeAll(event?: keyof TEvents): void;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
declare const ErrorCode: {
|
|
275
|
+
readonly CHANNEL_CLOSED: "CHANNEL_CLOSED";
|
|
276
|
+
readonly CHANNEL_SEND_FAILED: "CHANNEL_SEND_FAILED";
|
|
277
|
+
readonly RPC_TIMEOUT: "RPC_TIMEOUT";
|
|
278
|
+
readonly RPC_NO_HANDLER: "RPC_NO_HANDLER";
|
|
279
|
+
readonly RPC_NO_LEADER: "RPC_NO_LEADER";
|
|
280
|
+
readonly RPC_HANDLER_ERROR: "RPC_HANDLER_ERROR";
|
|
281
|
+
readonly RPC_DESTROYED: "RPC_DESTROYED";
|
|
282
|
+
readonly STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED";
|
|
283
|
+
readonly MIDDLEWARE_REJECTED: "MIDDLEWARE_REJECTED";
|
|
284
|
+
readonly ALREADY_DESTROYED: "ALREADY_DESTROYED";
|
|
285
|
+
};
|
|
286
|
+
type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
|
|
287
|
+
declare class TabSyncError extends Error {
|
|
288
|
+
readonly code: ErrorCode;
|
|
289
|
+
readonly name = "TabSyncError";
|
|
290
|
+
readonly cause?: unknown;
|
|
291
|
+
constructor(message: string, code: ErrorCode, cause?: unknown);
|
|
292
|
+
static timeout(method: string, ms: number): TabSyncError;
|
|
293
|
+
static noLeader(): TabSyncError;
|
|
294
|
+
static noHandler(method: string): TabSyncError;
|
|
295
|
+
static destroyed(): TabSyncError;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
declare const isBrowser: boolean;
|
|
299
|
+
declare const hasDocument: boolean;
|
|
300
|
+
declare const hasLocalStorage: boolean;
|
|
301
|
+
declare const hasBroadcastChannel: boolean;
|
|
302
|
+
declare const hasCrypto: boolean;
|
|
303
|
+
|
|
304
|
+
export { type Batcher, BroadcastChannelTransport, ChangeMeta, type Channel, Emitter, ErrorCode, LeaderElection, type LeaderElectionOptions, MessageOf, MessagePayloadMap, MessageType, Middleware, MiddlewareContext, RPCHandler, type RPCHandlerOptions, RPCMap, SendFn, StateManager, type StateManagerOptions, StorageChannel, TabInfo, TabMessage, TabRegistry, type TabRegistryOptions, TabSyncError, TabSyncInstance, TabSyncOptions, createBatcher, createChannel, createMessage, createTabSync, destroyMiddleware, generateTabId, hasBroadcastChannel, hasCrypto, hasDocument, hasLocalStorage, isBrowser, monotonic, notifyMiddleware, runMiddleware };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PROTOCOL_VERSION, monotonic } from './chunk-42VOZR6E.js';
|
|
2
|
+
export { BroadcastChannelTransport, ErrorCode, LeaderElection, PROTOCOL_VERSION, RPCHandler, StateManager, StorageChannel, TabRegistry, TabSyncError, createBatcher, createChannel, createTabSync, destroyMiddleware, generateTabId, hasBroadcastChannel, hasCrypto, hasDocument, hasLocalStorage, isBrowser, monotonic, notifyMiddleware, runMiddleware } from './chunk-42VOZR6E.js';
|
|
3
|
+
|
|
4
|
+
// src/utils/message.ts
|
|
5
|
+
function createMessage(type, senderId, payload, targetId) {
|
|
6
|
+
return {
|
|
7
|
+
type,
|
|
8
|
+
senderId,
|
|
9
|
+
targetId,
|
|
10
|
+
timestamp: monotonic(),
|
|
11
|
+
version: PROTOCOL_VERSION,
|
|
12
|
+
payload
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/utils/emitter.ts
|
|
17
|
+
var Emitter = class {
|
|
18
|
+
constructor() {
|
|
19
|
+
this._handlers = /* @__PURE__ */ new Map();
|
|
20
|
+
}
|
|
21
|
+
on(event, handler) {
|
|
22
|
+
let set = this._handlers.get(event);
|
|
23
|
+
if (!set) {
|
|
24
|
+
set = /* @__PURE__ */ new Set();
|
|
25
|
+
this._handlers.set(event, set);
|
|
26
|
+
}
|
|
27
|
+
set.add(handler);
|
|
28
|
+
return () => {
|
|
29
|
+
set.delete(handler);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
once(event, handler) {
|
|
33
|
+
const off = this.on(event, ((...args) => {
|
|
34
|
+
off();
|
|
35
|
+
handler(...args);
|
|
36
|
+
}));
|
|
37
|
+
return off;
|
|
38
|
+
}
|
|
39
|
+
emit(...args) {
|
|
40
|
+
const [event, data] = args;
|
|
41
|
+
const set = this._handlers.get(event);
|
|
42
|
+
if (!set) return;
|
|
43
|
+
for (const handler of set) {
|
|
44
|
+
handler(data);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
listenerCount(event) {
|
|
48
|
+
return this._handlers.get(event)?.size ?? 0;
|
|
49
|
+
}
|
|
50
|
+
removeAll(event) {
|
|
51
|
+
if (event) {
|
|
52
|
+
this._handlers.delete(event);
|
|
53
|
+
} else {
|
|
54
|
+
this._handlers.clear();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { Emitter, createMessage };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkBQCNBNBT_cjs = require('../chunk-BQCNBNBT.cjs');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
var TabSyncContext = react.createContext(null);
|
|
8
|
+
function TabSyncProvider({
|
|
9
|
+
options,
|
|
10
|
+
children
|
|
11
|
+
}) {
|
|
12
|
+
const instanceRef = react.useRef(null);
|
|
13
|
+
if (!instanceRef.current) {
|
|
14
|
+
instanceRef.current = chunkBQCNBNBT_cjs.createTabSync(options);
|
|
15
|
+
}
|
|
16
|
+
react.useEffect(() => {
|
|
17
|
+
return () => {
|
|
18
|
+
instanceRef.current?.destroy();
|
|
19
|
+
instanceRef.current = null;
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
return /* @__PURE__ */ jsxRuntime.jsx(TabSyncContext.Provider, { value: instanceRef.current, children });
|
|
23
|
+
}
|
|
24
|
+
var EMPTY_TABS = [];
|
|
25
|
+
function useInstance(options) {
|
|
26
|
+
const contextInstance = react.useContext(TabSyncContext);
|
|
27
|
+
const [ownInstance] = react.useState(() => {
|
|
28
|
+
if (!contextInstance && options) {
|
|
29
|
+
return chunkBQCNBNBT_cjs.createTabSync(options);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
});
|
|
33
|
+
react.useEffect(() => {
|
|
34
|
+
return () => ownInstance?.destroy();
|
|
35
|
+
}, [ownInstance]);
|
|
36
|
+
const instance = contextInstance ?? ownInstance;
|
|
37
|
+
if (!instance) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"useTabSync: provide options for standalone use, or wrap with <TabSyncProvider>"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return instance;
|
|
43
|
+
}
|
|
44
|
+
function useTabSync(options) {
|
|
45
|
+
const instance = useInstance(options);
|
|
46
|
+
const stateSubscribe = react.useCallback(
|
|
47
|
+
(cb) => instance.onChange(() => cb()),
|
|
48
|
+
[instance]
|
|
49
|
+
);
|
|
50
|
+
const getStateSnapshot = react.useCallback(() => instance.getAll(), [instance]);
|
|
51
|
+
const state = react.useSyncExternalStore(
|
|
52
|
+
stateSubscribe,
|
|
53
|
+
getStateSnapshot,
|
|
54
|
+
getStateSnapshot
|
|
55
|
+
);
|
|
56
|
+
const leaderRef = react.useRef(instance.isLeader());
|
|
57
|
+
const leaderSubscribe = react.useCallback(
|
|
58
|
+
(onStoreChange) => {
|
|
59
|
+
const unsub = instance.onLeader(() => {
|
|
60
|
+
leaderRef.current = true;
|
|
61
|
+
onStoreChange();
|
|
62
|
+
return () => {
|
|
63
|
+
leaderRef.current = false;
|
|
64
|
+
onStoreChange();
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
return unsub;
|
|
68
|
+
},
|
|
69
|
+
[instance]
|
|
70
|
+
);
|
|
71
|
+
const getLeaderSnapshot = react.useCallback(() => leaderRef.current, []);
|
|
72
|
+
const getLeaderServerSnapshot = react.useCallback(() => false, []);
|
|
73
|
+
const isLeader = react.useSyncExternalStore(
|
|
74
|
+
leaderSubscribe,
|
|
75
|
+
getLeaderSnapshot,
|
|
76
|
+
getLeaderServerSnapshot
|
|
77
|
+
);
|
|
78
|
+
const tabsRef = react.useRef(instance.getTabs());
|
|
79
|
+
const tabsSubscribe = react.useCallback(
|
|
80
|
+
(cb) => instance.onTabChange((tabs2) => {
|
|
81
|
+
tabsRef.current = tabs2;
|
|
82
|
+
cb();
|
|
83
|
+
}),
|
|
84
|
+
[instance]
|
|
85
|
+
);
|
|
86
|
+
const getTabsSnapshot = react.useCallback(() => tabsRef.current, []);
|
|
87
|
+
const getTabsServerSnapshot = react.useCallback(() => EMPTY_TABS, []);
|
|
88
|
+
const tabs = react.useSyncExternalStore(
|
|
89
|
+
tabsSubscribe,
|
|
90
|
+
getTabsSnapshot,
|
|
91
|
+
getTabsServerSnapshot
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
state,
|
|
95
|
+
set: instance.set,
|
|
96
|
+
patch: instance.patch,
|
|
97
|
+
isLeader,
|
|
98
|
+
tabs,
|
|
99
|
+
tabId: instance.id
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function useTabSyncValue(key) {
|
|
103
|
+
const instance = react.useContext(TabSyncContext);
|
|
104
|
+
if (!instance) {
|
|
105
|
+
throw new Error("useTabSyncValue must be used within a <TabSyncProvider>");
|
|
106
|
+
}
|
|
107
|
+
const subscribe = react.useCallback(
|
|
108
|
+
(cb) => instance.on(key, () => cb()),
|
|
109
|
+
[instance, key]
|
|
110
|
+
);
|
|
111
|
+
const getSnapshot = react.useCallback(() => instance.get(key), [instance, key]);
|
|
112
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
113
|
+
}
|
|
114
|
+
function useTabSyncSelector(selector, isEqual) {
|
|
115
|
+
const instance = react.useContext(TabSyncContext);
|
|
116
|
+
if (!instance) {
|
|
117
|
+
throw new Error("useTabSyncSelector must be used within a <TabSyncProvider>");
|
|
118
|
+
}
|
|
119
|
+
const selectorRef = react.useRef(selector);
|
|
120
|
+
const isEqualRef = react.useRef(isEqual);
|
|
121
|
+
const resultRef = react.useRef(void 0);
|
|
122
|
+
const initializedRef = react.useRef(false);
|
|
123
|
+
selectorRef.current = selector;
|
|
124
|
+
isEqualRef.current = isEqual;
|
|
125
|
+
if (!initializedRef.current) {
|
|
126
|
+
resultRef.current = selector(instance.getAll());
|
|
127
|
+
initializedRef.current = true;
|
|
128
|
+
}
|
|
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)) {
|
|
134
|
+
resultRef.current = next;
|
|
135
|
+
onStoreChange();
|
|
136
|
+
}
|
|
137
|
+
}),
|
|
138
|
+
[instance]
|
|
139
|
+
);
|
|
140
|
+
const getSnapshot = react.useCallback(() => resultRef.current, []);
|
|
141
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
142
|
+
}
|
|
143
|
+
var SERVER_SNAPSHOT = false;
|
|
144
|
+
function useIsLeader() {
|
|
145
|
+
const instance = react.useContext(TabSyncContext);
|
|
146
|
+
if (!instance) {
|
|
147
|
+
throw new Error("useIsLeader must be used within a <TabSyncProvider>");
|
|
148
|
+
}
|
|
149
|
+
const leaderRef = react.useRef(instance.isLeader());
|
|
150
|
+
const subscribe = react.useCallback(
|
|
151
|
+
(onStoreChange) => {
|
|
152
|
+
const unsub = instance.onLeader(() => {
|
|
153
|
+
leaderRef.current = true;
|
|
154
|
+
onStoreChange();
|
|
155
|
+
return () => {
|
|
156
|
+
leaderRef.current = false;
|
|
157
|
+
onStoreChange();
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
return unsub;
|
|
161
|
+
},
|
|
162
|
+
[instance]
|
|
163
|
+
);
|
|
164
|
+
const getSnapshot = react.useCallback(() => leaderRef.current, []);
|
|
165
|
+
const getServerSnapshot = react.useCallback(() => SERVER_SNAPSHOT, []);
|
|
166
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
exports.TabSyncContext = TabSyncContext;
|
|
170
|
+
exports.TabSyncProvider = TabSyncProvider;
|
|
171
|
+
exports.useIsLeader = useIsLeader;
|
|
172
|
+
exports.useTabSync = useTabSync;
|
|
173
|
+
exports.useTabSyncSelector = useTabSyncSelector;
|
|
174
|
+
exports.useTabSyncValue = useTabSyncValue;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { T as TabSyncOptions, a as TabSyncInstance, R as RPCMap, c as TabInfo } from '../types-BtK4ixKz.cjs';
|
|
5
|
+
|
|
6
|
+
interface TabSyncProviderProps<TState extends Record<string, unknown>> {
|
|
7
|
+
options: TabSyncOptions<TState>;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Provides a shared `TabSyncInstance` to all children via context.
|
|
12
|
+
* The instance is created once and destroyed on unmount.
|
|
13
|
+
*/
|
|
14
|
+
declare function TabSyncProvider<TState extends Record<string, unknown>>({ options, children, }: TabSyncProviderProps<TState>): react_jsx_runtime.JSX.Element;
|
|
15
|
+
|
|
16
|
+
declare const TabSyncContext: react.Context<TabSyncInstance<any, RPCMap> | null>;
|
|
17
|
+
|
|
18
|
+
declare function useTabSync<TState extends Record<string, unknown>>(options?: TabSyncOptions<TState>): {
|
|
19
|
+
state: Readonly<TState>;
|
|
20
|
+
set: <K extends keyof TState>(key: K, value: TState[K]) => void;
|
|
21
|
+
patch: (partial: Partial<TState>) => void;
|
|
22
|
+
isLeader: boolean;
|
|
23
|
+
tabs: TabInfo[];
|
|
24
|
+
tabId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe to a single key for minimal re-renders.
|
|
29
|
+
*
|
|
30
|
+
* Must be used within a `<TabSyncProvider>`.
|
|
31
|
+
*/
|
|
32
|
+
declare function useTabSyncValue<TState extends Record<string, unknown>, K extends keyof TState>(key: K): TState[K];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to a **derived value** from the synced state.
|
|
36
|
+
* Only re-renders when the selector's output actually changes.
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const doneCount = useTabSyncSelector(
|
|
40
|
+
* (s) => s.todos.filter(t => t.done).length,
|
|
41
|
+
* );
|
|
42
|
+
*
|
|
43
|
+
* // With custom equality (e.g. for arrays/objects):
|
|
44
|
+
* const activeTabs = useTabSyncSelector(
|
|
45
|
+
* (s) => s.tabs.filter(t => t.active),
|
|
46
|
+
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
|
|
47
|
+
* );
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Must be used within a `<TabSyncProvider>`.
|
|
51
|
+
*/
|
|
52
|
+
declare function useTabSyncSelector<TState extends Record<string, unknown>, TResult>(selector: (state: Readonly<TState>) => TResult, isEqual?: (a: TResult, b: TResult) => boolean): TResult;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to leader status. Re-renders only when leadership changes.
|
|
56
|
+
*
|
|
57
|
+
* Must be used within a `<TabSyncProvider>`.
|
|
58
|
+
*/
|
|
59
|
+
declare function useIsLeader(): boolean;
|
|
60
|
+
|
|
61
|
+
export { TabSyncContext, TabSyncProvider, type TabSyncProviderProps, useIsLeader, useTabSync, useTabSyncSelector, useTabSyncValue };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { T as TabSyncOptions, a as TabSyncInstance, R as RPCMap, c as TabInfo } from '../types-BtK4ixKz.js';
|
|
5
|
+
|
|
6
|
+
interface TabSyncProviderProps<TState extends Record<string, unknown>> {
|
|
7
|
+
options: TabSyncOptions<TState>;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Provides a shared `TabSyncInstance` to all children via context.
|
|
12
|
+
* The instance is created once and destroyed on unmount.
|
|
13
|
+
*/
|
|
14
|
+
declare function TabSyncProvider<TState extends Record<string, unknown>>({ options, children, }: TabSyncProviderProps<TState>): react_jsx_runtime.JSX.Element;
|
|
15
|
+
|
|
16
|
+
declare const TabSyncContext: react.Context<TabSyncInstance<any, RPCMap> | null>;
|
|
17
|
+
|
|
18
|
+
declare function useTabSync<TState extends Record<string, unknown>>(options?: TabSyncOptions<TState>): {
|
|
19
|
+
state: Readonly<TState>;
|
|
20
|
+
set: <K extends keyof TState>(key: K, value: TState[K]) => void;
|
|
21
|
+
patch: (partial: Partial<TState>) => void;
|
|
22
|
+
isLeader: boolean;
|
|
23
|
+
tabs: TabInfo[];
|
|
24
|
+
tabId: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subscribe to a single key for minimal re-renders.
|
|
29
|
+
*
|
|
30
|
+
* Must be used within a `<TabSyncProvider>`.
|
|
31
|
+
*/
|
|
32
|
+
declare function useTabSyncValue<TState extends Record<string, unknown>, K extends keyof TState>(key: K): TState[K];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to a **derived value** from the synced state.
|
|
36
|
+
* Only re-renders when the selector's output actually changes.
|
|
37
|
+
*
|
|
38
|
+
* ```tsx
|
|
39
|
+
* const doneCount = useTabSyncSelector(
|
|
40
|
+
* (s) => s.todos.filter(t => t.done).length,
|
|
41
|
+
* );
|
|
42
|
+
*
|
|
43
|
+
* // With custom equality (e.g. for arrays/objects):
|
|
44
|
+
* const activeTabs = useTabSyncSelector(
|
|
45
|
+
* (s) => s.tabs.filter(t => t.active),
|
|
46
|
+
* (a, b) => a.length === b.length && a.every((v, i) => v === b[i]),
|
|
47
|
+
* );
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Must be used within a `<TabSyncProvider>`.
|
|
51
|
+
*/
|
|
52
|
+
declare function useTabSyncSelector<TState extends Record<string, unknown>, TResult>(selector: (state: Readonly<TState>) => TResult, isEqual?: (a: TResult, b: TResult) => boolean): TResult;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subscribe to leader status. Re-renders only when leadership changes.
|
|
56
|
+
*
|
|
57
|
+
* Must be used within a `<TabSyncProvider>`.
|
|
58
|
+
*/
|
|
59
|
+
declare function useIsLeader(): boolean;
|
|
60
|
+
|
|
61
|
+
export { TabSyncContext, TabSyncProvider, type TabSyncProviderProps, useIsLeader, useTabSync, useTabSyncSelector, useTabSyncValue };
|