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.
- package/LICENSE +21 -0
- package/README.md +1968 -0
- package/dist/index.cjs +125 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +114 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/persist.cjs +147 -0
- package/dist/persist.cjs.map +1 -0
- package/dist/persist.d.cts +146 -0
- package/dist/persist.d.ts +146 -0
- package/dist/persist.js +142 -0
- package/dist/persist.js.map +1 -0
- package/dist/react.cjs +126 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +100 -0
- package/dist/react.d.ts +100 -0
- package/dist/react.js +122 -0
- package/dist/react.js.map +1 -0
- package/dist/sync.cjs +249 -0
- package/dist/sync.cjs.map +1 -0
- package/dist/sync.d.cts +154 -0
- package/dist/sync.d.ts +154 -0
- package/dist/sync.js +246 -0
- package/dist/sync.js.map +1 -0
- package/package.json +103 -0
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/** State must be a plain object */
|
|
4
|
+
type State = Record<string, unknown>;
|
|
5
|
+
/** Full-state change listener */
|
|
6
|
+
type Listener<T extends State> = (state: T, previousState: T) => void;
|
|
7
|
+
/** Selector-based change listener */
|
|
8
|
+
type SelectorListener<_T extends State, U> = (slice: U, previousSlice: U) => void;
|
|
9
|
+
/** setState accepts partial updates or an updater function */
|
|
10
|
+
interface SetState<T extends State> {
|
|
11
|
+
(partial: Partial<T> | ((state: T) => Partial<T>)): void;
|
|
12
|
+
(state: T | ((state: T) => T), replace: true): void;
|
|
13
|
+
}
|
|
14
|
+
/** Subscribe overloads: full-state or selector-based */
|
|
15
|
+
interface Subscribe<T extends State> {
|
|
16
|
+
(listener: Listener<T>): () => void;
|
|
17
|
+
<U>(selector: (state: T) => U, listener: SelectorListener<T, U>, options?: SubscribeOptions<U>): () => void;
|
|
18
|
+
}
|
|
19
|
+
interface SubscribeOptions<U> {
|
|
20
|
+
equalityFn?: (a: U, b: U) => boolean;
|
|
21
|
+
fireImmediately?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** The bridge instance API */
|
|
24
|
+
interface BridgeApi<T extends State> {
|
|
25
|
+
getState: () => T;
|
|
26
|
+
setState: SetState<T>;
|
|
27
|
+
subscribe: Subscribe<T>;
|
|
28
|
+
getInitialState: () => T;
|
|
29
|
+
destroy: () => void;
|
|
30
|
+
getName: () => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BridgeProviderProps<T extends State> {
|
|
34
|
+
bridge: BridgeApi<T>;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Provides a bridge instance to the React component tree.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const bridge = createBridge({ name: 'app', initialState: { count: 0 } })
|
|
43
|
+
*
|
|
44
|
+
* function App() {
|
|
45
|
+
* return (
|
|
46
|
+
* <BridgeProvider bridge={bridge}>
|
|
47
|
+
* <Counter />
|
|
48
|
+
* </BridgeProvider>
|
|
49
|
+
* )
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function BridgeProvider<T extends State>({ bridge, children, }: BridgeProviderProps<T>): React.JSX.Element;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the bridge instance from the nearest BridgeProvider.
|
|
57
|
+
*
|
|
58
|
+
* @throws If used outside a BridgeProvider
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* function Counter() {
|
|
63
|
+
* const bridge = useBridge<AppState>()
|
|
64
|
+
* return <button onClick={() => bridge.setState(s => ({ count: s.count + 1 }))}>+</button>
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function useBridge<T extends State = State>(): BridgeApi<T>;
|
|
69
|
+
interface UseBridgeStateOptions {
|
|
70
|
+
shallow?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to bridge state with selector support and automatic re-render optimization.
|
|
74
|
+
*
|
|
75
|
+
* Supports three call signatures:
|
|
76
|
+
* - `useBridgeState(selector)` — uses bridge from BridgeProvider context
|
|
77
|
+
* - `useBridgeState(bridge, selector)` — direct bridge reference
|
|
78
|
+
* - `useBridgeState(bridge, selector, { shallow: true })` — shallow equality for object selectors
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* // From context
|
|
83
|
+
* const count = useBridgeState(s => s.count)
|
|
84
|
+
*
|
|
85
|
+
* // Direct bridge reference
|
|
86
|
+
* const theme = useBridgeState(bridge, s => s.theme)
|
|
87
|
+
*
|
|
88
|
+
* // Shallow equality for object selectors (prevents unnecessary re-renders)
|
|
89
|
+
* const { name, email } = useBridgeState(
|
|
90
|
+
* bridge,
|
|
91
|
+
* s => ({ name: s.name, email: s.email }),
|
|
92
|
+
* { shallow: true }
|
|
93
|
+
* )
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function useBridgeState<T extends State, U>(bridge: BridgeApi<T>, selector: (state: T) => U, options?: UseBridgeStateOptions): U;
|
|
97
|
+
declare function useBridgeState<T extends State, U>(selector: (state: T) => U, options?: UseBridgeStateOptions): U;
|
|
98
|
+
declare function useBridgeState<T extends State>(bridge: BridgeApi<T>): T;
|
|
99
|
+
|
|
100
|
+
export { BridgeProvider, type BridgeProviderProps, useBridge, useBridgeState };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/** State must be a plain object */
|
|
4
|
+
type State = Record<string, unknown>;
|
|
5
|
+
/** Full-state change listener */
|
|
6
|
+
type Listener<T extends State> = (state: T, previousState: T) => void;
|
|
7
|
+
/** Selector-based change listener */
|
|
8
|
+
type SelectorListener<_T extends State, U> = (slice: U, previousSlice: U) => void;
|
|
9
|
+
/** setState accepts partial updates or an updater function */
|
|
10
|
+
interface SetState<T extends State> {
|
|
11
|
+
(partial: Partial<T> | ((state: T) => Partial<T>)): void;
|
|
12
|
+
(state: T | ((state: T) => T), replace: true): void;
|
|
13
|
+
}
|
|
14
|
+
/** Subscribe overloads: full-state or selector-based */
|
|
15
|
+
interface Subscribe<T extends State> {
|
|
16
|
+
(listener: Listener<T>): () => void;
|
|
17
|
+
<U>(selector: (state: T) => U, listener: SelectorListener<T, U>, options?: SubscribeOptions<U>): () => void;
|
|
18
|
+
}
|
|
19
|
+
interface SubscribeOptions<U> {
|
|
20
|
+
equalityFn?: (a: U, b: U) => boolean;
|
|
21
|
+
fireImmediately?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** The bridge instance API */
|
|
24
|
+
interface BridgeApi<T extends State> {
|
|
25
|
+
getState: () => T;
|
|
26
|
+
setState: SetState<T>;
|
|
27
|
+
subscribe: Subscribe<T>;
|
|
28
|
+
getInitialState: () => T;
|
|
29
|
+
destroy: () => void;
|
|
30
|
+
getName: () => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BridgeProviderProps<T extends State> {
|
|
34
|
+
bridge: BridgeApi<T>;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Provides a bridge instance to the React component tree.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const bridge = createBridge({ name: 'app', initialState: { count: 0 } })
|
|
43
|
+
*
|
|
44
|
+
* function App() {
|
|
45
|
+
* return (
|
|
46
|
+
* <BridgeProvider bridge={bridge}>
|
|
47
|
+
* <Counter />
|
|
48
|
+
* </BridgeProvider>
|
|
49
|
+
* )
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare function BridgeProvider<T extends State>({ bridge, children, }: BridgeProviderProps<T>): React.JSX.Element;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the bridge instance from the nearest BridgeProvider.
|
|
57
|
+
*
|
|
58
|
+
* @throws If used outside a BridgeProvider
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* function Counter() {
|
|
63
|
+
* const bridge = useBridge<AppState>()
|
|
64
|
+
* return <button onClick={() => bridge.setState(s => ({ count: s.count + 1 }))}>+</button>
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
declare function useBridge<T extends State = State>(): BridgeApi<T>;
|
|
69
|
+
interface UseBridgeStateOptions {
|
|
70
|
+
shallow?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Subscribe to bridge state with selector support and automatic re-render optimization.
|
|
74
|
+
*
|
|
75
|
+
* Supports three call signatures:
|
|
76
|
+
* - `useBridgeState(selector)` — uses bridge from BridgeProvider context
|
|
77
|
+
* - `useBridgeState(bridge, selector)` — direct bridge reference
|
|
78
|
+
* - `useBridgeState(bridge, selector, { shallow: true })` — shallow equality for object selectors
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```tsx
|
|
82
|
+
* // From context
|
|
83
|
+
* const count = useBridgeState(s => s.count)
|
|
84
|
+
*
|
|
85
|
+
* // Direct bridge reference
|
|
86
|
+
* const theme = useBridgeState(bridge, s => s.theme)
|
|
87
|
+
*
|
|
88
|
+
* // Shallow equality for object selectors (prevents unnecessary re-renders)
|
|
89
|
+
* const { name, email } = useBridgeState(
|
|
90
|
+
* bridge,
|
|
91
|
+
* s => ({ name: s.name, email: s.email }),
|
|
92
|
+
* { shallow: true }
|
|
93
|
+
* )
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function useBridgeState<T extends State, U>(bridge: BridgeApi<T>, selector: (state: T) => U, options?: UseBridgeStateOptions): U;
|
|
97
|
+
declare function useBridgeState<T extends State, U>(selector: (state: T) => U, options?: UseBridgeStateOptions): U;
|
|
98
|
+
declare function useBridgeState<T extends State>(bridge: BridgeApi<T>): T;
|
|
99
|
+
|
|
100
|
+
export { BridgeProvider, type BridgeProviderProps, useBridge, useBridgeState };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createContext, useContext, useRef, useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/react/context.ts
|
|
5
|
+
var BridgeContext = createContext(null);
|
|
6
|
+
function BridgeProvider({
|
|
7
|
+
bridge,
|
|
8
|
+
children
|
|
9
|
+
}) {
|
|
10
|
+
return /* @__PURE__ */ jsx(BridgeContext.Provider, { value: bridge, children });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/core/utils.ts
|
|
14
|
+
function shallowEqual(a, b) {
|
|
15
|
+
if (Object.is(a, b)) return true;
|
|
16
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
17
|
+
if (a === null || b === null) return false;
|
|
18
|
+
const keysA = Object.keys(a);
|
|
19
|
+
const keysB = Object.keys(b);
|
|
20
|
+
if (keysA.length !== keysB.length) return false;
|
|
21
|
+
for (const key of keysA) {
|
|
22
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) || !Object.is(
|
|
23
|
+
a[key],
|
|
24
|
+
b[key]
|
|
25
|
+
)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/react/hooks.ts
|
|
33
|
+
function useBridge() {
|
|
34
|
+
const bridge = useContext(BridgeContext);
|
|
35
|
+
if (!bridge) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"[shared-state-bridge] useBridge() must be used within a <BridgeProvider>. Alternatively, pass the bridge directly to useBridgeState(bridge, selector)."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return bridge;
|
|
41
|
+
}
|
|
42
|
+
function useBridgeState(bridgeOrSelector, selectorOrOptions, maybeOptions) {
|
|
43
|
+
const contextBridge = useContext(BridgeContext);
|
|
44
|
+
let bridge;
|
|
45
|
+
let selector;
|
|
46
|
+
let options = {};
|
|
47
|
+
if (typeof bridgeOrSelector === "function") {
|
|
48
|
+
if (!contextBridge) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"[shared-state-bridge] useBridgeState(selector) requires a <BridgeProvider>. Or pass the bridge directly: useBridgeState(bridge, selector)."
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
bridge = contextBridge;
|
|
54
|
+
selector = bridgeOrSelector;
|
|
55
|
+
options = selectorOrOptions ?? {};
|
|
56
|
+
} else {
|
|
57
|
+
bridge = bridgeOrSelector;
|
|
58
|
+
if (typeof selectorOrOptions === "function") {
|
|
59
|
+
selector = selectorOrOptions;
|
|
60
|
+
options = maybeOptions ?? {};
|
|
61
|
+
} else {
|
|
62
|
+
selector = ((s) => s);
|
|
63
|
+
options = selectorOrOptions ?? {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const selectorRef = useRef(selector);
|
|
67
|
+
selectorRef.current = selector;
|
|
68
|
+
const subscribe = useCallback(
|
|
69
|
+
(onStoreChange) => {
|
|
70
|
+
return bridge.subscribe(onStoreChange);
|
|
71
|
+
},
|
|
72
|
+
[bridge]
|
|
73
|
+
);
|
|
74
|
+
const getSnapshot = useCallback(
|
|
75
|
+
() => selectorRef.current(bridge.getState()),
|
|
76
|
+
[bridge]
|
|
77
|
+
);
|
|
78
|
+
const getServerSnapshot = useCallback(
|
|
79
|
+
() => selectorRef.current(bridge.getInitialState()),
|
|
80
|
+
[bridge]
|
|
81
|
+
);
|
|
82
|
+
if (options.shallow) {
|
|
83
|
+
return useSyncExternalStoreWithShallow(
|
|
84
|
+
subscribe,
|
|
85
|
+
getSnapshot,
|
|
86
|
+
getServerSnapshot
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
90
|
+
}
|
|
91
|
+
function useSyncExternalStoreWithShallow(subscribe, getSnapshot, getServerSnapshot) {
|
|
92
|
+
const prevRef = useRef({
|
|
93
|
+
value: void 0,
|
|
94
|
+
initialized: false
|
|
95
|
+
});
|
|
96
|
+
const getSnapshotWithShallow = useCallback(() => {
|
|
97
|
+
const next = getSnapshot();
|
|
98
|
+
if (!prevRef.current.initialized) {
|
|
99
|
+
prevRef.current = { value: next, initialized: true };
|
|
100
|
+
return next;
|
|
101
|
+
}
|
|
102
|
+
if (shallowEqual(prevRef.current.value, next)) {
|
|
103
|
+
return prevRef.current.value;
|
|
104
|
+
}
|
|
105
|
+
prevRef.current = { value: next, initialized: true };
|
|
106
|
+
return next;
|
|
107
|
+
}, [getSnapshot]);
|
|
108
|
+
const getServerSnapshotWithShallow = useCallback(() => {
|
|
109
|
+
const next = getServerSnapshot();
|
|
110
|
+
prevRef.current = { value: next, initialized: true };
|
|
111
|
+
return next;
|
|
112
|
+
}, [getServerSnapshot]);
|
|
113
|
+
return useSyncExternalStore(
|
|
114
|
+
subscribe,
|
|
115
|
+
getSnapshotWithShallow,
|
|
116
|
+
getServerSnapshotWithShallow
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { BridgeProvider, useBridge, useBridgeState };
|
|
121
|
+
//# sourceMappingURL=react.js.map
|
|
122
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/context.ts","../src/react/provider.tsx","../src/core/utils.ts","../src/react/hooks.ts"],"names":[],"mappings":"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","file":"react.js","sourcesContent":["import { createContext } from 'react'\nimport type { BridgeApi, State } from '../core/types'\n\nexport const BridgeContext = createContext<BridgeApi<State> | null>(null)\n","import React from 'react'\nimport type { BridgeApi, State } from '../core/types'\nimport { BridgeContext } from './context'\n\nexport interface BridgeProviderProps<T extends State> {\n bridge: BridgeApi<T>\n children: React.ReactNode\n}\n\n/**\n * Provides a bridge instance to the React component tree.\n *\n * @example\n * ```tsx\n * const bridge = createBridge({ name: 'app', initialState: { count: 0 } })\n *\n * function App() {\n * return (\n * <BridgeProvider bridge={bridge}>\n * <Counter />\n * </BridgeProvider>\n * )\n * }\n * ```\n */\nexport function BridgeProvider<T extends State>({\n bridge,\n children,\n}: BridgeProviderProps<T>): React.JSX.Element {\n return (\n <BridgeContext.Provider value={bridge as BridgeApi<State>}>\n {children}\n </BridgeContext.Provider>\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 { useContext, useCallback, useRef } from 'react'\nimport { useSyncExternalStore } from 'react'\nimport type { BridgeApi, State } from '../core/types'\nimport { BridgeContext } from './context'\nimport { shallowEqual } from '../core/utils'\n\n/**\n * Get the bridge instance from the nearest BridgeProvider.\n *\n * @throws If used outside a BridgeProvider\n *\n * @example\n * ```tsx\n * function Counter() {\n * const bridge = useBridge<AppState>()\n * return <button onClick={() => bridge.setState(s => ({ count: s.count + 1 }))}>+</button>\n * }\n * ```\n */\nexport function useBridge<T extends State = State>(): BridgeApi<T> {\n const bridge = useContext(BridgeContext)\n if (!bridge) {\n throw new Error(\n '[shared-state-bridge] useBridge() must be used within a <BridgeProvider>. ' +\n 'Alternatively, pass the bridge directly to useBridgeState(bridge, selector).',\n )\n }\n return bridge as unknown as BridgeApi<T>\n}\n\ninterface UseBridgeStateOptions {\n shallow?: boolean\n}\n\n/**\n * Subscribe to bridge state with selector support and automatic re-render optimization.\n *\n * Supports three call signatures:\n * - `useBridgeState(selector)` — uses bridge from BridgeProvider context\n * - `useBridgeState(bridge, selector)` — direct bridge reference\n * - `useBridgeState(bridge, selector, { shallow: true })` — shallow equality for object selectors\n *\n * @example\n * ```tsx\n * // From context\n * const count = useBridgeState(s => s.count)\n *\n * // Direct bridge reference\n * const theme = useBridgeState(bridge, s => s.theme)\n *\n * // Shallow equality for object selectors (prevents unnecessary re-renders)\n * const { name, email } = useBridgeState(\n * bridge,\n * s => ({ name: s.name, email: s.email }),\n * { shallow: true }\n * )\n * ```\n */\nexport function useBridgeState<T extends State, U>(\n bridge: BridgeApi<T>,\n selector: (state: T) => U,\n options?: UseBridgeStateOptions,\n): U\nexport function useBridgeState<T extends State, U>(\n selector: (state: T) => U,\n options?: UseBridgeStateOptions,\n): U\nexport function useBridgeState<T extends State>(\n bridge: BridgeApi<T>,\n): T\nexport function useBridgeState<T extends State, U = T>(\n bridgeOrSelector: BridgeApi<T> | ((state: T) => U),\n selectorOrOptions?: ((state: T) => U) | UseBridgeStateOptions,\n maybeOptions?: UseBridgeStateOptions,\n): U {\n const contextBridge = useContext(BridgeContext)\n\n let bridge: BridgeApi<T>\n let selector: (state: T) => U\n let options: UseBridgeStateOptions = {}\n\n if (typeof bridgeOrSelector === 'function') {\n // useBridgeState(selector) or useBridgeState(selector, options)\n if (!contextBridge) {\n throw new Error(\n '[shared-state-bridge] useBridgeState(selector) requires a <BridgeProvider>. ' +\n 'Or pass the bridge directly: useBridgeState(bridge, selector).',\n )\n }\n bridge = contextBridge as unknown as BridgeApi<T>\n selector = bridgeOrSelector as (state: T) => U\n options = (selectorOrOptions as UseBridgeStateOptions) ?? {}\n } else {\n // useBridgeState(bridge), useBridgeState(bridge, selector), useBridgeState(bridge, selector, options)\n bridge = bridgeOrSelector\n if (typeof selectorOrOptions === 'function') {\n selector = selectorOrOptions\n options = maybeOptions ?? {}\n } else {\n selector = ((s: T) => s) as unknown as (state: T) => U\n options = selectorOrOptions ?? {}\n }\n }\n\n const selectorRef = useRef(selector)\n selectorRef.current = selector\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n return bridge.subscribe(onStoreChange as unknown as (state: T, prev: T) => void)\n },\n [bridge],\n )\n\n const getSnapshot = useCallback(\n () => selectorRef.current(bridge.getState()),\n [bridge],\n )\n\n const getServerSnapshot = useCallback(\n () => selectorRef.current(bridge.getInitialState()),\n [bridge],\n )\n\n if (options.shallow) {\n return useSyncExternalStoreWithShallow(\n subscribe,\n getSnapshot,\n getServerSnapshot,\n )\n }\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)\n}\n\n/**\n * Wraps useSyncExternalStore with shallow equality comparison.\n *\n * When a selector returns a new object reference every render\n * (e.g. `s => ({ a: s.a, b: s.b })`), Object.is would always\n * return false, causing unnecessary re-renders.\n *\n * This wrapper caches the previous snapshot and returns the cached\n * reference when the new value is shallowly equal, so Object.is\n * sees the same reference and skips the re-render.\n */\nfunction useSyncExternalStoreWithShallow<U>(\n subscribe: (onStoreChange: () => void) => () => void,\n getSnapshot: () => U,\n getServerSnapshot: () => U,\n): U {\n const prevRef = useRef<{ value: U; initialized: boolean }>({\n value: undefined as U,\n initialized: false,\n })\n\n const getSnapshotWithShallow = useCallback(() => {\n const next = getSnapshot()\n if (!prevRef.current.initialized) {\n prevRef.current = { value: next, initialized: true }\n return next\n }\n if (shallowEqual(prevRef.current.value, next)) {\n return prevRef.current.value\n }\n prevRef.current = { value: next, initialized: true }\n return next\n }, [getSnapshot])\n\n const getServerSnapshotWithShallow = useCallback(() => {\n const next = getServerSnapshot()\n prevRef.current = { value: next, initialized: true }\n return next\n }, [getServerSnapshot])\n\n return useSyncExternalStore(\n subscribe,\n getSnapshotWithShallow,\n getServerSnapshotWithShallow,\n )\n}\n"]}
|
package/dist/sync.cjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/sync/connection.ts
|
|
4
|
+
var SyncConnection = class {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.ws = null;
|
|
7
|
+
this.buffer = [];
|
|
8
|
+
this.reconnectAttempts = 0;
|
|
9
|
+
this.reconnectTimer = null;
|
|
10
|
+
this.destroyed = false;
|
|
11
|
+
this._connected = false;
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
get connected() {
|
|
15
|
+
return this._connected;
|
|
16
|
+
}
|
|
17
|
+
connect() {
|
|
18
|
+
if (this.destroyed) return;
|
|
19
|
+
this.createSocket();
|
|
20
|
+
}
|
|
21
|
+
send(message) {
|
|
22
|
+
const data = JSON.stringify(message);
|
|
23
|
+
if (this._connected && this.ws?.readyState === WebSocket.OPEN) {
|
|
24
|
+
this.ws.send(data);
|
|
25
|
+
} else {
|
|
26
|
+
this.buffer.push(data);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
destroy() {
|
|
30
|
+
this.destroyed = true;
|
|
31
|
+
if (this.reconnectTimer) {
|
|
32
|
+
clearTimeout(this.reconnectTimer);
|
|
33
|
+
this.reconnectTimer = null;
|
|
34
|
+
}
|
|
35
|
+
if (this.ws) {
|
|
36
|
+
this.ws.onopen = null;
|
|
37
|
+
this.ws.onclose = null;
|
|
38
|
+
this.ws.onmessage = null;
|
|
39
|
+
this.ws.onerror = null;
|
|
40
|
+
this.ws.close();
|
|
41
|
+
this.ws = null;
|
|
42
|
+
}
|
|
43
|
+
this.buffer = [];
|
|
44
|
+
this._connected = false;
|
|
45
|
+
}
|
|
46
|
+
createSocket() {
|
|
47
|
+
try {
|
|
48
|
+
this.ws = new WebSocket(this.options.url);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this.options.onError(err);
|
|
51
|
+
this.scheduleReconnect();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.ws.onopen = () => {
|
|
55
|
+
this._connected = true;
|
|
56
|
+
this.reconnectAttempts = 0;
|
|
57
|
+
this.flushBuffer();
|
|
58
|
+
this.options.onConnect();
|
|
59
|
+
};
|
|
60
|
+
this.ws.onclose = () => {
|
|
61
|
+
const wasConnected = this._connected;
|
|
62
|
+
this._connected = false;
|
|
63
|
+
if (wasConnected) {
|
|
64
|
+
this.options.onDisconnect();
|
|
65
|
+
}
|
|
66
|
+
this.scheduleReconnect();
|
|
67
|
+
};
|
|
68
|
+
this.ws.onerror = (event) => {
|
|
69
|
+
this.options.onError(event);
|
|
70
|
+
};
|
|
71
|
+
this.ws.onmessage = (event) => {
|
|
72
|
+
try {
|
|
73
|
+
const message = JSON.parse(
|
|
74
|
+
typeof event.data === "string" ? event.data : String(event.data)
|
|
75
|
+
);
|
|
76
|
+
this.options.onMessage(message);
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
flushBuffer() {
|
|
82
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
83
|
+
const messages = this.buffer.splice(0);
|
|
84
|
+
for (const data of messages) {
|
|
85
|
+
this.ws.send(data);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
scheduleReconnect() {
|
|
89
|
+
if (this.destroyed) return;
|
|
90
|
+
if (!this.options.reconnect) return;
|
|
91
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) return;
|
|
92
|
+
const delay = Math.min(
|
|
93
|
+
this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts),
|
|
94
|
+
this.options.maxReconnectInterval
|
|
95
|
+
);
|
|
96
|
+
this.reconnectAttempts++;
|
|
97
|
+
this.reconnectTimer = setTimeout(() => {
|
|
98
|
+
this.reconnectTimer = null;
|
|
99
|
+
if (!this.destroyed) {
|
|
100
|
+
this.createSocket();
|
|
101
|
+
}
|
|
102
|
+
}, delay);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/core/utils.ts
|
|
107
|
+
function throttle(fn, intervalMs) {
|
|
108
|
+
let lastCall = 0;
|
|
109
|
+
let timeoutId = null;
|
|
110
|
+
let latestArgs;
|
|
111
|
+
return (...args) => {
|
|
112
|
+
latestArgs = args;
|
|
113
|
+
const now = Date.now();
|
|
114
|
+
const remaining = intervalMs - (now - lastCall);
|
|
115
|
+
if (remaining <= 0) {
|
|
116
|
+
if (timeoutId) {
|
|
117
|
+
clearTimeout(timeoutId);
|
|
118
|
+
timeoutId = null;
|
|
119
|
+
}
|
|
120
|
+
lastCall = now;
|
|
121
|
+
fn(...latestArgs);
|
|
122
|
+
} else if (!timeoutId) {
|
|
123
|
+
timeoutId = setTimeout(() => {
|
|
124
|
+
lastCall = Date.now();
|
|
125
|
+
timeoutId = null;
|
|
126
|
+
fn(...latestArgs);
|
|
127
|
+
}, remaining);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/sync/plugin.ts
|
|
133
|
+
function generateClientId() {
|
|
134
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
135
|
+
}
|
|
136
|
+
function sync(options) {
|
|
137
|
+
const {
|
|
138
|
+
url,
|
|
139
|
+
channel,
|
|
140
|
+
pick,
|
|
141
|
+
omit,
|
|
142
|
+
throttleMs = 50,
|
|
143
|
+
reconnect = true,
|
|
144
|
+
maxReconnectAttempts = Infinity,
|
|
145
|
+
reconnectInterval = 1e3,
|
|
146
|
+
maxReconnectInterval = 3e4,
|
|
147
|
+
onConnect,
|
|
148
|
+
onDisconnect,
|
|
149
|
+
onError,
|
|
150
|
+
resolve
|
|
151
|
+
} = options;
|
|
152
|
+
const clientId = generateClientId();
|
|
153
|
+
let connection = null;
|
|
154
|
+
let bridge = null;
|
|
155
|
+
let sendState = null;
|
|
156
|
+
let isApplyingRemote = false;
|
|
157
|
+
function filterState(state) {
|
|
158
|
+
if (pick) {
|
|
159
|
+
const filtered = {};
|
|
160
|
+
for (const k of pick) {
|
|
161
|
+
if (k in state) {
|
|
162
|
+
filtered[k] = state[k];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return filtered;
|
|
166
|
+
}
|
|
167
|
+
if (omit) {
|
|
168
|
+
const filtered = { ...state };
|
|
169
|
+
for (const k of omit) {
|
|
170
|
+
delete filtered[k];
|
|
171
|
+
}
|
|
172
|
+
return filtered;
|
|
173
|
+
}
|
|
174
|
+
return state;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
name: "sync",
|
|
178
|
+
onInit: (bridgeApi) => {
|
|
179
|
+
bridge = bridgeApi;
|
|
180
|
+
sendState = throttle((state) => {
|
|
181
|
+
if (!connection) return;
|
|
182
|
+
const filtered = filterState(state);
|
|
183
|
+
connection.send({
|
|
184
|
+
type: "state",
|
|
185
|
+
channel,
|
|
186
|
+
clientId,
|
|
187
|
+
state: filtered,
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
});
|
|
190
|
+
}, throttleMs);
|
|
191
|
+
connection = new SyncConnection({
|
|
192
|
+
url,
|
|
193
|
+
reconnect,
|
|
194
|
+
maxReconnectAttempts,
|
|
195
|
+
reconnectInterval,
|
|
196
|
+
maxReconnectInterval,
|
|
197
|
+
onConnect: () => {
|
|
198
|
+
connection.send({
|
|
199
|
+
type: "join",
|
|
200
|
+
channel,
|
|
201
|
+
clientId
|
|
202
|
+
});
|
|
203
|
+
onConnect?.();
|
|
204
|
+
},
|
|
205
|
+
onDisconnect: () => {
|
|
206
|
+
onDisconnect?.();
|
|
207
|
+
},
|
|
208
|
+
onError: (err) => {
|
|
209
|
+
onError?.(err);
|
|
210
|
+
},
|
|
211
|
+
onMessage: (message) => {
|
|
212
|
+
if (!bridge) return;
|
|
213
|
+
if ("clientId" in message && message.clientId === clientId) return;
|
|
214
|
+
if (message.type === "state" || message.type === "full_state") {
|
|
215
|
+
const remoteState = message.state;
|
|
216
|
+
let stateToApply;
|
|
217
|
+
if (resolve) {
|
|
218
|
+
stateToApply = resolve(bridge.getState(), remoteState);
|
|
219
|
+
} else {
|
|
220
|
+
stateToApply = remoteState;
|
|
221
|
+
}
|
|
222
|
+
isApplyingRemote = true;
|
|
223
|
+
try {
|
|
224
|
+
bridge.setState(stateToApply);
|
|
225
|
+
} finally {
|
|
226
|
+
isApplyingRemote = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
connection.connect();
|
|
232
|
+
},
|
|
233
|
+
onStateChange: (state) => {
|
|
234
|
+
if (isApplyingRemote) return;
|
|
235
|
+
sendState?.(state);
|
|
236
|
+
},
|
|
237
|
+
onDestroy: () => {
|
|
238
|
+
connection?.destroy();
|
|
239
|
+
connection = null;
|
|
240
|
+
bridge = null;
|
|
241
|
+
sendState = null;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
exports.SyncConnection = SyncConnection;
|
|
247
|
+
exports.sync = sync;
|
|
248
|
+
//# sourceMappingURL=sync.cjs.map
|
|
249
|
+
//# sourceMappingURL=sync.cjs.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.cjs","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"]}
|