@codebelt/classy-store 0.0.2 → 0.1.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/dist/index.cjs +8 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -10
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +15 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +8 -8
- package/dist/index.mjs.map +1 -1
- package/dist/react/react.cjs +1 -1
- package/dist/react/react.mjs +1 -1
- package/dist/{snapshot-CR8nA2Ob.cjs → snapshot-BKVFJLuo.cjs} +24 -19
- package/dist/snapshot-BKVFJLuo.cjs.map +1 -0
- package/dist/{snapshot-C8JDLu8L.mjs → snapshot-P0QPV1ER.mjs} +19 -14
- package/dist/snapshot-P0QPV1ER.mjs.map +1 -0
- package/dist/utils/index.cjs +2 -2
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +2 -2
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/snapshot-C8JDLu8L.mjs.map +0 -1
- package/dist/snapshot-CR8nA2Ob.cjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-BKVFJLuo.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 * 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 {\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 return evaluateComputed(internal, prop, getterDesc.get, receiver);\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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"}
|
|
@@ -125,7 +125,7 @@ function evaluateComputed(internal, prop, getterFn, receiver) {
|
|
|
125
125
|
}
|
|
126
126
|
/**
|
|
127
127
|
* Retrieve the internal bookkeeping for a store proxy.
|
|
128
|
-
* Throws if the object was not created with `
|
|
128
|
+
* Throws if the object was not created with `createClassyStore()`.
|
|
129
129
|
*/
|
|
130
130
|
function getInternal(proxy) {
|
|
131
131
|
const internal = internalsMap.get(proxy);
|
|
@@ -247,15 +247,20 @@ function createStoreProxy(target, parent) {
|
|
|
247
247
|
*
|
|
248
248
|
* @param instance - A class instance (or plain object) to make reactive.
|
|
249
249
|
* @returns The same object wrapped in a reactive Proxy.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* const myStore = createClassyStore(new MyClass());
|
|
254
|
+
* ```
|
|
250
255
|
*/
|
|
251
|
-
function
|
|
256
|
+
function createClassyStore(instance) {
|
|
252
257
|
return createStoreProxy(instance, null);
|
|
253
258
|
}
|
|
254
259
|
/**
|
|
255
260
|
* Subscribe to store changes. The callback fires once per batched mutation
|
|
256
261
|
* (coalesced via `queueMicrotask`), not once per individual property write.
|
|
257
262
|
*
|
|
258
|
-
* @param proxy - A reactive proxy created by `
|
|
263
|
+
* @param proxy - A reactive proxy created by `createClassyStore()`.
|
|
259
264
|
* @param callback - Invoked after each batched mutation.
|
|
260
265
|
* @returns An unsubscribe function. Call it to stop receiving notifications.
|
|
261
266
|
*/
|
|
@@ -283,7 +288,7 @@ function getVersion(proxy) {
|
|
|
283
288
|
* Version-stamped snapshot cache for tracked (proxied) sub-trees.
|
|
284
289
|
* Key: raw target object → [version, frozen snapshot].
|
|
285
290
|
*/
|
|
286
|
-
const
|
|
291
|
+
const snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
287
292
|
/**
|
|
288
293
|
* Cache for untracked nested objects (never accessed through the proxy).
|
|
289
294
|
* Key: the raw mutable object → frozen deep clone.
|
|
@@ -302,7 +307,7 @@ const snapshotGetterCache = /* @__PURE__ */ new WeakMap();
|
|
|
302
307
|
* boundaries: if `this.items` hasn't changed between snapshots (same
|
|
303
308
|
* reference via structural sharing), the getter returns the same result.
|
|
304
309
|
*/
|
|
305
|
-
const
|
|
310
|
+
const crossSnapshotMemo = /* @__PURE__ */ new WeakMap();
|
|
306
311
|
/**
|
|
307
312
|
* Run a getter against the snapshot with dependency tracking.
|
|
308
313
|
*
|
|
@@ -336,7 +341,7 @@ function computeWithTracking(snap, getterFn) {
|
|
|
336
341
|
* guarantees stable refs for unchanged sub-trees). For getter properties
|
|
337
342
|
* this invokes the getter (which is itself memoized), then compares.
|
|
338
343
|
*/
|
|
339
|
-
function
|
|
344
|
+
function areMemoizedDepsValid(currentSnap, deps) {
|
|
340
345
|
for (const dep of deps) {
|
|
341
346
|
const currentValue = Reflect.get(currentSnap, dep.prop, currentSnap);
|
|
342
347
|
if (!Object.is(currentValue, dep.value)) return false;
|
|
@@ -355,16 +360,16 @@ function areMemoedDepsValid(currentSnap, deps) {
|
|
|
355
360
|
function evaluateSnapshotGetter(currentSnap, target, key, getterFn) {
|
|
356
361
|
const perSnapCache = snapshotGetterCache.get(currentSnap);
|
|
357
362
|
if (perSnapCache?.has(key)) return perSnapCache.get(key);
|
|
358
|
-
let memoMap =
|
|
363
|
+
let memoMap = crossSnapshotMemo.get(target);
|
|
359
364
|
const prev = memoMap?.get(key);
|
|
360
365
|
let result;
|
|
361
|
-
if (prev &&
|
|
366
|
+
if (prev && areMemoizedDepsValid(currentSnap, prev.deps)) result = prev.result;
|
|
362
367
|
else {
|
|
363
368
|
const computation = computeWithTracking(currentSnap, getterFn);
|
|
364
369
|
result = computation.result;
|
|
365
370
|
if (!memoMap) {
|
|
366
371
|
memoMap = /* @__PURE__ */ new Map();
|
|
367
|
-
|
|
372
|
+
crossSnapshotMemo.set(target, memoMap);
|
|
368
373
|
}
|
|
369
374
|
memoMap.set(key, {
|
|
370
375
|
deps: computation.deps,
|
|
@@ -478,7 +483,7 @@ function installMemoizedGetters(snap, target) {
|
|
|
478
483
|
* are installed as lazy-memoizing accessors via `installMemoizedGetters`.
|
|
479
484
|
*/
|
|
480
485
|
function createSnapshotRecursive(target, internal) {
|
|
481
|
-
const cached =
|
|
486
|
+
const cached = snapshotCache.get(target);
|
|
482
487
|
if (cached && cached[0] === internal.version) return cached[1];
|
|
483
488
|
let snap;
|
|
484
489
|
if (Array.isArray(target)) {
|
|
@@ -495,7 +500,7 @@ function createSnapshotRecursive(target, internal) {
|
|
|
495
500
|
installMemoizedGetters(snap, target);
|
|
496
501
|
}
|
|
497
502
|
Object.freeze(snap);
|
|
498
|
-
|
|
503
|
+
snapshotCache.set(target, [internal.version, snap]);
|
|
499
504
|
return snap;
|
|
500
505
|
}
|
|
501
506
|
/**
|
|
@@ -511,7 +516,7 @@ function createSnapshotRecursive(target, internal) {
|
|
|
511
516
|
* per snapshot and their results are stable across snapshots when dependencies
|
|
512
517
|
* haven't changed (cross-snapshot memoization).
|
|
513
518
|
*
|
|
514
|
-
* @param proxyStore - A reactive proxy created by `
|
|
519
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
515
520
|
* @returns A deeply frozen plain-JS object (Snapshot<T>).
|
|
516
521
|
*/
|
|
517
522
|
function snapshot(proxyStore) {
|
|
@@ -520,5 +525,5 @@ function snapshot(proxyStore) {
|
|
|
520
525
|
}
|
|
521
526
|
|
|
522
527
|
//#endregion
|
|
523
|
-
export { subscribe as a,
|
|
524
|
-
//# sourceMappingURL=snapshot-
|
|
528
|
+
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-P0QPV1ER.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 * 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 {\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 return evaluateComputed(internal, prop, getterDesc.get, receiver);\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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"}
|
package/dist/utils/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_snapshot = require('../snapshot-
|
|
2
|
+
const require_snapshot = require('../snapshot-BKVFJLuo.cjs');
|
|
3
3
|
|
|
4
4
|
//#region src/utils/persist/persist.ts
|
|
5
5
|
/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */
|
|
@@ -54,7 +54,7 @@ function getDefaultStorage() {
|
|
|
54
54
|
* On init (or manual rehydrate), reads from storage and applies the state back
|
|
55
55
|
* to the store proxy.
|
|
56
56
|
*
|
|
57
|
-
* @param proxyStore - A reactive proxy created by `
|
|
57
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
58
58
|
* @param options - Persistence configuration.
|
|
59
59
|
* @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).
|
|
60
60
|
*/
|
package/dist/utils/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["snapshot","findGetterDescriptor","subscribe"],"sources":["../../src/utils/persist/persist.ts"],"sourcesContent":["import {subscribe} from '../../core/core';\nimport {snapshot} from '../../snapshot/snapshot';\nimport {findGetterDescriptor} from '../internal/internal';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Storage adapter interface. Compatible with `localStorage`, `sessionStorage`,\n * `AsyncStorage`, `localForage`, or any custom implementation.\n *\n * Methods may return synchronously or asynchronously — `persist` handles both.\n */\nexport type StorageAdapter = {\n getItem: (name: string) => string | null | Promise<string | null>;\n setItem: (name: string, value: string) => void | Promise<void>;\n removeItem: (name: string) => void | Promise<void>;\n};\n\n/**\n * Describes a per-property serialization transform.\n *\n * Use this when a property's value is not JSON-serializable (Date, ReactiveMap, etc.)\n * and needs custom conversion to/from a storable format.\n */\nexport type PropertyTransform<T extends object> = {\n /** The property key on the store class. */\n key: keyof T;\n\n /** Transform the value BEFORE saving to storage (after snapshot, before JSON.stringify). */\n serialize: (value: T[keyof T]) => unknown;\n\n /** Transform the value AFTER loading from storage (after JSON.parse, before applying to store). */\n deserialize: (stored: unknown) => T[keyof T];\n};\n\n/**\n * Options for `persist()`.\n */\nexport type PersistOptions<T extends object> = {\n /** Unique storage key. Required. */\n name: string;\n\n /**\n * Storage adapter. Defaults to `globalThis.localStorage`.\n * Any object with getItem/setItem/removeItem (sync or async).\n * Works with: localStorage, sessionStorage, AsyncStorage, localForage, etc.\n */\n storage?: StorageAdapter;\n\n /**\n * Which properties to persist.\n * Each entry is either a plain key (string) or a transform descriptor.\n * Defaults to all own enumerable data properties.\n *\n * **Getters and methods are always excluded.** Class getters (e.g., `get remaining()`)\n * are computed/derived values — they are not source-of-truth state. They recompute\n * automatically from the persisted data properties when accessed. Persisting a getter\n * result would be redundant and could produce stale values on restore.\n */\n properties?: Array<keyof T | PropertyTransform<T>>;\n\n /**\n * Debounce writes to storage (milliseconds).\n * Multiple rapid mutations coalesce into one write.\n * Default: 0 (write after every batched mutation).\n */\n debounce?: number;\n\n /**\n * Schema version number. Stored alongside the data.\n * When the stored version differs from this value, `migrate` is called.\n * Default: 0.\n */\n version?: number;\n\n /**\n * Migration function. Called when the stored version does not match `version`.\n * Receives the raw parsed state and the old version number.\n * Must return the state in the current shape.\n */\n migrate?: (\n persistedState: Record<string, unknown>,\n oldVersion: number,\n ) => Record<string, unknown>;\n\n /**\n * How to merge persisted state with current store state during hydration.\n *\n * - `'shallow'` (default): persisted values overwrite current values one key at a time.\n * Properties not in storage keep their current value.\n * - `'replace'`: same behavior as `'shallow'` for flat stores — only stored keys are assigned.\n * For nested objects, the entire object is replaced rather than merged.\n * - Custom function: receives `(persistedState, currentState)` and returns merged state.\n * Enables deep merge or any custom logic.\n *\n * This matters when the store adds new properties that don't exist in old\n * persisted data. `'shallow'` preserves the new defaults.\n */\n merge?:\n | 'shallow'\n | 'replace'\n | ((\n persisted: Record<string, unknown>,\n current: Record<string, unknown>,\n ) => Record<string, unknown>);\n\n /**\n * If true, do NOT hydrate automatically on init.\n * You must call `handle.rehydrate()` manually (e.g., in a `useEffect` for SSR).\n * Default: false.\n */\n skipHydration?: boolean;\n\n /**\n * Sync state across browser tabs via the `window.storage` event.\n * When another tab writes to the same storage key, this tab automatically\n * re-hydrates from the new value.\n *\n * Only works with `localStorage` (storage events don't fire for sessionStorage\n * or async adapters).\n *\n * Default: `true` when storage is `localStorage`, `false` otherwise.\n */\n syncTabs?: boolean;\n\n /**\n * Time-to-live in milliseconds. After this duration, stored data is\n * considered expired and skipped during hydration. The TTL resets on\n * every write (active sessions stay fresh as long as mutations happen).\n */\n expireIn?: number;\n\n /**\n * When `true`, automatically remove the storage key if data is found\n * expired during hydration. Default: `false` (expired data is skipped\n * but left in storage).\n */\n clearOnExpire?: boolean;\n};\n\n/**\n * Handle returned by `persist()`. Provides control over the persist lifecycle.\n */\nexport type PersistHandle = {\n /** Stop persisting and clean up (unsubscribe + cancel pending debounce + remove storage event listener). */\n unsubscribe: () => void;\n\n /** Promise that resolves when initial hydration from storage is complete. */\n hydrated: Promise<void>;\n\n /** Whether hydration from storage has completed. */\n isHydrated: boolean;\n\n /** Manually trigger a write to storage (bypasses debounce). */\n save: () => Promise<void>;\n\n /** Clear this store's persisted data from storage. */\n clear: () => Promise<void>;\n\n /** Manually re-hydrate the store from storage. */\n rehydrate: () => Promise<void>;\n\n /** True if the last hydration found expired data (requires `expireIn`). */\n isExpired: boolean;\n};\n\n// ── Storage envelope ─────────────────────────────────────────────────────────\n\n/** Internal shape of the JSON stored in storage. */\ntype PersistEnvelope = {\n version: number;\n state: Record<string, unknown>;\n expiresAt?: number;\n};\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */\nfunction isTransform<T extends object>(\n entry: keyof T | PropertyTransform<T>,\n): entry is PropertyTransform<T> {\n return (\n typeof entry === 'object' &&\n entry !== null &&\n 'key' in entry &&\n 'serialize' in entry\n );\n}\n\n/**\n * Build the normalized list of property keys and their optional transforms.\n * If `properties` is not provided, defaults to all own enumerable data properties\n * of the store (excluding getters and methods).\n */\nfunction resolveProperties<T extends object>(\n proxyStore: T,\n properties?: Array<keyof T | PropertyTransform<T>>,\n): Array<{key: string; transform?: PropertyTransform<T>}> {\n if (properties) {\n return properties.map((entry) => {\n if (isTransform(entry)) {\n return {key: entry.key as string, transform: entry};\n }\n return {key: entry as string};\n });\n }\n\n // Default: all own enumerable keys that are not getters or methods.\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const result: Array<{key: string}> = [];\n for (const key of Object.keys(snap)) {\n // Skip getters (they live on the prototype, but snapshot installs them).\n // We check the original store's target for getter descriptors.\n if (findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get)\n continue;\n // Skip functions (methods).\n const value = (proxyStore as Record<string, unknown>)[key];\n if (typeof value === 'function') continue;\n result.push({key});\n }\n return result;\n}\n\n/**\n * Detect if the given storage adapter is `globalThis.localStorage`.\n */\nfunction isLocalStorage(storage: StorageAdapter): boolean {\n try {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined' &&\n storage === (globalThis.localStorage as unknown as StorageAdapter)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Get the default storage adapter (`localStorage`), or `undefined` if unavailable.\n */\nfunction getDefaultStorage(): StorageAdapter | undefined {\n try {\n if (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined'\n ) {\n return globalThis.localStorage as unknown as StorageAdapter;\n }\n } catch {\n // SSR or restricted environment — no localStorage.\n }\n return undefined;\n}\n\n// ── Main implementation ──────────────────────────────────────────────────────\n\n/**\n * Persist store state to a storage adapter.\n *\n * Subscribes to store mutations and writes the selected properties to storage\n * (with optional per-property transforms, debouncing, and versioned envelopes).\n * On init (or manual rehydrate), reads from storage and applies the state back\n * to the store proxy.\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n * @param options - Persistence configuration.\n * @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).\n */\nexport function persist<T extends object>(\n proxyStore: T,\n options: PersistOptions<T>,\n): PersistHandle {\n const {\n name,\n properties: propertiesOption,\n debounce: debounceMs = 0,\n version = 0,\n migrate,\n merge = 'shallow',\n skipHydration = false,\n syncTabs: syncTabsOption,\n expireIn,\n clearOnExpire = false,\n } = options;\n\n const maybeStorage = options.storage ?? getDefaultStorage();\n if (!maybeStorage) {\n throw new Error(\n '@codebelt/classy-store: persist() requires a storage adapter. ' +\n 'No localStorage found — provide a `storage` option.',\n );\n }\n const storage: StorageAdapter = maybeStorage;\n\n const resolvedProps = resolveProperties(proxyStore, propertiesOption);\n\n // Build a map of key → transform for fast lookup during save/restore.\n const transformMap = new Map<string, PropertyTransform<T>>();\n for (const prop of resolvedProps) {\n if (prop.transform) {\n transformMap.set(prop.key, prop.transform);\n }\n }\n\n const propKeys = resolvedProps.map((p) => p.key);\n\n // ── State ────────────────────────────────────────────────────────────────\n\n let disposed = false;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let hydratedFlag = false;\n let expiredFlag = false;\n\n // Hydration promise + resolver.\n let resolveHydrated: () => void;\n let rejectHydrated: (error: unknown) => void;\n const hydratedPromise = new Promise<void>((resolve, reject) => {\n resolveHydrated = resolve;\n rejectHydrated = reject;\n });\n\n // ── Save logic ───────────────────────────────────────────────────────────\n\n /** Serialize the current store state into a JSON string (versioned envelope). */\n function serializeState(): string {\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const state: Record<string, unknown> = {};\n\n for (const key of propKeys) {\n let value = snap[key];\n const transform = transformMap.get(key);\n if (transform) {\n value = transform.serialize(value as T[keyof T]);\n }\n state[key] = value;\n }\n\n const envelope: PersistEnvelope = {version, state};\n if (expireIn != null) {\n envelope.expiresAt = Date.now() + expireIn;\n }\n return JSON.stringify(envelope);\n }\n\n /** Write the current state to storage. */\n async function writeToStorage(): Promise<void> {\n if (disposed) return;\n const json = serializeState();\n await storage.setItem(name, json);\n }\n\n /** Schedule a debounced write (or write immediately if debounce is 0). */\n function scheduleWrite(): void {\n if (disposed) return;\n\n if (debounceMs <= 0) {\n void writeToStorage();\n return;\n }\n\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void writeToStorage();\n }, debounceMs);\n }\n\n // ── Restore logic ────────────────────────────────────────────────────────\n\n /**\n * Parse a raw JSON string from storage, apply migration and transforms,\n * and merge the result into the store proxy.\n */\n function applyPersistedState(raw: string): void {\n let envelope: PersistEnvelope;\n try {\n envelope = JSON.parse(raw) as PersistEnvelope;\n } catch {\n // Corrupted data — skip.\n return;\n }\n\n if (\n !envelope ||\n typeof envelope !== 'object' ||\n typeof envelope.state !== 'object'\n ) {\n return;\n }\n\n // Expiry check — skip hydration if data has expired.\n if (\n typeof envelope.expiresAt === 'number' &&\n Date.now() >= envelope.expiresAt\n ) {\n expiredFlag = true;\n if (clearOnExpire) void storage.removeItem(name);\n return;\n }\n\n let {state} = envelope;\n\n // Version migration.\n if (migrate && envelope.version !== version) {\n state = migrate(state, envelope.version);\n }\n\n // Per-property deserialize transforms.\n for (const key of Object.keys(state)) {\n const transform = transformMap.get(key);\n if (transform) {\n state[key] = transform.deserialize(state[key]);\n }\n }\n\n // Build current state for merge.\n const currentSnap = snapshot(proxyStore) as Record<string, unknown>;\n const currentState: Record<string, unknown> = {};\n for (const key of propKeys) {\n currentState[key] = currentSnap[key];\n }\n\n // Merge strategy.\n let merged: Record<string, unknown>;\n if (typeof merge === 'function') {\n merged = merge(state, currentState);\n } else {\n // Both 'shallow' and 'replace' assign persisted keys onto the store.\n // The difference is conceptual for nested objects, but at this level\n // both just assign the persisted value per key.\n merged = {...currentState, ...state};\n }\n\n // Apply to store proxy (goes through SET traps → reactivity).\n for (const key of propKeys) {\n if (key in merged) {\n (proxyStore as Record<string, unknown>)[key] = merged[key];\n }\n }\n }\n\n /** Read from storage and apply to the store. */\n async function hydrateFromStorage(): Promise<void> {\n const raw = await storage.getItem(name);\n if (raw !== null) {\n applyPersistedState(raw);\n }\n }\n\n // ── Cross-tab sync ───────────────────────────────────────────────────────\n\n const shouldSyncTabs =\n syncTabsOption !== undefined ? syncTabsOption : isLocalStorage(storage);\n\n /** Handler for `window.storage` events. */\n function onStorageEvent(event: StorageEvent): void {\n if (disposed) return;\n if (event.key !== name) return;\n if (event.newValue === null) return; // cleared\n applyPersistedState(event.newValue);\n }\n\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.addEventListener === 'function'\n ) {\n globalThis.addEventListener('storage', onStorageEvent);\n }\n\n // ── Subscribe to store mutations ─────────────────────────────────────────\n\n const unsubscribeFromStore = subscribe(proxyStore, scheduleWrite);\n\n // ── Kick off initial hydration ───────────────────────────────────────────\n\n if (!skipHydration) {\n void hydrateFromStorage()\n .then(() => {\n hydratedFlag = true;\n resolveHydrated();\n })\n .catch((error) => {\n hydratedFlag = true;\n rejectHydrated(error);\n });\n } else {\n // When hydration is skipped, the promise is left pending until\n // the user calls handle.rehydrate() manually.\n }\n\n // ── Build handle ─────────────────────────────────────────────────────────\n\n const handle: PersistHandle = {\n get isHydrated() {\n return hydratedFlag;\n },\n\n get isExpired() {\n return expiredFlag;\n },\n\n hydrated: hydratedPromise,\n\n unsubscribe() {\n if (disposed) return;\n disposed = true;\n\n // Cancel pending debounce.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n\n // Unsubscribe from store mutations.\n unsubscribeFromStore();\n\n // Remove cross-tab sync listener.\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.removeEventListener === 'function'\n ) {\n globalThis.removeEventListener('storage', onStorageEvent);\n }\n },\n\n async save() {\n if (disposed) return;\n // Cancel pending debounce and write immediately.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n await writeToStorage();\n },\n\n async clear() {\n await storage.removeItem(name);\n },\n\n async rehydrate() {\n await hydrateFromStorage();\n if (!hydratedFlag) {\n hydratedFlag = true;\n resolveHydrated();\n }\n },\n };\n\n return handle;\n}\n"],"mappings":";;;;;AAkLA,SAAS,YACP,OAC+B;AAC/B,QACE,OAAO,UAAU,YACjB,UAAU,QACV,SAAS,SACT,eAAe;;;;;;;AASnB,SAAS,kBACP,YACA,YACwD;AACxD,KAAI,WACF,QAAO,WAAW,KAAK,UAAU;AAC/B,MAAI,YAAY,MAAM,CACpB,QAAO;GAAC,KAAK,MAAM;GAAe,WAAW;GAAM;AAErD,SAAO,EAAC,KAAK,OAAgB;GAC7B;CAIJ,MAAM,OAAOA,0BAAS,WAAW;CACjC,MAAM,SAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AAGnC,MAAIC,sCAAqB,OAAO,eAAe,WAAW,EAAE,IAAI,EAAE,IAChE;AAGF,MAAI,OADW,WAAuC,SACjC,WAAY;AACjC,SAAO,KAAK,EAAC,KAAI,CAAC;;AAEpB,QAAO;;;;;AAMT,SAAS,eAAe,SAAkC;AACxD,KAAI;AACF,SACE,OAAO,eAAe,eACtB,OAAO,WAAW,iBAAiB,eACnC,YAAa,WAAW;SAEpB;AACN,SAAO;;;;;;AAOX,SAAS,oBAAgD;AACvD,KAAI;AACF,MACE,OAAO,eAAe,eACtB,OAAO,WAAW,iBAAiB,YAEnC,QAAO,WAAW;SAEd;;;;;;;;;;;;;;AAoBV,SAAgB,QACd,YACA,SACe;CACf,MAAM,EACJ,MACA,YAAY,kBACZ,UAAU,aAAa,GACvB,UAAU,GACV,SACA,QAAQ,WACR,gBAAgB,OAChB,UAAU,gBACV,UACA,gBAAgB,UACd;CAEJ,MAAM,eAAe,QAAQ,WAAW,mBAAmB;AAC3D,KAAI,CAAC,aACH,OAAM,IAAI,MACR,oHAED;CAEH,MAAM,UAA0B;CAEhC,MAAM,gBAAgB,kBAAkB,YAAY,iBAAiB;CAGrE,MAAM,+BAAe,IAAI,KAAmC;AAC5D,MAAK,MAAM,QAAQ,cACjB,KAAI,KAAK,UACP,cAAa,IAAI,KAAK,KAAK,KAAK,UAAU;CAI9C,MAAM,WAAW,cAAc,KAAK,MAAM,EAAE,IAAI;CAIhD,IAAI,WAAW;CACf,IAAI,gBAAsD;CAC1D,IAAI,eAAe;CACnB,IAAI,cAAc;CAGlB,IAAI;CACJ,IAAI;CACJ,MAAM,kBAAkB,IAAI,SAAe,SAAS,WAAW;AAC7D,oBAAkB;AAClB,mBAAiB;GACjB;;CAKF,SAAS,iBAAyB;EAChC,MAAM,OAAOD,0BAAS,WAAW;EACjC,MAAM,QAAiC,EAAE;AAEzC,OAAK,MAAM,OAAO,UAAU;GAC1B,IAAI,QAAQ,KAAK;GACjB,MAAM,YAAY,aAAa,IAAI,IAAI;AACvC,OAAI,UACF,SAAQ,UAAU,UAAU,MAAoB;AAElD,SAAM,OAAO;;EAGf,MAAM,WAA4B;GAAC;GAAS;GAAM;AAClD,MAAI,YAAY,KACd,UAAS,YAAY,KAAK,KAAK,GAAG;AAEpC,SAAO,KAAK,UAAU,SAAS;;;CAIjC,eAAe,iBAAgC;AAC7C,MAAI,SAAU;EACd,MAAM,OAAO,gBAAgB;AAC7B,QAAM,QAAQ,QAAQ,MAAM,KAAK;;;CAInC,SAAS,gBAAsB;AAC7B,MAAI,SAAU;AAEd,MAAI,cAAc,GAAG;AACnB,GAAK,gBAAgB;AACrB;;AAGF,MAAI,kBAAkB,KACpB,cAAa,cAAc;AAE7B,kBAAgB,iBAAiB;AAC/B,mBAAgB;AAChB,GAAK,gBAAgB;KACpB,WAAW;;;;;;CAShB,SAAS,oBAAoB,KAAmB;EAC9C,IAAI;AACJ,MAAI;AACF,cAAW,KAAK,MAAM,IAAI;UACpB;AAEN;;AAGF,MACE,CAAC,YACD,OAAO,aAAa,YACpB,OAAO,SAAS,UAAU,SAE1B;AAIF,MACE,OAAO,SAAS,cAAc,YAC9B,KAAK,KAAK,IAAI,SAAS,WACvB;AACA,iBAAc;AACd,OAAI,cAAe,CAAK,QAAQ,WAAW,KAAK;AAChD;;EAGF,IAAI,EAAC,UAAS;AAGd,MAAI,WAAW,SAAS,YAAY,QAClC,SAAQ,QAAQ,OAAO,SAAS,QAAQ;AAI1C,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,EAAE;GACpC,MAAM,YAAY,aAAa,IAAI,IAAI;AACvC,OAAI,UACF,OAAM,OAAO,UAAU,YAAY,MAAM,KAAK;;EAKlD,MAAM,cAAcA,0BAAS,WAAW;EACxC,MAAM,eAAwC,EAAE;AAChD,OAAK,MAAM,OAAO,SAChB,cAAa,OAAO,YAAY;EAIlC,IAAI;AACJ,MAAI,OAAO,UAAU,WACnB,UAAS,MAAM,OAAO,aAAa;MAKnC,UAAS;GAAC,GAAG;GAAc,GAAG;GAAM;AAItC,OAAK,MAAM,OAAO,SAChB,KAAI,OAAO,OACT,CAAC,WAAuC,OAAO,OAAO;;;CAM5D,eAAe,qBAAoC;EACjD,MAAM,MAAM,MAAM,QAAQ,QAAQ,KAAK;AACvC,MAAI,QAAQ,KACV,qBAAoB,IAAI;;CAM5B,MAAM,iBACJ,mBAAmB,SAAY,iBAAiB,eAAe,QAAQ;;CAGzE,SAAS,eAAe,OAA2B;AACjD,MAAI,SAAU;AACd,MAAI,MAAM,QAAQ,KAAM;AACxB,MAAI,MAAM,aAAa,KAAM;AAC7B,sBAAoB,MAAM,SAAS;;AAGrC,KACE,kBACA,OAAO,eAAe,eACtB,OAAO,WAAW,qBAAqB,WAEvC,YAAW,iBAAiB,WAAW,eAAe;CAKxD,MAAM,uBAAuBE,2BAAU,YAAY,cAAc;AAIjE,KAAI,CAAC,cACH,CAAK,oBAAoB,CACtB,WAAW;AACV,iBAAe;AACf,mBAAiB;GACjB,CACD,OAAO,UAAU;AAChB,iBAAe;AACf,iBAAe,MAAM;GACrB;AAiEN,QAzD8B;EAC5B,IAAI,aAAa;AACf,UAAO;;EAGT,IAAI,YAAY;AACd,UAAO;;EAGT,UAAU;EAEV,cAAc;AACZ,OAAI,SAAU;AACd,cAAW;AAGX,OAAI,kBAAkB,MAAM;AAC1B,iBAAa,cAAc;AAC3B,oBAAgB;;AAIlB,yBAAsB;AAGtB,OACE,kBACA,OAAO,eAAe,eACtB,OAAO,WAAW,wBAAwB,WAE1C,YAAW,oBAAoB,WAAW,eAAe;;EAI7D,MAAM,OAAO;AACX,OAAI,SAAU;AAEd,OAAI,kBAAkB,MAAM;AAC1B,iBAAa,cAAc;AAC3B,oBAAgB;;AAElB,SAAM,gBAAgB;;EAGxB,MAAM,QAAQ;AACZ,SAAM,QAAQ,WAAW,KAAK;;EAGhC,MAAM,YAAY;AAChB,SAAM,oBAAoB;AAC1B,OAAI,CAAC,cAAc;AACjB,mBAAe;AACf,qBAAiB;;;EAGtB"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["snapshot","findGetterDescriptor","subscribe"],"sources":["../../src/utils/persist/persist.ts"],"sourcesContent":["import {subscribe} from '../../core/core';\nimport {snapshot} from '../../snapshot/snapshot';\nimport {findGetterDescriptor} from '../internal/internal';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Storage adapter interface. Compatible with `localStorage`, `sessionStorage`,\n * `AsyncStorage`, `localForage`, or any custom implementation.\n *\n * Methods may return synchronously or asynchronously — `persist` handles both.\n */\nexport type StorageAdapter = {\n getItem: (name: string) => string | null | Promise<string | null>;\n setItem: (name: string, value: string) => void | Promise<void>;\n removeItem: (name: string) => void | Promise<void>;\n};\n\n/**\n * Describes a per-property serialization transform.\n *\n * Use this when a property's value is not JSON-serializable (Date, ReactiveMap, etc.)\n * and needs custom conversion to/from a storable format.\n */\nexport type PropertyTransform<T extends object> = {\n /** The property key on the store class. */\n key: keyof T;\n\n /** Transform the value BEFORE saving to storage (after snapshot, before JSON.stringify). */\n serialize: (value: T[keyof T]) => unknown;\n\n /** Transform the value AFTER loading from storage (after JSON.parse, before applying to store). */\n deserialize: (stored: unknown) => T[keyof T];\n};\n\n/**\n * Options for `persist()`.\n */\nexport type PersistOptions<T extends object> = {\n /** Unique storage key. Required. */\n name: string;\n\n /**\n * Storage adapter. Defaults to `globalThis.localStorage`.\n * Any object with getItem/setItem/removeItem (sync or async).\n * Works with: localStorage, sessionStorage, AsyncStorage, localForage, etc.\n */\n storage?: StorageAdapter;\n\n /**\n * Which properties to persist.\n * Each entry is either a plain key (string) or a transform descriptor.\n * Defaults to all own enumerable data properties.\n *\n * **Getters and methods are always excluded.** Class getters (e.g., `get remaining()`)\n * are computed/derived values — they are not source-of-truth state. They recompute\n * automatically from the persisted data properties when accessed. Persisting a getter\n * result would be redundant and could produce stale values on restore.\n */\n properties?: Array<keyof T | PropertyTransform<T>>;\n\n /**\n * Debounce writes to storage (milliseconds).\n * Multiple rapid mutations coalesce into one write.\n * Default: 0 (write after every batched mutation).\n */\n debounce?: number;\n\n /**\n * Schema version number. Stored alongside the data.\n * When the stored version differs from this value, `migrate` is called.\n * Default: 0.\n */\n version?: number;\n\n /**\n * Migration function. Called when the stored version does not match `version`.\n * Receives the raw parsed state and the old version number.\n * Must return the state in the current shape.\n */\n migrate?: (\n persistedState: Record<string, unknown>,\n oldVersion: number,\n ) => Record<string, unknown>;\n\n /**\n * How to merge persisted state with current store state during hydration.\n *\n * - `'shallow'` (default): persisted values overwrite current values one key at a time.\n * Properties not in storage keep their current value.\n * - `'replace'`: same behavior as `'shallow'` for flat stores — only stored keys are assigned.\n * For nested objects, the entire object is replaced rather than merged.\n * - Custom function: receives `(persistedState, currentState)` and returns merged state.\n * Enables deep merge or any custom logic.\n *\n * This matters when the store adds new properties that don't exist in old\n * persisted data. `'shallow'` preserves the new defaults.\n */\n merge?:\n | 'shallow'\n | 'replace'\n | ((\n persisted: Record<string, unknown>,\n current: Record<string, unknown>,\n ) => Record<string, unknown>);\n\n /**\n * If true, do NOT hydrate automatically on init.\n * You must call `handle.rehydrate()` manually (e.g., in a `useEffect` for SSR).\n * Default: false.\n */\n skipHydration?: boolean;\n\n /**\n * Sync state across browser tabs via the `window.storage` event.\n * When another tab writes to the same storage key, this tab automatically\n * re-hydrates from the new value.\n *\n * Only works with `localStorage` (storage events don't fire for sessionStorage\n * or async adapters).\n *\n * Default: `true` when storage is `localStorage`, `false` otherwise.\n */\n syncTabs?: boolean;\n\n /**\n * Time-to-live in milliseconds. After this duration, stored data is\n * considered expired and skipped during hydration. The TTL resets on\n * every write (active sessions stay fresh as long as mutations happen).\n */\n expireIn?: number;\n\n /**\n * When `true`, automatically remove the storage key if data is found\n * expired during hydration. Default: `false` (expired data is skipped\n * but left in storage).\n */\n clearOnExpire?: boolean;\n};\n\n/**\n * Handle returned by `persist()`. Provides control over the persist lifecycle.\n */\nexport type PersistHandle = {\n /** Stop persisting and clean up (unsubscribe + cancel pending debounce + remove storage event listener). */\n unsubscribe: () => void;\n\n /** Promise that resolves when initial hydration from storage is complete. */\n hydrated: Promise<void>;\n\n /** Whether hydration from storage has completed. */\n isHydrated: boolean;\n\n /** Manually trigger a write to storage (bypasses debounce). */\n save: () => Promise<void>;\n\n /** Clear this store's persisted data from storage. */\n clear: () => Promise<void>;\n\n /** Manually re-hydrate the store from storage. */\n rehydrate: () => Promise<void>;\n\n /** True if the last hydration found expired data (requires `expireIn`). */\n isExpired: boolean;\n};\n\n// ── Storage envelope ─────────────────────────────────────────────────────────\n\n/** Internal shape of the JSON stored in storage. */\ntype PersistEnvelope = {\n version: number;\n state: Record<string, unknown>;\n expiresAt?: number;\n};\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */\nfunction isTransform<T extends object>(\n entry: keyof T | PropertyTransform<T>,\n): entry is PropertyTransform<T> {\n return (\n typeof entry === 'object' &&\n entry !== null &&\n 'key' in entry &&\n 'serialize' in entry\n );\n}\n\n/**\n * Build the normalized list of property keys and their optional transforms.\n * If `properties` is not provided, defaults to all own enumerable data properties\n * of the store (excluding getters and methods).\n */\nfunction resolveProperties<T extends object>(\n proxyStore: T,\n properties?: Array<keyof T | PropertyTransform<T>>,\n): Array<{key: string; transform?: PropertyTransform<T>}> {\n if (properties) {\n return properties.map((entry) => {\n if (isTransform(entry)) {\n return {key: entry.key as string, transform: entry};\n }\n return {key: entry as string};\n });\n }\n\n // Default: all own enumerable keys that are not getters or methods.\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const result: Array<{key: string}> = [];\n for (const key of Object.keys(snap)) {\n // Skip getters (they live on the prototype, but snapshot installs them).\n // We check the original store's target for getter descriptors.\n if (findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get)\n continue;\n // Skip functions (methods).\n const value = (proxyStore as Record<string, unknown>)[key];\n if (typeof value === 'function') continue;\n result.push({key});\n }\n return result;\n}\n\n/**\n * Detect if the given storage adapter is `globalThis.localStorage`.\n */\nfunction isLocalStorage(storage: StorageAdapter): boolean {\n try {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined' &&\n storage === (globalThis.localStorage as unknown as StorageAdapter)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Get the default storage adapter (`localStorage`), or `undefined` if unavailable.\n */\nfunction getDefaultStorage(): StorageAdapter | undefined {\n try {\n if (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined'\n ) {\n return globalThis.localStorage as unknown as StorageAdapter;\n }\n } catch {\n // SSR or restricted environment — no localStorage.\n }\n return undefined;\n}\n\n// ── Main implementation ──────────────────────────────────────────────────────\n\n/**\n * Persist store state to a storage adapter.\n *\n * Subscribes to store mutations and writes the selected properties to storage\n * (with optional per-property transforms, debouncing, and versioned envelopes).\n * On init (or manual rehydrate), reads from storage and applies the state back\n * to the store proxy.\n *\n * @param proxyStore - A reactive proxy created by `createClassyStore()`.\n * @param options - Persistence configuration.\n * @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).\n */\nexport function persist<T extends object>(\n proxyStore: T,\n options: PersistOptions<T>,\n): PersistHandle {\n const {\n name,\n properties: propertiesOption,\n debounce: debounceMs = 0,\n version = 0,\n migrate,\n merge = 'shallow',\n skipHydration = false,\n syncTabs: syncTabsOption,\n expireIn,\n clearOnExpire = false,\n } = options;\n\n const maybeStorage = options.storage ?? getDefaultStorage();\n if (!maybeStorage) {\n throw new Error(\n '@codebelt/classy-store: persist() requires a storage adapter. ' +\n 'No localStorage found — provide a `storage` option.',\n );\n }\n const storage: StorageAdapter = maybeStorage;\n\n const resolvedProps = resolveProperties(proxyStore, propertiesOption);\n\n // Build a map of key → transform for fast lookup during save/restore.\n const transformMap = new Map<string, PropertyTransform<T>>();\n for (const prop of resolvedProps) {\n if (prop.transform) {\n transformMap.set(prop.key, prop.transform);\n }\n }\n\n const propKeys = resolvedProps.map((p) => p.key);\n\n // ── State ────────────────────────────────────────────────────────────────\n\n let disposed = false;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let hydratedFlag = false;\n let expiredFlag = false;\n\n // Hydration promise + resolver.\n let resolveHydrated: () => void;\n let rejectHydrated: (error: unknown) => void;\n const hydratedPromise = new Promise<void>((resolve, reject) => {\n resolveHydrated = resolve;\n rejectHydrated = reject;\n });\n\n // ── Save logic ───────────────────────────────────────────────────────────\n\n /** Serialize the current store state into a JSON string (versioned envelope). */\n function serializeState(): string {\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const state: Record<string, unknown> = {};\n\n for (const key of propKeys) {\n let value = snap[key];\n const transform = transformMap.get(key);\n if (transform) {\n value = transform.serialize(value as T[keyof T]);\n }\n state[key] = value;\n }\n\n const envelope: PersistEnvelope = {version, state};\n if (expireIn != null) {\n envelope.expiresAt = Date.now() + expireIn;\n }\n return JSON.stringify(envelope);\n }\n\n /** Write the current state to storage. */\n async function writeToStorage(): Promise<void> {\n if (disposed) return;\n const json = serializeState();\n await storage.setItem(name, json);\n }\n\n /** Schedule a debounced write (or write immediately if debounce is 0). */\n function scheduleWrite(): void {\n if (disposed) return;\n\n if (debounceMs <= 0) {\n void writeToStorage();\n return;\n }\n\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void writeToStorage();\n }, debounceMs);\n }\n\n // ── Restore logic ────────────────────────────────────────────────────────\n\n /**\n * Parse a raw JSON string from storage, apply migration and transforms,\n * and merge the result into the store proxy.\n */\n function applyPersistedState(raw: string): void {\n let envelope: PersistEnvelope;\n try {\n envelope = JSON.parse(raw) as PersistEnvelope;\n } catch {\n // Corrupted data — skip.\n return;\n }\n\n if (\n !envelope ||\n typeof envelope !== 'object' ||\n typeof envelope.state !== 'object'\n ) {\n return;\n }\n\n // Expiry check — skip hydration if data has expired.\n if (\n typeof envelope.expiresAt === 'number' &&\n Date.now() >= envelope.expiresAt\n ) {\n expiredFlag = true;\n if (clearOnExpire) void storage.removeItem(name);\n return;\n }\n\n let {state} = envelope;\n\n // Version migration.\n if (migrate && envelope.version !== version) {\n state = migrate(state, envelope.version);\n }\n\n // Per-property deserialize transforms.\n for (const key of Object.keys(state)) {\n const transform = transformMap.get(key);\n if (transform) {\n state[key] = transform.deserialize(state[key]);\n }\n }\n\n // Build current state for merge.\n const currentSnap = snapshot(proxyStore) as Record<string, unknown>;\n const currentState: Record<string, unknown> = {};\n for (const key of propKeys) {\n currentState[key] = currentSnap[key];\n }\n\n // Merge strategy.\n let merged: Record<string, unknown>;\n if (typeof merge === 'function') {\n merged = merge(state, currentState);\n } else {\n // Both 'shallow' and 'replace' assign persisted keys onto the store.\n // The difference is conceptual for nested objects, but at this level\n // both just assign the persisted value per key.\n merged = {...currentState, ...state};\n }\n\n // Apply to store proxy (goes through SET traps → reactivity).\n for (const key of propKeys) {\n if (key in merged) {\n (proxyStore as Record<string, unknown>)[key] = merged[key];\n }\n }\n }\n\n /** Read from storage and apply to the store. */\n async function hydrateFromStorage(): Promise<void> {\n const raw = await storage.getItem(name);\n if (raw !== null) {\n applyPersistedState(raw);\n }\n }\n\n // ── Cross-tab sync ───────────────────────────────────────────────────────\n\n const shouldSyncTabs =\n syncTabsOption !== undefined ? syncTabsOption : isLocalStorage(storage);\n\n /** Handler for `window.storage` events. */\n function onStorageEvent(event: StorageEvent): void {\n if (disposed) return;\n if (event.key !== name) return;\n if (event.newValue === null) return; // cleared\n applyPersistedState(event.newValue);\n }\n\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.addEventListener === 'function'\n ) {\n globalThis.addEventListener('storage', onStorageEvent);\n }\n\n // ── Subscribe to store mutations ─────────────────────────────────────────\n\n const unsubscribeFromStore = subscribe(proxyStore, scheduleWrite);\n\n // ── Kick off initial hydration ───────────────────────────────────────────\n\n if (!skipHydration) {\n void hydrateFromStorage()\n .then(() => {\n hydratedFlag = true;\n resolveHydrated();\n })\n .catch((error) => {\n hydratedFlag = true;\n rejectHydrated(error);\n });\n } else {\n // When hydration is skipped, the promise is left pending until\n // the user calls handle.rehydrate() manually.\n }\n\n // ── Build handle ─────────────────────────────────────────────────────────\n\n const handle: PersistHandle = {\n get isHydrated() {\n return hydratedFlag;\n },\n\n get isExpired() {\n return expiredFlag;\n },\n\n hydrated: hydratedPromise,\n\n unsubscribe() {\n if (disposed) return;\n disposed = true;\n\n // Cancel pending debounce.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n\n // Unsubscribe from store mutations.\n unsubscribeFromStore();\n\n // Remove cross-tab sync listener.\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.removeEventListener === 'function'\n ) {\n globalThis.removeEventListener('storage', onStorageEvent);\n }\n },\n\n async save() {\n if (disposed) return;\n // Cancel pending debounce and write immediately.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n await writeToStorage();\n },\n\n async clear() {\n await storage.removeItem(name);\n },\n\n async rehydrate() {\n await hydrateFromStorage();\n if (!hydratedFlag) {\n hydratedFlag = true;\n resolveHydrated();\n }\n },\n };\n\n return handle;\n}\n"],"mappings":";;;;;AAkLA,SAAS,YACP,OAC+B;AAC/B,QACE,OAAO,UAAU,YACjB,UAAU,QACV,SAAS,SACT,eAAe;;;;;;;AASnB,SAAS,kBACP,YACA,YACwD;AACxD,KAAI,WACF,QAAO,WAAW,KAAK,UAAU;AAC/B,MAAI,YAAY,MAAM,CACpB,QAAO;GAAC,KAAK,MAAM;GAAe,WAAW;GAAM;AAErD,SAAO,EAAC,KAAK,OAAgB;GAC7B;CAIJ,MAAM,OAAOA,0BAAS,WAAW;CACjC,MAAM,SAA+B,EAAE;AACvC,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AAGnC,MAAIC,sCAAqB,OAAO,eAAe,WAAW,EAAE,IAAI,EAAE,IAChE;AAGF,MAAI,OADW,WAAuC,SACjC,WAAY;AACjC,SAAO,KAAK,EAAC,KAAI,CAAC;;AAEpB,QAAO;;;;;AAMT,SAAS,eAAe,SAAkC;AACxD,KAAI;AACF,SACE,OAAO,eAAe,eACtB,OAAO,WAAW,iBAAiB,eACnC,YAAa,WAAW;SAEpB;AACN,SAAO;;;;;;AAOX,SAAS,oBAAgD;AACvD,KAAI;AACF,MACE,OAAO,eAAe,eACtB,OAAO,WAAW,iBAAiB,YAEnC,QAAO,WAAW;SAEd;;;;;;;;;;;;;;AAoBV,SAAgB,QACd,YACA,SACe;CACf,MAAM,EACJ,MACA,YAAY,kBACZ,UAAU,aAAa,GACvB,UAAU,GACV,SACA,QAAQ,WACR,gBAAgB,OAChB,UAAU,gBACV,UACA,gBAAgB,UACd;CAEJ,MAAM,eAAe,QAAQ,WAAW,mBAAmB;AAC3D,KAAI,CAAC,aACH,OAAM,IAAI,MACR,oHAED;CAEH,MAAM,UAA0B;CAEhC,MAAM,gBAAgB,kBAAkB,YAAY,iBAAiB;CAGrE,MAAM,+BAAe,IAAI,KAAmC;AAC5D,MAAK,MAAM,QAAQ,cACjB,KAAI,KAAK,UACP,cAAa,IAAI,KAAK,KAAK,KAAK,UAAU;CAI9C,MAAM,WAAW,cAAc,KAAK,MAAM,EAAE,IAAI;CAIhD,IAAI,WAAW;CACf,IAAI,gBAAsD;CAC1D,IAAI,eAAe;CACnB,IAAI,cAAc;CAGlB,IAAI;CACJ,IAAI;CACJ,MAAM,kBAAkB,IAAI,SAAe,SAAS,WAAW;AAC7D,oBAAkB;AAClB,mBAAiB;GACjB;;CAKF,SAAS,iBAAyB;EAChC,MAAM,OAAOD,0BAAS,WAAW;EACjC,MAAM,QAAiC,EAAE;AAEzC,OAAK,MAAM,OAAO,UAAU;GAC1B,IAAI,QAAQ,KAAK;GACjB,MAAM,YAAY,aAAa,IAAI,IAAI;AACvC,OAAI,UACF,SAAQ,UAAU,UAAU,MAAoB;AAElD,SAAM,OAAO;;EAGf,MAAM,WAA4B;GAAC;GAAS;GAAM;AAClD,MAAI,YAAY,KACd,UAAS,YAAY,KAAK,KAAK,GAAG;AAEpC,SAAO,KAAK,UAAU,SAAS;;;CAIjC,eAAe,iBAAgC;AAC7C,MAAI,SAAU;EACd,MAAM,OAAO,gBAAgB;AAC7B,QAAM,QAAQ,QAAQ,MAAM,KAAK;;;CAInC,SAAS,gBAAsB;AAC7B,MAAI,SAAU;AAEd,MAAI,cAAc,GAAG;AACnB,GAAK,gBAAgB;AACrB;;AAGF,MAAI,kBAAkB,KACpB,cAAa,cAAc;AAE7B,kBAAgB,iBAAiB;AAC/B,mBAAgB;AAChB,GAAK,gBAAgB;KACpB,WAAW;;;;;;CAShB,SAAS,oBAAoB,KAAmB;EAC9C,IAAI;AACJ,MAAI;AACF,cAAW,KAAK,MAAM,IAAI;UACpB;AAEN;;AAGF,MACE,CAAC,YACD,OAAO,aAAa,YACpB,OAAO,SAAS,UAAU,SAE1B;AAIF,MACE,OAAO,SAAS,cAAc,YAC9B,KAAK,KAAK,IAAI,SAAS,WACvB;AACA,iBAAc;AACd,OAAI,cAAe,CAAK,QAAQ,WAAW,KAAK;AAChD;;EAGF,IAAI,EAAC,UAAS;AAGd,MAAI,WAAW,SAAS,YAAY,QAClC,SAAQ,QAAQ,OAAO,SAAS,QAAQ;AAI1C,OAAK,MAAM,OAAO,OAAO,KAAK,MAAM,EAAE;GACpC,MAAM,YAAY,aAAa,IAAI,IAAI;AACvC,OAAI,UACF,OAAM,OAAO,UAAU,YAAY,MAAM,KAAK;;EAKlD,MAAM,cAAcA,0BAAS,WAAW;EACxC,MAAM,eAAwC,EAAE;AAChD,OAAK,MAAM,OAAO,SAChB,cAAa,OAAO,YAAY;EAIlC,IAAI;AACJ,MAAI,OAAO,UAAU,WACnB,UAAS,MAAM,OAAO,aAAa;MAKnC,UAAS;GAAC,GAAG;GAAc,GAAG;GAAM;AAItC,OAAK,MAAM,OAAO,SAChB,KAAI,OAAO,OACT,CAAC,WAAuC,OAAO,OAAO;;;CAM5D,eAAe,qBAAoC;EACjD,MAAM,MAAM,MAAM,QAAQ,QAAQ,KAAK;AACvC,MAAI,QAAQ,KACV,qBAAoB,IAAI;;CAM5B,MAAM,iBACJ,mBAAmB,SAAY,iBAAiB,eAAe,QAAQ;;CAGzE,SAAS,eAAe,OAA2B;AACjD,MAAI,SAAU;AACd,MAAI,MAAM,QAAQ,KAAM;AACxB,MAAI,MAAM,aAAa,KAAM;AAC7B,sBAAoB,MAAM,SAAS;;AAGrC,KACE,kBACA,OAAO,eAAe,eACtB,OAAO,WAAW,qBAAqB,WAEvC,YAAW,iBAAiB,WAAW,eAAe;CAKxD,MAAM,uBAAuBE,2BAAU,YAAY,cAAc;AAIjE,KAAI,CAAC,cACH,CAAK,oBAAoB,CACtB,WAAW;AACV,iBAAe;AACf,mBAAiB;GACjB,CACD,OAAO,UAAU;AAChB,iBAAe;AACf,iBAAe,MAAM;GACrB;AAiEN,QAzD8B;EAC5B,IAAI,aAAa;AACf,UAAO;;EAGT,IAAI,YAAY;AACd,UAAO;;EAGT,UAAU;EAEV,cAAc;AACZ,OAAI,SAAU;AACd,cAAW;AAGX,OAAI,kBAAkB,MAAM;AAC1B,iBAAa,cAAc;AAC3B,oBAAgB;;AAIlB,yBAAsB;AAGtB,OACE,kBACA,OAAO,eAAe,eACtB,OAAO,WAAW,wBAAwB,WAE1C,YAAW,oBAAoB,WAAW,eAAe;;EAI7D,MAAM,OAAO;AACX,OAAI,SAAU;AAEd,OAAI,kBAAkB,MAAM;AAC1B,iBAAa,cAAc;AAC3B,oBAAgB;;AAElB,SAAM,gBAAgB;;EAGxB,MAAM,QAAQ;AACZ,SAAM,QAAQ,WAAW,KAAK;;EAGhC,MAAM,YAAY;AAChB,SAAM,oBAAoB;AAC1B,OAAI,CAAC,cAAc;AACjB,mBAAe;AACf,qBAAiB;;;EAGtB"}
|
package/dist/utils/index.d.cts
CHANGED
|
@@ -125,7 +125,7 @@ type PersistHandle = {
|
|
|
125
125
|
* On init (or manual rehydrate), reads from storage and applies the state back
|
|
126
126
|
* to the store proxy.
|
|
127
127
|
*
|
|
128
|
-
* @param proxyStore - A reactive proxy created by `
|
|
128
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
129
129
|
* @param options - Persistence configuration.
|
|
130
130
|
* @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).
|
|
131
131
|
*/
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -125,7 +125,7 @@ type PersistHandle = {
|
|
|
125
125
|
* On init (or manual rehydrate), reads from storage and applies the state back
|
|
126
126
|
* to the store proxy.
|
|
127
127
|
*
|
|
128
|
-
* @param proxyStore - A reactive proxy created by `
|
|
128
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
129
129
|
* @param options - Persistence configuration.
|
|
130
130
|
* @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).
|
|
131
131
|
*/
|
package/dist/utils/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as subscribe, s as findGetterDescriptor, t as snapshot } from "../snapshot-
|
|
1
|
+
import { a as subscribe, s as findGetterDescriptor, t as snapshot } from "../snapshot-P0QPV1ER.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/utils/persist/persist.ts
|
|
4
4
|
/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */
|
|
@@ -53,7 +53,7 @@ function getDefaultStorage() {
|
|
|
53
53
|
* On init (or manual rehydrate), reads from storage and applies the state back
|
|
54
54
|
* to the store proxy.
|
|
55
55
|
*
|
|
56
|
-
* @param proxyStore - A reactive proxy created by `
|
|
56
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
57
57
|
* @param options - Persistence configuration.
|
|
58
58
|
* @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).
|
|
59
59
|
*/
|
package/dist/utils/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/persist/persist.ts"],"sourcesContent":["import {subscribe} from '../../core/core';\nimport {snapshot} from '../../snapshot/snapshot';\nimport {findGetterDescriptor} from '../internal/internal';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Storage adapter interface. Compatible with `localStorage`, `sessionStorage`,\n * `AsyncStorage`, `localForage`, or any custom implementation.\n *\n * Methods may return synchronously or asynchronously — `persist` handles both.\n */\nexport type StorageAdapter = {\n getItem: (name: string) => string | null | Promise<string | null>;\n setItem: (name: string, value: string) => void | Promise<void>;\n removeItem: (name: string) => void | Promise<void>;\n};\n\n/**\n * Describes a per-property serialization transform.\n *\n * Use this when a property's value is not JSON-serializable (Date, ReactiveMap, etc.)\n * and needs custom conversion to/from a storable format.\n */\nexport type PropertyTransform<T extends object> = {\n /** The property key on the store class. */\n key: keyof T;\n\n /** Transform the value BEFORE saving to storage (after snapshot, before JSON.stringify). */\n serialize: (value: T[keyof T]) => unknown;\n\n /** Transform the value AFTER loading from storage (after JSON.parse, before applying to store). */\n deserialize: (stored: unknown) => T[keyof T];\n};\n\n/**\n * Options for `persist()`.\n */\nexport type PersistOptions<T extends object> = {\n /** Unique storage key. Required. */\n name: string;\n\n /**\n * Storage adapter. Defaults to `globalThis.localStorage`.\n * Any object with getItem/setItem/removeItem (sync or async).\n * Works with: localStorage, sessionStorage, AsyncStorage, localForage, etc.\n */\n storage?: StorageAdapter;\n\n /**\n * Which properties to persist.\n * Each entry is either a plain key (string) or a transform descriptor.\n * Defaults to all own enumerable data properties.\n *\n * **Getters and methods are always excluded.** Class getters (e.g., `get remaining()`)\n * are computed/derived values — they are not source-of-truth state. They recompute\n * automatically from the persisted data properties when accessed. Persisting a getter\n * result would be redundant and could produce stale values on restore.\n */\n properties?: Array<keyof T | PropertyTransform<T>>;\n\n /**\n * Debounce writes to storage (milliseconds).\n * Multiple rapid mutations coalesce into one write.\n * Default: 0 (write after every batched mutation).\n */\n debounce?: number;\n\n /**\n * Schema version number. Stored alongside the data.\n * When the stored version differs from this value, `migrate` is called.\n * Default: 0.\n */\n version?: number;\n\n /**\n * Migration function. Called when the stored version does not match `version`.\n * Receives the raw parsed state and the old version number.\n * Must return the state in the current shape.\n */\n migrate?: (\n persistedState: Record<string, unknown>,\n oldVersion: number,\n ) => Record<string, unknown>;\n\n /**\n * How to merge persisted state with current store state during hydration.\n *\n * - `'shallow'` (default): persisted values overwrite current values one key at a time.\n * Properties not in storage keep their current value.\n * - `'replace'`: same behavior as `'shallow'` for flat stores — only stored keys are assigned.\n * For nested objects, the entire object is replaced rather than merged.\n * - Custom function: receives `(persistedState, currentState)` and returns merged state.\n * Enables deep merge or any custom logic.\n *\n * This matters when the store adds new properties that don't exist in old\n * persisted data. `'shallow'` preserves the new defaults.\n */\n merge?:\n | 'shallow'\n | 'replace'\n | ((\n persisted: Record<string, unknown>,\n current: Record<string, unknown>,\n ) => Record<string, unknown>);\n\n /**\n * If true, do NOT hydrate automatically on init.\n * You must call `handle.rehydrate()` manually (e.g., in a `useEffect` for SSR).\n * Default: false.\n */\n skipHydration?: boolean;\n\n /**\n * Sync state across browser tabs via the `window.storage` event.\n * When another tab writes to the same storage key, this tab automatically\n * re-hydrates from the new value.\n *\n * Only works with `localStorage` (storage events don't fire for sessionStorage\n * or async adapters).\n *\n * Default: `true` when storage is `localStorage`, `false` otherwise.\n */\n syncTabs?: boolean;\n\n /**\n * Time-to-live in milliseconds. After this duration, stored data is\n * considered expired and skipped during hydration. The TTL resets on\n * every write (active sessions stay fresh as long as mutations happen).\n */\n expireIn?: number;\n\n /**\n * When `true`, automatically remove the storage key if data is found\n * expired during hydration. Default: `false` (expired data is skipped\n * but left in storage).\n */\n clearOnExpire?: boolean;\n};\n\n/**\n * Handle returned by `persist()`. Provides control over the persist lifecycle.\n */\nexport type PersistHandle = {\n /** Stop persisting and clean up (unsubscribe + cancel pending debounce + remove storage event listener). */\n unsubscribe: () => void;\n\n /** Promise that resolves when initial hydration from storage is complete. */\n hydrated: Promise<void>;\n\n /** Whether hydration from storage has completed. */\n isHydrated: boolean;\n\n /** Manually trigger a write to storage (bypasses debounce). */\n save: () => Promise<void>;\n\n /** Clear this store's persisted data from storage. */\n clear: () => Promise<void>;\n\n /** Manually re-hydrate the store from storage. */\n rehydrate: () => Promise<void>;\n\n /** True if the last hydration found expired data (requires `expireIn`). */\n isExpired: boolean;\n};\n\n// ── Storage envelope ─────────────────────────────────────────────────────────\n\n/** Internal shape of the JSON stored in storage. */\ntype PersistEnvelope = {\n version: number;\n state: Record<string, unknown>;\n expiresAt?: number;\n};\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */\nfunction isTransform<T extends object>(\n entry: keyof T | PropertyTransform<T>,\n): entry is PropertyTransform<T> {\n return (\n typeof entry === 'object' &&\n entry !== null &&\n 'key' in entry &&\n 'serialize' in entry\n );\n}\n\n/**\n * Build the normalized list of property keys and their optional transforms.\n * If `properties` is not provided, defaults to all own enumerable data properties\n * of the store (excluding getters and methods).\n */\nfunction resolveProperties<T extends object>(\n proxyStore: T,\n properties?: Array<keyof T | PropertyTransform<T>>,\n): Array<{key: string; transform?: PropertyTransform<T>}> {\n if (properties) {\n return properties.map((entry) => {\n if (isTransform(entry)) {\n return {key: entry.key as string, transform: entry};\n }\n return {key: entry as string};\n });\n }\n\n // Default: all own enumerable keys that are not getters or methods.\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const result: Array<{key: string}> = [];\n for (const key of Object.keys(snap)) {\n // Skip getters (they live on the prototype, but snapshot installs them).\n // We check the original store's target for getter descriptors.\n if (findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get)\n continue;\n // Skip functions (methods).\n const value = (proxyStore as Record<string, unknown>)[key];\n if (typeof value === 'function') continue;\n result.push({key});\n }\n return result;\n}\n\n/**\n * Detect if the given storage adapter is `globalThis.localStorage`.\n */\nfunction isLocalStorage(storage: StorageAdapter): boolean {\n try {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined' &&\n storage === (globalThis.localStorage as unknown as StorageAdapter)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Get the default storage adapter (`localStorage`), or `undefined` if unavailable.\n */\nfunction getDefaultStorage(): StorageAdapter | undefined {\n try {\n if (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined'\n ) {\n return globalThis.localStorage as unknown as StorageAdapter;\n }\n } catch {\n // SSR or restricted environment — no localStorage.\n }\n return undefined;\n}\n\n// ── Main implementation ──────────────────────────────────────────────────────\n\n/**\n * Persist store state to a storage adapter.\n *\n * Subscribes to store mutations and writes the selected properties to storage\n * (with optional per-property transforms, debouncing, and versioned envelopes).\n * On init (or manual rehydrate), reads from storage and applies the state back\n * to the store proxy.\n *\n * @param proxyStore - A reactive proxy created by `store()`.\n * @param options - Persistence configuration.\n * @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).\n */\nexport function persist<T extends object>(\n proxyStore: T,\n options: PersistOptions<T>,\n): PersistHandle {\n const {\n name,\n properties: propertiesOption,\n debounce: debounceMs = 0,\n version = 0,\n migrate,\n merge = 'shallow',\n skipHydration = false,\n syncTabs: syncTabsOption,\n expireIn,\n clearOnExpire = false,\n } = options;\n\n const maybeStorage = options.storage ?? getDefaultStorage();\n if (!maybeStorage) {\n throw new Error(\n '@codebelt/classy-store: persist() requires a storage adapter. ' +\n 'No localStorage found — provide a `storage` option.',\n );\n }\n const storage: StorageAdapter = maybeStorage;\n\n const resolvedProps = resolveProperties(proxyStore, propertiesOption);\n\n // Build a map of key → transform for fast lookup during save/restore.\n const transformMap = new Map<string, PropertyTransform<T>>();\n for (const prop of resolvedProps) {\n if (prop.transform) {\n transformMap.set(prop.key, prop.transform);\n }\n }\n\n const propKeys = resolvedProps.map((p) => p.key);\n\n // ── State ────────────────────────────────────────────────────────────────\n\n let disposed = false;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let hydratedFlag = false;\n let expiredFlag = false;\n\n // Hydration promise + resolver.\n let resolveHydrated: () => void;\n let rejectHydrated: (error: unknown) => void;\n const hydratedPromise = new Promise<void>((resolve, reject) => {\n resolveHydrated = resolve;\n rejectHydrated = reject;\n });\n\n // ── Save logic ───────────────────────────────────────────────────────────\n\n /** Serialize the current store state into a JSON string (versioned envelope). */\n function serializeState(): string {\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const state: Record<string, unknown> = {};\n\n for (const key of propKeys) {\n let value = snap[key];\n const transform = transformMap.get(key);\n if (transform) {\n value = transform.serialize(value as T[keyof T]);\n }\n state[key] = value;\n }\n\n const envelope: PersistEnvelope = {version, state};\n if (expireIn != null) {\n envelope.expiresAt = Date.now() + expireIn;\n }\n return JSON.stringify(envelope);\n }\n\n /** Write the current state to storage. */\n async function writeToStorage(): Promise<void> {\n if (disposed) return;\n const json = serializeState();\n await storage.setItem(name, json);\n }\n\n /** Schedule a debounced write (or write immediately if debounce is 0). */\n function scheduleWrite(): void {\n if (disposed) return;\n\n if (debounceMs <= 0) {\n void writeToStorage();\n return;\n }\n\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void writeToStorage();\n }, debounceMs);\n }\n\n // ── Restore logic ────────────────────────────────────────────────────────\n\n /**\n * Parse a raw JSON string from storage, apply migration and transforms,\n * and merge the result into the store proxy.\n */\n function applyPersistedState(raw: string): void {\n let envelope: PersistEnvelope;\n try {\n envelope = JSON.parse(raw) as PersistEnvelope;\n } catch {\n // Corrupted data — skip.\n return;\n }\n\n if (\n !envelope ||\n typeof envelope !== 'object' ||\n typeof envelope.state !== 'object'\n ) {\n return;\n }\n\n // Expiry check — skip hydration if data has expired.\n if (\n typeof envelope.expiresAt === 'number' &&\n Date.now() >= envelope.expiresAt\n ) {\n expiredFlag = true;\n if (clearOnExpire) void storage.removeItem(name);\n return;\n }\n\n let {state} = envelope;\n\n // Version migration.\n if (migrate && envelope.version !== version) {\n state = migrate(state, envelope.version);\n }\n\n // Per-property deserialize transforms.\n for (const key of Object.keys(state)) {\n const transform = transformMap.get(key);\n if (transform) {\n state[key] = transform.deserialize(state[key]);\n }\n }\n\n // Build current state for merge.\n const currentSnap = snapshot(proxyStore) as Record<string, unknown>;\n const currentState: Record<string, unknown> = {};\n for (const key of propKeys) {\n currentState[key] = currentSnap[key];\n }\n\n // Merge strategy.\n let merged: Record<string, unknown>;\n if (typeof merge === 'function') {\n merged = merge(state, currentState);\n } else {\n // Both 'shallow' and 'replace' assign persisted keys onto the store.\n // The difference is conceptual for nested objects, but at this level\n // both just assign the persisted value per key.\n merged = {...currentState, ...state};\n }\n\n // Apply to store proxy (goes through SET traps → reactivity).\n for (const key of propKeys) {\n if (key in merged) {\n (proxyStore as Record<string, unknown>)[key] = merged[key];\n }\n }\n }\n\n /** Read from storage and apply to the store. */\n async function hydrateFromStorage(): Promise<void> {\n const raw = await storage.getItem(name);\n if (raw !== null) {\n applyPersistedState(raw);\n }\n }\n\n // ── Cross-tab sync ───────────────────────────────────────────────────────\n\n const shouldSyncTabs =\n syncTabsOption !== undefined ? syncTabsOption : isLocalStorage(storage);\n\n /** Handler for `window.storage` events. */\n function onStorageEvent(event: StorageEvent): void {\n if (disposed) return;\n if (event.key !== name) return;\n if (event.newValue === null) return; // cleared\n applyPersistedState(event.newValue);\n }\n\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.addEventListener === 'function'\n ) {\n globalThis.addEventListener('storage', onStorageEvent);\n }\n\n // ── Subscribe to store mutations ─────────────────────────────────────────\n\n const unsubscribeFromStore = subscribe(proxyStore, scheduleWrite);\n\n // ── Kick off initial hydration ───────────────────────────────────────────\n\n if (!skipHydration) {\n void hydrateFromStorage()\n .then(() => {\n hydratedFlag = true;\n resolveHydrated();\n })\n .catch((error) => {\n hydratedFlag = true;\n rejectHydrated(error);\n });\n } else {\n // When hydration is skipped, the promise is left pending until\n // the user calls handle.rehydrate() manually.\n }\n\n // ── Build handle ─────────────────────────────────────────────────────────\n\n const handle: PersistHandle = {\n get isHydrated() {\n return hydratedFlag;\n },\n\n get isExpired() {\n return expiredFlag;\n },\n\n hydrated: hydratedPromise,\n\n unsubscribe() {\n if (disposed) return;\n disposed = true;\n\n // Cancel pending debounce.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n\n // Unsubscribe from store mutations.\n unsubscribeFromStore();\n\n // Remove cross-tab sync listener.\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.removeEventListener === 'function'\n ) {\n globalThis.removeEventListener('storage', onStorageEvent);\n }\n },\n\n async save() {\n if (disposed) return;\n // Cancel pending debounce and write immediately.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n await writeToStorage();\n },\n\n async clear() {\n await storage.removeItem(name);\n },\n\n async rehydrate() {\n await hydrateFromStorage();\n if (!hydratedFlag) {\n hydratedFlag = true;\n resolveHydrated();\n }\n },\n };\n\n return handle;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/utils/persist/persist.ts"],"sourcesContent":["import {subscribe} from '../../core/core';\nimport {snapshot} from '../../snapshot/snapshot';\nimport {findGetterDescriptor} from '../internal/internal';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Storage adapter interface. Compatible with `localStorage`, `sessionStorage`,\n * `AsyncStorage`, `localForage`, or any custom implementation.\n *\n * Methods may return synchronously or asynchronously — `persist` handles both.\n */\nexport type StorageAdapter = {\n getItem: (name: string) => string | null | Promise<string | null>;\n setItem: (name: string, value: string) => void | Promise<void>;\n removeItem: (name: string) => void | Promise<void>;\n};\n\n/**\n * Describes a per-property serialization transform.\n *\n * Use this when a property's value is not JSON-serializable (Date, ReactiveMap, etc.)\n * and needs custom conversion to/from a storable format.\n */\nexport type PropertyTransform<T extends object> = {\n /** The property key on the store class. */\n key: keyof T;\n\n /** Transform the value BEFORE saving to storage (after snapshot, before JSON.stringify). */\n serialize: (value: T[keyof T]) => unknown;\n\n /** Transform the value AFTER loading from storage (after JSON.parse, before applying to store). */\n deserialize: (stored: unknown) => T[keyof T];\n};\n\n/**\n * Options for `persist()`.\n */\nexport type PersistOptions<T extends object> = {\n /** Unique storage key. Required. */\n name: string;\n\n /**\n * Storage adapter. Defaults to `globalThis.localStorage`.\n * Any object with getItem/setItem/removeItem (sync or async).\n * Works with: localStorage, sessionStorage, AsyncStorage, localForage, etc.\n */\n storage?: StorageAdapter;\n\n /**\n * Which properties to persist.\n * Each entry is either a plain key (string) or a transform descriptor.\n * Defaults to all own enumerable data properties.\n *\n * **Getters and methods are always excluded.** Class getters (e.g., `get remaining()`)\n * are computed/derived values — they are not source-of-truth state. They recompute\n * automatically from the persisted data properties when accessed. Persisting a getter\n * result would be redundant and could produce stale values on restore.\n */\n properties?: Array<keyof T | PropertyTransform<T>>;\n\n /**\n * Debounce writes to storage (milliseconds).\n * Multiple rapid mutations coalesce into one write.\n * Default: 0 (write after every batched mutation).\n */\n debounce?: number;\n\n /**\n * Schema version number. Stored alongside the data.\n * When the stored version differs from this value, `migrate` is called.\n * Default: 0.\n */\n version?: number;\n\n /**\n * Migration function. Called when the stored version does not match `version`.\n * Receives the raw parsed state and the old version number.\n * Must return the state in the current shape.\n */\n migrate?: (\n persistedState: Record<string, unknown>,\n oldVersion: number,\n ) => Record<string, unknown>;\n\n /**\n * How to merge persisted state with current store state during hydration.\n *\n * - `'shallow'` (default): persisted values overwrite current values one key at a time.\n * Properties not in storage keep their current value.\n * - `'replace'`: same behavior as `'shallow'` for flat stores — only stored keys are assigned.\n * For nested objects, the entire object is replaced rather than merged.\n * - Custom function: receives `(persistedState, currentState)` and returns merged state.\n * Enables deep merge or any custom logic.\n *\n * This matters when the store adds new properties that don't exist in old\n * persisted data. `'shallow'` preserves the new defaults.\n */\n merge?:\n | 'shallow'\n | 'replace'\n | ((\n persisted: Record<string, unknown>,\n current: Record<string, unknown>,\n ) => Record<string, unknown>);\n\n /**\n * If true, do NOT hydrate automatically on init.\n * You must call `handle.rehydrate()` manually (e.g., in a `useEffect` for SSR).\n * Default: false.\n */\n skipHydration?: boolean;\n\n /**\n * Sync state across browser tabs via the `window.storage` event.\n * When another tab writes to the same storage key, this tab automatically\n * re-hydrates from the new value.\n *\n * Only works with `localStorage` (storage events don't fire for sessionStorage\n * or async adapters).\n *\n * Default: `true` when storage is `localStorage`, `false` otherwise.\n */\n syncTabs?: boolean;\n\n /**\n * Time-to-live in milliseconds. After this duration, stored data is\n * considered expired and skipped during hydration. The TTL resets on\n * every write (active sessions stay fresh as long as mutations happen).\n */\n expireIn?: number;\n\n /**\n * When `true`, automatically remove the storage key if data is found\n * expired during hydration. Default: `false` (expired data is skipped\n * but left in storage).\n */\n clearOnExpire?: boolean;\n};\n\n/**\n * Handle returned by `persist()`. Provides control over the persist lifecycle.\n */\nexport type PersistHandle = {\n /** Stop persisting and clean up (unsubscribe + cancel pending debounce + remove storage event listener). */\n unsubscribe: () => void;\n\n /** Promise that resolves when initial hydration from storage is complete. */\n hydrated: Promise<void>;\n\n /** Whether hydration from storage has completed. */\n isHydrated: boolean;\n\n /** Manually trigger a write to storage (bypasses debounce). */\n save: () => Promise<void>;\n\n /** Clear this store's persisted data from storage. */\n clear: () => Promise<void>;\n\n /** Manually re-hydrate the store from storage. */\n rehydrate: () => Promise<void>;\n\n /** True if the last hydration found expired data (requires `expireIn`). */\n isExpired: boolean;\n};\n\n// ── Storage envelope ─────────────────────────────────────────────────────────\n\n/** Internal shape of the JSON stored in storage. */\ntype PersistEnvelope = {\n version: number;\n state: Record<string, unknown>;\n expiresAt?: number;\n};\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */\nfunction isTransform<T extends object>(\n entry: keyof T | PropertyTransform<T>,\n): entry is PropertyTransform<T> {\n return (\n typeof entry === 'object' &&\n entry !== null &&\n 'key' in entry &&\n 'serialize' in entry\n );\n}\n\n/**\n * Build the normalized list of property keys and their optional transforms.\n * If `properties` is not provided, defaults to all own enumerable data properties\n * of the store (excluding getters and methods).\n */\nfunction resolveProperties<T extends object>(\n proxyStore: T,\n properties?: Array<keyof T | PropertyTransform<T>>,\n): Array<{key: string; transform?: PropertyTransform<T>}> {\n if (properties) {\n return properties.map((entry) => {\n if (isTransform(entry)) {\n return {key: entry.key as string, transform: entry};\n }\n return {key: entry as string};\n });\n }\n\n // Default: all own enumerable keys that are not getters or methods.\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const result: Array<{key: string}> = [];\n for (const key of Object.keys(snap)) {\n // Skip getters (they live on the prototype, but snapshot installs them).\n // We check the original store's target for getter descriptors.\n if (findGetterDescriptor(Object.getPrototypeOf(proxyStore), key)?.get)\n continue;\n // Skip functions (methods).\n const value = (proxyStore as Record<string, unknown>)[key];\n if (typeof value === 'function') continue;\n result.push({key});\n }\n return result;\n}\n\n/**\n * Detect if the given storage adapter is `globalThis.localStorage`.\n */\nfunction isLocalStorage(storage: StorageAdapter): boolean {\n try {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined' &&\n storage === (globalThis.localStorage as unknown as StorageAdapter)\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Get the default storage adapter (`localStorage`), or `undefined` if unavailable.\n */\nfunction getDefaultStorage(): StorageAdapter | undefined {\n try {\n if (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.localStorage !== 'undefined'\n ) {\n return globalThis.localStorage as unknown as StorageAdapter;\n }\n } catch {\n // SSR or restricted environment — no localStorage.\n }\n return undefined;\n}\n\n// ── Main implementation ──────────────────────────────────────────────────────\n\n/**\n * Persist store state to a storage adapter.\n *\n * Subscribes to store mutations and writes the selected properties to storage\n * (with optional per-property transforms, debouncing, and versioned envelopes).\n * On init (or manual rehydrate), reads from storage and applies the state back\n * to the store proxy.\n *\n * @param proxyStore - A reactive proxy created by `createClassyStore()`.\n * @param options - Persistence configuration.\n * @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).\n */\nexport function persist<T extends object>(\n proxyStore: T,\n options: PersistOptions<T>,\n): PersistHandle {\n const {\n name,\n properties: propertiesOption,\n debounce: debounceMs = 0,\n version = 0,\n migrate,\n merge = 'shallow',\n skipHydration = false,\n syncTabs: syncTabsOption,\n expireIn,\n clearOnExpire = false,\n } = options;\n\n const maybeStorage = options.storage ?? getDefaultStorage();\n if (!maybeStorage) {\n throw new Error(\n '@codebelt/classy-store: persist() requires a storage adapter. ' +\n 'No localStorage found — provide a `storage` option.',\n );\n }\n const storage: StorageAdapter = maybeStorage;\n\n const resolvedProps = resolveProperties(proxyStore, propertiesOption);\n\n // Build a map of key → transform for fast lookup during save/restore.\n const transformMap = new Map<string, PropertyTransform<T>>();\n for (const prop of resolvedProps) {\n if (prop.transform) {\n transformMap.set(prop.key, prop.transform);\n }\n }\n\n const propKeys = resolvedProps.map((p) => p.key);\n\n // ── State ────────────────────────────────────────────────────────────────\n\n let disposed = false;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n let hydratedFlag = false;\n let expiredFlag = false;\n\n // Hydration promise + resolver.\n let resolveHydrated: () => void;\n let rejectHydrated: (error: unknown) => void;\n const hydratedPromise = new Promise<void>((resolve, reject) => {\n resolveHydrated = resolve;\n rejectHydrated = reject;\n });\n\n // ── Save logic ───────────────────────────────────────────────────────────\n\n /** Serialize the current store state into a JSON string (versioned envelope). */\n function serializeState(): string {\n const snap = snapshot(proxyStore) as Record<string, unknown>;\n const state: Record<string, unknown> = {};\n\n for (const key of propKeys) {\n let value = snap[key];\n const transform = transformMap.get(key);\n if (transform) {\n value = transform.serialize(value as T[keyof T]);\n }\n state[key] = value;\n }\n\n const envelope: PersistEnvelope = {version, state};\n if (expireIn != null) {\n envelope.expiresAt = Date.now() + expireIn;\n }\n return JSON.stringify(envelope);\n }\n\n /** Write the current state to storage. */\n async function writeToStorage(): Promise<void> {\n if (disposed) return;\n const json = serializeState();\n await storage.setItem(name, json);\n }\n\n /** Schedule a debounced write (or write immediately if debounce is 0). */\n function scheduleWrite(): void {\n if (disposed) return;\n\n if (debounceMs <= 0) {\n void writeToStorage();\n return;\n }\n\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n }\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n void writeToStorage();\n }, debounceMs);\n }\n\n // ── Restore logic ────────────────────────────────────────────────────────\n\n /**\n * Parse a raw JSON string from storage, apply migration and transforms,\n * and merge the result into the store proxy.\n */\n function applyPersistedState(raw: string): void {\n let envelope: PersistEnvelope;\n try {\n envelope = JSON.parse(raw) as PersistEnvelope;\n } catch {\n // Corrupted data — skip.\n return;\n }\n\n if (\n !envelope ||\n typeof envelope !== 'object' ||\n typeof envelope.state !== 'object'\n ) {\n return;\n }\n\n // Expiry check — skip hydration if data has expired.\n if (\n typeof envelope.expiresAt === 'number' &&\n Date.now() >= envelope.expiresAt\n ) {\n expiredFlag = true;\n if (clearOnExpire) void storage.removeItem(name);\n return;\n }\n\n let {state} = envelope;\n\n // Version migration.\n if (migrate && envelope.version !== version) {\n state = migrate(state, envelope.version);\n }\n\n // Per-property deserialize transforms.\n for (const key of Object.keys(state)) {\n const transform = transformMap.get(key);\n if (transform) {\n state[key] = transform.deserialize(state[key]);\n }\n }\n\n // Build current state for merge.\n const currentSnap = snapshot(proxyStore) as Record<string, unknown>;\n const currentState: Record<string, unknown> = {};\n for (const key of propKeys) {\n currentState[key] = currentSnap[key];\n }\n\n // Merge strategy.\n let merged: Record<string, unknown>;\n if (typeof merge === 'function') {\n merged = merge(state, currentState);\n } else {\n // Both 'shallow' and 'replace' assign persisted keys onto the store.\n // The difference is conceptual for nested objects, but at this level\n // both just assign the persisted value per key.\n merged = {...currentState, ...state};\n }\n\n // Apply to store proxy (goes through SET traps → reactivity).\n for (const key of propKeys) {\n if (key in merged) {\n (proxyStore as Record<string, unknown>)[key] = merged[key];\n }\n }\n }\n\n /** Read from storage and apply to the store. */\n async function hydrateFromStorage(): Promise<void> {\n const raw = await storage.getItem(name);\n if (raw !== null) {\n applyPersistedState(raw);\n }\n }\n\n // ── Cross-tab sync ───────────────────────────────────────────────────────\n\n const shouldSyncTabs =\n syncTabsOption !== undefined ? syncTabsOption : isLocalStorage(storage);\n\n /** Handler for `window.storage` events. */\n function onStorageEvent(event: StorageEvent): void {\n if (disposed) return;\n if (event.key !== name) return;\n if (event.newValue === null) return; // cleared\n applyPersistedState(event.newValue);\n }\n\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.addEventListener === 'function'\n ) {\n globalThis.addEventListener('storage', onStorageEvent);\n }\n\n // ── Subscribe to store mutations ─────────────────────────────────────────\n\n const unsubscribeFromStore = subscribe(proxyStore, scheduleWrite);\n\n // ── Kick off initial hydration ───────────────────────────────────────────\n\n if (!skipHydration) {\n void hydrateFromStorage()\n .then(() => {\n hydratedFlag = true;\n resolveHydrated();\n })\n .catch((error) => {\n hydratedFlag = true;\n rejectHydrated(error);\n });\n } else {\n // When hydration is skipped, the promise is left pending until\n // the user calls handle.rehydrate() manually.\n }\n\n // ── Build handle ─────────────────────────────────────────────────────────\n\n const handle: PersistHandle = {\n get isHydrated() {\n return hydratedFlag;\n },\n\n get isExpired() {\n return expiredFlag;\n },\n\n hydrated: hydratedPromise,\n\n unsubscribe() {\n if (disposed) return;\n disposed = true;\n\n // Cancel pending debounce.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n\n // Unsubscribe from store mutations.\n unsubscribeFromStore();\n\n // Remove cross-tab sync listener.\n if (\n shouldSyncTabs &&\n typeof globalThis !== 'undefined' &&\n typeof globalThis.removeEventListener === 'function'\n ) {\n globalThis.removeEventListener('storage', onStorageEvent);\n }\n },\n\n async save() {\n if (disposed) return;\n // Cancel pending debounce and write immediately.\n if (debounceTimer !== null) {\n clearTimeout(debounceTimer);\n debounceTimer = null;\n }\n await writeToStorage();\n },\n\n async clear() {\n await storage.removeItem(name);\n },\n\n async rehydrate() {\n await hydrateFromStorage();\n if (!hydratedFlag) {\n hydratedFlag = true;\n resolveHydrated();\n }\n },\n };\n\n return handle;\n}\n"],"mappings":"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"}
|