@codebelt/classy-store 0.1.0 → 0.1.1

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.
Files changed (41) hide show
  1. package/dist/equality-5F2bPn7E.d.mts +13 -0
  2. package/dist/equality-5F2bPn7E.d.mts.map +1 -0
  3. package/dist/equality-BA46H9AL.mjs +27 -0
  4. package/dist/equality-BA46H9AL.mjs.map +1 -0
  5. package/dist/equality-C1s0kqxg.d.cts +13 -0
  6. package/dist/equality-C1s0kqxg.d.cts.map +1 -0
  7. package/dist/equality-Cz6riknL.cjs +33 -0
  8. package/dist/equality-Cz6riknL.cjs.map +1 -0
  9. package/dist/index.cjs +3 -27
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1 -11
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.mts +2 -12
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +2 -26
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/react/react.cjs +30 -1
  18. package/dist/react/react.cjs.map +1 -1
  19. package/dist/react/react.d.cts +26 -1
  20. package/dist/react/react.d.cts.map +1 -1
  21. package/dist/react/react.d.mts +27 -2
  22. package/dist/react/react.d.mts.map +1 -1
  23. package/dist/react/react.mjs +31 -3
  24. package/dist/react/react.mjs.map +1 -1
  25. package/dist/{snapshot-P0QPV1ER.mjs → snapshot-COzEerMu.mjs} +27 -2
  26. package/dist/snapshot-COzEerMu.mjs.map +1 -0
  27. package/dist/{snapshot-BKVFJLuo.cjs → snapshot-CbVbxG7s.cjs} +27 -2
  28. package/dist/snapshot-CbVbxG7s.cjs.map +1 -0
  29. package/dist/{types-vWYkF3tH.d.mts → types-Cf8Fp7kA.d.mts} +1 -1
  30. package/dist/{types-vWYkF3tH.d.mts.map → types-Cf8Fp7kA.d.mts.map} +1 -1
  31. package/dist/utils/index.cjs +176 -5
  32. package/dist/utils/index.cjs.map +1 -1
  33. package/dist/utils/index.d.cts +89 -1
  34. package/dist/utils/index.d.cts.map +1 -1
  35. package/dist/utils/index.d.mts +89 -1
  36. package/dist/utils/index.d.mts.map +1 -1
  37. package/dist/utils/index.mjs +173 -6
  38. package/dist/utils/index.mjs.map +1 -1
  39. package/package.json +4 -3
  40. package/dist/snapshot-BKVFJLuo.cjs.map +0 -1
  41. package/dist/snapshot-P0QPV1ER.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"react.mjs","names":["subscribe","coreSubscribe"],"sources":["../../src/react/react.ts"],"sourcesContent":["import {createProxy, isChanged} from 'proxy-compare';\nimport {useCallback, useRef, useSyncExternalStore} from 'react';\nimport {subscribe as coreSubscribe, getInternal} from '../core/core';\nimport {snapshot} from '../snapshot/snapshot';\nimport type {Snapshot} from '../types';\n\n// ── Overloads ─────────────────────────────────────────────────────────────────\n\n/**\n * Subscribe to a store proxy with an explicit selector.\n *\n * Re-renders only when the selected value changes (compared via `Object.is`\n * by default, or a custom `isEqual`).\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n * @param selector - Picks data from the immutable snapshot.\n * @param isEqual - Optional custom equality function (default: `Object.is`).\n */\nexport function useStore<T extends object, S>(\n proxyStore: T,\n selector: (snap: Snapshot<T>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S;\n\n/**\n * Subscribe to a store proxy **without** a selector (auto-tracked mode).\n *\n * Returns a `proxy-compare` tracking proxy over the immutable snapshot.\n * The component only re-renders when a property it actually read changes.\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n */\nexport function useStore<T extends object>(proxyStore: T): Snapshot<T>;\n\n// ── Implementation ────────────────────────────────────────────────────────────\n\nexport function useStore<T extends object, S>(\n proxyStore: T,\n selector?: (snap: Snapshot<T>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): Snapshot<T> | S {\n // Validate that the argument is actually a store proxy (throws if not).\n getInternal(proxyStore);\n\n // Stable subscribe function (internal identity never changes for a given store).\n const subscribe = useCallback(\n (onStoreChange: () => void) => coreSubscribe(proxyStore, onStoreChange),\n [proxyStore],\n );\n\n // ── Refs used by both modes (always allocated to satisfy Rules of Hooks) ──\n\n // Selector mode refs\n const snapRef = useRef<Snapshot<T> | undefined>(undefined);\n const resultRef = useRef<S | undefined>(undefined);\n\n // Auto-track mode refs\n const affected = useRef(new WeakMap<object, unknown>()).current;\n const proxyCache = useRef(new WeakMap<object, unknown>()).current;\n const prevSnapRef = useRef<Snapshot<T> | undefined>(undefined);\n const wrappedRef = useRef<Snapshot<T> | undefined>(undefined);\n\n // ── Single getSnapshot for useSyncExternalStore ───────────────────────────\n\n const getSnapshot = (): Snapshot<T> | S =>\n selector\n ? getSelectorSnapshot(proxyStore, snapRef, resultRef, selector, isEqual)\n : getAutoTrackSnapshot(\n proxyStore,\n affected,\n proxyCache,\n prevSnapRef,\n wrappedRef,\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n// ── Selector mode logic (pure function, no hooks) ─────────────────────────────\n\n/**\n * `getSnapshot` implementation for selector mode.\n *\n * Fast-paths when the snapshot reference hasn't changed (O(1)). Otherwise\n * runs the selector against the new snapshot and compares the result to the\n * previous one via `Object.is` (or a custom `isEqual`). Returns the previous\n * result reference when equal, preventing unnecessary React re-renders.\n *\n * Pure function -- no hooks, safe to call from `useSyncExternalStore`.\n */\nfunction getSelectorSnapshot<T extends object, S>(\n proxyStore: T,\n snapRef: React.RefObject<Snapshot<T> | undefined>,\n resultRef: React.RefObject<S | undefined>,\n selector: (snap: Snapshot<T>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const nextSnap = snapshot(proxyStore);\n\n // Fast path: same snapshot reference → same result.\n if (snapRef.current === nextSnap && resultRef.current !== undefined) {\n return resultRef.current;\n }\n\n const nextResult = selector(nextSnap);\n snapRef.current = nextSnap;\n\n // Check equality with previous result.\n if (\n resultRef.current !== undefined &&\n (isEqual\n ? isEqual(resultRef.current, nextResult)\n : Object.is(resultRef.current, nextResult))\n ) {\n return resultRef.current;\n }\n\n resultRef.current = nextResult;\n return nextResult;\n}\n\n// ── Auto-tracked (selectorless) mode logic (pure function, no hooks) ──────────\n\n/**\n * `getSnapshot` implementation for auto-tracked (selectorless) mode.\n *\n * Uses `proxy-compare` to diff only the properties the component actually read.\n * If the snapshot reference is the same, returns the cached tracking proxy.\n * If the snapshot changed but no tracked property differs (`isChanged` returns\n * false), also returns the cached proxy -- avoiding re-render. Only when a\n * relevant property changed does it create a new `createProxy` wrapper.\n *\n * Pure function -- no hooks, safe to call from `useSyncExternalStore`.\n */\nfunction getAutoTrackSnapshot<T extends object>(\n proxyStore: T,\n affected: WeakMap<object, unknown>,\n proxyCache: WeakMap<object, unknown>,\n prevSnapRef: React.RefObject<Snapshot<T> | undefined>,\n wrappedRef: React.RefObject<Snapshot<T> | undefined>,\n): Snapshot<T> {\n const nextSnap = snapshot(proxyStore);\n\n // If the raw snapshot is the same reference, nothing changed.\n if (prevSnapRef.current === nextSnap) {\n return wrappedRef.current as Snapshot<T>;\n }\n\n // Check if any property the component actually read has changed.\n if (\n prevSnapRef.current !== undefined &&\n !isChanged(prevSnapRef.current, nextSnap, affected)\n ) {\n // No property the component cares about changed → return same wrapped proxy.\n return wrappedRef.current as Snapshot<T>;\n }\n\n // Something relevant changed — create a new tracking proxy.\n prevSnapRef.current = nextSnap;\n const wrapped = createProxy(nextSnap, affected, proxyCache) as Snapshot<T>;\n wrappedRef.current = wrapped;\n return wrapped;\n}\n"],"mappings":";;;;;AAoCA,SAAgB,SACd,YACA,UACA,SACiB;AAEjB,aAAY,WAAW;CAGvB,MAAMA,cAAY,aACf,kBAA8BC,UAAc,YAAY,cAAc,EACvE,CAAC,WAAW,CACb;CAKD,MAAM,UAAU,OAAgC,OAAU;CAC1D,MAAM,YAAY,OAAsB,OAAU;CAGlD,MAAM,WAAW,uBAAO,IAAI,SAA0B,CAAC,CAAC;CACxD,MAAM,aAAa,uBAAO,IAAI,SAA0B,CAAC,CAAC;CAC1D,MAAM,cAAc,OAAgC,OAAU;CAC9D,MAAM,aAAa,OAAgC,OAAU;CAI7D,MAAM,oBACJ,WACI,oBAAoB,YAAY,SAAS,WAAW,UAAU,QAAQ,GACtE,qBACE,YACA,UACA,YACA,aACA,WACD;AAEP,QAAO,qBAAqBD,aAAW,aAAa,YAAY;;;;;;;;;;;;AAelE,SAAS,oBACP,YACA,SACA,WACA,UACA,SACG;CACH,MAAM,WAAW,SAAS,WAAW;AAGrC,KAAI,QAAQ,YAAY,YAAY,UAAU,YAAY,OACxD,QAAO,UAAU;CAGnB,MAAM,aAAa,SAAS,SAAS;AACrC,SAAQ,UAAU;AAGlB,KACE,UAAU,YAAY,WACrB,UACG,QAAQ,UAAU,SAAS,WAAW,GACtC,OAAO,GAAG,UAAU,SAAS,WAAW,EAE5C,QAAO,UAAU;AAGnB,WAAU,UAAU;AACpB,QAAO;;;;;;;;;;;;;AAgBT,SAAS,qBACP,YACA,UACA,YACA,aACA,YACa;CACb,MAAM,WAAW,SAAS,WAAW;AAGrC,KAAI,YAAY,YAAY,SAC1B,QAAO,WAAW;AAIpB,KACE,YAAY,YAAY,UACxB,CAAC,UAAU,YAAY,SAAS,UAAU,SAAS,CAGnD,QAAO,WAAW;AAIpB,aAAY,UAAU;CACtB,MAAM,UAAU,YAAY,UAAU,UAAU,WAAW;AAC3D,YAAW,UAAU;AACrB,QAAO"}
1
+ {"version":3,"file":"react.mjs","names":["subscribe","coreSubscribe"],"sources":["../../src/react/react.ts"],"sourcesContent":["import {createProxy, isChanged} from 'proxy-compare';\nimport {useCallback, useRef, useState, useSyncExternalStore} from 'react';\nimport {\n subscribe as coreSubscribe,\n createClassyStore,\n getInternal,\n} from '../core/core';\nimport {snapshot} from '../snapshot/snapshot';\nimport type {Snapshot} from '../types';\n\n// ── Overloads ─────────────────────────────────────────────────────────────────\n\n/**\n * Subscribe to a store proxy with an explicit selector.\n *\n * Re-renders only when the selected value changes (compared via `Object.is`\n * by default, or a custom `isEqual`).\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n * @param selector - Picks data from the immutable snapshot.\n * @param isEqual - Optional custom equality function (default: `Object.is`).\n */\nexport function useStore<T extends object, S>(\n proxyStore: T,\n selector: (snap: Snapshot<T>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S;\n\n/**\n * Subscribe to a store proxy **without** a selector (auto-tracked mode).\n *\n * Returns a `proxy-compare` tracking proxy over the immutable snapshot.\n * The component only re-renders when a property it actually read changes.\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n */\nexport function useStore<T extends object>(proxyStore: T): Snapshot<T>;\n\n// ── Implementation ────────────────────────────────────────────────────────────\n\nexport function useStore<T extends object, S>(\n proxyStore: T,\n selector?: (snap: Snapshot<T>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): Snapshot<T> | S {\n // Validate that the argument is actually a store proxy (throws if not).\n getInternal(proxyStore);\n\n // Stable subscribe function (internal identity never changes for a given store).\n const subscribe = useCallback(\n (onStoreChange: () => void) => coreSubscribe(proxyStore, onStoreChange),\n [proxyStore],\n );\n\n // ── Refs used by both modes (always allocated to satisfy Rules of Hooks) ──\n\n // Selector mode refs\n const snapRef = useRef<Snapshot<T> | undefined>(undefined);\n const resultRef = useRef<S | undefined>(undefined);\n\n // Auto-track mode refs\n const affected = useRef(new WeakMap<object, unknown>()).current;\n const proxyCache = useRef(new WeakMap<object, unknown>()).current;\n const prevSnapRef = useRef<Snapshot<T> | undefined>(undefined);\n const wrappedRef = useRef<Snapshot<T> | undefined>(undefined);\n\n // ── Single getSnapshot for useSyncExternalStore ───────────────────────────\n\n const getSnapshot = (): Snapshot<T> | S =>\n selector\n ? getSelectorSnapshot(proxyStore, snapRef, resultRef, selector, isEqual)\n : getAutoTrackSnapshot(\n proxyStore,\n affected,\n proxyCache,\n prevSnapRef,\n wrappedRef,\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n// ── Selector mode logic (pure function, no hooks) ─────────────────────────────\n\n/**\n * `getSnapshot` implementation for selector mode.\n *\n * Fast-paths when the snapshot reference hasn't changed (O(1)). Otherwise\n * runs the selector against the new snapshot and compares the result to the\n * previous one via `Object.is` (or a custom `isEqual`). Returns the previous\n * result reference when equal, preventing unnecessary React re-renders.\n *\n * Pure function -- no hooks, safe to call from `useSyncExternalStore`.\n */\nfunction getSelectorSnapshot<T extends object, S>(\n proxyStore: T,\n snapRef: React.RefObject<Snapshot<T> | undefined>,\n resultRef: React.RefObject<S | undefined>,\n selector: (snap: Snapshot<T>) => S,\n isEqual?: (a: S, b: S) => boolean,\n): S {\n const nextSnap = snapshot(proxyStore);\n\n // Fast path: same snapshot reference → same result.\n if (snapRef.current === nextSnap && resultRef.current !== undefined) {\n return resultRef.current;\n }\n\n const nextResult = selector(nextSnap);\n snapRef.current = nextSnap;\n\n // Check equality with previous result.\n if (\n resultRef.current !== undefined &&\n (isEqual\n ? isEqual(resultRef.current, nextResult)\n : Object.is(resultRef.current, nextResult))\n ) {\n return resultRef.current;\n }\n\n resultRef.current = nextResult;\n return nextResult;\n}\n\n// ── Auto-tracked (selectorless) mode logic (pure function, no hooks) ──────────\n\n/**\n * `getSnapshot` implementation for auto-tracked (selectorless) mode.\n *\n * Uses `proxy-compare` to diff only the properties the component actually read.\n * If the snapshot reference is the same, returns the cached tracking proxy.\n * If the snapshot changed but no tracked property differs (`isChanged` returns\n * false), also returns the cached proxy -- avoiding re-render. Only when a\n * relevant property changed does it create a new `createProxy` wrapper.\n *\n * Pure function -- no hooks, safe to call from `useSyncExternalStore`.\n */\nfunction getAutoTrackSnapshot<T extends object>(\n proxyStore: T,\n affected: WeakMap<object, unknown>,\n proxyCache: WeakMap<object, unknown>,\n prevSnapRef: React.RefObject<Snapshot<T> | undefined>,\n wrappedRef: React.RefObject<Snapshot<T> | undefined>,\n): Snapshot<T> {\n const nextSnap = snapshot(proxyStore);\n\n // If the raw snapshot is the same reference, nothing changed.\n if (prevSnapRef.current === nextSnap) {\n return wrappedRef.current as Snapshot<T>;\n }\n\n // Check if any property the component actually read has changed.\n if (\n prevSnapRef.current !== undefined &&\n !isChanged(prevSnapRef.current, nextSnap, affected)\n ) {\n // No property the component cares about changed → return same wrapped proxy.\n return wrappedRef.current as Snapshot<T>;\n }\n\n // Something relevant changed — create a new tracking proxy.\n prevSnapRef.current = nextSnap;\n const wrapped = createProxy(nextSnap, affected, proxyCache) as Snapshot<T>;\n wrappedRef.current = wrapped;\n return wrapped;\n}\n\n// ── Component-scoped store ────────────────────────────────────────────────────\n\n/**\n * Create a component-scoped reactive store that lives for the lifetime of the\n * component. When the component unmounts, the store becomes unreferenced and is\n * garbage collected (all internal bookkeeping uses `WeakMap`).\n *\n * The factory function runs **once** per mount (via `useState` initializer).\n * Each component instance gets its own isolated store.\n *\n * Use the returned proxy with `useStore()` to read state in the same component\n * or pass it down via props/context to share within a subtree.\n *\n * @param factory - A function that returns a class instance (or plain object).\n * Called once per component mount.\n * @returns A reactive store proxy scoped to the component's lifetime.\n *\n * @example\n * ```tsx\n * function Counter() {\n * const store = useLocalStore(() => new CounterStore());\n * const count = useStore(store, s => s.count);\n * return <button onClick={() => store.increment()}>{count}</button>;\n * }\n * ```\n */\nexport function useLocalStore<T extends object>(factory: () => T): T {\n const [store] = useState(() => createClassyStore(factory()));\n return store;\n}\n"],"mappings":";;;;;AAwCA,SAAgB,SACd,YACA,UACA,SACiB;AAEjB,aAAY,WAAW;CAGvB,MAAMA,cAAY,aACf,kBAA8BC,UAAc,YAAY,cAAc,EACvE,CAAC,WAAW,CACb;CAKD,MAAM,UAAU,OAAgC,OAAU;CAC1D,MAAM,YAAY,OAAsB,OAAU;CAGlD,MAAM,WAAW,uBAAO,IAAI,SAA0B,CAAC,CAAC;CACxD,MAAM,aAAa,uBAAO,IAAI,SAA0B,CAAC,CAAC;CAC1D,MAAM,cAAc,OAAgC,OAAU;CAC9D,MAAM,aAAa,OAAgC,OAAU;CAI7D,MAAM,oBACJ,WACI,oBAAoB,YAAY,SAAS,WAAW,UAAU,QAAQ,GACtE,qBACE,YACA,UACA,YACA,aACA,WACD;AAEP,QAAO,qBAAqBD,aAAW,aAAa,YAAY;;;;;;;;;;;;AAelE,SAAS,oBACP,YACA,SACA,WACA,UACA,SACG;CACH,MAAM,WAAW,SAAS,WAAW;AAGrC,KAAI,QAAQ,YAAY,YAAY,UAAU,YAAY,OACxD,QAAO,UAAU;CAGnB,MAAM,aAAa,SAAS,SAAS;AACrC,SAAQ,UAAU;AAGlB,KACE,UAAU,YAAY,WACrB,UACG,QAAQ,UAAU,SAAS,WAAW,GACtC,OAAO,GAAG,UAAU,SAAS,WAAW,EAE5C,QAAO,UAAU;AAGnB,WAAU,UAAU;AACpB,QAAO;;;;;;;;;;;;;AAgBT,SAAS,qBACP,YACA,UACA,YACA,aACA,YACa;CACb,MAAM,WAAW,SAAS,WAAW;AAGrC,KAAI,YAAY,YAAY,SAC1B,QAAO,WAAW;AAIpB,KACE,YAAY,YAAY,UACxB,CAAC,UAAU,YAAY,SAAS,UAAU,SAAS,CAGnD,QAAO,WAAW;AAIpB,aAAY,UAAU;CACtB,MAAM,UAAU,YAAY,UAAU,UAAU,WAAW;AAC3D,YAAW,UAAU;AACrB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BT,SAAgB,cAAgC,SAAqB;CACnE,MAAM,CAAC,SAAS,eAAe,kBAAkB,SAAS,CAAC,CAAC;AAC5D,QAAO"}
@@ -87,12 +87,33 @@ function recordDep(internal, prop, value) {
87
87
  });
88
88
  }
89
89
  /**
90
+ * Record a computed getter dependency on the active tracker (if any).
91
+ * Unlike `recordDep`, this creates a `'computed'` dep entry that validates
92
+ * through the computed cache rather than re-executing the raw getter.
93
+ */
94
+ function recordComputedDep(internal, prop, value) {
95
+ const tracker = activeTracker();
96
+ if (!tracker) return;
97
+ if (tracker.internal !== internal) return;
98
+ tracker.deps.push({
99
+ kind: "computed",
100
+ internal,
101
+ prop,
102
+ value
103
+ });
104
+ }
105
+ /**
90
106
  * Check whether all dependencies from a previous computation are still valid.
91
107
  */
92
108
  function areDepsValid(deps) {
93
109
  for (const dep of deps) if (dep.kind === "version") {
94
110
  if (!Object.is(Reflect.get(dep.parentTarget, dep.prop), dep.internal.target)) return false;
95
111
  if (dep.internal.version !== dep.version) return false;
112
+ } else if (dep.kind === "computed") {
113
+ const cached = dep.internal.computedCache.get(dep.prop);
114
+ if (!cached) return false;
115
+ if (!areDepsValid(cached.deps)) return false;
116
+ if (!Object.is(cached.value, dep.value)) return false;
96
117
  } else {
97
118
  if (!Reflect.has(dep.target, dep.prop)) return false;
98
119
  if (!Object.is(Reflect.get(dep.target, dep.prop), dep.value)) return false;
@@ -199,7 +220,11 @@ function createStoreProxy(target, parent) {
199
220
  },
200
221
  get(_target, prop, receiver) {
201
222
  const getterDesc = findGetterDescriptor(_target, prop);
202
- if (getterDesc?.get) return evaluateComputed(internal, prop, getterDesc.get, receiver);
223
+ if (getterDesc?.get) {
224
+ const value = evaluateComputed(internal, prop, getterDesc.get, receiver);
225
+ recordComputedDep(internal, prop, value);
226
+ return value;
227
+ }
203
228
  const value = Reflect.get(_target, prop);
204
229
  if (typeof value === "function") {
205
230
  if (Array.isArray(_target)) return value.bind(receiver);
@@ -526,4 +551,4 @@ function snapshot(proxyStore) {
526
551
 
527
552
  //#endregion
528
553
  export { subscribe as a, getVersion as i, createClassyStore as n, PROXYABLE as o, getInternal as r, findGetterDescriptor as s, snapshot as t };
529
- //# sourceMappingURL=snapshot-P0QPV1ER.mjs.map
554
+ //# sourceMappingURL=snapshot-COzEerMu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-COzEerMu.mjs","names":[],"sources":["../src/utils/internal/internal.ts","../src/core/core.ts","../src/snapshot/snapshot.ts"],"sourcesContent":["const objectProto = Object.getPrototypeOf({});\n\n/**\n * Symbol that class instances can use to opt-in to deep proxying.\n * Classes with `static [PROXYABLE] = true` will be wrapped by the store proxy\n * just like plain objects, enabling nested reactivity.\n */\nexport const PROXYABLE = Symbol.for('@codebelt/classy-store.proxyable');\n\n/**\n * Returns `true` if `value` is a plain object (created via `{}` or `new Object()`).\n * Needed by `canProxy()` to distinguish plain objects (which should be deep-proxied)\n * from class instances, Date, Map, etc. (which should not).\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string | symbol, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === objectProto || proto === null;\n}\n\n/**\n * Central gatekeeper for the proxy system: determines which values get wrapped\n * in child proxies (`core.ts`) or deep-cloned in snapshots (`snapshot.ts`).\n *\n * Returns `true` for arrays, plain objects, and class instances that opt-in\n * via `static [PROXYABLE] = true`. Everything else (Date, Map, Set, class\n * instances without PROXYABLE, primitives) is left as-is.\n */\nexport function canProxy(value: unknown): value is object {\n if (typeof value !== 'object' || value === null) return false;\n if (Array.isArray(value)) return true;\n // Allow class instances that opt-in via the PROXYABLE symbol.\n const ctor = (value as {constructor?: unknown}).constructor;\n if (ctor && (ctor as Record<symbol, unknown>)[PROXYABLE]) {\n return true;\n }\n return isPlainObject(value);\n}\n\n/**\n * Walk the prototype chain of `target` looking for a getter descriptor for `prop`.\n * Returns the first (most-derived) getter found, or `undefined` if none exists.\n *\n * Used by `core.ts` (GET trap) to detect class getters for memoized evaluation,\n * and by `snapshot.ts` to skip own-property copying for getter-backed keys.\n */\nexport function findGetterDescriptor(\n target: object,\n prop: string | symbol,\n): PropertyDescriptor | undefined {\n let proto: object | null = target;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, prop);\n if (desc?.get) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n return undefined;\n}\n","import type {DepEntry, StoreInternal} from '../types';\nimport {canProxy, findGetterDescriptor} from '../utils/internal/internal';\n\n// ── Global state ──────────────────────────────────────────────────────────────\n\n/** Global version counter shared across all stores. */\nlet globalVersion = 0;\n\n/** Maps every store proxy → its internal bookkeeping. */\nconst internalsMap = new WeakMap<object, StoreInternal>();\n\n// ── Dependency tracking for computed getters ──────────────────────────────────\n\n/**\n * Stack of active dependency trackers. Each entry records which properties\n * a getter reads during evaluation. A stack (not a single variable) is needed\n * because getter A can read getter B, which pushes a second tracker.\n */\nconst trackerStack: {internal: StoreInternal; deps: DepEntry[]}[] = [];\n\n/** Returns the tracker currently recording deps, or `null` if none is active. */\nfunction activeTracker() {\n return trackerStack.length > 0 ? trackerStack[trackerStack.length - 1] : null;\n}\n\n/**\n * Record a dependency on the active tracker (if any).\n * Called from the GET trap whenever a non-getter, non-method property is read.\n */\nfunction recordDep(\n internal: StoreInternal,\n prop: string | symbol,\n value: unknown,\n): void {\n const tracker = activeTracker();\n if (!tracker) return;\n\n // Only record deps for reads on the SAME proxy that owns the getter.\n // Child-proxy reads don't need explicit tracking because child mutations\n // bubble up via version numbers on the parent's childInternals.\n if (tracker.internal !== internal) return;\n\n const childInternal = internal.childInternals.get(prop);\n if (childInternal) {\n tracker.deps.push({\n kind: 'version',\n internal: childInternal,\n version: childInternal.version,\n parentTarget: internal.target,\n prop,\n });\n } else {\n tracker.deps.push({kind: 'value', target: internal.target, prop, value});\n }\n}\n\n/**\n * Record a computed getter dependency on the active tracker (if any).\n * Unlike `recordDep`, this creates a `'computed'` dep entry that validates\n * through the computed cache rather than re-executing the raw getter.\n */\nfunction recordComputedDep(\n internal: StoreInternal,\n prop: string | symbol,\n value: unknown,\n): void {\n const tracker = activeTracker();\n if (!tracker) return;\n if (tracker.internal !== internal) return;\n\n tracker.deps.push({kind: 'computed', internal, prop, value});\n}\n\n/**\n * Check whether all dependencies from a previous computation are still valid.\n */\nfunction areDepsValid(deps: DepEntry[]): boolean {\n for (const dep of deps) {\n if (dep.kind === 'version') {\n // Verify the parent property still references the same child object.\n // If the property was replaced entirely (e.g., store.items = newArray),\n // the old child internal is detached and its version never changes.\n // This check catches that case.\n if (\n !Object.is(Reflect.get(dep.parentTarget, dep.prop), dep.internal.target)\n )\n return false;\n if (dep.internal.version !== dep.version) return false;\n } else if (dep.kind === 'computed') {\n // Validate through the computed cache — never re-execute the raw getter.\n const cached = dep.internal.computedCache.get(dep.prop);\n if (!cached) return false; // no cache → must recompute\n if (!areDepsValid(cached.deps)) return false; // nested deps changed\n if (!Object.is(cached.value, dep.value)) return false; // value changed\n } else {\n // Check that the property still exists — if it was deleted and\n // dep.value was `undefined`, Object.is(undefined, undefined) would\n // incorrectly pass without this guard.\n if (!Reflect.has(dep.target, dep.prop)) return false;\n if (!Object.is(Reflect.get(dep.target, dep.prop), dep.value))\n return false;\n }\n }\n return true;\n}\n\n/**\n * Evaluate a computed getter with memoization.\n * Returns the cached value if dependencies haven't changed.\n * Otherwise re-evaluates with dependency tracking and caches the result.\n */\nfunction evaluateComputed(\n internal: StoreInternal,\n prop: string | symbol,\n getterFn: () => unknown,\n receiver: object,\n): unknown {\n const cached = internal.computedCache.get(prop);\n if (cached && areDepsValid(cached.deps)) {\n return cached.value;\n }\n\n // Push a new tracker frame for this getter evaluation.\n const frame = {internal, deps: [] as DepEntry[]};\n trackerStack.push(frame);\n try {\n const value = getterFn.call(receiver);\n internal.computedCache.set(prop, {value, deps: frame.deps});\n return value;\n } finally {\n trackerStack.pop();\n }\n}\n\n// ── Public helpers ────────────────────────────────────────────────────────────\n\n/**\n * Retrieve the internal bookkeeping for a store proxy.\n * Throws if the object was not created with `createClassyStore()`.\n */\nexport function getInternal(proxy: object): StoreInternal {\n const internal = internalsMap.get(proxy);\n if (!internal)\n throw new Error('@codebelt/classy-store: object is not a store proxy');\n return internal;\n}\n\n// ── Notification batching ─────────────────────────────────────────────────────\n\n/**\n * Bump version from the mutated node up to the root, invalidate snapshot caches,\n * and schedule a single microtask notification (deduped at the root level).\n *\n * Version propagation is what enables structural sharing in snapshots: unchanged\n * children keep their old version, so the snapshot cache returns the same frozen ref.\n */\nfunction scheduleNotify(internal: StoreInternal): void {\n let current: StoreInternal | null = internal;\n while (current) {\n current.version = ++globalVersion;\n current = current.parent;\n }\n\n const root = getRoot(internal);\n if (!root.notifyScheduled) {\n root.notifyScheduled = true;\n queueMicrotask(() => {\n root.notifyScheduled = false;\n for (const listener of root.listeners) {\n listener();\n }\n });\n }\n}\n\n/** Walk the parent chain to find the root StoreInternal (the notification hub). */\nfunction getRoot(internal: StoreInternal): StoreInternal {\n let current = internal;\n while (current.parent) {\n current = current.parent;\n }\n return current;\n}\n\n// ── Create proxy (recursive) ──────────────────────────────────────────────────\n\n/**\n * Create an ES6 Proxy around `target` with SET/GET/DELETE traps.\n *\n * This is the core of the library's reactivity. The proxy intercepts:\n * - **SET**: compares old/new with `Object.is`, cleans up child proxies on replacement,\n * forwards the write, and schedules a batched notification.\n * - **GET**: detects class getters (memoized), binds methods to the proxy, lazily wraps\n * nested objects/arrays in child proxies, and records dependencies for computed getters.\n * - **DELETE**: cleans up child proxies and schedules notification.\n *\n * Nested objects are recursively wrapped on first access (lazy deep proxy).\n */\nfunction createStoreProxy<T extends object>(\n target: T,\n parent: StoreInternal | null,\n): T {\n const internal: StoreInternal = {\n target,\n version: ++globalVersion,\n listeners: new Set(),\n childProxies: new Map(),\n childInternals: new Map(),\n parent,\n notifyScheduled: false,\n computedCache: new Map(),\n };\n\n /** Cache for bound methods so we return the same reference each time. */\n const boundMethods = new Map<\n string | symbol,\n (...args: unknown[]) => unknown\n >();\n\n const proxy = new Proxy(target, {\n set(_target, prop, value, _receiver) {\n const oldValue = Reflect.get(_target, prop);\n if (Object.is(oldValue, value)) return true; // noop — same value\n\n // If the new value replaces a child proxy, clean it up.\n if (internal.childProxies.has(prop)) {\n internal.childProxies.delete(prop);\n internal.childInternals.delete(prop);\n }\n\n Reflect.set(_target, prop, value);\n scheduleNotify(internal);\n return true;\n },\n\n get(_target, prop, receiver) {\n // 1. Check for getters on the prototype chain (computed values).\n const getterDesc = findGetterDescriptor(_target, prop);\n if (getterDesc?.get) {\n // Memoized: evaluate with dependency tracking, return cached if deps unchanged.\n const value = evaluateComputed(\n internal,\n prop,\n getterDesc.get,\n receiver,\n );\n // Record as a computed dependency for any parent getter currently tracking.\n // Uses 'computed' dep kind so validation goes through the cache, not raw getter.\n recordComputedDep(internal, prop, value);\n return value;\n }\n\n const value = Reflect.get(_target, prop);\n\n // 2. Methods: bind to the proxy so `this.prop = x` goes through SET trap.\n if (typeof value === 'function') {\n // Array prototype methods should not be cached the same way as class methods.\n if (Array.isArray(_target)) {\n return value.bind(receiver);\n }\n const cached = boundMethods.get(prop);\n if (cached) return cached;\n const bound = (value as (...args: unknown[]) => unknown).bind(receiver);\n boundMethods.set(prop, bound);\n return bound;\n }\n\n // 3. Nested plain objects/arrays: lazy-wrap in a child proxy.\n if (canProxy(value)) {\n let childProxy = internal.childProxies.get(prop);\n if (!childProxy) {\n childProxy = createStoreProxy(value as object, internal);\n internal.childProxies.set(prop, childProxy);\n const childInternal = internalsMap.get(childProxy) as StoreInternal;\n internal.childInternals.set(prop, childInternal);\n }\n // Record dependency AFTER ensuring child proxy/internal exists.\n recordDep(internal, prop, value);\n return childProxy;\n }\n\n // 4. Primitives — record dependency and return.\n recordDep(internal, prop, value);\n return value;\n },\n\n deleteProperty(_target, prop) {\n if (internal.childProxies.has(prop)) {\n internal.childProxies.delete(prop);\n internal.childInternals.delete(prop);\n }\n const deleted = Reflect.deleteProperty(_target, prop);\n if (deleted) {\n scheduleNotify(internal);\n }\n return deleted;\n },\n });\n\n internalsMap.set(proxy, internal);\n return proxy;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Wraps a class instance in a reactive proxy.\n *\n * - Mutations (property writes, array push/splice, etc.) are intercepted and\n * batched into a single notification per microtask.\n * - Class getters are automatically memoized — they only recompute when a\n * dependency they read changes.\n * - Methods are automatically bound so `this` mutations go through the proxy.\n *\n * @param instance - A class instance (or plain object) to make reactive.\n * @returns The same object wrapped in a reactive Proxy.\n *\n * @example\n * ```ts\n * const myStore = createClassyStore(new MyClass());\n * ```\n */\nexport function createClassyStore<T extends object>(instance: T): T {\n return createStoreProxy(instance, null);\n}\n\n/**\n * Subscribe to store changes. The callback fires once per batched mutation\n * (coalesced via `queueMicrotask`), not once per individual property write.\n *\n * @param proxy - A reactive proxy created by `createClassyStore()`.\n * @param callback - Invoked after each batched mutation.\n * @returns An unsubscribe function. Call it to stop receiving notifications.\n */\nexport function subscribe(proxy: object, callback: () => void): () => void {\n const internal = getInternal(proxy);\n // Always subscribe on the root so notifications fire regardless of\n // whether the user subscribes to the root proxy or a child proxy.\n const root = getRoot(internal);\n root.listeners.add(callback);\n return () => {\n root.listeners.delete(callback);\n };\n}\n\n/**\n * Returns the current version number of a store proxy.\n *\n * Versions are monotonically increasing and bump on any mutation in the\n * store's subtree (child mutations propagate up to the root). Useful for\n * debugging, custom cache invalidation, or testing whether a store has changed.\n */\nexport function getVersion(proxy: object): number {\n return getInternal(proxy).version;\n}\n","import {getInternal} from '../core/core';\nimport type {Snapshot, StoreInternal} from '../types';\nimport {canProxy, findGetterDescriptor} from '../utils/internal/internal';\n\n// ── Caches ────────────────────────────────────────────────────────────────────\n\n/**\n * Version-stamped snapshot cache for tracked (proxied) sub-trees.\n * Key: raw target object → [version, frozen snapshot].\n */\nconst snapshotCache = new WeakMap<object, [version: number, snap: object]>();\n\n/**\n * Cache for untracked nested objects (never accessed through the proxy).\n * Key: the raw mutable object → frozen deep clone.\n * Safe because untracked objects can't be mutated through the proxy.\n */\nconst untrackedCache = new WeakMap<object, object>();\n\n/**\n * Per-snapshot, per-getter cache. Ensures repeated access to the same getter\n * on the same frozen snapshot returns the identical reference.\n */\nconst snapshotGetterCache = new WeakMap<\n object,\n Map<string | symbol, unknown>\n>();\n\n// ── Cross-snapshot getter memoization ─────────────────────────────────────────\n\n/**\n * A dependency recorded during getter execution.\n * prop: the property name read on `this`.\n * value: the value returned (reference).\n */\ntype GetterDep = {prop: string | symbol; value: unknown};\n\n/**\n * Cached result of a snapshot getter with the dependencies it read,\n * used for cross-snapshot memoization. When a new snapshot is created,\n * we check if the deps are still reference-equal (via structural sharing)\n * and return the cached result if so -- avoiding re-execution of the getter.\n */\ntype GetterMemoEntry = {deps: GetterDep[]; result: unknown};\n\n/**\n * Cross-snapshot getter memoization cache.\n * Keyed by raw class instance (target), maps each getter name to its last\n * deps + result. This enables getter result stability across snapshot\n * boundaries: if `this.items` hasn't changed between snapshots (same\n * reference via structural sharing), the getter returns the same result.\n */\nconst crossSnapshotMemo = new WeakMap<\n object,\n Map<string | symbol, GetterMemoEntry>\n>();\n\n/**\n * Run a getter against the snapshot with dependency tracking.\n *\n * Creates a lightweight Proxy that intercepts `this.prop` reads and records\n * which properties (and their values/references) the getter accessed. The\n * Proxy delegates to the real frozen snapshot for actual values, so getters\n * that read other getters trigger the memoized getter chain correctly.\n */\nfunction computeWithTracking(\n snap: object,\n getterFn: () => unknown,\n): {deps: GetterDep[]; result: unknown} {\n const deps: GetterDep[] = [];\n const readProps = new Set<string | symbol>();\n\n // Use an empty non-frozen target — we delegate everything to `snap`.\n const handler: ProxyHandler<object> = {\n get(_dummyTarget, prop, _receiver) {\n // Delegate to the real snapshot (receiver = snap so installed getters\n // run with `this = snap`, triggering their own memoization chain).\n const value = Reflect.get(snap, prop, snap);\n if (!readProps.has(prop)) {\n readProps.add(prop);\n deps.push({prop, value});\n }\n return value;\n },\n };\n\n const tracked = new Proxy({}, handler);\n const result = getterFn.call(tracked);\n return {deps, result};\n}\n\n/**\n * Check if all previously recorded deps still hold on the current snapshot.\n * For data properties this is a reference comparison (structural sharing\n * guarantees stable refs for unchanged sub-trees). For getter properties\n * this invokes the getter (which is itself memoized), then compares.\n */\nfunction areMemoizedDepsValid(currentSnap: object, deps: GetterDep[]): boolean {\n for (const dep of deps) {\n const currentValue = Reflect.get(currentSnap, dep.prop, currentSnap);\n if (!Object.is(currentValue, dep.value)) return false;\n }\n return true;\n}\n\n/**\n * Evaluate a snapshot getter with two layers of caching:\n *\n * 1. **Per-snapshot cache** — same getter on the same frozen snapshot always\n * returns the same reference.\n * 2. **Cross-snapshot memo** — if the properties the getter read last time are\n * structurally the same (reference equality via structural sharing), the\n * previous result is returned without re-running the getter body.\n */\nfunction evaluateSnapshotGetter(\n currentSnap: object,\n target: object,\n key: string | symbol,\n getterFn: () => unknown,\n): unknown {\n // ── Per-snapshot fast path ──\n const perSnapCache = snapshotGetterCache.get(currentSnap);\n if (perSnapCache?.has(key)) return perSnapCache.get(key);\n\n // ── Cross-snapshot memo ──\n let memoMap = crossSnapshotMemo.get(target);\n const prev = memoMap?.get(key);\n\n let result: unknown;\n\n if (prev && areMemoizedDepsValid(currentSnap, prev.deps)) {\n // Dependencies unchanged → reuse previous result.\n result = prev.result;\n } else {\n // Compute fresh with dep tracking.\n const computation = computeWithTracking(currentSnap, getterFn);\n result = computation.result;\n\n // Save cross-snapshot memo.\n if (!memoMap) {\n memoMap = new Map();\n crossSnapshotMemo.set(target, memoMap);\n }\n memoMap.set(key, {deps: computation.deps, result});\n }\n\n // Save per-snapshot cache.\n let cache = snapshotGetterCache.get(currentSnap);\n if (!cache) {\n cache = new Map();\n snapshotGetterCache.set(currentSnap, cache);\n }\n cache.set(key, result);\n\n return result;\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\n/**\n * Resolve a single property value into its snapshot equivalent.\n *\n * - If the key has a tracked child internal → recurse (version-cached, structural sharing).\n * - If the value is a nested plain object/array without tracking → deep-clone & freeze (cached by identity).\n * - Otherwise → return the value as-is (primitive, Date, Map, function, etc.).\n */\nfunction snapshotValue(\n value: unknown,\n parentInternal: StoreInternal,\n key: string | symbol,\n): unknown {\n const childInternal = parentInternal.childInternals.get(key);\n if (childInternal) {\n return createSnapshotRecursive(childInternal.target, childInternal);\n }\n if (canProxy(value)) {\n return deepFreezeClone(value as object);\n }\n return value;\n}\n\n/**\n * Deep-clone and freeze a plain object or array that is NOT tracked by a proxy.\n * Cached by raw object identity for structural sharing across snapshots.\n */\nfunction deepFreezeClone(value: object): object {\n const cached = untrackedCache.get(value);\n if (cached) return cached;\n\n let clone: Record<string | symbol, unknown> | unknown[];\n\n if (Array.isArray(value)) {\n clone = [];\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n (clone as unknown[])[i] = canProxy(item)\n ? deepFreezeClone(item as object)\n : item;\n }\n } else {\n clone = Object.create(Object.getPrototypeOf(value));\n for (const key of Reflect.ownKeys(value)) {\n const desc = Object.getOwnPropertyDescriptor(value, key);\n if (!desc || !('value' in desc)) continue;\n const item = desc.value;\n (clone as Record<string | symbol, unknown>)[key] = canProxy(item)\n ? deepFreezeClone(item as object)\n : item;\n }\n }\n\n Object.freeze(clone);\n untrackedCache.set(value, clone);\n return clone;\n}\n\n/**\n * Collect all getter descriptors from the prototype chain of `target`.\n * Returns an array of [propertyName, getterFunction] pairs.\n *\n * Only includes getters defined on the prototype (class getters), not on the\n * instance itself. When a getter is overridden in a subclass, the most-derived\n * version wins (we walk from the instance's direct prototype upward and skip\n * keys already seen).\n */\nfunction collectGetters(\n target: object,\n): Array<[string | symbol, () => unknown]> {\n const getters: Array<[string | symbol, () => unknown]> = [];\n const seen = new Set<string | symbol>();\n let proto: object | null = Object.getPrototypeOf(target);\n while (proto && proto !== Object.prototype) {\n for (const key of Reflect.ownKeys(proto)) {\n if (key === 'constructor') continue;\n if (seen.has(key)) continue; // most-derived version already collected\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get) {\n getters.push([key, desc.get]);\n seen.add(key);\n }\n }\n proto = Object.getPrototypeOf(proto);\n }\n return getters;\n}\n\n/**\n * Install lazy-memoizing getters on a snapshot object.\n *\n * Each getter uses cross-snapshot memoization:\n * - Tracks which `this` properties the getter reads on first evaluation.\n * - On subsequent snapshots, if those properties are structurally the same\n * (thanks to structural sharing), the previous result is returned.\n * - Within the same snapshot, repeated accesses always return the same ref.\n */\nfunction installMemoizedGetters(\n snap: Record<string | symbol, unknown>,\n target: object,\n): void {\n const getters = collectGetters(target);\n for (const [key, getterFn] of getters) {\n Object.defineProperty(snap, key, {\n get() {\n return evaluateSnapshotGetter(this as object, target, key, getterFn);\n },\n enumerable: true,\n configurable: true, // required so Object.freeze can make it non-configurable\n });\n }\n}\n\n/**\n * Recursively creates a frozen snapshot from a tracked (proxied) sub-tree.\n *\n * Each node checks its version-stamped cache first (O(1) hit). On a miss,\n * it builds a new frozen object by recursing into child internals (tracked\n * sub-trees) and deep-cloning untracked nested objects. Unchanged children\n * return the same cached reference, achieving structural sharing -- the key\n * to efficient `Object.is` equality in selectors.\n *\n * For class instances, the prototype chain is preserved and class getters\n * are installed as lazy-memoizing accessors via `installMemoizedGetters`.\n */\nfunction createSnapshotRecursive<T extends object>(\n target: T,\n internal: StoreInternal,\n): T {\n // Cache hit: version unchanged → return the same frozen snapshot reference.\n const cached = snapshotCache.get(target);\n if (cached && cached[0] === internal.version) {\n return cached[1] as T;\n }\n\n let snap: Record<string | symbol, unknown> | unknown[];\n\n if (Array.isArray(target)) {\n snap = [];\n for (let i = 0; i < target.length; i++) {\n (snap as unknown[])[i] = snapshotValue(target[i], internal, String(i));\n }\n } else {\n // Preserve the prototype chain and install memoized getters on the snapshot.\n snap = Object.create(Object.getPrototypeOf(target));\n for (const key of Reflect.ownKeys(target)) {\n // Skip prototype getters — they re-evaluate via the preserved prototype.\n if (findGetterDescriptor(target, key)?.get) continue;\n\n const desc = Object.getOwnPropertyDescriptor(target, key);\n if (!desc || !('value' in desc)) continue;\n\n (snap as Record<string | symbol, unknown>)[key] = snapshotValue(\n desc.value,\n internal,\n key,\n );\n }\n\n // Install lazy-memoizing getters with cross-snapshot caching.\n installMemoizedGetters(snap as Record<string | symbol, unknown>, target);\n }\n\n Object.freeze(snap);\n // Cache AFTER populating + freezing. The reference is stable.\n snapshotCache.set(target, [internal.version, snap]);\n return snap as T;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Creates an immutable, deeply-frozen snapshot of the store proxy's current state.\n *\n * **Structural sharing:** unchanged sub-trees reuse the previous snapshot's\n * object reference, so `===` comparison can cheaply detect changes.\n *\n * **Version-cached:** calling `snapshot()` multiple times without intervening\n * mutations returns the identical snapshot object (O(1) cache hit).\n *\n * **Getters:** class getters are automatically memoized — they compute once\n * per snapshot and their results are stable across snapshots when dependencies\n * haven't changed (cross-snapshot memoization).\n *\n * @param proxyStore - A reactive proxy created by `createClassyStore()`.\n * @returns A deeply frozen plain-JS object (Snapshot<T>).\n */\nexport function snapshot<T extends object>(proxyStore: T): Snapshot<T> {\n const internal = getInternal(proxyStore);\n return createSnapshotRecursive(internal.target, internal) as Snapshot<T>;\n}\n"],"mappings":"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"}
@@ -88,12 +88,33 @@ function recordDep(internal, prop, value) {
88
88
  });
89
89
  }
90
90
  /**
91
+ * Record a computed getter dependency on the active tracker (if any).
92
+ * Unlike `recordDep`, this creates a `'computed'` dep entry that validates
93
+ * through the computed cache rather than re-executing the raw getter.
94
+ */
95
+ function recordComputedDep(internal, prop, value) {
96
+ const tracker = activeTracker();
97
+ if (!tracker) return;
98
+ if (tracker.internal !== internal) return;
99
+ tracker.deps.push({
100
+ kind: "computed",
101
+ internal,
102
+ prop,
103
+ value
104
+ });
105
+ }
106
+ /**
91
107
  * Check whether all dependencies from a previous computation are still valid.
92
108
  */
93
109
  function areDepsValid(deps) {
94
110
  for (const dep of deps) if (dep.kind === "version") {
95
111
  if (!Object.is(Reflect.get(dep.parentTarget, dep.prop), dep.internal.target)) return false;
96
112
  if (dep.internal.version !== dep.version) return false;
113
+ } else if (dep.kind === "computed") {
114
+ const cached = dep.internal.computedCache.get(dep.prop);
115
+ if (!cached) return false;
116
+ if (!areDepsValid(cached.deps)) return false;
117
+ if (!Object.is(cached.value, dep.value)) return false;
97
118
  } else {
98
119
  if (!Reflect.has(dep.target, dep.prop)) return false;
99
120
  if (!Object.is(Reflect.get(dep.target, dep.prop), dep.value)) return false;
@@ -200,7 +221,11 @@ function createStoreProxy(target, parent) {
200
221
  },
201
222
  get(_target, prop, receiver) {
202
223
  const getterDesc = findGetterDescriptor(_target, prop);
203
- if (getterDesc?.get) return evaluateComputed(internal, prop, getterDesc.get, receiver);
224
+ if (getterDesc?.get) {
225
+ const value = evaluateComputed(internal, prop, getterDesc.get, receiver);
226
+ recordComputedDep(internal, prop, value);
227
+ return value;
228
+ }
204
229
  const value = Reflect.get(_target, prop);
205
230
  if (typeof value === "function") {
206
231
  if (Array.isArray(_target)) return value.bind(receiver);
@@ -568,4 +593,4 @@ Object.defineProperty(exports, 'subscribe', {
568
593
  return subscribe;
569
594
  }
570
595
  });
571
- //# sourceMappingURL=snapshot-BKVFJLuo.cjs.map
596
+ //# sourceMappingURL=snapshot-CbVbxG7s.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-CbVbxG7s.cjs","names":[],"sources":["../src/utils/internal/internal.ts","../src/core/core.ts","../src/snapshot/snapshot.ts"],"sourcesContent":["const objectProto = Object.getPrototypeOf({});\n\n/**\n * Symbol that class instances can use to opt-in to deep proxying.\n * Classes with `static [PROXYABLE] = true` will be wrapped by the store proxy\n * just like plain objects, enabling nested reactivity.\n */\nexport const PROXYABLE = Symbol.for('@codebelt/classy-store.proxyable');\n\n/**\n * Returns `true` if `value` is a plain object (created via `{}` or `new Object()`).\n * Needed by `canProxy()` to distinguish plain objects (which should be deep-proxied)\n * from class instances, Date, Map, etc. (which should not).\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string | symbol, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === objectProto || proto === null;\n}\n\n/**\n * Central gatekeeper for the proxy system: determines which values get wrapped\n * in child proxies (`core.ts`) or deep-cloned in snapshots (`snapshot.ts`).\n *\n * Returns `true` for arrays, plain objects, and class instances that opt-in\n * via `static [PROXYABLE] = true`. Everything else (Date, Map, Set, class\n * instances without PROXYABLE, primitives) is left as-is.\n */\nexport function canProxy(value: unknown): value is object {\n if (typeof value !== 'object' || value === null) return false;\n if (Array.isArray(value)) return true;\n // Allow class instances that opt-in via the PROXYABLE symbol.\n const ctor = (value as {constructor?: unknown}).constructor;\n if (ctor && (ctor as Record<symbol, unknown>)[PROXYABLE]) {\n return true;\n }\n return isPlainObject(value);\n}\n\n/**\n * Walk the prototype chain of `target` looking for a getter descriptor for `prop`.\n * Returns the first (most-derived) getter found, or `undefined` if none exists.\n *\n * Used by `core.ts` (GET trap) to detect class getters for memoized evaluation,\n * and by `snapshot.ts` to skip own-property copying for getter-backed keys.\n */\nexport function findGetterDescriptor(\n target: object,\n prop: string | symbol,\n): PropertyDescriptor | undefined {\n let proto: object | null = target;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, prop);\n if (desc?.get) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n return undefined;\n}\n","import type {DepEntry, StoreInternal} from '../types';\nimport {canProxy, findGetterDescriptor} from '../utils/internal/internal';\n\n// ── Global state ──────────────────────────────────────────────────────────────\n\n/** Global version counter shared across all stores. */\nlet globalVersion = 0;\n\n/** Maps every store proxy → its internal bookkeeping. */\nconst internalsMap = new WeakMap<object, StoreInternal>();\n\n// ── Dependency tracking for computed getters ──────────────────────────────────\n\n/**\n * Stack of active dependency trackers. Each entry records which properties\n * a getter reads during evaluation. A stack (not a single variable) is needed\n * because getter A can read getter B, which pushes a second tracker.\n */\nconst trackerStack: {internal: StoreInternal; deps: DepEntry[]}[] = [];\n\n/** Returns the tracker currently recording deps, or `null` if none is active. */\nfunction activeTracker() {\n return trackerStack.length > 0 ? trackerStack[trackerStack.length - 1] : null;\n}\n\n/**\n * Record a dependency on the active tracker (if any).\n * Called from the GET trap whenever a non-getter, non-method property is read.\n */\nfunction recordDep(\n internal: StoreInternal,\n prop: string | symbol,\n value: unknown,\n): void {\n const tracker = activeTracker();\n if (!tracker) return;\n\n // Only record deps for reads on the SAME proxy that owns the getter.\n // Child-proxy reads don't need explicit tracking because child mutations\n // bubble up via version numbers on the parent's childInternals.\n if (tracker.internal !== internal) return;\n\n const childInternal = internal.childInternals.get(prop);\n if (childInternal) {\n tracker.deps.push({\n kind: 'version',\n internal: childInternal,\n version: childInternal.version,\n parentTarget: internal.target,\n prop,\n });\n } else {\n tracker.deps.push({kind: 'value', target: internal.target, prop, value});\n }\n}\n\n/**\n * Record a computed getter dependency on the active tracker (if any).\n * Unlike `recordDep`, this creates a `'computed'` dep entry that validates\n * through the computed cache rather than re-executing the raw getter.\n */\nfunction recordComputedDep(\n internal: StoreInternal,\n prop: string | symbol,\n value: unknown,\n): void {\n const tracker = activeTracker();\n if (!tracker) return;\n if (tracker.internal !== internal) return;\n\n tracker.deps.push({kind: 'computed', internal, prop, value});\n}\n\n/**\n * Check whether all dependencies from a previous computation are still valid.\n */\nfunction areDepsValid(deps: DepEntry[]): boolean {\n for (const dep of deps) {\n if (dep.kind === 'version') {\n // Verify the parent property still references the same child object.\n // If the property was replaced entirely (e.g., store.items = newArray),\n // the old child internal is detached and its version never changes.\n // This check catches that case.\n if (\n !Object.is(Reflect.get(dep.parentTarget, dep.prop), dep.internal.target)\n )\n return false;\n if (dep.internal.version !== dep.version) return false;\n } else if (dep.kind === 'computed') {\n // Validate through the computed cache — never re-execute the raw getter.\n const cached = dep.internal.computedCache.get(dep.prop);\n if (!cached) return false; // no cache → must recompute\n if (!areDepsValid(cached.deps)) return false; // nested deps changed\n if (!Object.is(cached.value, dep.value)) return false; // value changed\n } else {\n // Check that the property still exists — if it was deleted and\n // dep.value was `undefined`, Object.is(undefined, undefined) would\n // incorrectly pass without this guard.\n if (!Reflect.has(dep.target, dep.prop)) return false;\n if (!Object.is(Reflect.get(dep.target, dep.prop), dep.value))\n return false;\n }\n }\n return true;\n}\n\n/**\n * Evaluate a computed getter with memoization.\n * Returns the cached value if dependencies haven't changed.\n * Otherwise re-evaluates with dependency tracking and caches the result.\n */\nfunction evaluateComputed(\n internal: StoreInternal,\n prop: string | symbol,\n getterFn: () => unknown,\n receiver: object,\n): unknown {\n const cached = internal.computedCache.get(prop);\n if (cached && areDepsValid(cached.deps)) {\n return cached.value;\n }\n\n // Push a new tracker frame for this getter evaluation.\n const frame = {internal, deps: [] as DepEntry[]};\n trackerStack.push(frame);\n try {\n const value = getterFn.call(receiver);\n internal.computedCache.set(prop, {value, deps: frame.deps});\n return value;\n } finally {\n trackerStack.pop();\n }\n}\n\n// ── Public helpers ────────────────────────────────────────────────────────────\n\n/**\n * Retrieve the internal bookkeeping for a store proxy.\n * Throws if the object was not created with `createClassyStore()`.\n */\nexport function getInternal(proxy: object): StoreInternal {\n const internal = internalsMap.get(proxy);\n if (!internal)\n throw new Error('@codebelt/classy-store: object is not a store proxy');\n return internal;\n}\n\n// ── Notification batching ─────────────────────────────────────────────────────\n\n/**\n * Bump version from the mutated node up to the root, invalidate snapshot caches,\n * and schedule a single microtask notification (deduped at the root level).\n *\n * Version propagation is what enables structural sharing in snapshots: unchanged\n * children keep their old version, so the snapshot cache returns the same frozen ref.\n */\nfunction scheduleNotify(internal: StoreInternal): void {\n let current: StoreInternal | null = internal;\n while (current) {\n current.version = ++globalVersion;\n current = current.parent;\n }\n\n const root = getRoot(internal);\n if (!root.notifyScheduled) {\n root.notifyScheduled = true;\n queueMicrotask(() => {\n root.notifyScheduled = false;\n for (const listener of root.listeners) {\n listener();\n }\n });\n }\n}\n\n/** Walk the parent chain to find the root StoreInternal (the notification hub). */\nfunction getRoot(internal: StoreInternal): StoreInternal {\n let current = internal;\n while (current.parent) {\n current = current.parent;\n }\n return current;\n}\n\n// ── Create proxy (recursive) ──────────────────────────────────────────────────\n\n/**\n * Create an ES6 Proxy around `target` with SET/GET/DELETE traps.\n *\n * This is the core of the library's reactivity. The proxy intercepts:\n * - **SET**: compares old/new with `Object.is`, cleans up child proxies on replacement,\n * forwards the write, and schedules a batched notification.\n * - **GET**: detects class getters (memoized), binds methods to the proxy, lazily wraps\n * nested objects/arrays in child proxies, and records dependencies for computed getters.\n * - **DELETE**: cleans up child proxies and schedules notification.\n *\n * Nested objects are recursively wrapped on first access (lazy deep proxy).\n */\nfunction createStoreProxy<T extends object>(\n target: T,\n parent: StoreInternal | null,\n): T {\n const internal: StoreInternal = {\n target,\n version: ++globalVersion,\n listeners: new Set(),\n childProxies: new Map(),\n childInternals: new Map(),\n parent,\n notifyScheduled: false,\n computedCache: new Map(),\n };\n\n /** Cache for bound methods so we return the same reference each time. */\n const boundMethods = new Map<\n string | symbol,\n (...args: unknown[]) => unknown\n >();\n\n const proxy = new Proxy(target, {\n set(_target, prop, value, _receiver) {\n const oldValue = Reflect.get(_target, prop);\n if (Object.is(oldValue, value)) return true; // noop — same value\n\n // If the new value replaces a child proxy, clean it up.\n if (internal.childProxies.has(prop)) {\n internal.childProxies.delete(prop);\n internal.childInternals.delete(prop);\n }\n\n Reflect.set(_target, prop, value);\n scheduleNotify(internal);\n return true;\n },\n\n get(_target, prop, receiver) {\n // 1. Check for getters on the prototype chain (computed values).\n const getterDesc = findGetterDescriptor(_target, prop);\n if (getterDesc?.get) {\n // Memoized: evaluate with dependency tracking, return cached if deps unchanged.\n const value = evaluateComputed(\n internal,\n prop,\n getterDesc.get,\n receiver,\n );\n // Record as a computed dependency for any parent getter currently tracking.\n // Uses 'computed' dep kind so validation goes through the cache, not raw getter.\n recordComputedDep(internal, prop, value);\n return value;\n }\n\n const value = Reflect.get(_target, prop);\n\n // 2. Methods: bind to the proxy so `this.prop = x` goes through SET trap.\n if (typeof value === 'function') {\n // Array prototype methods should not be cached the same way as class methods.\n if (Array.isArray(_target)) {\n return value.bind(receiver);\n }\n const cached = boundMethods.get(prop);\n if (cached) return cached;\n const bound = (value as (...args: unknown[]) => unknown).bind(receiver);\n boundMethods.set(prop, bound);\n return bound;\n }\n\n // 3. Nested plain objects/arrays: lazy-wrap in a child proxy.\n if (canProxy(value)) {\n let childProxy = internal.childProxies.get(prop);\n if (!childProxy) {\n childProxy = createStoreProxy(value as object, internal);\n internal.childProxies.set(prop, childProxy);\n const childInternal = internalsMap.get(childProxy) as StoreInternal;\n internal.childInternals.set(prop, childInternal);\n }\n // Record dependency AFTER ensuring child proxy/internal exists.\n recordDep(internal, prop, value);\n return childProxy;\n }\n\n // 4. Primitives — record dependency and return.\n recordDep(internal, prop, value);\n return value;\n },\n\n deleteProperty(_target, prop) {\n if (internal.childProxies.has(prop)) {\n internal.childProxies.delete(prop);\n internal.childInternals.delete(prop);\n }\n const deleted = Reflect.deleteProperty(_target, prop);\n if (deleted) {\n scheduleNotify(internal);\n }\n return deleted;\n },\n });\n\n internalsMap.set(proxy, internal);\n return proxy;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Wraps a class instance in a reactive proxy.\n *\n * - Mutations (property writes, array push/splice, etc.) are intercepted and\n * batched into a single notification per microtask.\n * - Class getters are automatically memoized — they only recompute when a\n * dependency they read changes.\n * - Methods are automatically bound so `this` mutations go through the proxy.\n *\n * @param instance - A class instance (or plain object) to make reactive.\n * @returns The same object wrapped in a reactive Proxy.\n *\n * @example\n * ```ts\n * const myStore = createClassyStore(new MyClass());\n * ```\n */\nexport function createClassyStore<T extends object>(instance: T): T {\n return createStoreProxy(instance, null);\n}\n\n/**\n * Subscribe to store changes. The callback fires once per batched mutation\n * (coalesced via `queueMicrotask`), not once per individual property write.\n *\n * @param proxy - A reactive proxy created by `createClassyStore()`.\n * @param callback - Invoked after each batched mutation.\n * @returns An unsubscribe function. Call it to stop receiving notifications.\n */\nexport function subscribe(proxy: object, callback: () => void): () => void {\n const internal = getInternal(proxy);\n // Always subscribe on the root so notifications fire regardless of\n // whether the user subscribes to the root proxy or a child proxy.\n const root = getRoot(internal);\n root.listeners.add(callback);\n return () => {\n root.listeners.delete(callback);\n };\n}\n\n/**\n * Returns the current version number of a store proxy.\n *\n * Versions are monotonically increasing and bump on any mutation in the\n * store's subtree (child mutations propagate up to the root). Useful for\n * debugging, custom cache invalidation, or testing whether a store has changed.\n */\nexport function getVersion(proxy: object): number {\n return getInternal(proxy).version;\n}\n","import {getInternal} from '../core/core';\nimport type {Snapshot, StoreInternal} from '../types';\nimport {canProxy, findGetterDescriptor} from '../utils/internal/internal';\n\n// ── Caches ────────────────────────────────────────────────────────────────────\n\n/**\n * Version-stamped snapshot cache for tracked (proxied) sub-trees.\n * Key: raw target object → [version, frozen snapshot].\n */\nconst snapshotCache = new WeakMap<object, [version: number, snap: object]>();\n\n/**\n * Cache for untracked nested objects (never accessed through the proxy).\n * Key: the raw mutable object → frozen deep clone.\n * Safe because untracked objects can't be mutated through the proxy.\n */\nconst untrackedCache = new WeakMap<object, object>();\n\n/**\n * Per-snapshot, per-getter cache. Ensures repeated access to the same getter\n * on the same frozen snapshot returns the identical reference.\n */\nconst snapshotGetterCache = new WeakMap<\n object,\n Map<string | symbol, unknown>\n>();\n\n// ── Cross-snapshot getter memoization ─────────────────────────────────────────\n\n/**\n * A dependency recorded during getter execution.\n * prop: the property name read on `this`.\n * value: the value returned (reference).\n */\ntype GetterDep = {prop: string | symbol; value: unknown};\n\n/**\n * Cached result of a snapshot getter with the dependencies it read,\n * used for cross-snapshot memoization. When a new snapshot is created,\n * we check if the deps are still reference-equal (via structural sharing)\n * and return the cached result if so -- avoiding re-execution of the getter.\n */\ntype GetterMemoEntry = {deps: GetterDep[]; result: unknown};\n\n/**\n * Cross-snapshot getter memoization cache.\n * Keyed by raw class instance (target), maps each getter name to its last\n * deps + result. This enables getter result stability across snapshot\n * boundaries: if `this.items` hasn't changed between snapshots (same\n * reference via structural sharing), the getter returns the same result.\n */\nconst crossSnapshotMemo = new WeakMap<\n object,\n Map<string | symbol, GetterMemoEntry>\n>();\n\n/**\n * Run a getter against the snapshot with dependency tracking.\n *\n * Creates a lightweight Proxy that intercepts `this.prop` reads and records\n * which properties (and their values/references) the getter accessed. The\n * Proxy delegates to the real frozen snapshot for actual values, so getters\n * that read other getters trigger the memoized getter chain correctly.\n */\nfunction computeWithTracking(\n snap: object,\n getterFn: () => unknown,\n): {deps: GetterDep[]; result: unknown} {\n const deps: GetterDep[] = [];\n const readProps = new Set<string | symbol>();\n\n // Use an empty non-frozen target — we delegate everything to `snap`.\n const handler: ProxyHandler<object> = {\n get(_dummyTarget, prop, _receiver) {\n // Delegate to the real snapshot (receiver = snap so installed getters\n // run with `this = snap`, triggering their own memoization chain).\n const value = Reflect.get(snap, prop, snap);\n if (!readProps.has(prop)) {\n readProps.add(prop);\n deps.push({prop, value});\n }\n return value;\n },\n };\n\n const tracked = new Proxy({}, handler);\n const result = getterFn.call(tracked);\n return {deps, result};\n}\n\n/**\n * Check if all previously recorded deps still hold on the current snapshot.\n * For data properties this is a reference comparison (structural sharing\n * guarantees stable refs for unchanged sub-trees). For getter properties\n * this invokes the getter (which is itself memoized), then compares.\n */\nfunction areMemoizedDepsValid(currentSnap: object, deps: GetterDep[]): boolean {\n for (const dep of deps) {\n const currentValue = Reflect.get(currentSnap, dep.prop, currentSnap);\n if (!Object.is(currentValue, dep.value)) return false;\n }\n return true;\n}\n\n/**\n * Evaluate a snapshot getter with two layers of caching:\n *\n * 1. **Per-snapshot cache** — same getter on the same frozen snapshot always\n * returns the same reference.\n * 2. **Cross-snapshot memo** — if the properties the getter read last time are\n * structurally the same (reference equality via structural sharing), the\n * previous result is returned without re-running the getter body.\n */\nfunction evaluateSnapshotGetter(\n currentSnap: object,\n target: object,\n key: string | symbol,\n getterFn: () => unknown,\n): unknown {\n // ── Per-snapshot fast path ──\n const perSnapCache = snapshotGetterCache.get(currentSnap);\n if (perSnapCache?.has(key)) return perSnapCache.get(key);\n\n // ── Cross-snapshot memo ──\n let memoMap = crossSnapshotMemo.get(target);\n const prev = memoMap?.get(key);\n\n let result: unknown;\n\n if (prev && areMemoizedDepsValid(currentSnap, prev.deps)) {\n // Dependencies unchanged → reuse previous result.\n result = prev.result;\n } else {\n // Compute fresh with dep tracking.\n const computation = computeWithTracking(currentSnap, getterFn);\n result = computation.result;\n\n // Save cross-snapshot memo.\n if (!memoMap) {\n memoMap = new Map();\n crossSnapshotMemo.set(target, memoMap);\n }\n memoMap.set(key, {deps: computation.deps, result});\n }\n\n // Save per-snapshot cache.\n let cache = snapshotGetterCache.get(currentSnap);\n if (!cache) {\n cache = new Map();\n snapshotGetterCache.set(currentSnap, cache);\n }\n cache.set(key, result);\n\n return result;\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\n/**\n * Resolve a single property value into its snapshot equivalent.\n *\n * - If the key has a tracked child internal → recurse (version-cached, structural sharing).\n * - If the value is a nested plain object/array without tracking → deep-clone & freeze (cached by identity).\n * - Otherwise → return the value as-is (primitive, Date, Map, function, etc.).\n */\nfunction snapshotValue(\n value: unknown,\n parentInternal: StoreInternal,\n key: string | symbol,\n): unknown {\n const childInternal = parentInternal.childInternals.get(key);\n if (childInternal) {\n return createSnapshotRecursive(childInternal.target, childInternal);\n }\n if (canProxy(value)) {\n return deepFreezeClone(value as object);\n }\n return value;\n}\n\n/**\n * Deep-clone and freeze a plain object or array that is NOT tracked by a proxy.\n * Cached by raw object identity for structural sharing across snapshots.\n */\nfunction deepFreezeClone(value: object): object {\n const cached = untrackedCache.get(value);\n if (cached) return cached;\n\n let clone: Record<string | symbol, unknown> | unknown[];\n\n if (Array.isArray(value)) {\n clone = [];\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n (clone as unknown[])[i] = canProxy(item)\n ? deepFreezeClone(item as object)\n : item;\n }\n } else {\n clone = Object.create(Object.getPrototypeOf(value));\n for (const key of Reflect.ownKeys(value)) {\n const desc = Object.getOwnPropertyDescriptor(value, key);\n if (!desc || !('value' in desc)) continue;\n const item = desc.value;\n (clone as Record<string | symbol, unknown>)[key] = canProxy(item)\n ? deepFreezeClone(item as object)\n : item;\n }\n }\n\n Object.freeze(clone);\n untrackedCache.set(value, clone);\n return clone;\n}\n\n/**\n * Collect all getter descriptors from the prototype chain of `target`.\n * Returns an array of [propertyName, getterFunction] pairs.\n *\n * Only includes getters defined on the prototype (class getters), not on the\n * instance itself. When a getter is overridden in a subclass, the most-derived\n * version wins (we walk from the instance's direct prototype upward and skip\n * keys already seen).\n */\nfunction collectGetters(\n target: object,\n): Array<[string | symbol, () => unknown]> {\n const getters: Array<[string | symbol, () => unknown]> = [];\n const seen = new Set<string | symbol>();\n let proto: object | null = Object.getPrototypeOf(target);\n while (proto && proto !== Object.prototype) {\n for (const key of Reflect.ownKeys(proto)) {\n if (key === 'constructor') continue;\n if (seen.has(key)) continue; // most-derived version already collected\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get) {\n getters.push([key, desc.get]);\n seen.add(key);\n }\n }\n proto = Object.getPrototypeOf(proto);\n }\n return getters;\n}\n\n/**\n * Install lazy-memoizing getters on a snapshot object.\n *\n * Each getter uses cross-snapshot memoization:\n * - Tracks which `this` properties the getter reads on first evaluation.\n * - On subsequent snapshots, if those properties are structurally the same\n * (thanks to structural sharing), the previous result is returned.\n * - Within the same snapshot, repeated accesses always return the same ref.\n */\nfunction installMemoizedGetters(\n snap: Record<string | symbol, unknown>,\n target: object,\n): void {\n const getters = collectGetters(target);\n for (const [key, getterFn] of getters) {\n Object.defineProperty(snap, key, {\n get() {\n return evaluateSnapshotGetter(this as object, target, key, getterFn);\n },\n enumerable: true,\n configurable: true, // required so Object.freeze can make it non-configurable\n });\n }\n}\n\n/**\n * Recursively creates a frozen snapshot from a tracked (proxied) sub-tree.\n *\n * Each node checks its version-stamped cache first (O(1) hit). On a miss,\n * it builds a new frozen object by recursing into child internals (tracked\n * sub-trees) and deep-cloning untracked nested objects. Unchanged children\n * return the same cached reference, achieving structural sharing -- the key\n * to efficient `Object.is` equality in selectors.\n *\n * For class instances, the prototype chain is preserved and class getters\n * are installed as lazy-memoizing accessors via `installMemoizedGetters`.\n */\nfunction createSnapshotRecursive<T extends object>(\n target: T,\n internal: StoreInternal,\n): T {\n // Cache hit: version unchanged → return the same frozen snapshot reference.\n const cached = snapshotCache.get(target);\n if (cached && cached[0] === internal.version) {\n return cached[1] as T;\n }\n\n let snap: Record<string | symbol, unknown> | unknown[];\n\n if (Array.isArray(target)) {\n snap = [];\n for (let i = 0; i < target.length; i++) {\n (snap as unknown[])[i] = snapshotValue(target[i], internal, String(i));\n }\n } else {\n // Preserve the prototype chain and install memoized getters on the snapshot.\n snap = Object.create(Object.getPrototypeOf(target));\n for (const key of Reflect.ownKeys(target)) {\n // Skip prototype getters — they re-evaluate via the preserved prototype.\n if (findGetterDescriptor(target, key)?.get) continue;\n\n const desc = Object.getOwnPropertyDescriptor(target, key);\n if (!desc || !('value' in desc)) continue;\n\n (snap as Record<string | symbol, unknown>)[key] = snapshotValue(\n desc.value,\n internal,\n key,\n );\n }\n\n // Install lazy-memoizing getters with cross-snapshot caching.\n installMemoizedGetters(snap as Record<string | symbol, unknown>, target);\n }\n\n Object.freeze(snap);\n // Cache AFTER populating + freezing. The reference is stable.\n snapshotCache.set(target, [internal.version, snap]);\n return snap as T;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Creates an immutable, deeply-frozen snapshot of the store proxy's current state.\n *\n * **Structural sharing:** unchanged sub-trees reuse the previous snapshot's\n * object reference, so `===` comparison can cheaply detect changes.\n *\n * **Version-cached:** calling `snapshot()` multiple times without intervening\n * mutations returns the identical snapshot object (O(1) cache hit).\n *\n * **Getters:** class getters are automatically memoized — they compute once\n * per snapshot and their results are stable across snapshots when dependencies\n * haven't changed (cross-snapshot memoization).\n *\n * @param proxyStore - A reactive proxy created by `createClassyStore()`.\n * @returns A deeply frozen plain-JS object (Snapshot<T>).\n */\nexport function snapshot<T extends object>(proxyStore: T): Snapshot<T> {\n const internal = getInternal(proxyStore);\n return createSnapshotRecursive(internal.target, internal) as Snapshot<T>;\n}\n"],"mappings":"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"}
@@ -24,4 +24,4 @@ type SnapshotLeaf = Primitive | AnyFunction | Date | RegExp | Error | Map<unknow
24
24
  type Snapshot<T> = T extends SnapshotLeaf ? T : T extends Array<infer U> ? ReadonlyArray<Snapshot<U>> : T extends object ? { readonly [K in keyof T]: Snapshot<T[K]> } : T;
25
25
  //#endregion
26
26
  export { Snapshot as t };
27
- //# sourceMappingURL=types-vWYkF3tH.d.mts.map
27
+ //# sourceMappingURL=types-Cf8Fp7kA.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-vWYkF3tH.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAIK,SAAA;;AAAS;;;;KAOT,WAAA,OAAkB,IAAA;AAAI;;;;;;AAAA,KAQtB,YAAA,GACD,SAAA,GACA,WAAA,GACA,IAAA,GACA,MAAA,GACA,KAAA,GACA,GAAA,qBACA,GAAA,YACA,OAAA,oBACA,OAAA,WACA,OAAA;;;;;KAMQ,QAAA,MAAc,CAAA,SAAU,YAAA,GAChC,CAAA,GACA,CAAA,SAAU,KAAA,YACR,aAAA,CAAc,QAAA,CAAS,CAAA,KACvB,CAAA,yCACwB,CAAA,GAAI,QAAA,CAAS,CAAA,CAAE,CAAA,OACrC,CAAA"}
1
+ {"version":3,"file":"types-Cf8Fp7kA.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAIK,SAAA;;AAAS;;;;KAOT,WAAA,OAAkB,IAAA;AAAI;;;;;;AAAA,KAQtB,YAAA,GACD,SAAA,GACA,WAAA,GACA,IAAA,GACA,MAAA,GACA,KAAA,GACA,GAAA,qBACA,GAAA,YACA,OAAA,oBACA,OAAA,WACA,OAAA;;;;;KAMQ,QAAA,MAAc,CAAA,SAAU,YAAA,GAChC,CAAA,GACA,CAAA,SAAU,KAAA,YACR,aAAA,CAAc,QAAA,CAAS,CAAA,KACvB,CAAA,yCACwB,CAAA,GAAI,QAAA,CAAS,CAAA,CAAE,CAAA,OACrC,CAAA"}
@@ -1,6 +1,138 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_snapshot = require('../snapshot-BKVFJLuo.cjs');
2
+ const require_snapshot = require('../snapshot-CbVbxG7s.cjs');
3
+ const require_equality = require('../equality-Cz6riknL.cjs');
3
4
 
5
+ //#region src/utils/devtools/devtools.ts
6
+ /**
7
+ * Connect a store proxy to Redux DevTools for state inspection and time-travel debugging.
8
+ *
9
+ * Uses `subscribe()` + `snapshot()` to send state on each change.
10
+ * Listens for `DISPATCH` messages (`JUMP_TO_STATE`, `JUMP_TO_ACTION`) and applies
11
+ * the received state back to the store proxy, skipping getters and methods.
12
+ *
13
+ * @param proxyStore - A reactive proxy created by `createClassyStore()`.
14
+ * @param options - Optional configuration.
15
+ * @returns A dispose function that disconnects from DevTools and unsubscribes.
16
+ */
17
+ function devtools(proxyStore, options) {
18
+ const { name = "ClassyStore", enabled = true } = options ?? {};
19
+ if (!enabled || typeof window === "undefined" || !window.__REDUX_DEVTOOLS_EXTENSION__) return () => {};
20
+ const connection = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name });
21
+ connection.init(require_snapshot.snapshot(proxyStore));
22
+ let isTimeTraveling = false;
23
+ const unsubscribeFromStore = require_snapshot.subscribe(proxyStore, () => {
24
+ if (isTimeTraveling) return;
25
+ connection.send({ type: "STORE_UPDATE" }, require_snapshot.snapshot(proxyStore));
26
+ });
27
+ const devToolsUnsub = connection.subscribe((message) => {
28
+ if (message.type === "DISPATCH" && message.state) {
29
+ const payloadType = message.payload?.type;
30
+ if (payloadType === "JUMP_TO_STATE" || payloadType === "JUMP_TO_ACTION") try {
31
+ const newState = JSON.parse(message.state);
32
+ isTimeTraveling = true;
33
+ for (const key of Object.keys(newState)) {
34
+ if (require_snapshot.findGetterDescriptor(proxyStore, key)?.get) continue;
35
+ if (typeof proxyStore[key] === "function") continue;
36
+ proxyStore[key] = newState[key];
37
+ }
38
+ queueMicrotask(() => {
39
+ isTimeTraveling = false;
40
+ });
41
+ } catch {
42
+ isTimeTraveling = false;
43
+ }
44
+ }
45
+ });
46
+ return () => {
47
+ unsubscribeFromStore();
48
+ if (typeof devToolsUnsub === "function") devToolsUnsub();
49
+ else if (devToolsUnsub && typeof devToolsUnsub.unsubscribe === "function") devToolsUnsub.unsubscribe();
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/utils/history/history.ts
55
+ /**
56
+ * Add undo/redo capability to a store proxy via a snapshot stack.
57
+ *
58
+ * Captures a snapshot on each mutation and maintains a history array with a
59
+ * pointer. `undo()` and `redo()` apply previous/next snapshots back to the
60
+ * store proxy, skipping getters and methods.
61
+ *
62
+ * @param proxyStore - A reactive proxy created by `createClassyStore()`.
63
+ * @param options - Optional configuration (e.g., history limit).
64
+ * @returns A `HistoryHandle` with undo/redo controls and a dispose function.
65
+ */
66
+ function withHistory(proxyStore, options) {
67
+ const limit = options?.limit ?? 100;
68
+ const history = [require_snapshot.snapshot(proxyStore)];
69
+ let pointer = 0;
70
+ let paused = false;
71
+ /**
72
+ * Apply a snapshot's data properties back to the store proxy.
73
+ * Skips getters and methods — same pattern as `persist`.
74
+ */
75
+ function applySnapshot(snap) {
76
+ const snapRecord = snap;
77
+ for (const key of Object.keys(snapRecord)) {
78
+ if (require_snapshot.findGetterDescriptor(proxyStore, key)?.get) continue;
79
+ if (typeof proxyStore[key] === "function") continue;
80
+ proxyStore[key] = snapRecord[key];
81
+ }
82
+ }
83
+ const unsubscribeFromStore = require_snapshot.subscribe(proxyStore, () => {
84
+ if (paused) return;
85
+ const snap = require_snapshot.snapshot(proxyStore);
86
+ if (pointer < history.length - 1) history.length = pointer + 1;
87
+ history.push(snap);
88
+ pointer = history.length - 1;
89
+ if (history.length > limit) {
90
+ history.shift();
91
+ pointer = history.length - 1;
92
+ }
93
+ });
94
+ return {
95
+ undo() {
96
+ if (pointer <= 0) return;
97
+ pointer--;
98
+ paused = true;
99
+ try {
100
+ applySnapshot(history[pointer]);
101
+ } finally {
102
+ paused = false;
103
+ }
104
+ },
105
+ redo() {
106
+ if (pointer >= history.length - 1) return;
107
+ pointer++;
108
+ paused = true;
109
+ try {
110
+ applySnapshot(history[pointer]);
111
+ } finally {
112
+ paused = false;
113
+ }
114
+ },
115
+ get canUndo() {
116
+ return pointer > 0;
117
+ },
118
+ get canRedo() {
119
+ return pointer < history.length - 1;
120
+ },
121
+ pause() {
122
+ paused = true;
123
+ },
124
+ resume() {
125
+ paused = false;
126
+ },
127
+ dispose() {
128
+ unsubscribeFromStore();
129
+ history.length = 0;
130
+ pointer = 0;
131
+ }
132
+ };
133
+ }
134
+
135
+ //#endregion
4
136
  //#region src/utils/persist/persist.ts
5
137
  /** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */
6
138
  function isTransform(entry) {
@@ -22,7 +154,7 @@ function resolveProperties(proxyStore, properties) {
22
154
  const snap = require_snapshot.snapshot(proxyStore);
23
155
  const result = [];
24
156
  for (const key of Object.keys(snap)) {
25
- if (require_snapshot.findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get) continue;
157
+ if (require_snapshot.findGetterDescriptor(proxyStore, key)?.get) continue;
26
158
  if (typeof proxyStore[key] === "function") continue;
27
159
  result.push({ key });
28
160
  }
@@ -68,6 +200,7 @@ function persist(proxyStore, options) {
68
200
  for (const prop of resolvedProps) if (prop.transform) transformMap.set(prop.key, prop.transform);
69
201
  const propKeys = resolvedProps.map((p) => p.key);
70
202
  let disposed = false;
203
+ let hydrating = false;
71
204
  let debounceTimer = null;
72
205
  let hydratedFlag = false;
73
206
  let expiredFlag = false;
@@ -102,7 +235,7 @@ function persist(proxyStore, options) {
102
235
  }
103
236
  /** Schedule a debounced write (or write immediately if debounce is 0). */
104
237
  function scheduleWrite() {
105
- if (disposed) return;
238
+ if (disposed || hydrating) return;
106
239
  if (debounceMs <= 0) {
107
240
  writeToStorage();
108
241
  return;
@@ -124,7 +257,7 @@ function persist(proxyStore, options) {
124
257
  } catch {
125
258
  return;
126
259
  }
127
- if (!envelope || typeof envelope !== "object" || typeof envelope.state !== "object") return;
260
+ if (!envelope || typeof envelope !== "object" || envelope.state === null || typeof envelope.state !== "object") return;
128
261
  if (typeof envelope.expiresAt === "number" && Date.now() >= envelope.expiresAt) {
129
262
  expiredFlag = true;
130
263
  if (clearOnExpire) storage.removeItem(name);
@@ -141,6 +274,7 @@ function persist(proxyStore, options) {
141
274
  for (const key of propKeys) currentState[key] = currentSnap[key];
142
275
  let merged;
143
276
  if (typeof merge === "function") merged = merge(state, currentState);
277
+ else if (merge === "replace") merged = state;
144
278
  else merged = {
145
279
  ...currentState,
146
280
  ...state
@@ -150,7 +284,12 @@ function persist(proxyStore, options) {
150
284
  /** Read from storage and apply to the store. */
151
285
  async function hydrateFromStorage() {
152
286
  const raw = await storage.getItem(name);
153
- if (raw !== null) applyPersistedState(raw);
287
+ if (raw !== null) {
288
+ hydrating = true;
289
+ applyPersistedState(raw);
290
+ await new Promise((r) => queueMicrotask(r));
291
+ hydrating = false;
292
+ }
154
293
  }
155
294
  const shouldSyncTabs = syncTabsOption !== void 0 ? syncTabsOption : isLocalStorage(storage);
156
295
  /** Handler for `window.storage` events. */
@@ -196,9 +335,11 @@ function persist(proxyStore, options) {
196
335
  await writeToStorage();
197
336
  },
198
337
  async clear() {
338
+ if (disposed) return;
199
339
  await storage.removeItem(name);
200
340
  },
201
341
  async rehydrate() {
342
+ expiredFlag = false;
202
343
  await hydrateFromStorage();
203
344
  if (!hydratedFlag) {
204
345
  hydratedFlag = true;
@@ -209,5 +350,35 @@ function persist(proxyStore, options) {
209
350
  }
210
351
 
211
352
  //#endregion
353
+ //#region src/utils/subscribe-key/subscribe-key.ts
354
+ /**
355
+ * Subscribe to changes on a single property of a store proxy.
356
+ *
357
+ * Wraps `subscribe()` + `snapshot()` and compares `snapshot()[key]` with the
358
+ * previous value via `Object.is()`. The callback fires only when the watched
359
+ * property actually changes.
360
+ *
361
+ * @param proxyStore - A reactive proxy created by `createClassyStore()`.
362
+ * @param key - The property key to watch.
363
+ * @param callback - Called with `(value, previousValue)` when the property changes.
364
+ * @returns An unsubscribe function.
365
+ */
366
+ function subscribeKey(proxyStore, key, callback) {
367
+ let previousValue = require_snapshot.snapshot(proxyStore)[key];
368
+ return require_snapshot.subscribe(proxyStore, () => {
369
+ const currentValue = require_snapshot.snapshot(proxyStore)[key];
370
+ if (!Object.is(currentValue, previousValue)) {
371
+ const prev = previousValue;
372
+ previousValue = currentValue;
373
+ callback(currentValue, prev);
374
+ }
375
+ });
376
+ }
377
+
378
+ //#endregion
379
+ exports.devtools = devtools;
212
380
  exports.persist = persist;
381
+ exports.shallowEqual = require_equality.shallowEqual;
382
+ exports.subscribeKey = subscribeKey;
383
+ exports.withHistory = withHistory;
213
384
  //# sourceMappingURL=index.cjs.map