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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/utils.ts","../src/persist/plugin.ts","../src/persist/adapters.ts"],"names":[],"mappings":"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","file":"persist.cjs","sourcesContent":["/**\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, PersistOptions } from '../core/types'\nimport { throttle } from '../core/utils'\n\ninterface Envelope {\n state: unknown\n version: number\n}\n\n/**\n * Create a persistence plugin for a bridge.\n *\n * On init, hydrates state from storage. On state changes, writes\n * back to storage (throttled). Supports schema versioning and migrations.\n *\n * @example\n * ```ts\n * import { createBridge } from 'shared-state-bridge'\n * import { persist, localStorageAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light', count: 0 },\n * plugins: [\n * persist({\n * adapter: localStorageAdapter,\n * key: 'app-state',\n * pick: ['theme'], // only persist theme\n * throttleMs: 200, // debounce writes\n * }),\n * ],\n * })\n * ```\n */\nexport function persist<T extends State>(options: PersistOptions<T>): BridgePlugin<T> {\n const {\n adapter,\n key,\n pick,\n omit,\n serialize = JSON.stringify,\n deserialize = JSON.parse,\n version = 0,\n migrate,\n throttleMs = 100,\n } = options\n\n let writeToStorage: ((state: T) => void) | null = null\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: 'persist',\n\n onInit: (bridge: BridgeApi<T>) => {\n // Set up throttled writer\n writeToStorage = throttle((state: T) => {\n const filtered = filterState(state)\n const envelope: Envelope = { state: filtered, version }\n adapter.setItem(key, serialize(envelope))\n }, throttleMs)\n\n // Hydrate from storage\n const hydrate = async () => {\n try {\n const raw = await adapter.getItem(key)\n if (raw === null) return\n\n const envelope = deserialize(raw) as Envelope\n let persisted = envelope.state as Partial<T>\n const persistedVersion = envelope.version ?? 0\n\n if (persistedVersion !== version) {\n if (migrate) {\n persisted = migrate(persisted, persistedVersion)\n } else {\n // Version mismatch without migration — discard stale data\n await adapter.removeItem(key)\n return\n }\n }\n\n bridge.setState(persisted as Partial<T>)\n } catch {\n // Hydration failed — continue with initial state\n }\n }\n\n hydrate()\n },\n\n onStateChange: (state: T) => {\n writeToStorage?.(state)\n },\n\n onDestroy: () => {\n writeToStorage = null\n },\n }\n}\n","import type { PersistAdapter } from '../core/types'\n\n/**\n * localStorage adapter for web environments.\n *\n * @example\n * ```ts\n * import { persist, localStorageAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light' },\n * plugins: [persist({ adapter: localStorageAdapter, key: 'app-state' })],\n * })\n * ```\n */\nexport const localStorageAdapter: PersistAdapter = {\n getItem: (key: string) => {\n try {\n return globalThis.localStorage.getItem(key)\n } catch {\n return null\n }\n },\n setItem: (key: string, value: string) => {\n try {\n globalThis.localStorage.setItem(key, value)\n } catch {\n // Storage full or restricted (SSR, incognito)\n }\n },\n removeItem: (key: string) => {\n try {\n globalThis.localStorage.removeItem(key)\n } catch {\n // Restricted environment\n }\n },\n}\n\n/**\n * AsyncStorage adapter factory for React Native.\n *\n * Accepts the AsyncStorage instance as a parameter to avoid a hard\n * dependency on `@react-native-async-storage/async-storage`.\n *\n * @example\n * ```ts\n * import AsyncStorage from '@react-native-async-storage/async-storage'\n * import { persist, asyncStorageAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light' },\n * plugins: [persist({ adapter: asyncStorageAdapter(AsyncStorage), key: 'app-state' })],\n * })\n * ```\n */\nexport function asyncStorageAdapter(asyncStorage: {\n getItem: (key: string) => Promise<string | null>\n setItem: (key: string, value: string) => Promise<void>\n removeItem: (key: string) => Promise<void>\n}): PersistAdapter {\n return {\n getItem: (key) => asyncStorage.getItem(key),\n setItem: (key, value) => asyncStorage.setItem(key, value),\n removeItem: (key) => asyncStorage.removeItem(key),\n }\n}\n\n/**\n * In-memory adapter for testing and SSR environments.\n *\n * @example\n * ```ts\n * import { persist, memoryAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'test',\n * initialState: { count: 0 },\n * plugins: [persist({ adapter: memoryAdapter(), key: 'test-state' })],\n * })\n * ```\n */\nexport function memoryAdapter(): PersistAdapter {\n const store = new Map<string, string>()\n return {\n getItem: (key) => store.get(key) ?? null,\n setItem: (key, value) => {\n store.set(key, value)\n },\n removeItem: (key) => {\n store.delete(key)\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
/** Storage adapter interface for persistence */
|
|
38
|
+
interface PersistAdapter {
|
|
39
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
40
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
41
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Persistence plugin options */
|
|
44
|
+
interface PersistOptions<T extends State> {
|
|
45
|
+
/** Storage adapter (localStorageAdapter, asyncStorageAdapter, etc.) */
|
|
46
|
+
adapter: PersistAdapter;
|
|
47
|
+
/** Storage key */
|
|
48
|
+
key: string;
|
|
49
|
+
/** Only persist these keys */
|
|
50
|
+
pick?: (keyof T)[];
|
|
51
|
+
/** Exclude these keys from persistence */
|
|
52
|
+
omit?: (keyof T)[];
|
|
53
|
+
/** Custom serializer (default: JSON.stringify) */
|
|
54
|
+
serialize?: (state: unknown) => string;
|
|
55
|
+
/** Custom deserializer (default: JSON.parse) */
|
|
56
|
+
deserialize?: (raw: string) => unknown;
|
|
57
|
+
/** Schema version for migrations */
|
|
58
|
+
version?: number;
|
|
59
|
+
/** Migration function when version changes */
|
|
60
|
+
migrate?: (persisted: unknown, version: number) => Partial<T>;
|
|
61
|
+
/** Throttle writes in ms (default: 100) */
|
|
62
|
+
throttleMs?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a persistence plugin for a bridge.
|
|
67
|
+
*
|
|
68
|
+
* On init, hydrates state from storage. On state changes, writes
|
|
69
|
+
* back to storage (throttled). Supports schema versioning and migrations.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { createBridge } from 'shared-state-bridge'
|
|
74
|
+
* import { persist, localStorageAdapter } from 'shared-state-bridge/persist'
|
|
75
|
+
*
|
|
76
|
+
* const bridge = createBridge({
|
|
77
|
+
* name: 'app',
|
|
78
|
+
* initialState: { theme: 'light', count: 0 },
|
|
79
|
+
* plugins: [
|
|
80
|
+
* persist({
|
|
81
|
+
* adapter: localStorageAdapter,
|
|
82
|
+
* key: 'app-state',
|
|
83
|
+
* pick: ['theme'], // only persist theme
|
|
84
|
+
* throttleMs: 200, // debounce writes
|
|
85
|
+
* }),
|
|
86
|
+
* ],
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function persist<T extends State>(options: PersistOptions<T>): BridgePlugin<T>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* localStorage adapter for web environments.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { persist, localStorageAdapter } from 'shared-state-bridge/persist'
|
|
98
|
+
*
|
|
99
|
+
* const bridge = createBridge({
|
|
100
|
+
* name: 'app',
|
|
101
|
+
* initialState: { theme: 'light' },
|
|
102
|
+
* plugins: [persist({ adapter: localStorageAdapter, key: 'app-state' })],
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare const localStorageAdapter: PersistAdapter;
|
|
107
|
+
/**
|
|
108
|
+
* AsyncStorage adapter factory for React Native.
|
|
109
|
+
*
|
|
110
|
+
* Accepts the AsyncStorage instance as a parameter to avoid a hard
|
|
111
|
+
* dependency on `@react-native-async-storage/async-storage`.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
116
|
+
* import { persist, asyncStorageAdapter } from 'shared-state-bridge/persist'
|
|
117
|
+
*
|
|
118
|
+
* const bridge = createBridge({
|
|
119
|
+
* name: 'app',
|
|
120
|
+
* initialState: { theme: 'light' },
|
|
121
|
+
* plugins: [persist({ adapter: asyncStorageAdapter(AsyncStorage), key: 'app-state' })],
|
|
122
|
+
* })
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
declare function asyncStorageAdapter(asyncStorage: {
|
|
126
|
+
getItem: (key: string) => Promise<string | null>;
|
|
127
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
128
|
+
removeItem: (key: string) => Promise<void>;
|
|
129
|
+
}): PersistAdapter;
|
|
130
|
+
/**
|
|
131
|
+
* In-memory adapter for testing and SSR environments.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { persist, memoryAdapter } from 'shared-state-bridge/persist'
|
|
136
|
+
*
|
|
137
|
+
* const bridge = createBridge({
|
|
138
|
+
* name: 'test',
|
|
139
|
+
* initialState: { count: 0 },
|
|
140
|
+
* plugins: [persist({ adapter: memoryAdapter(), key: 'test-state' })],
|
|
141
|
+
* })
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function memoryAdapter(): PersistAdapter;
|
|
145
|
+
|
|
146
|
+
export { asyncStorageAdapter, localStorageAdapter, memoryAdapter, persist };
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
/** Storage adapter interface for persistence */
|
|
38
|
+
interface PersistAdapter {
|
|
39
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
40
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
41
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Persistence plugin options */
|
|
44
|
+
interface PersistOptions<T extends State> {
|
|
45
|
+
/** Storage adapter (localStorageAdapter, asyncStorageAdapter, etc.) */
|
|
46
|
+
adapter: PersistAdapter;
|
|
47
|
+
/** Storage key */
|
|
48
|
+
key: string;
|
|
49
|
+
/** Only persist these keys */
|
|
50
|
+
pick?: (keyof T)[];
|
|
51
|
+
/** Exclude these keys from persistence */
|
|
52
|
+
omit?: (keyof T)[];
|
|
53
|
+
/** Custom serializer (default: JSON.stringify) */
|
|
54
|
+
serialize?: (state: unknown) => string;
|
|
55
|
+
/** Custom deserializer (default: JSON.parse) */
|
|
56
|
+
deserialize?: (raw: string) => unknown;
|
|
57
|
+
/** Schema version for migrations */
|
|
58
|
+
version?: number;
|
|
59
|
+
/** Migration function when version changes */
|
|
60
|
+
migrate?: (persisted: unknown, version: number) => Partial<T>;
|
|
61
|
+
/** Throttle writes in ms (default: 100) */
|
|
62
|
+
throttleMs?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a persistence plugin for a bridge.
|
|
67
|
+
*
|
|
68
|
+
* On init, hydrates state from storage. On state changes, writes
|
|
69
|
+
* back to storage (throttled). Supports schema versioning and migrations.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* import { createBridge } from 'shared-state-bridge'
|
|
74
|
+
* import { persist, localStorageAdapter } from 'shared-state-bridge/persist'
|
|
75
|
+
*
|
|
76
|
+
* const bridge = createBridge({
|
|
77
|
+
* name: 'app',
|
|
78
|
+
* initialState: { theme: 'light', count: 0 },
|
|
79
|
+
* plugins: [
|
|
80
|
+
* persist({
|
|
81
|
+
* adapter: localStorageAdapter,
|
|
82
|
+
* key: 'app-state',
|
|
83
|
+
* pick: ['theme'], // only persist theme
|
|
84
|
+
* throttleMs: 200, // debounce writes
|
|
85
|
+
* }),
|
|
86
|
+
* ],
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
declare function persist<T extends State>(options: PersistOptions<T>): BridgePlugin<T>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* localStorage adapter for web environments.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { persist, localStorageAdapter } from 'shared-state-bridge/persist'
|
|
98
|
+
*
|
|
99
|
+
* const bridge = createBridge({
|
|
100
|
+
* name: 'app',
|
|
101
|
+
* initialState: { theme: 'light' },
|
|
102
|
+
* plugins: [persist({ adapter: localStorageAdapter, key: 'app-state' })],
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare const localStorageAdapter: PersistAdapter;
|
|
107
|
+
/**
|
|
108
|
+
* AsyncStorage adapter factory for React Native.
|
|
109
|
+
*
|
|
110
|
+
* Accepts the AsyncStorage instance as a parameter to avoid a hard
|
|
111
|
+
* dependency on `@react-native-async-storage/async-storage`.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
116
|
+
* import { persist, asyncStorageAdapter } from 'shared-state-bridge/persist'
|
|
117
|
+
*
|
|
118
|
+
* const bridge = createBridge({
|
|
119
|
+
* name: 'app',
|
|
120
|
+
* initialState: { theme: 'light' },
|
|
121
|
+
* plugins: [persist({ adapter: asyncStorageAdapter(AsyncStorage), key: 'app-state' })],
|
|
122
|
+
* })
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
declare function asyncStorageAdapter(asyncStorage: {
|
|
126
|
+
getItem: (key: string) => Promise<string | null>;
|
|
127
|
+
setItem: (key: string, value: string) => Promise<void>;
|
|
128
|
+
removeItem: (key: string) => Promise<void>;
|
|
129
|
+
}): PersistAdapter;
|
|
130
|
+
/**
|
|
131
|
+
* In-memory adapter for testing and SSR environments.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```ts
|
|
135
|
+
* import { persist, memoryAdapter } from 'shared-state-bridge/persist'
|
|
136
|
+
*
|
|
137
|
+
* const bridge = createBridge({
|
|
138
|
+
* name: 'test',
|
|
139
|
+
* initialState: { count: 0 },
|
|
140
|
+
* plugins: [persist({ adapter: memoryAdapter(), key: 'test-state' })],
|
|
141
|
+
* })
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function memoryAdapter(): PersistAdapter;
|
|
145
|
+
|
|
146
|
+
export { asyncStorageAdapter, localStorageAdapter, memoryAdapter, persist };
|
package/dist/persist.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/core/utils.ts
|
|
2
|
+
function throttle(fn, intervalMs) {
|
|
3
|
+
let lastCall = 0;
|
|
4
|
+
let timeoutId = null;
|
|
5
|
+
let latestArgs;
|
|
6
|
+
return (...args) => {
|
|
7
|
+
latestArgs = args;
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
const remaining = intervalMs - (now - lastCall);
|
|
10
|
+
if (remaining <= 0) {
|
|
11
|
+
if (timeoutId) {
|
|
12
|
+
clearTimeout(timeoutId);
|
|
13
|
+
timeoutId = null;
|
|
14
|
+
}
|
|
15
|
+
lastCall = now;
|
|
16
|
+
fn(...latestArgs);
|
|
17
|
+
} else if (!timeoutId) {
|
|
18
|
+
timeoutId = setTimeout(() => {
|
|
19
|
+
lastCall = Date.now();
|
|
20
|
+
timeoutId = null;
|
|
21
|
+
fn(...latestArgs);
|
|
22
|
+
}, remaining);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/persist/plugin.ts
|
|
28
|
+
function persist(options) {
|
|
29
|
+
const {
|
|
30
|
+
adapter,
|
|
31
|
+
key,
|
|
32
|
+
pick,
|
|
33
|
+
omit,
|
|
34
|
+
serialize = JSON.stringify,
|
|
35
|
+
deserialize = JSON.parse,
|
|
36
|
+
version = 0,
|
|
37
|
+
migrate,
|
|
38
|
+
throttleMs = 100
|
|
39
|
+
} = options;
|
|
40
|
+
let writeToStorage = null;
|
|
41
|
+
function filterState(state) {
|
|
42
|
+
if (pick) {
|
|
43
|
+
const filtered = {};
|
|
44
|
+
for (const k of pick) {
|
|
45
|
+
if (k in state) {
|
|
46
|
+
filtered[k] = state[k];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return filtered;
|
|
50
|
+
}
|
|
51
|
+
if (omit) {
|
|
52
|
+
const filtered = { ...state };
|
|
53
|
+
for (const k of omit) {
|
|
54
|
+
delete filtered[k];
|
|
55
|
+
}
|
|
56
|
+
return filtered;
|
|
57
|
+
}
|
|
58
|
+
return state;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
name: "persist",
|
|
62
|
+
onInit: (bridge) => {
|
|
63
|
+
writeToStorage = throttle((state) => {
|
|
64
|
+
const filtered = filterState(state);
|
|
65
|
+
const envelope = { state: filtered, version };
|
|
66
|
+
adapter.setItem(key, serialize(envelope));
|
|
67
|
+
}, throttleMs);
|
|
68
|
+
const hydrate = async () => {
|
|
69
|
+
try {
|
|
70
|
+
const raw = await adapter.getItem(key);
|
|
71
|
+
if (raw === null) return;
|
|
72
|
+
const envelope = deserialize(raw);
|
|
73
|
+
let persisted = envelope.state;
|
|
74
|
+
const persistedVersion = envelope.version ?? 0;
|
|
75
|
+
if (persistedVersion !== version) {
|
|
76
|
+
if (migrate) {
|
|
77
|
+
persisted = migrate(persisted, persistedVersion);
|
|
78
|
+
} else {
|
|
79
|
+
await adapter.removeItem(key);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
bridge.setState(persisted);
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
hydrate();
|
|
88
|
+
},
|
|
89
|
+
onStateChange: (state) => {
|
|
90
|
+
writeToStorage?.(state);
|
|
91
|
+
},
|
|
92
|
+
onDestroy: () => {
|
|
93
|
+
writeToStorage = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/persist/adapters.ts
|
|
99
|
+
var localStorageAdapter = {
|
|
100
|
+
getItem: (key) => {
|
|
101
|
+
try {
|
|
102
|
+
return globalThis.localStorage.getItem(key);
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
setItem: (key, value) => {
|
|
108
|
+
try {
|
|
109
|
+
globalThis.localStorage.setItem(key, value);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
removeItem: (key) => {
|
|
114
|
+
try {
|
|
115
|
+
globalThis.localStorage.removeItem(key);
|
|
116
|
+
} catch {
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function asyncStorageAdapter(asyncStorage) {
|
|
121
|
+
return {
|
|
122
|
+
getItem: (key) => asyncStorage.getItem(key),
|
|
123
|
+
setItem: (key, value) => asyncStorage.setItem(key, value),
|
|
124
|
+
removeItem: (key) => asyncStorage.removeItem(key)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function memoryAdapter() {
|
|
128
|
+
const store = /* @__PURE__ */ new Map();
|
|
129
|
+
return {
|
|
130
|
+
getItem: (key) => store.get(key) ?? null,
|
|
131
|
+
setItem: (key, value) => {
|
|
132
|
+
store.set(key, value);
|
|
133
|
+
},
|
|
134
|
+
removeItem: (key) => {
|
|
135
|
+
store.delete(key);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { asyncStorageAdapter, localStorageAdapter, memoryAdapter, persist };
|
|
141
|
+
//# sourceMappingURL=persist.js.map
|
|
142
|
+
//# sourceMappingURL=persist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/utils.ts","../src/persist/plugin.ts","../src/persist/adapters.ts"],"names":[],"mappings":"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","file":"persist.js","sourcesContent":["/**\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, PersistOptions } from '../core/types'\nimport { throttle } from '../core/utils'\n\ninterface Envelope {\n state: unknown\n version: number\n}\n\n/**\n * Create a persistence plugin for a bridge.\n *\n * On init, hydrates state from storage. On state changes, writes\n * back to storage (throttled). Supports schema versioning and migrations.\n *\n * @example\n * ```ts\n * import { createBridge } from 'shared-state-bridge'\n * import { persist, localStorageAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light', count: 0 },\n * plugins: [\n * persist({\n * adapter: localStorageAdapter,\n * key: 'app-state',\n * pick: ['theme'], // only persist theme\n * throttleMs: 200, // debounce writes\n * }),\n * ],\n * })\n * ```\n */\nexport function persist<T extends State>(options: PersistOptions<T>): BridgePlugin<T> {\n const {\n adapter,\n key,\n pick,\n omit,\n serialize = JSON.stringify,\n deserialize = JSON.parse,\n version = 0,\n migrate,\n throttleMs = 100,\n } = options\n\n let writeToStorage: ((state: T) => void) | null = null\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: 'persist',\n\n onInit: (bridge: BridgeApi<T>) => {\n // Set up throttled writer\n writeToStorage = throttle((state: T) => {\n const filtered = filterState(state)\n const envelope: Envelope = { state: filtered, version }\n adapter.setItem(key, serialize(envelope))\n }, throttleMs)\n\n // Hydrate from storage\n const hydrate = async () => {\n try {\n const raw = await adapter.getItem(key)\n if (raw === null) return\n\n const envelope = deserialize(raw) as Envelope\n let persisted = envelope.state as Partial<T>\n const persistedVersion = envelope.version ?? 0\n\n if (persistedVersion !== version) {\n if (migrate) {\n persisted = migrate(persisted, persistedVersion)\n } else {\n // Version mismatch without migration — discard stale data\n await adapter.removeItem(key)\n return\n }\n }\n\n bridge.setState(persisted as Partial<T>)\n } catch {\n // Hydration failed — continue with initial state\n }\n }\n\n hydrate()\n },\n\n onStateChange: (state: T) => {\n writeToStorage?.(state)\n },\n\n onDestroy: () => {\n writeToStorage = null\n },\n }\n}\n","import type { PersistAdapter } from '../core/types'\n\n/**\n * localStorage adapter for web environments.\n *\n * @example\n * ```ts\n * import { persist, localStorageAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light' },\n * plugins: [persist({ adapter: localStorageAdapter, key: 'app-state' })],\n * })\n * ```\n */\nexport const localStorageAdapter: PersistAdapter = {\n getItem: (key: string) => {\n try {\n return globalThis.localStorage.getItem(key)\n } catch {\n return null\n }\n },\n setItem: (key: string, value: string) => {\n try {\n globalThis.localStorage.setItem(key, value)\n } catch {\n // Storage full or restricted (SSR, incognito)\n }\n },\n removeItem: (key: string) => {\n try {\n globalThis.localStorage.removeItem(key)\n } catch {\n // Restricted environment\n }\n },\n}\n\n/**\n * AsyncStorage adapter factory for React Native.\n *\n * Accepts the AsyncStorage instance as a parameter to avoid a hard\n * dependency on `@react-native-async-storage/async-storage`.\n *\n * @example\n * ```ts\n * import AsyncStorage from '@react-native-async-storage/async-storage'\n * import { persist, asyncStorageAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { theme: 'light' },\n * plugins: [persist({ adapter: asyncStorageAdapter(AsyncStorage), key: 'app-state' })],\n * })\n * ```\n */\nexport function asyncStorageAdapter(asyncStorage: {\n getItem: (key: string) => Promise<string | null>\n setItem: (key: string, value: string) => Promise<void>\n removeItem: (key: string) => Promise<void>\n}): PersistAdapter {\n return {\n getItem: (key) => asyncStorage.getItem(key),\n setItem: (key, value) => asyncStorage.setItem(key, value),\n removeItem: (key) => asyncStorage.removeItem(key),\n }\n}\n\n/**\n * In-memory adapter for testing and SSR environments.\n *\n * @example\n * ```ts\n * import { persist, memoryAdapter } from 'shared-state-bridge/persist'\n *\n * const bridge = createBridge({\n * name: 'test',\n * initialState: { count: 0 },\n * plugins: [persist({ adapter: memoryAdapter(), key: 'test-state' })],\n * })\n * ```\n */\nexport function memoryAdapter(): PersistAdapter {\n const store = new Map<string, string>()\n return {\n getItem: (key) => store.get(key) ?? null,\n setItem: (key, value) => {\n store.set(key, value)\n },\n removeItem: (key) => {\n store.delete(key)\n },\n }\n}\n"]}
|
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/react/context.ts
|
|
7
|
+
var BridgeContext = react.createContext(null);
|
|
8
|
+
function BridgeProvider({
|
|
9
|
+
bridge,
|
|
10
|
+
children
|
|
11
|
+
}) {
|
|
12
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BridgeContext.Provider, { value: bridge, children });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/core/utils.ts
|
|
16
|
+
function shallowEqual(a, b) {
|
|
17
|
+
if (Object.is(a, b)) return true;
|
|
18
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
19
|
+
if (a === null || b === null) return false;
|
|
20
|
+
const keysA = Object.keys(a);
|
|
21
|
+
const keysB = Object.keys(b);
|
|
22
|
+
if (keysA.length !== keysB.length) return false;
|
|
23
|
+
for (const key of keysA) {
|
|
24
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) || !Object.is(
|
|
25
|
+
a[key],
|
|
26
|
+
b[key]
|
|
27
|
+
)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/react/hooks.ts
|
|
35
|
+
function useBridge() {
|
|
36
|
+
const bridge = react.useContext(BridgeContext);
|
|
37
|
+
if (!bridge) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"[shared-state-bridge] useBridge() must be used within a <BridgeProvider>. Alternatively, pass the bridge directly to useBridgeState(bridge, selector)."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return bridge;
|
|
43
|
+
}
|
|
44
|
+
function useBridgeState(bridgeOrSelector, selectorOrOptions, maybeOptions) {
|
|
45
|
+
const contextBridge = react.useContext(BridgeContext);
|
|
46
|
+
let bridge;
|
|
47
|
+
let selector;
|
|
48
|
+
let options = {};
|
|
49
|
+
if (typeof bridgeOrSelector === "function") {
|
|
50
|
+
if (!contextBridge) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"[shared-state-bridge] useBridgeState(selector) requires a <BridgeProvider>. Or pass the bridge directly: useBridgeState(bridge, selector)."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
bridge = contextBridge;
|
|
56
|
+
selector = bridgeOrSelector;
|
|
57
|
+
options = selectorOrOptions ?? {};
|
|
58
|
+
} else {
|
|
59
|
+
bridge = bridgeOrSelector;
|
|
60
|
+
if (typeof selectorOrOptions === "function") {
|
|
61
|
+
selector = selectorOrOptions;
|
|
62
|
+
options = maybeOptions ?? {};
|
|
63
|
+
} else {
|
|
64
|
+
selector = ((s) => s);
|
|
65
|
+
options = selectorOrOptions ?? {};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const selectorRef = react.useRef(selector);
|
|
69
|
+
selectorRef.current = selector;
|
|
70
|
+
const subscribe = react.useCallback(
|
|
71
|
+
(onStoreChange) => {
|
|
72
|
+
return bridge.subscribe(onStoreChange);
|
|
73
|
+
},
|
|
74
|
+
[bridge]
|
|
75
|
+
);
|
|
76
|
+
const getSnapshot = react.useCallback(
|
|
77
|
+
() => selectorRef.current(bridge.getState()),
|
|
78
|
+
[bridge]
|
|
79
|
+
);
|
|
80
|
+
const getServerSnapshot = react.useCallback(
|
|
81
|
+
() => selectorRef.current(bridge.getInitialState()),
|
|
82
|
+
[bridge]
|
|
83
|
+
);
|
|
84
|
+
if (options.shallow) {
|
|
85
|
+
return useSyncExternalStoreWithShallow(
|
|
86
|
+
subscribe,
|
|
87
|
+
getSnapshot,
|
|
88
|
+
getServerSnapshot
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
92
|
+
}
|
|
93
|
+
function useSyncExternalStoreWithShallow(subscribe, getSnapshot, getServerSnapshot) {
|
|
94
|
+
const prevRef = react.useRef({
|
|
95
|
+
value: void 0,
|
|
96
|
+
initialized: false
|
|
97
|
+
});
|
|
98
|
+
const getSnapshotWithShallow = react.useCallback(() => {
|
|
99
|
+
const next = getSnapshot();
|
|
100
|
+
if (!prevRef.current.initialized) {
|
|
101
|
+
prevRef.current = { value: next, initialized: true };
|
|
102
|
+
return next;
|
|
103
|
+
}
|
|
104
|
+
if (shallowEqual(prevRef.current.value, next)) {
|
|
105
|
+
return prevRef.current.value;
|
|
106
|
+
}
|
|
107
|
+
prevRef.current = { value: next, initialized: true };
|
|
108
|
+
return next;
|
|
109
|
+
}, [getSnapshot]);
|
|
110
|
+
const getServerSnapshotWithShallow = react.useCallback(() => {
|
|
111
|
+
const next = getServerSnapshot();
|
|
112
|
+
prevRef.current = { value: next, initialized: true };
|
|
113
|
+
return next;
|
|
114
|
+
}, [getServerSnapshot]);
|
|
115
|
+
return react.useSyncExternalStore(
|
|
116
|
+
subscribe,
|
|
117
|
+
getSnapshotWithShallow,
|
|
118
|
+
getServerSnapshotWithShallow
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
exports.BridgeProvider = BridgeProvider;
|
|
123
|
+
exports.useBridge = useBridge;
|
|
124
|
+
exports.useBridgeState = useBridgeState;
|
|
125
|
+
//# sourceMappingURL=react.cjs.map
|
|
126
|
+
//# sourceMappingURL=react.cjs.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":["createContext","useContext","useRef","useCallback","useSyncExternalStore"],"mappings":"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","file":"react.cjs","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"]}
|