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/index.cjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/utils.ts
|
|
4
|
+
function isFunction(value) {
|
|
5
|
+
return typeof value === "function";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/core/registry.ts
|
|
9
|
+
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("shared-state-bridge.registry");
|
|
10
|
+
function getRegistry() {
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (!g[REGISTRY_KEY]) {
|
|
13
|
+
g[REGISTRY_KEY] = /* @__PURE__ */ new Map();
|
|
14
|
+
}
|
|
15
|
+
return g[REGISTRY_KEY];
|
|
16
|
+
}
|
|
17
|
+
function registerBridge(name, bridge) {
|
|
18
|
+
const registry = getRegistry();
|
|
19
|
+
if (registry.has(name)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`[shared-state-bridge] A bridge named "${name}" already exists. Use getBridge("${name}") to access it, or destroy the existing one first.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
registry.set(name, bridge);
|
|
25
|
+
}
|
|
26
|
+
function unregisterBridge(name) {
|
|
27
|
+
getRegistry().delete(name);
|
|
28
|
+
}
|
|
29
|
+
function getBridge(name) {
|
|
30
|
+
const registry = getRegistry();
|
|
31
|
+
const bridge = registry.get(name);
|
|
32
|
+
if (!bridge) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`[shared-state-bridge] No bridge named "${name}" found. Make sure createBridge({ name: "${name}" }) is called before getBridge().`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return bridge;
|
|
38
|
+
}
|
|
39
|
+
function hasBridge(name) {
|
|
40
|
+
return getRegistry().has(name);
|
|
41
|
+
}
|
|
42
|
+
function listBridges() {
|
|
43
|
+
return Array.from(getRegistry().keys());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/core/bridge.ts
|
|
47
|
+
function createBridge(config) {
|
|
48
|
+
const { name, initialState, plugins = [] } = config;
|
|
49
|
+
let state = initialState;
|
|
50
|
+
const frozenInitialState = initialState;
|
|
51
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
52
|
+
let isDestroyed = false;
|
|
53
|
+
const getState = () => state;
|
|
54
|
+
const getInitialState = () => frozenInitialState;
|
|
55
|
+
const getName = () => name;
|
|
56
|
+
const setState = (partial, replace) => {
|
|
57
|
+
if (isDestroyed) return;
|
|
58
|
+
const previousState = state;
|
|
59
|
+
const nextPartial = isFunction(partial) ? partial(state) : partial;
|
|
60
|
+
if (replace) {
|
|
61
|
+
state = nextPartial;
|
|
62
|
+
} else {
|
|
63
|
+
state = Object.assign({}, state, nextPartial);
|
|
64
|
+
}
|
|
65
|
+
if (!Object.is(state, previousState)) {
|
|
66
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
67
|
+
plugins.forEach((plugin) => plugin.onStateChange?.(state, previousState));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const subscribe = (selectorOrListener, listenerOrUndefined, options) => {
|
|
71
|
+
if (isDestroyed) return () => {
|
|
72
|
+
};
|
|
73
|
+
if (typeof listenerOrUndefined === "undefined") {
|
|
74
|
+
const listener = selectorOrListener;
|
|
75
|
+
listeners.add(listener);
|
|
76
|
+
return () => {
|
|
77
|
+
listeners.delete(listener);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const selector = selectorOrListener;
|
|
81
|
+
const selectorListener = listenerOrUndefined;
|
|
82
|
+
const equalityFn = options?.equalityFn ?? Object.is;
|
|
83
|
+
let previousSlice = selector(state);
|
|
84
|
+
const internalListener = (currentState) => {
|
|
85
|
+
const nextSlice = selector(currentState);
|
|
86
|
+
if (!equalityFn(previousSlice, nextSlice)) {
|
|
87
|
+
const prevSlice = previousSlice;
|
|
88
|
+
previousSlice = nextSlice;
|
|
89
|
+
selectorListener(nextSlice, prevSlice);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
listeners.add(internalListener);
|
|
93
|
+
if (options?.fireImmediately) {
|
|
94
|
+
selectorListener(previousSlice, previousSlice);
|
|
95
|
+
}
|
|
96
|
+
return () => {
|
|
97
|
+
listeners.delete(internalListener);
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const destroy = () => {
|
|
101
|
+
if (isDestroyed) return;
|
|
102
|
+
unregisterBridge(name);
|
|
103
|
+
plugins.forEach((plugin) => plugin.onDestroy?.());
|
|
104
|
+
listeners.clear();
|
|
105
|
+
isDestroyed = true;
|
|
106
|
+
};
|
|
107
|
+
const api = {
|
|
108
|
+
getState,
|
|
109
|
+
setState,
|
|
110
|
+
subscribe,
|
|
111
|
+
getInitialState,
|
|
112
|
+
destroy,
|
|
113
|
+
getName
|
|
114
|
+
};
|
|
115
|
+
registerBridge(name, api);
|
|
116
|
+
plugins.forEach((plugin) => plugin.onInit?.(api));
|
|
117
|
+
return api;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
exports.createBridge = createBridge;
|
|
121
|
+
exports.getBridge = getBridge;
|
|
122
|
+
exports.hasBridge = hasBridge;
|
|
123
|
+
exports.listBridges = listBridges;
|
|
124
|
+
//# sourceMappingURL=index.cjs.map
|
|
125
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/utils.ts","../src/core/registry.ts","../src/core/bridge.ts"],"names":[],"mappings":";;;AA8BO,SAAS,WAAW,KAAA,EAA0D;AACnF,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA;AAC1B;;;ACtBA,IAAM,YAAA,mBAAe,MAAA,CAAO,GAAA,CAAI,8BAA8B,CAAA;AAE9D,SAAS,WAAA,GAA6C;AACpD,EAAA,MAAM,CAAA,GAAI,UAAA;AACV,EAAA,IAAI,CAAC,CAAA,CAAE,YAAY,CAAA,EAAG;AACpB,IAAA,CAAA,CAAE,YAAY,CAAA,mBAAI,IAAI,GAAA,EAAI;AAAA,EAC5B;AACA,EAAA,OAAO,EAAE,YAAY,CAAA;AACvB;AAOO,SAAS,cAAA,CACd,MACA,MAAA,EACM;AACN,EAAA,MAAM,WAAW,WAAA,EAAY;AAC7B,EAAA,IAAI,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA,EAAG;AACtB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,sCAAA,EAAyC,IAAI,CAAA,iCAAA,EACzB,IAAI,CAAA,mDAAA;AAAA,KAC1B;AAAA,EACF;AACA,EAAA,QAAA,CAAS,GAAA,CAAI,MAAM,MAA0B,CAAA;AAC/C;AAMO,SAAS,iBAAiB,IAAA,EAAoB;AACnD,EAAA,WAAA,EAAY,CAAE,OAAO,IAAI,CAAA;AAC3B;AAYO,SAAS,UAAmC,IAAA,EAA4B;AAC7E,EAAA,MAAM,WAAW,WAAA,EAAY;AAC7B,EAAA,MAAM,MAAA,GAAS,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA;AAChC,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,uCAAA,EAA0C,IAAI,CAAA,yCAAA,EACT,IAAI,CAAA,kCAAA;AAAA,KAC3C;AAAA,EACF;AACA,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,UAAU,IAAA,EAAuB;AAC/C,EAAA,OAAO,WAAA,EAAY,CAAE,GAAA,CAAI,IAAI,CAAA;AAC/B;AAGO,SAAS,WAAA,GAAwB;AACtC,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,WAAA,EAAY,CAAE,MAAM,CAAA;AACxC;;;AChDO,SAAS,aAA8B,MAAA,EAAuC;AACnF,EAAA,MAAM,EAAE,IAAA,EAAM,YAAA,EAAc,OAAA,GAAU,IAAG,GAAI,MAAA;AAE7C,EAAA,IAAI,KAAA,GAAW,YAAA;AACf,EAAA,MAAM,kBAAA,GAAwB,YAAA;AAC9B,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiB;AACvC,EAAA,IAAI,WAAA,GAAc,KAAA;AAElB,EAAA,MAAM,WAAW,MAAS,KAAA;AAE1B,EAAA,MAAM,kBAAkB,MAAS,kBAAA;AAEjC,EAAA,MAAM,UAAU,MAAc,IAAA;AAE9B,EAAA,MAAM,QAAA,GAAqC,CACzC,OAAA,EACA,OAAA,KACS;AACT,IAAA,IAAI,WAAA,EAAa;AAEjB,IAAA,MAAM,aAAA,GAAgB,KAAA;AACtB,IAAA,MAAM,cAAc,UAAA,CAAW,OAAO,CAAA,GAAI,OAAA,CAAQ,KAAK,CAAA,GAAI,OAAA;AAE3D,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,KAAA,GAAQ,WAAA;AAAA,IACV,CAAA,MAAO;AACL,MAAA,KAAA,GAAQ,MAAA,CAAO,MAAA,CAAO,EAAC,EAAG,OAAO,WAAW,CAAA;AAAA,IAC9C;AAEA,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,KAAA,EAAO,aAAa,CAAA,EAAG;AACpC,MAAA,SAAA,CAAU,QAAQ,CAAC,QAAA,KAAa,QAAA,CAAS,KAAA,EAAO,aAAa,CAAC,CAAA;AAC9D,MAAA,OAAA,CAAQ,QAAQ,CAAC,MAAA,KAAW,OAAO,aAAA,GAAgB,KAAA,EAAO,aAAa,CAAC,CAAA;AAAA,IAC1E;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,SAAA,GAAuC,CAC3C,kBAAA,EAGA,mBAAA,EACA,OAAA,KACiB;AACjB,IAAA,IAAI,WAAA,SAAoB,MAAM;AAAA,IAAC,CAAA;AAG/B,IAAA,IAAI,OAAO,wBAAwB,WAAA,EAAa;AAC9C,MAAA,MAAM,QAAA,GAAW,kBAAA;AACjB,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,OAAO,MAAM;AACX,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA;AAAA,IACF;AAGA,IAAA,MAAM,QAAA,GAAW,kBAAA;AACjB,IAAA,MAAM,gBAAA,GAAmB,mBAAA;AACzB,IAAA,MAAM,UAAA,GAAa,OAAA,EAAS,UAAA,IAAc,MAAA,CAAO,EAAA;AACjD,IAAA,IAAI,aAAA,GAAgB,SAAS,KAAK,CAAA;AAElC,IAAA,MAAM,gBAAA,GAAgC,CAAC,YAAA,KAAiB;AACtD,MAAA,MAAM,SAAA,GAAY,SAAS,YAAY,CAAA;AACvC,MAAA,IAAI,CAAC,UAAA,CAAW,aAAA,EAAe,SAAS,CAAA,EAAG;AACzC,QAAA,MAAM,SAAA,GAAY,aAAA;AAClB,QAAA,aAAA,GAAgB,SAAA;AAChB,QAAA,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAAA,MACvC;AAAA,IACF,CAAA;AAEA,IAAA,SAAA,CAAU,IAAI,gBAAgB,CAAA;AAE9B,IAAA,IAAI,SAAS,eAAA,EAAiB;AAC5B,MAAA,gBAAA,CAAiB,eAAe,aAAa,CAAA;AAAA,IAC/C;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,CAAU,OAAO,gBAAgB,CAAA;AAAA,IACnC,CAAA;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,UAAU,MAAY;AAC1B,IAAA,IAAI,WAAA,EAAa;AACjB,IAAA,gBAAA,CAAiB,IAAI,CAAA;AACrB,IAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,MAAA,KAAW,MAAA,CAAO,aAAa,CAAA;AAChD,IAAA,SAAA,CAAU,KAAA,EAAM;AAChB,IAAA,WAAA,GAAc,IAAA;AAAA,EAChB,CAAA;AAEA,EAAA,MAAM,GAAA,GAAoB;AAAA,IACxB,QAAA;AAAA,IACA,QAAA;AAAA,IACA,SAAA;AAAA,IACA,eAAA;AAAA,IACA,OAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,cAAA,CAAe,MAAM,GAAG,CAAA;AACxB,EAAA,OAAA,CAAQ,QAAQ,CAAC,MAAA,KAAW,MAAA,CAAO,MAAA,GAAS,GAAG,CAAC,CAAA;AAEhD,EAAA,OAAO,GAAA;AACT","file":"index.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, BridgeApi } from './types'\n\n/**\n * Unique symbol key for the global registry.\n *\n * Symbol.for() returns the SAME symbol across all modules, even if\n * shared-state-bridge is accidentally duplicated in node_modules.\n * This is the critical trick that makes cross-package sharing work\n * in monorepos.\n */\nconst REGISTRY_KEY = Symbol.for('shared-state-bridge.registry')\n\nfunction getRegistry(): Map<string, BridgeApi<State>> {\n const g = globalThis as Record<symbol, Map<string, BridgeApi<State>>>\n if (!g[REGISTRY_KEY]) {\n g[REGISTRY_KEY] = new Map()\n }\n return g[REGISTRY_KEY]\n}\n\n/**\n * Register a bridge in the global registry.\n * Throws if a bridge with the same name already exists.\n * @internal Called by createBridge\n */\nexport function registerBridge<T extends State>(\n name: string,\n bridge: BridgeApi<T>,\n): void {\n const registry = getRegistry()\n if (registry.has(name)) {\n throw new Error(\n `[shared-state-bridge] A bridge named \"${name}\" already exists. ` +\n `Use getBridge(\"${name}\") to access it, or destroy the existing one first.`,\n )\n }\n registry.set(name, bridge as BridgeApi<State>)\n}\n\n/**\n * Remove a bridge from the global registry.\n * @internal Called by bridge.destroy()\n */\nexport function unregisterBridge(name: string): void {\n getRegistry().delete(name)\n}\n\n/**\n * Retrieve a bridge by name from the global registry.\n *\n * Use the generic parameter to assert the expected state shape:\n * ```ts\n * const auth = getBridge<AuthState>('auth')\n * ```\n *\n * @throws If no bridge with that name exists\n */\nexport function getBridge<T extends State = State>(name: string): BridgeApi<T> {\n const registry = getRegistry()\n const bridge = registry.get(name)\n if (!bridge) {\n throw new Error(\n `[shared-state-bridge] No bridge named \"${name}\" found. ` +\n `Make sure createBridge({ name: \"${name}\" }) is called before getBridge().`,\n )\n }\n return bridge as unknown as BridgeApi<T>\n}\n\n/** Check if a bridge exists without throwing */\nexport function hasBridge(name: string): boolean {\n return getRegistry().has(name)\n}\n\n/** List all registered bridge names (useful for debugging) */\nexport function listBridges(): string[] {\n return Array.from(getRegistry().keys())\n}\n","import type {\n State,\n BridgeConfig,\n BridgeApi,\n Listener,\n SelectorListener,\n SubscribeOptions,\n} from './types'\nimport { isFunction } from './utils'\nimport { registerBridge, unregisterBridge } from './registry'\n\n/**\n * Create a new Bridge store instance.\n *\n * The bridge is automatically registered in the global registry under\n * `config.name`, making it accessible from any package in a monorepo\n * via `getBridge(name)`.\n *\n * @example\n * ```ts\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { count: 0, theme: 'light' },\n * })\n *\n * bridge.setState({ count: 1 })\n * bridge.subscribe(state => console.log(state))\n * ```\n */\nexport function createBridge<T extends State>(config: BridgeConfig<T>): BridgeApi<T> {\n const { name, initialState, plugins = [] } = config\n\n let state: T = initialState\n const frozenInitialState: T = initialState\n const listeners = new Set<Listener<T>>()\n let isDestroyed = false\n\n const getState = (): T => state\n\n const getInitialState = (): T => frozenInitialState\n\n const getName = (): string => name\n\n const setState: BridgeApi<T>['setState'] = (\n partial: Partial<T> | ((state: T) => Partial<T>) | T | ((state: T) => T),\n replace?: boolean,\n ): void => {\n if (isDestroyed) return\n\n const previousState = state\n const nextPartial = isFunction(partial) ? partial(state) : partial\n\n if (replace) {\n state = nextPartial as T\n } else {\n state = Object.assign({}, state, nextPartial)\n }\n\n if (!Object.is(state, previousState)) {\n listeners.forEach((listener) => listener(state, previousState))\n plugins.forEach((plugin) => plugin.onStateChange?.(state, previousState))\n }\n }\n\n const subscribe: BridgeApi<T>['subscribe'] = <U = T>(\n selectorOrListener:\n | Listener<T>\n | ((state: T) => U),\n listenerOrUndefined?: SelectorListener<T, U>,\n options?: SubscribeOptions<U>,\n ): (() => void) => {\n if (isDestroyed) return () => {}\n\n // Overload 1: subscribe(listener)\n if (typeof listenerOrUndefined === 'undefined') {\n const listener = selectorOrListener as Listener<T>\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n }\n\n // Overload 2: subscribe(selector, listener, options?)\n const selector = selectorOrListener as (state: T) => U\n const selectorListener = listenerOrUndefined\n const equalityFn = options?.equalityFn ?? Object.is\n let previousSlice = selector(state)\n\n const internalListener: Listener<T> = (currentState) => {\n const nextSlice = selector(currentState)\n if (!equalityFn(previousSlice, nextSlice)) {\n const prevSlice = previousSlice\n previousSlice = nextSlice\n selectorListener(nextSlice, prevSlice)\n }\n }\n\n listeners.add(internalListener)\n\n if (options?.fireImmediately) {\n selectorListener(previousSlice, previousSlice)\n }\n\n return () => {\n listeners.delete(internalListener)\n }\n }\n\n const destroy = (): void => {\n if (isDestroyed) return\n unregisterBridge(name)\n plugins.forEach((plugin) => plugin.onDestroy?.())\n listeners.clear()\n isDestroyed = true\n }\n\n const api: BridgeApi<T> = {\n getState,\n setState,\n subscribe,\n getInitialState,\n destroy,\n getName,\n }\n\n registerBridge(name, api)\n plugins.forEach((plugin) => plugin.onInit?.(api))\n\n return api\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
/** Configuration for createBridge */
|
|
29
|
+
interface BridgeConfig<T extends State> {
|
|
30
|
+
/** Unique name for global registry lookup */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Initial state value */
|
|
33
|
+
initialState: T;
|
|
34
|
+
/** Optional plugins (e.g. persist) */
|
|
35
|
+
plugins?: BridgePlugin<T>[];
|
|
36
|
+
}
|
|
37
|
+
/** The bridge instance API */
|
|
38
|
+
interface BridgeApi<T extends State> {
|
|
39
|
+
getState: () => T;
|
|
40
|
+
setState: SetState<T>;
|
|
41
|
+
subscribe: Subscribe<T>;
|
|
42
|
+
getInitialState: () => T;
|
|
43
|
+
destroy: () => void;
|
|
44
|
+
getName: () => string;
|
|
45
|
+
}
|
|
46
|
+
/** Storage adapter interface for persistence */
|
|
47
|
+
interface PersistAdapter {
|
|
48
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
49
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
50
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** Persistence plugin options */
|
|
53
|
+
interface PersistOptions<T extends State> {
|
|
54
|
+
/** Storage adapter (localStorageAdapter, asyncStorageAdapter, etc.) */
|
|
55
|
+
adapter: PersistAdapter;
|
|
56
|
+
/** Storage key */
|
|
57
|
+
key: string;
|
|
58
|
+
/** Only persist these keys */
|
|
59
|
+
pick?: (keyof T)[];
|
|
60
|
+
/** Exclude these keys from persistence */
|
|
61
|
+
omit?: (keyof T)[];
|
|
62
|
+
/** Custom serializer (default: JSON.stringify) */
|
|
63
|
+
serialize?: (state: unknown) => string;
|
|
64
|
+
/** Custom deserializer (default: JSON.parse) */
|
|
65
|
+
deserialize?: (raw: string) => unknown;
|
|
66
|
+
/** Schema version for migrations */
|
|
67
|
+
version?: number;
|
|
68
|
+
/** Migration function when version changes */
|
|
69
|
+
migrate?: (persisted: unknown, version: number) => Partial<T>;
|
|
70
|
+
/** Throttle writes in ms (default: 100) */
|
|
71
|
+
throttleMs?: number;
|
|
72
|
+
}
|
|
73
|
+
/** Selector function type */
|
|
74
|
+
type Selector<T extends State, U> = (state: T) => U;
|
|
75
|
+
/** Equality function type */
|
|
76
|
+
type EqualityFn<U> = (a: U, b: U) => boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new Bridge store instance.
|
|
80
|
+
*
|
|
81
|
+
* The bridge is automatically registered in the global registry under
|
|
82
|
+
* `config.name`, making it accessible from any package in a monorepo
|
|
83
|
+
* via `getBridge(name)`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const bridge = createBridge({
|
|
88
|
+
* name: 'app',
|
|
89
|
+
* initialState: { count: 0, theme: 'light' },
|
|
90
|
+
* })
|
|
91
|
+
*
|
|
92
|
+
* bridge.setState({ count: 1 })
|
|
93
|
+
* bridge.subscribe(state => console.log(state))
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function createBridge<T extends State>(config: BridgeConfig<T>): BridgeApi<T>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retrieve a bridge by name from the global registry.
|
|
100
|
+
*
|
|
101
|
+
* Use the generic parameter to assert the expected state shape:
|
|
102
|
+
* ```ts
|
|
103
|
+
* const auth = getBridge<AuthState>('auth')
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @throws If no bridge with that name exists
|
|
107
|
+
*/
|
|
108
|
+
declare function getBridge<T extends State = State>(name: string): BridgeApi<T>;
|
|
109
|
+
/** Check if a bridge exists without throwing */
|
|
110
|
+
declare function hasBridge(name: string): boolean;
|
|
111
|
+
/** List all registered bridge names (useful for debugging) */
|
|
112
|
+
declare function listBridges(): string[];
|
|
113
|
+
|
|
114
|
+
export { type BridgeApi, type BridgeConfig, type BridgePlugin, type EqualityFn, type Listener, type PersistAdapter, type PersistOptions, type Selector, type SelectorListener, type SetState, type State, type Subscribe, type SubscribeOptions, createBridge, getBridge, hasBridge, listBridges };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
/** Configuration for createBridge */
|
|
29
|
+
interface BridgeConfig<T extends State> {
|
|
30
|
+
/** Unique name for global registry lookup */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Initial state value */
|
|
33
|
+
initialState: T;
|
|
34
|
+
/** Optional plugins (e.g. persist) */
|
|
35
|
+
plugins?: BridgePlugin<T>[];
|
|
36
|
+
}
|
|
37
|
+
/** The bridge instance API */
|
|
38
|
+
interface BridgeApi<T extends State> {
|
|
39
|
+
getState: () => T;
|
|
40
|
+
setState: SetState<T>;
|
|
41
|
+
subscribe: Subscribe<T>;
|
|
42
|
+
getInitialState: () => T;
|
|
43
|
+
destroy: () => void;
|
|
44
|
+
getName: () => string;
|
|
45
|
+
}
|
|
46
|
+
/** Storage adapter interface for persistence */
|
|
47
|
+
interface PersistAdapter {
|
|
48
|
+
getItem: (key: string) => string | null | Promise<string | null>;
|
|
49
|
+
setItem: (key: string, value: string) => void | Promise<void>;
|
|
50
|
+
removeItem: (key: string) => void | Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** Persistence plugin options */
|
|
53
|
+
interface PersistOptions<T extends State> {
|
|
54
|
+
/** Storage adapter (localStorageAdapter, asyncStorageAdapter, etc.) */
|
|
55
|
+
adapter: PersistAdapter;
|
|
56
|
+
/** Storage key */
|
|
57
|
+
key: string;
|
|
58
|
+
/** Only persist these keys */
|
|
59
|
+
pick?: (keyof T)[];
|
|
60
|
+
/** Exclude these keys from persistence */
|
|
61
|
+
omit?: (keyof T)[];
|
|
62
|
+
/** Custom serializer (default: JSON.stringify) */
|
|
63
|
+
serialize?: (state: unknown) => string;
|
|
64
|
+
/** Custom deserializer (default: JSON.parse) */
|
|
65
|
+
deserialize?: (raw: string) => unknown;
|
|
66
|
+
/** Schema version for migrations */
|
|
67
|
+
version?: number;
|
|
68
|
+
/** Migration function when version changes */
|
|
69
|
+
migrate?: (persisted: unknown, version: number) => Partial<T>;
|
|
70
|
+
/** Throttle writes in ms (default: 100) */
|
|
71
|
+
throttleMs?: number;
|
|
72
|
+
}
|
|
73
|
+
/** Selector function type */
|
|
74
|
+
type Selector<T extends State, U> = (state: T) => U;
|
|
75
|
+
/** Equality function type */
|
|
76
|
+
type EqualityFn<U> = (a: U, b: U) => boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create a new Bridge store instance.
|
|
80
|
+
*
|
|
81
|
+
* The bridge is automatically registered in the global registry under
|
|
82
|
+
* `config.name`, making it accessible from any package in a monorepo
|
|
83
|
+
* via `getBridge(name)`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const bridge = createBridge({
|
|
88
|
+
* name: 'app',
|
|
89
|
+
* initialState: { count: 0, theme: 'light' },
|
|
90
|
+
* })
|
|
91
|
+
*
|
|
92
|
+
* bridge.setState({ count: 1 })
|
|
93
|
+
* bridge.subscribe(state => console.log(state))
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
declare function createBridge<T extends State>(config: BridgeConfig<T>): BridgeApi<T>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Retrieve a bridge by name from the global registry.
|
|
100
|
+
*
|
|
101
|
+
* Use the generic parameter to assert the expected state shape:
|
|
102
|
+
* ```ts
|
|
103
|
+
* const auth = getBridge<AuthState>('auth')
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @throws If no bridge with that name exists
|
|
107
|
+
*/
|
|
108
|
+
declare function getBridge<T extends State = State>(name: string): BridgeApi<T>;
|
|
109
|
+
/** Check if a bridge exists without throwing */
|
|
110
|
+
declare function hasBridge(name: string): boolean;
|
|
111
|
+
/** List all registered bridge names (useful for debugging) */
|
|
112
|
+
declare function listBridges(): string[];
|
|
113
|
+
|
|
114
|
+
export { type BridgeApi, type BridgeConfig, type BridgePlugin, type EqualityFn, type Listener, type PersistAdapter, type PersistOptions, type Selector, type SelectorListener, type SetState, type State, type Subscribe, type SubscribeOptions, createBridge, getBridge, hasBridge, listBridges };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/core/utils.ts
|
|
2
|
+
function isFunction(value) {
|
|
3
|
+
return typeof value === "function";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/core/registry.ts
|
|
7
|
+
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("shared-state-bridge.registry");
|
|
8
|
+
function getRegistry() {
|
|
9
|
+
const g = globalThis;
|
|
10
|
+
if (!g[REGISTRY_KEY]) {
|
|
11
|
+
g[REGISTRY_KEY] = /* @__PURE__ */ new Map();
|
|
12
|
+
}
|
|
13
|
+
return g[REGISTRY_KEY];
|
|
14
|
+
}
|
|
15
|
+
function registerBridge(name, bridge) {
|
|
16
|
+
const registry = getRegistry();
|
|
17
|
+
if (registry.has(name)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`[shared-state-bridge] A bridge named "${name}" already exists. Use getBridge("${name}") to access it, or destroy the existing one first.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
registry.set(name, bridge);
|
|
23
|
+
}
|
|
24
|
+
function unregisterBridge(name) {
|
|
25
|
+
getRegistry().delete(name);
|
|
26
|
+
}
|
|
27
|
+
function getBridge(name) {
|
|
28
|
+
const registry = getRegistry();
|
|
29
|
+
const bridge = registry.get(name);
|
|
30
|
+
if (!bridge) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`[shared-state-bridge] No bridge named "${name}" found. Make sure createBridge({ name: "${name}" }) is called before getBridge().`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return bridge;
|
|
36
|
+
}
|
|
37
|
+
function hasBridge(name) {
|
|
38
|
+
return getRegistry().has(name);
|
|
39
|
+
}
|
|
40
|
+
function listBridges() {
|
|
41
|
+
return Array.from(getRegistry().keys());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/core/bridge.ts
|
|
45
|
+
function createBridge(config) {
|
|
46
|
+
const { name, initialState, plugins = [] } = config;
|
|
47
|
+
let state = initialState;
|
|
48
|
+
const frozenInitialState = initialState;
|
|
49
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
50
|
+
let isDestroyed = false;
|
|
51
|
+
const getState = () => state;
|
|
52
|
+
const getInitialState = () => frozenInitialState;
|
|
53
|
+
const getName = () => name;
|
|
54
|
+
const setState = (partial, replace) => {
|
|
55
|
+
if (isDestroyed) return;
|
|
56
|
+
const previousState = state;
|
|
57
|
+
const nextPartial = isFunction(partial) ? partial(state) : partial;
|
|
58
|
+
if (replace) {
|
|
59
|
+
state = nextPartial;
|
|
60
|
+
} else {
|
|
61
|
+
state = Object.assign({}, state, nextPartial);
|
|
62
|
+
}
|
|
63
|
+
if (!Object.is(state, previousState)) {
|
|
64
|
+
listeners.forEach((listener) => listener(state, previousState));
|
|
65
|
+
plugins.forEach((plugin) => plugin.onStateChange?.(state, previousState));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const subscribe = (selectorOrListener, listenerOrUndefined, options) => {
|
|
69
|
+
if (isDestroyed) return () => {
|
|
70
|
+
};
|
|
71
|
+
if (typeof listenerOrUndefined === "undefined") {
|
|
72
|
+
const listener = selectorOrListener;
|
|
73
|
+
listeners.add(listener);
|
|
74
|
+
return () => {
|
|
75
|
+
listeners.delete(listener);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const selector = selectorOrListener;
|
|
79
|
+
const selectorListener = listenerOrUndefined;
|
|
80
|
+
const equalityFn = options?.equalityFn ?? Object.is;
|
|
81
|
+
let previousSlice = selector(state);
|
|
82
|
+
const internalListener = (currentState) => {
|
|
83
|
+
const nextSlice = selector(currentState);
|
|
84
|
+
if (!equalityFn(previousSlice, nextSlice)) {
|
|
85
|
+
const prevSlice = previousSlice;
|
|
86
|
+
previousSlice = nextSlice;
|
|
87
|
+
selectorListener(nextSlice, prevSlice);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
listeners.add(internalListener);
|
|
91
|
+
if (options?.fireImmediately) {
|
|
92
|
+
selectorListener(previousSlice, previousSlice);
|
|
93
|
+
}
|
|
94
|
+
return () => {
|
|
95
|
+
listeners.delete(internalListener);
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const destroy = () => {
|
|
99
|
+
if (isDestroyed) return;
|
|
100
|
+
unregisterBridge(name);
|
|
101
|
+
plugins.forEach((plugin) => plugin.onDestroy?.());
|
|
102
|
+
listeners.clear();
|
|
103
|
+
isDestroyed = true;
|
|
104
|
+
};
|
|
105
|
+
const api = {
|
|
106
|
+
getState,
|
|
107
|
+
setState,
|
|
108
|
+
subscribe,
|
|
109
|
+
getInitialState,
|
|
110
|
+
destroy,
|
|
111
|
+
getName
|
|
112
|
+
};
|
|
113
|
+
registerBridge(name, api);
|
|
114
|
+
plugins.forEach((plugin) => plugin.onInit?.(api));
|
|
115
|
+
return api;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export { createBridge, getBridge, hasBridge, listBridges };
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
|
120
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/utils.ts","../src/core/registry.ts","../src/core/bridge.ts"],"names":[],"mappings":"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","file":"index.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, BridgeApi } from './types'\n\n/**\n * Unique symbol key for the global registry.\n *\n * Symbol.for() returns the SAME symbol across all modules, even if\n * shared-state-bridge is accidentally duplicated in node_modules.\n * This is the critical trick that makes cross-package sharing work\n * in monorepos.\n */\nconst REGISTRY_KEY = Symbol.for('shared-state-bridge.registry')\n\nfunction getRegistry(): Map<string, BridgeApi<State>> {\n const g = globalThis as Record<symbol, Map<string, BridgeApi<State>>>\n if (!g[REGISTRY_KEY]) {\n g[REGISTRY_KEY] = new Map()\n }\n return g[REGISTRY_KEY]\n}\n\n/**\n * Register a bridge in the global registry.\n * Throws if a bridge with the same name already exists.\n * @internal Called by createBridge\n */\nexport function registerBridge<T extends State>(\n name: string,\n bridge: BridgeApi<T>,\n): void {\n const registry = getRegistry()\n if (registry.has(name)) {\n throw new Error(\n `[shared-state-bridge] A bridge named \"${name}\" already exists. ` +\n `Use getBridge(\"${name}\") to access it, or destroy the existing one first.`,\n )\n }\n registry.set(name, bridge as BridgeApi<State>)\n}\n\n/**\n * Remove a bridge from the global registry.\n * @internal Called by bridge.destroy()\n */\nexport function unregisterBridge(name: string): void {\n getRegistry().delete(name)\n}\n\n/**\n * Retrieve a bridge by name from the global registry.\n *\n * Use the generic parameter to assert the expected state shape:\n * ```ts\n * const auth = getBridge<AuthState>('auth')\n * ```\n *\n * @throws If no bridge with that name exists\n */\nexport function getBridge<T extends State = State>(name: string): BridgeApi<T> {\n const registry = getRegistry()\n const bridge = registry.get(name)\n if (!bridge) {\n throw new Error(\n `[shared-state-bridge] No bridge named \"${name}\" found. ` +\n `Make sure createBridge({ name: \"${name}\" }) is called before getBridge().`,\n )\n }\n return bridge as unknown as BridgeApi<T>\n}\n\n/** Check if a bridge exists without throwing */\nexport function hasBridge(name: string): boolean {\n return getRegistry().has(name)\n}\n\n/** List all registered bridge names (useful for debugging) */\nexport function listBridges(): string[] {\n return Array.from(getRegistry().keys())\n}\n","import type {\n State,\n BridgeConfig,\n BridgeApi,\n Listener,\n SelectorListener,\n SubscribeOptions,\n} from './types'\nimport { isFunction } from './utils'\nimport { registerBridge, unregisterBridge } from './registry'\n\n/**\n * Create a new Bridge store instance.\n *\n * The bridge is automatically registered in the global registry under\n * `config.name`, making it accessible from any package in a monorepo\n * via `getBridge(name)`.\n *\n * @example\n * ```ts\n * const bridge = createBridge({\n * name: 'app',\n * initialState: { count: 0, theme: 'light' },\n * })\n *\n * bridge.setState({ count: 1 })\n * bridge.subscribe(state => console.log(state))\n * ```\n */\nexport function createBridge<T extends State>(config: BridgeConfig<T>): BridgeApi<T> {\n const { name, initialState, plugins = [] } = config\n\n let state: T = initialState\n const frozenInitialState: T = initialState\n const listeners = new Set<Listener<T>>()\n let isDestroyed = false\n\n const getState = (): T => state\n\n const getInitialState = (): T => frozenInitialState\n\n const getName = (): string => name\n\n const setState: BridgeApi<T>['setState'] = (\n partial: Partial<T> | ((state: T) => Partial<T>) | T | ((state: T) => T),\n replace?: boolean,\n ): void => {\n if (isDestroyed) return\n\n const previousState = state\n const nextPartial = isFunction(partial) ? partial(state) : partial\n\n if (replace) {\n state = nextPartial as T\n } else {\n state = Object.assign({}, state, nextPartial)\n }\n\n if (!Object.is(state, previousState)) {\n listeners.forEach((listener) => listener(state, previousState))\n plugins.forEach((plugin) => plugin.onStateChange?.(state, previousState))\n }\n }\n\n const subscribe: BridgeApi<T>['subscribe'] = <U = T>(\n selectorOrListener:\n | Listener<T>\n | ((state: T) => U),\n listenerOrUndefined?: SelectorListener<T, U>,\n options?: SubscribeOptions<U>,\n ): (() => void) => {\n if (isDestroyed) return () => {}\n\n // Overload 1: subscribe(listener)\n if (typeof listenerOrUndefined === 'undefined') {\n const listener = selectorOrListener as Listener<T>\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n }\n\n // Overload 2: subscribe(selector, listener, options?)\n const selector = selectorOrListener as (state: T) => U\n const selectorListener = listenerOrUndefined\n const equalityFn = options?.equalityFn ?? Object.is\n let previousSlice = selector(state)\n\n const internalListener: Listener<T> = (currentState) => {\n const nextSlice = selector(currentState)\n if (!equalityFn(previousSlice, nextSlice)) {\n const prevSlice = previousSlice\n previousSlice = nextSlice\n selectorListener(nextSlice, prevSlice)\n }\n }\n\n listeners.add(internalListener)\n\n if (options?.fireImmediately) {\n selectorListener(previousSlice, previousSlice)\n }\n\n return () => {\n listeners.delete(internalListener)\n }\n }\n\n const destroy = (): void => {\n if (isDestroyed) return\n unregisterBridge(name)\n plugins.forEach((plugin) => plugin.onDestroy?.())\n listeners.clear()\n isDestroyed = true\n }\n\n const api: BridgeApi<T> = {\n getState,\n setState,\n subscribe,\n getInitialState,\n destroy,\n getName,\n }\n\n registerBridge(name, api)\n plugins.forEach((plugin) => plugin.onInit?.(api))\n\n return api\n}\n"]}
|
package/dist/persist.cjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/utils.ts
|
|
4
|
+
function throttle(fn, intervalMs) {
|
|
5
|
+
let lastCall = 0;
|
|
6
|
+
let timeoutId = null;
|
|
7
|
+
let latestArgs;
|
|
8
|
+
return (...args) => {
|
|
9
|
+
latestArgs = args;
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
const remaining = intervalMs - (now - lastCall);
|
|
12
|
+
if (remaining <= 0) {
|
|
13
|
+
if (timeoutId) {
|
|
14
|
+
clearTimeout(timeoutId);
|
|
15
|
+
timeoutId = null;
|
|
16
|
+
}
|
|
17
|
+
lastCall = now;
|
|
18
|
+
fn(...latestArgs);
|
|
19
|
+
} else if (!timeoutId) {
|
|
20
|
+
timeoutId = setTimeout(() => {
|
|
21
|
+
lastCall = Date.now();
|
|
22
|
+
timeoutId = null;
|
|
23
|
+
fn(...latestArgs);
|
|
24
|
+
}, remaining);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/persist/plugin.ts
|
|
30
|
+
function persist(options) {
|
|
31
|
+
const {
|
|
32
|
+
adapter,
|
|
33
|
+
key,
|
|
34
|
+
pick,
|
|
35
|
+
omit,
|
|
36
|
+
serialize = JSON.stringify,
|
|
37
|
+
deserialize = JSON.parse,
|
|
38
|
+
version = 0,
|
|
39
|
+
migrate,
|
|
40
|
+
throttleMs = 100
|
|
41
|
+
} = options;
|
|
42
|
+
let writeToStorage = null;
|
|
43
|
+
function filterState(state) {
|
|
44
|
+
if (pick) {
|
|
45
|
+
const filtered = {};
|
|
46
|
+
for (const k of pick) {
|
|
47
|
+
if (k in state) {
|
|
48
|
+
filtered[k] = state[k];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return filtered;
|
|
52
|
+
}
|
|
53
|
+
if (omit) {
|
|
54
|
+
const filtered = { ...state };
|
|
55
|
+
for (const k of omit) {
|
|
56
|
+
delete filtered[k];
|
|
57
|
+
}
|
|
58
|
+
return filtered;
|
|
59
|
+
}
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
name: "persist",
|
|
64
|
+
onInit: (bridge) => {
|
|
65
|
+
writeToStorage = throttle((state) => {
|
|
66
|
+
const filtered = filterState(state);
|
|
67
|
+
const envelope = { state: filtered, version };
|
|
68
|
+
adapter.setItem(key, serialize(envelope));
|
|
69
|
+
}, throttleMs);
|
|
70
|
+
const hydrate = async () => {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await adapter.getItem(key);
|
|
73
|
+
if (raw === null) return;
|
|
74
|
+
const envelope = deserialize(raw);
|
|
75
|
+
let persisted = envelope.state;
|
|
76
|
+
const persistedVersion = envelope.version ?? 0;
|
|
77
|
+
if (persistedVersion !== version) {
|
|
78
|
+
if (migrate) {
|
|
79
|
+
persisted = migrate(persisted, persistedVersion);
|
|
80
|
+
} else {
|
|
81
|
+
await adapter.removeItem(key);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
bridge.setState(persisted);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
hydrate();
|
|
90
|
+
},
|
|
91
|
+
onStateChange: (state) => {
|
|
92
|
+
writeToStorage?.(state);
|
|
93
|
+
},
|
|
94
|
+
onDestroy: () => {
|
|
95
|
+
writeToStorage = null;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/persist/adapters.ts
|
|
101
|
+
var localStorageAdapter = {
|
|
102
|
+
getItem: (key) => {
|
|
103
|
+
try {
|
|
104
|
+
return globalThis.localStorage.getItem(key);
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
setItem: (key, value) => {
|
|
110
|
+
try {
|
|
111
|
+
globalThis.localStorage.setItem(key, value);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
removeItem: (key) => {
|
|
116
|
+
try {
|
|
117
|
+
globalThis.localStorage.removeItem(key);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
function asyncStorageAdapter(asyncStorage) {
|
|
123
|
+
return {
|
|
124
|
+
getItem: (key) => asyncStorage.getItem(key),
|
|
125
|
+
setItem: (key, value) => asyncStorage.setItem(key, value),
|
|
126
|
+
removeItem: (key) => asyncStorage.removeItem(key)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function memoryAdapter() {
|
|
130
|
+
const store = /* @__PURE__ */ new Map();
|
|
131
|
+
return {
|
|
132
|
+
getItem: (key) => store.get(key) ?? null,
|
|
133
|
+
setItem: (key, value) => {
|
|
134
|
+
store.set(key, value);
|
|
135
|
+
},
|
|
136
|
+
removeItem: (key) => {
|
|
137
|
+
store.delete(key);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
exports.asyncStorageAdapter = asyncStorageAdapter;
|
|
143
|
+
exports.localStorageAdapter = localStorageAdapter;
|
|
144
|
+
exports.memoryAdapter = memoryAdapter;
|
|
145
|
+
exports.persist = persist;
|
|
146
|
+
//# sourceMappingURL=persist.cjs.map
|
|
147
|
+
//# sourceMappingURL=persist.cjs.map
|