shared-state-bridge 1.0.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,154 @@
1
+ /** State must be a plain object */
2
+ type State = Record<string, unknown>;
3
+ /** Full-state change listener */
4
+ type Listener<T extends State> = (state: T, previousState: T) => void;
5
+ /** Selector-based change listener */
6
+ type SelectorListener<_T extends State, U> = (slice: U, previousSlice: U) => void;
7
+ /** setState accepts partial updates or an updater function */
8
+ interface SetState<T extends State> {
9
+ (partial: Partial<T> | ((state: T) => Partial<T>)): void;
10
+ (state: T | ((state: T) => T), replace: true): void;
11
+ }
12
+ /** Subscribe overloads: full-state or selector-based */
13
+ interface Subscribe<T extends State> {
14
+ (listener: Listener<T>): () => void;
15
+ <U>(selector: (state: T) => U, listener: SelectorListener<T, U>, options?: SubscribeOptions<U>): () => void;
16
+ }
17
+ interface SubscribeOptions<U> {
18
+ equalityFn?: (a: U, b: U) => boolean;
19
+ fireImmediately?: boolean;
20
+ }
21
+ /** Plugin lifecycle hooks */
22
+ interface BridgePlugin<T extends State> {
23
+ name: string;
24
+ onInit?: (bridge: BridgeApi<T>) => void;
25
+ onStateChange?: (state: T, previousState: T) => void;
26
+ onDestroy?: () => void;
27
+ }
28
+ /** The bridge instance API */
29
+ interface BridgeApi<T extends State> {
30
+ getState: () => T;
31
+ setState: SetState<T>;
32
+ subscribe: Subscribe<T>;
33
+ getInitialState: () => T;
34
+ destroy: () => void;
35
+ getName: () => string;
36
+ }
37
+
38
+ /** Configuration for the sync plugin */
39
+ interface SyncOptions<T extends State> {
40
+ /** WebSocket server URL (e.g. 'wss://your-server.com/sync') */
41
+ url: string;
42
+ /** Channel/room name — clients on the same channel sync state */
43
+ channel: string;
44
+ /** Only sync these keys */
45
+ pick?: (keyof T)[];
46
+ /** Exclude these keys from sync */
47
+ omit?: (keyof T)[];
48
+ /** Throttle outbound messages in ms (default: 50) */
49
+ throttleMs?: number;
50
+ /** Auto-reconnect on disconnect (default: true) */
51
+ reconnect?: boolean;
52
+ /** Max reconnect attempts (default: Infinity) */
53
+ maxReconnectAttempts?: number;
54
+ /** Base reconnect interval in ms, doubles each attempt (default: 1000) */
55
+ reconnectInterval?: number;
56
+ /** Max reconnect interval cap in ms (default: 30000) */
57
+ maxReconnectInterval?: number;
58
+ /** Called when WebSocket connects */
59
+ onConnect?: () => void;
60
+ /** Called when WebSocket disconnects */
61
+ onDisconnect?: () => void;
62
+ /** Called on WebSocket or protocol error */
63
+ onError?: (error: unknown) => void;
64
+ /** Custom conflict resolver: receives local and remote state, returns merged */
65
+ resolve?: (localState: T, remoteState: Partial<T>) => Partial<T>;
66
+ }
67
+ /** Client -> Server: join a channel */
68
+ interface JoinMessage {
69
+ type: 'join';
70
+ channel: string;
71
+ clientId: string;
72
+ }
73
+ /** Client -> Server: broadcast state change */
74
+ interface StateMessage {
75
+ type: 'state';
76
+ channel: string;
77
+ clientId: string;
78
+ state: Record<string, unknown>;
79
+ timestamp: number;
80
+ }
81
+ /** Server -> Client: full state snapshot (sent on join if server supports it) */
82
+ interface FullStateMessage {
83
+ type: 'full_state';
84
+ channel: string;
85
+ state: Record<string, unknown>;
86
+ timestamp: number;
87
+ }
88
+ /** All outbound message types */
89
+ type OutboundMessage = JoinMessage | StateMessage;
90
+ /** All inbound message types */
91
+ type InboundMessage = StateMessage | FullStateMessage;
92
+
93
+ /**
94
+ * Create a sync plugin that synchronizes bridge state across apps
95
+ * via WebSocket in real-time.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * import { createBridge } from 'shared-state-bridge'
100
+ * import { sync } from 'shared-state-bridge/sync'
101
+ *
102
+ * const bridge = createBridge({
103
+ * name: 'app',
104
+ * initialState: { theme: 'light', count: 0 },
105
+ * plugins: [
106
+ * sync({
107
+ * url: 'wss://your-server.com/sync',
108
+ * channel: 'my-room',
109
+ * pick: ['theme'],
110
+ * }),
111
+ * ],
112
+ * })
113
+ * ```
114
+ */
115
+ declare function sync<T extends State>(options: SyncOptions<T>): BridgePlugin<T>;
116
+
117
+ interface ConnectionOptions {
118
+ url: string;
119
+ reconnect: boolean;
120
+ maxReconnectAttempts: number;
121
+ reconnectInterval: number;
122
+ maxReconnectInterval: number;
123
+ onMessage: (message: InboundMessage) => void;
124
+ onConnect: () => void;
125
+ onDisconnect: () => void;
126
+ onError: (error: unknown) => void;
127
+ }
128
+ /**
129
+ * WebSocket connection manager with auto-reconnect and message buffering.
130
+ *
131
+ * - Connects to the WebSocket server
132
+ * - Buffers outbound messages while disconnected
133
+ * - Flushes buffer on reconnect
134
+ * - Exponential backoff reconnection (1s, 2s, 4s, 8s... capped)
135
+ */
136
+ declare class SyncConnection {
137
+ private ws;
138
+ private options;
139
+ private buffer;
140
+ private reconnectAttempts;
141
+ private reconnectTimer;
142
+ private destroyed;
143
+ private _connected;
144
+ constructor(options: ConnectionOptions);
145
+ get connected(): boolean;
146
+ connect(): void;
147
+ send(message: OutboundMessage): void;
148
+ destroy(): void;
149
+ private createSocket;
150
+ private flushBuffer;
151
+ private scheduleReconnect;
152
+ }
153
+
154
+ export { type FullStateMessage, type InboundMessage, type JoinMessage, type OutboundMessage, type StateMessage, SyncConnection, type SyncOptions, sync };
package/dist/sync.d.ts ADDED
@@ -0,0 +1,154 @@
1
+ /** State must be a plain object */
2
+ type State = Record<string, unknown>;
3
+ /** Full-state change listener */
4
+ type Listener<T extends State> = (state: T, previousState: T) => void;
5
+ /** Selector-based change listener */
6
+ type SelectorListener<_T extends State, U> = (slice: U, previousSlice: U) => void;
7
+ /** setState accepts partial updates or an updater function */
8
+ interface SetState<T extends State> {
9
+ (partial: Partial<T> | ((state: T) => Partial<T>)): void;
10
+ (state: T | ((state: T) => T), replace: true): void;
11
+ }
12
+ /** Subscribe overloads: full-state or selector-based */
13
+ interface Subscribe<T extends State> {
14
+ (listener: Listener<T>): () => void;
15
+ <U>(selector: (state: T) => U, listener: SelectorListener<T, U>, options?: SubscribeOptions<U>): () => void;
16
+ }
17
+ interface SubscribeOptions<U> {
18
+ equalityFn?: (a: U, b: U) => boolean;
19
+ fireImmediately?: boolean;
20
+ }
21
+ /** Plugin lifecycle hooks */
22
+ interface BridgePlugin<T extends State> {
23
+ name: string;
24
+ onInit?: (bridge: BridgeApi<T>) => void;
25
+ onStateChange?: (state: T, previousState: T) => void;
26
+ onDestroy?: () => void;
27
+ }
28
+ /** The bridge instance API */
29
+ interface BridgeApi<T extends State> {
30
+ getState: () => T;
31
+ setState: SetState<T>;
32
+ subscribe: Subscribe<T>;
33
+ getInitialState: () => T;
34
+ destroy: () => void;
35
+ getName: () => string;
36
+ }
37
+
38
+ /** Configuration for the sync plugin */
39
+ interface SyncOptions<T extends State> {
40
+ /** WebSocket server URL (e.g. 'wss://your-server.com/sync') */
41
+ url: string;
42
+ /** Channel/room name — clients on the same channel sync state */
43
+ channel: string;
44
+ /** Only sync these keys */
45
+ pick?: (keyof T)[];
46
+ /** Exclude these keys from sync */
47
+ omit?: (keyof T)[];
48
+ /** Throttle outbound messages in ms (default: 50) */
49
+ throttleMs?: number;
50
+ /** Auto-reconnect on disconnect (default: true) */
51
+ reconnect?: boolean;
52
+ /** Max reconnect attempts (default: Infinity) */
53
+ maxReconnectAttempts?: number;
54
+ /** Base reconnect interval in ms, doubles each attempt (default: 1000) */
55
+ reconnectInterval?: number;
56
+ /** Max reconnect interval cap in ms (default: 30000) */
57
+ maxReconnectInterval?: number;
58
+ /** Called when WebSocket connects */
59
+ onConnect?: () => void;
60
+ /** Called when WebSocket disconnects */
61
+ onDisconnect?: () => void;
62
+ /** Called on WebSocket or protocol error */
63
+ onError?: (error: unknown) => void;
64
+ /** Custom conflict resolver: receives local and remote state, returns merged */
65
+ resolve?: (localState: T, remoteState: Partial<T>) => Partial<T>;
66
+ }
67
+ /** Client -> Server: join a channel */
68
+ interface JoinMessage {
69
+ type: 'join';
70
+ channel: string;
71
+ clientId: string;
72
+ }
73
+ /** Client -> Server: broadcast state change */
74
+ interface StateMessage {
75
+ type: 'state';
76
+ channel: string;
77
+ clientId: string;
78
+ state: Record<string, unknown>;
79
+ timestamp: number;
80
+ }
81
+ /** Server -> Client: full state snapshot (sent on join if server supports it) */
82
+ interface FullStateMessage {
83
+ type: 'full_state';
84
+ channel: string;
85
+ state: Record<string, unknown>;
86
+ timestamp: number;
87
+ }
88
+ /** All outbound message types */
89
+ type OutboundMessage = JoinMessage | StateMessage;
90
+ /** All inbound message types */
91
+ type InboundMessage = StateMessage | FullStateMessage;
92
+
93
+ /**
94
+ * Create a sync plugin that synchronizes bridge state across apps
95
+ * via WebSocket in real-time.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * import { createBridge } from 'shared-state-bridge'
100
+ * import { sync } from 'shared-state-bridge/sync'
101
+ *
102
+ * const bridge = createBridge({
103
+ * name: 'app',
104
+ * initialState: { theme: 'light', count: 0 },
105
+ * plugins: [
106
+ * sync({
107
+ * url: 'wss://your-server.com/sync',
108
+ * channel: 'my-room',
109
+ * pick: ['theme'],
110
+ * }),
111
+ * ],
112
+ * })
113
+ * ```
114
+ */
115
+ declare function sync<T extends State>(options: SyncOptions<T>): BridgePlugin<T>;
116
+
117
+ interface ConnectionOptions {
118
+ url: string;
119
+ reconnect: boolean;
120
+ maxReconnectAttempts: number;
121
+ reconnectInterval: number;
122
+ maxReconnectInterval: number;
123
+ onMessage: (message: InboundMessage) => void;
124
+ onConnect: () => void;
125
+ onDisconnect: () => void;
126
+ onError: (error: unknown) => void;
127
+ }
128
+ /**
129
+ * WebSocket connection manager with auto-reconnect and message buffering.
130
+ *
131
+ * - Connects to the WebSocket server
132
+ * - Buffers outbound messages while disconnected
133
+ * - Flushes buffer on reconnect
134
+ * - Exponential backoff reconnection (1s, 2s, 4s, 8s... capped)
135
+ */
136
+ declare class SyncConnection {
137
+ private ws;
138
+ private options;
139
+ private buffer;
140
+ private reconnectAttempts;
141
+ private reconnectTimer;
142
+ private destroyed;
143
+ private _connected;
144
+ constructor(options: ConnectionOptions);
145
+ get connected(): boolean;
146
+ connect(): void;
147
+ send(message: OutboundMessage): void;
148
+ destroy(): void;
149
+ private createSocket;
150
+ private flushBuffer;
151
+ private scheduleReconnect;
152
+ }
153
+
154
+ export { type FullStateMessage, type InboundMessage, type JoinMessage, type OutboundMessage, type StateMessage, SyncConnection, type SyncOptions, sync };
package/dist/sync.js ADDED
@@ -0,0 +1,246 @@
1
+ // src/sync/connection.ts
2
+ var SyncConnection = class {
3
+ constructor(options) {
4
+ this.ws = null;
5
+ this.buffer = [];
6
+ this.reconnectAttempts = 0;
7
+ this.reconnectTimer = null;
8
+ this.destroyed = false;
9
+ this._connected = false;
10
+ this.options = options;
11
+ }
12
+ get connected() {
13
+ return this._connected;
14
+ }
15
+ connect() {
16
+ if (this.destroyed) return;
17
+ this.createSocket();
18
+ }
19
+ send(message) {
20
+ const data = JSON.stringify(message);
21
+ if (this._connected && this.ws?.readyState === WebSocket.OPEN) {
22
+ this.ws.send(data);
23
+ } else {
24
+ this.buffer.push(data);
25
+ }
26
+ }
27
+ destroy() {
28
+ this.destroyed = true;
29
+ if (this.reconnectTimer) {
30
+ clearTimeout(this.reconnectTimer);
31
+ this.reconnectTimer = null;
32
+ }
33
+ if (this.ws) {
34
+ this.ws.onopen = null;
35
+ this.ws.onclose = null;
36
+ this.ws.onmessage = null;
37
+ this.ws.onerror = null;
38
+ this.ws.close();
39
+ this.ws = null;
40
+ }
41
+ this.buffer = [];
42
+ this._connected = false;
43
+ }
44
+ createSocket() {
45
+ try {
46
+ this.ws = new WebSocket(this.options.url);
47
+ } catch (err) {
48
+ this.options.onError(err);
49
+ this.scheduleReconnect();
50
+ return;
51
+ }
52
+ this.ws.onopen = () => {
53
+ this._connected = true;
54
+ this.reconnectAttempts = 0;
55
+ this.flushBuffer();
56
+ this.options.onConnect();
57
+ };
58
+ this.ws.onclose = () => {
59
+ const wasConnected = this._connected;
60
+ this._connected = false;
61
+ if (wasConnected) {
62
+ this.options.onDisconnect();
63
+ }
64
+ this.scheduleReconnect();
65
+ };
66
+ this.ws.onerror = (event) => {
67
+ this.options.onError(event);
68
+ };
69
+ this.ws.onmessage = (event) => {
70
+ try {
71
+ const message = JSON.parse(
72
+ typeof event.data === "string" ? event.data : String(event.data)
73
+ );
74
+ this.options.onMessage(message);
75
+ } catch {
76
+ }
77
+ };
78
+ }
79
+ flushBuffer() {
80
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
81
+ const messages = this.buffer.splice(0);
82
+ for (const data of messages) {
83
+ this.ws.send(data);
84
+ }
85
+ }
86
+ scheduleReconnect() {
87
+ if (this.destroyed) return;
88
+ if (!this.options.reconnect) return;
89
+ if (this.reconnectAttempts >= this.options.maxReconnectAttempts) return;
90
+ const delay = Math.min(
91
+ this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts),
92
+ this.options.maxReconnectInterval
93
+ );
94
+ this.reconnectAttempts++;
95
+ this.reconnectTimer = setTimeout(() => {
96
+ this.reconnectTimer = null;
97
+ if (!this.destroyed) {
98
+ this.createSocket();
99
+ }
100
+ }, delay);
101
+ }
102
+ };
103
+
104
+ // src/core/utils.ts
105
+ function throttle(fn, intervalMs) {
106
+ let lastCall = 0;
107
+ let timeoutId = null;
108
+ let latestArgs;
109
+ return (...args) => {
110
+ latestArgs = args;
111
+ const now = Date.now();
112
+ const remaining = intervalMs - (now - lastCall);
113
+ if (remaining <= 0) {
114
+ if (timeoutId) {
115
+ clearTimeout(timeoutId);
116
+ timeoutId = null;
117
+ }
118
+ lastCall = now;
119
+ fn(...latestArgs);
120
+ } else if (!timeoutId) {
121
+ timeoutId = setTimeout(() => {
122
+ lastCall = Date.now();
123
+ timeoutId = null;
124
+ fn(...latestArgs);
125
+ }, remaining);
126
+ }
127
+ };
128
+ }
129
+
130
+ // src/sync/plugin.ts
131
+ function generateClientId() {
132
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
133
+ }
134
+ function sync(options) {
135
+ const {
136
+ url,
137
+ channel,
138
+ pick,
139
+ omit,
140
+ throttleMs = 50,
141
+ reconnect = true,
142
+ maxReconnectAttempts = Infinity,
143
+ reconnectInterval = 1e3,
144
+ maxReconnectInterval = 3e4,
145
+ onConnect,
146
+ onDisconnect,
147
+ onError,
148
+ resolve
149
+ } = options;
150
+ const clientId = generateClientId();
151
+ let connection = null;
152
+ let bridge = null;
153
+ let sendState = null;
154
+ let isApplyingRemote = false;
155
+ function filterState(state) {
156
+ if (pick) {
157
+ const filtered = {};
158
+ for (const k of pick) {
159
+ if (k in state) {
160
+ filtered[k] = state[k];
161
+ }
162
+ }
163
+ return filtered;
164
+ }
165
+ if (omit) {
166
+ const filtered = { ...state };
167
+ for (const k of omit) {
168
+ delete filtered[k];
169
+ }
170
+ return filtered;
171
+ }
172
+ return state;
173
+ }
174
+ return {
175
+ name: "sync",
176
+ onInit: (bridgeApi) => {
177
+ bridge = bridgeApi;
178
+ sendState = throttle((state) => {
179
+ if (!connection) return;
180
+ const filtered = filterState(state);
181
+ connection.send({
182
+ type: "state",
183
+ channel,
184
+ clientId,
185
+ state: filtered,
186
+ timestamp: Date.now()
187
+ });
188
+ }, throttleMs);
189
+ connection = new SyncConnection({
190
+ url,
191
+ reconnect,
192
+ maxReconnectAttempts,
193
+ reconnectInterval,
194
+ maxReconnectInterval,
195
+ onConnect: () => {
196
+ connection.send({
197
+ type: "join",
198
+ channel,
199
+ clientId
200
+ });
201
+ onConnect?.();
202
+ },
203
+ onDisconnect: () => {
204
+ onDisconnect?.();
205
+ },
206
+ onError: (err) => {
207
+ onError?.(err);
208
+ },
209
+ onMessage: (message) => {
210
+ if (!bridge) return;
211
+ if ("clientId" in message && message.clientId === clientId) return;
212
+ if (message.type === "state" || message.type === "full_state") {
213
+ const remoteState = message.state;
214
+ let stateToApply;
215
+ if (resolve) {
216
+ stateToApply = resolve(bridge.getState(), remoteState);
217
+ } else {
218
+ stateToApply = remoteState;
219
+ }
220
+ isApplyingRemote = true;
221
+ try {
222
+ bridge.setState(stateToApply);
223
+ } finally {
224
+ isApplyingRemote = false;
225
+ }
226
+ }
227
+ }
228
+ });
229
+ connection.connect();
230
+ },
231
+ onStateChange: (state) => {
232
+ if (isApplyingRemote) return;
233
+ sendState?.(state);
234
+ },
235
+ onDestroy: () => {
236
+ connection?.destroy();
237
+ connection = null;
238
+ bridge = null;
239
+ sendState = null;
240
+ }
241
+ };
242
+ }
243
+
244
+ export { SyncConnection, sync };
245
+ //# sourceMappingURL=sync.js.map
246
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sync/connection.ts","../src/core/utils.ts","../src/sync/plugin.ts"],"names":[],"mappings":"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","file":"sync.js","sourcesContent":["import type { OutboundMessage, InboundMessage } from './types'\n\nexport interface ConnectionOptions {\n url: string\n reconnect: boolean\n maxReconnectAttempts: number\n reconnectInterval: number\n maxReconnectInterval: number\n onMessage: (message: InboundMessage) => void\n onConnect: () => void\n onDisconnect: () => void\n onError: (error: unknown) => void\n}\n\n/**\n * WebSocket connection manager with auto-reconnect and message buffering.\n *\n * - Connects to the WebSocket server\n * - Buffers outbound messages while disconnected\n * - Flushes buffer on reconnect\n * - Exponential backoff reconnection (1s, 2s, 4s, 8s... capped)\n */\nexport class SyncConnection {\n private ws: WebSocket | null = null\n private options: ConnectionOptions\n private buffer: string[] = []\n private reconnectAttempts = 0\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null\n private destroyed = false\n private _connected = false\n\n constructor(options: ConnectionOptions) {\n this.options = options\n }\n\n get connected(): boolean {\n return this._connected\n }\n\n connect(): void {\n if (this.destroyed) return\n this.createSocket()\n }\n\n send(message: OutboundMessage): void {\n const data = JSON.stringify(message)\n if (this._connected && this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(data)\n } else {\n this.buffer.push(data)\n }\n }\n\n destroy(): void {\n this.destroyed = true\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer)\n this.reconnectTimer = null\n }\n if (this.ws) {\n this.ws.onopen = null\n this.ws.onclose = null\n this.ws.onmessage = null\n this.ws.onerror = null\n this.ws.close()\n this.ws = null\n }\n this.buffer = []\n this._connected = false\n }\n\n private createSocket(): void {\n try {\n this.ws = new WebSocket(this.options.url)\n } catch (err) {\n this.options.onError(err)\n this.scheduleReconnect()\n return\n }\n\n this.ws.onopen = () => {\n this._connected = true\n this.reconnectAttempts = 0\n this.flushBuffer()\n this.options.onConnect()\n }\n\n this.ws.onclose = () => {\n const wasConnected = this._connected\n this._connected = false\n if (wasConnected) {\n this.options.onDisconnect()\n }\n this.scheduleReconnect()\n }\n\n this.ws.onerror = (event) => {\n this.options.onError(event)\n }\n\n this.ws.onmessage = (event) => {\n try {\n const message = JSON.parse(\n typeof event.data === 'string' ? event.data : String(event.data),\n ) as InboundMessage\n this.options.onMessage(message)\n } catch {\n // Ignore malformed messages\n }\n }\n }\n\n private flushBuffer(): void {\n if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return\n const messages = this.buffer.splice(0)\n for (const data of messages) {\n this.ws.send(data)\n }\n }\n\n private scheduleReconnect(): void {\n if (this.destroyed) return\n if (!this.options.reconnect) return\n if (this.reconnectAttempts >= this.options.maxReconnectAttempts) return\n\n const delay = Math.min(\n this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts),\n this.options.maxReconnectInterval,\n )\n this.reconnectAttempts++\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null\n if (!this.destroyed) {\n this.createSocket()\n }\n }, delay)\n }\n}\n","/**\n * Shallow equality comparison for objects.\n * Returns true if both arguments have the same keys with Object.is-equal values.\n */\nexport function shallowEqual<T>(a: T, b: T): boolean {\n if (Object.is(a, b)) return true\n if (typeof a !== 'object' || typeof b !== 'object') return false\n if (a === null || b === null) return false\n\n const keysA = Object.keys(a as object)\n const keysB = Object.keys(b as object)\n\n if (keysA.length !== keysB.length) return false\n\n for (const key of keysA) {\n if (\n !Object.prototype.hasOwnProperty.call(b, key) ||\n !Object.is(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false\n }\n }\n\n return true\n}\n\n/** Type guard for functions */\nexport function isFunction(value: unknown): value is (...args: unknown[]) => unknown {\n return typeof value === 'function'\n}\n\n/**\n * Throttle function execution. Executes at most once per intervalMs.\n * Trailing calls are preserved (the last call during a throttle window\n * will fire after the interval).\n */\nexport function throttle<T extends (...args: never[]) => void>(\n fn: T,\n intervalMs: number,\n): ((...args: Parameters<T>) => void) {\n let lastCall = 0\n let timeoutId: ReturnType<typeof setTimeout> | null = null\n let latestArgs: Parameters<T>\n\n return (...args: Parameters<T>) => {\n latestArgs = args\n const now = Date.now()\n const remaining = intervalMs - (now - lastCall)\n\n if (remaining <= 0) {\n if (timeoutId) {\n clearTimeout(timeoutId)\n timeoutId = null\n }\n lastCall = now\n fn(...latestArgs)\n } else if (!timeoutId) {\n timeoutId = setTimeout(() => {\n lastCall = Date.now()\n timeoutId = null\n fn(...latestArgs)\n }, remaining)\n }\n }\n}\n","import type { State, BridgePlugin, BridgeApi } from '../core/types'\nimport type { SyncOptions } from './types'\nimport { SyncConnection } from './connection'\nimport { throttle } from '../core/utils'\n\n/**\n * Generate a random client ID for echo prevention.\n */\nfunction generateClientId(): string {\n return Math.random().toString(36).slice(2) + Date.now().toString(36)\n}\n\n/**\n * Create a sync plugin that synchronizes bridge state across apps\n * via WebSocket in real-time.\n *\n * @example\n * ```ts\n * import { createBridge } from 'shared-state-bridge'\n * import { sync } from 'shared-state-bridge/sync'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light', count: 0 },\n * plugins: [\n * sync({\n * url: 'wss://your-server.com/sync',\n * channel: 'my-room',\n * pick: ['theme'],\n * }),\n * ],\n * })\n * ```\n */\nexport function sync<T extends State>(options: SyncOptions<T>): BridgePlugin<T> {\n const {\n url,\n channel,\n pick,\n omit,\n throttleMs = 50,\n reconnect = true,\n maxReconnectAttempts = Infinity,\n reconnectInterval = 1000,\n maxReconnectInterval = 30000,\n onConnect,\n onDisconnect,\n onError,\n resolve,\n } = options\n\n const clientId = generateClientId()\n let connection: SyncConnection | null = null\n let bridge: BridgeApi<T> | null = null\n let sendState: ((state: T) => void) | null = null\n\n // Guard: when true, we are applying a remote state update.\n // onStateChange should NOT broadcast while this is true (echo prevention).\n let isApplyingRemote = false\n\n /**\n * Filter state keys based on pick/omit options.\n * Same pattern as the persist plugin.\n */\n function filterState(state: T): Partial<T> {\n if (pick) {\n const filtered: Partial<T> = {}\n for (const k of pick) {\n if (k in state) {\n filtered[k] = state[k]\n }\n }\n return filtered\n }\n if (omit) {\n const filtered = { ...state }\n for (const k of omit) {\n delete filtered[k]\n }\n return filtered\n }\n return state\n }\n\n return {\n name: 'sync',\n\n onInit: (bridgeApi: BridgeApi<T>) => {\n bridge = bridgeApi\n\n // Set up throttled send function\n sendState = throttle((state: T) => {\n if (!connection) return\n const filtered = filterState(state)\n connection.send({\n type: 'state',\n channel,\n clientId,\n state: filtered as Record<string, unknown>,\n timestamp: Date.now(),\n })\n }, throttleMs)\n\n // Create WebSocket connection\n connection = new SyncConnection({\n url,\n reconnect,\n maxReconnectAttempts,\n reconnectInterval,\n maxReconnectInterval,\n\n onConnect: () => {\n // Join the channel\n connection!.send({\n type: 'join',\n channel,\n clientId,\n })\n onConnect?.()\n },\n\n onDisconnect: () => {\n onDisconnect?.()\n },\n\n onError: (err) => {\n onError?.(err)\n },\n\n onMessage: (message) => {\n if (!bridge) return\n\n // Echo prevention: skip messages from ourselves\n if ('clientId' in message && message.clientId === clientId) return\n\n if (message.type === 'state' || message.type === 'full_state') {\n const remoteState = message.state as Partial<T>\n\n let stateToApply: Partial<T>\n\n if (resolve) {\n // Custom conflict resolution\n stateToApply = resolve(bridge.getState(), remoteState)\n } else {\n // Default: last-write-wins — just merge remote state\n stateToApply = remoteState\n }\n\n // Apply remote state with echo guard\n isApplyingRemote = true\n try {\n bridge.setState(stateToApply)\n } finally {\n isApplyingRemote = false\n }\n }\n },\n })\n\n connection.connect()\n },\n\n onStateChange: (state: T) => {\n // Don't broadcast if we're applying a remote update (echo prevention)\n if (isApplyingRemote) return\n sendState?.(state)\n },\n\n onDestroy: () => {\n connection?.destroy()\n connection = null\n bridge = null\n sendState = null\n },\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "shared-state-bridge",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight shared state bridge for monorepo apps — sync state across packages with TypeScript, React hooks, and optional persistence",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./react": {
20
+ "import": {
21
+ "types": "./dist/react.d.ts",
22
+ "default": "./dist/react.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/react.d.cts",
26
+ "default": "./dist/react.cjs"
27
+ }
28
+ },
29
+ "./persist": {
30
+ "import": {
31
+ "types": "./dist/persist.d.ts",
32
+ "default": "./dist/persist.js"
33
+ },
34
+ "require": {
35
+ "types": "./dist/persist.d.cts",
36
+ "default": "./dist/persist.cjs"
37
+ }
38
+ },
39
+ "./sync": {
40
+ "import": {
41
+ "types": "./dist/sync.d.ts",
42
+ "default": "./dist/sync.js"
43
+ },
44
+ "require": {
45
+ "types": "./dist/sync.d.cts",
46
+ "default": "./dist/sync.cjs"
47
+ }
48
+ }
49
+ },
50
+ "main": "./dist/index.cjs",
51
+ "module": "./dist/index.js",
52
+ "types": "./dist/index.d.ts",
53
+ "files": [
54
+ "dist",
55
+ "README.md",
56
+ "LICENSE"
57
+ ],
58
+ "scripts": {
59
+ "build": "tsup",
60
+ "dev": "tsup --watch",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "test:coverage": "vitest run --coverage",
64
+ "typecheck": "tsc --noEmit",
65
+ "prepublishOnly": "npm run build"
66
+ },
67
+ "peerDependencies": {
68
+ "react": ">=18.0.0"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "react": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@testing-library/react": "^16.1.0",
77
+ "@types/node": "^25.2.3",
78
+ "@types/react": "^19.2.14",
79
+ "@types/react-dom": "^19.2.3",
80
+ "jsdom": "^25.0.1",
81
+ "react": "^18.3.1",
82
+ "react-dom": "^18.3.1",
83
+ "tsup": "^8.3.5",
84
+ "typescript": "^5.7.2",
85
+ "vitest": "^2.1.8"
86
+ },
87
+ "keywords": [
88
+ "state-management",
89
+ "monorepo",
90
+ "shared-state",
91
+ "react",
92
+ "react-native",
93
+ "turborepo",
94
+ "nx",
95
+ "cross-package",
96
+ "typescript",
97
+ "hooks",
98
+ "state-bridge",
99
+ "websocket",
100
+ "real-time",
101
+ "sync"
102
+ ]
103
+ }