@codebelt/classy-store 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/equality-5F2bPn7E.d.mts +13 -0
- package/dist/equality-5F2bPn7E.d.mts.map +1 -0
- package/dist/equality-BA46H9AL.mjs +27 -0
- package/dist/equality-BA46H9AL.mjs.map +1 -0
- package/dist/equality-C1s0kqxg.d.cts +13 -0
- package/dist/equality-C1s0kqxg.d.cts.map +1 -0
- package/dist/equality-Cz6riknL.cjs +33 -0
- package/dist/equality-Cz6riknL.cjs.map +1 -0
- package/dist/index.cjs +10 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +17 -22
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +9 -33
- package/dist/index.mjs.map +1 -1
- package/dist/react/react.cjs +30 -1
- package/dist/react/react.cjs.map +1 -1
- package/dist/react/react.d.cts +26 -1
- package/dist/react/react.d.cts.map +1 -1
- package/dist/react/react.d.mts +27 -2
- package/dist/react/react.d.mts.map +1 -1
- package/dist/react/react.mjs +31 -3
- package/dist/react/react.mjs.map +1 -1
- package/dist/{snapshot-C8JDLu8L.mjs → snapshot-COzEerMu.mjs} +45 -15
- package/dist/snapshot-COzEerMu.mjs.map +1 -0
- package/dist/{snapshot-CR8nA2Ob.cjs → snapshot-CbVbxG7s.cjs} +50 -20
- package/dist/snapshot-CbVbxG7s.cjs.map +1 -0
- package/dist/{types-vWYkF3tH.d.mts → types-Cf8Fp7kA.d.mts} +1 -1
- package/dist/{types-vWYkF3tH.d.mts.map → types-Cf8Fp7kA.d.mts.map} +1 -1
- package/dist/utils/index.cjs +177 -6
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +90 -2
- package/dist/utils/index.d.cts.map +1 -1
- package/dist/utils/index.d.mts +90 -2
- package/dist/utils/index.d.mts.map +1 -1
- package/dist/utils/index.mjs +174 -7
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/snapshot-C8JDLu8L.mjs.map +0 -1
- package/dist/snapshot-CR8nA2Ob.cjs.map +0 -1
|
@@ -88,12 +88,33 @@ function recordDep(internal, prop, value) {
|
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
+
* Record a computed getter dependency on the active tracker (if any).
|
|
92
|
+
* Unlike `recordDep`, this creates a `'computed'` dep entry that validates
|
|
93
|
+
* through the computed cache rather than re-executing the raw getter.
|
|
94
|
+
*/
|
|
95
|
+
function recordComputedDep(internal, prop, value) {
|
|
96
|
+
const tracker = activeTracker();
|
|
97
|
+
if (!tracker) return;
|
|
98
|
+
if (tracker.internal !== internal) return;
|
|
99
|
+
tracker.deps.push({
|
|
100
|
+
kind: "computed",
|
|
101
|
+
internal,
|
|
102
|
+
prop,
|
|
103
|
+
value
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
91
107
|
* Check whether all dependencies from a previous computation are still valid.
|
|
92
108
|
*/
|
|
93
109
|
function areDepsValid(deps) {
|
|
94
110
|
for (const dep of deps) if (dep.kind === "version") {
|
|
95
111
|
if (!Object.is(Reflect.get(dep.parentTarget, dep.prop), dep.internal.target)) return false;
|
|
96
112
|
if (dep.internal.version !== dep.version) return false;
|
|
113
|
+
} else if (dep.kind === "computed") {
|
|
114
|
+
const cached = dep.internal.computedCache.get(dep.prop);
|
|
115
|
+
if (!cached) return false;
|
|
116
|
+
if (!areDepsValid(cached.deps)) return false;
|
|
117
|
+
if (!Object.is(cached.value, dep.value)) return false;
|
|
97
118
|
} else {
|
|
98
119
|
if (!Reflect.has(dep.target, dep.prop)) return false;
|
|
99
120
|
if (!Object.is(Reflect.get(dep.target, dep.prop), dep.value)) return false;
|
|
@@ -126,7 +147,7 @@ function evaluateComputed(internal, prop, getterFn, receiver) {
|
|
|
126
147
|
}
|
|
127
148
|
/**
|
|
128
149
|
* Retrieve the internal bookkeeping for a store proxy.
|
|
129
|
-
* Throws if the object was not created with `
|
|
150
|
+
* Throws if the object was not created with `createClassyStore()`.
|
|
130
151
|
*/
|
|
131
152
|
function getInternal(proxy) {
|
|
132
153
|
const internal = internalsMap.get(proxy);
|
|
@@ -200,7 +221,11 @@ function createStoreProxy(target, parent) {
|
|
|
200
221
|
},
|
|
201
222
|
get(_target, prop, receiver) {
|
|
202
223
|
const getterDesc = findGetterDescriptor(_target, prop);
|
|
203
|
-
if (getterDesc?.get)
|
|
224
|
+
if (getterDesc?.get) {
|
|
225
|
+
const value = evaluateComputed(internal, prop, getterDesc.get, receiver);
|
|
226
|
+
recordComputedDep(internal, prop, value);
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
204
229
|
const value = Reflect.get(_target, prop);
|
|
205
230
|
if (typeof value === "function") {
|
|
206
231
|
if (Array.isArray(_target)) return value.bind(receiver);
|
|
@@ -248,15 +273,20 @@ function createStoreProxy(target, parent) {
|
|
|
248
273
|
*
|
|
249
274
|
* @param instance - A class instance (or plain object) to make reactive.
|
|
250
275
|
* @returns The same object wrapped in a reactive Proxy.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* const myStore = createClassyStore(new MyClass());
|
|
280
|
+
* ```
|
|
251
281
|
*/
|
|
252
|
-
function
|
|
282
|
+
function createClassyStore(instance) {
|
|
253
283
|
return createStoreProxy(instance, null);
|
|
254
284
|
}
|
|
255
285
|
/**
|
|
256
286
|
* Subscribe to store changes. The callback fires once per batched mutation
|
|
257
287
|
* (coalesced via `queueMicrotask`), not once per individual property write.
|
|
258
288
|
*
|
|
259
|
-
* @param proxy - A reactive proxy created by `
|
|
289
|
+
* @param proxy - A reactive proxy created by `createClassyStore()`.
|
|
260
290
|
* @param callback - Invoked after each batched mutation.
|
|
261
291
|
* @returns An unsubscribe function. Call it to stop receiving notifications.
|
|
262
292
|
*/
|
|
@@ -284,7 +314,7 @@ function getVersion(proxy) {
|
|
|
284
314
|
* Version-stamped snapshot cache for tracked (proxied) sub-trees.
|
|
285
315
|
* Key: raw target object → [version, frozen snapshot].
|
|
286
316
|
*/
|
|
287
|
-
const
|
|
317
|
+
const snapshotCache = /* @__PURE__ */ new WeakMap();
|
|
288
318
|
/**
|
|
289
319
|
* Cache for untracked nested objects (never accessed through the proxy).
|
|
290
320
|
* Key: the raw mutable object → frozen deep clone.
|
|
@@ -303,7 +333,7 @@ const snapshotGetterCache = /* @__PURE__ */ new WeakMap();
|
|
|
303
333
|
* boundaries: if `this.items` hasn't changed between snapshots (same
|
|
304
334
|
* reference via structural sharing), the getter returns the same result.
|
|
305
335
|
*/
|
|
306
|
-
const
|
|
336
|
+
const crossSnapshotMemo = /* @__PURE__ */ new WeakMap();
|
|
307
337
|
/**
|
|
308
338
|
* Run a getter against the snapshot with dependency tracking.
|
|
309
339
|
*
|
|
@@ -337,7 +367,7 @@ function computeWithTracking(snap, getterFn) {
|
|
|
337
367
|
* guarantees stable refs for unchanged sub-trees). For getter properties
|
|
338
368
|
* this invokes the getter (which is itself memoized), then compares.
|
|
339
369
|
*/
|
|
340
|
-
function
|
|
370
|
+
function areMemoizedDepsValid(currentSnap, deps) {
|
|
341
371
|
for (const dep of deps) {
|
|
342
372
|
const currentValue = Reflect.get(currentSnap, dep.prop, currentSnap);
|
|
343
373
|
if (!Object.is(currentValue, dep.value)) return false;
|
|
@@ -356,16 +386,16 @@ function areMemoedDepsValid(currentSnap, deps) {
|
|
|
356
386
|
function evaluateSnapshotGetter(currentSnap, target, key, getterFn) {
|
|
357
387
|
const perSnapCache = snapshotGetterCache.get(currentSnap);
|
|
358
388
|
if (perSnapCache?.has(key)) return perSnapCache.get(key);
|
|
359
|
-
let memoMap =
|
|
389
|
+
let memoMap = crossSnapshotMemo.get(target);
|
|
360
390
|
const prev = memoMap?.get(key);
|
|
361
391
|
let result;
|
|
362
|
-
if (prev &&
|
|
392
|
+
if (prev && areMemoizedDepsValid(currentSnap, prev.deps)) result = prev.result;
|
|
363
393
|
else {
|
|
364
394
|
const computation = computeWithTracking(currentSnap, getterFn);
|
|
365
395
|
result = computation.result;
|
|
366
396
|
if (!memoMap) {
|
|
367
397
|
memoMap = /* @__PURE__ */ new Map();
|
|
368
|
-
|
|
398
|
+
crossSnapshotMemo.set(target, memoMap);
|
|
369
399
|
}
|
|
370
400
|
memoMap.set(key, {
|
|
371
401
|
deps: computation.deps,
|
|
@@ -479,7 +509,7 @@ function installMemoizedGetters(snap, target) {
|
|
|
479
509
|
* are installed as lazy-memoizing accessors via `installMemoizedGetters`.
|
|
480
510
|
*/
|
|
481
511
|
function createSnapshotRecursive(target, internal) {
|
|
482
|
-
const cached =
|
|
512
|
+
const cached = snapshotCache.get(target);
|
|
483
513
|
if (cached && cached[0] === internal.version) return cached[1];
|
|
484
514
|
let snap;
|
|
485
515
|
if (Array.isArray(target)) {
|
|
@@ -496,7 +526,7 @@ function createSnapshotRecursive(target, internal) {
|
|
|
496
526
|
installMemoizedGetters(snap, target);
|
|
497
527
|
}
|
|
498
528
|
Object.freeze(snap);
|
|
499
|
-
|
|
529
|
+
snapshotCache.set(target, [internal.version, snap]);
|
|
500
530
|
return snap;
|
|
501
531
|
}
|
|
502
532
|
/**
|
|
@@ -512,7 +542,7 @@ function createSnapshotRecursive(target, internal) {
|
|
|
512
542
|
* per snapshot and their results are stable across snapshots when dependencies
|
|
513
543
|
* haven't changed (cross-snapshot memoization).
|
|
514
544
|
*
|
|
515
|
-
* @param proxyStore - A reactive proxy created by `
|
|
545
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
516
546
|
* @returns A deeply frozen plain-JS object (Snapshot<T>).
|
|
517
547
|
*/
|
|
518
548
|
function snapshot(proxyStore) {
|
|
@@ -527,6 +557,12 @@ Object.defineProperty(exports, 'PROXYABLE', {
|
|
|
527
557
|
return PROXYABLE;
|
|
528
558
|
}
|
|
529
559
|
});
|
|
560
|
+
Object.defineProperty(exports, 'createClassyStore', {
|
|
561
|
+
enumerable: true,
|
|
562
|
+
get: function () {
|
|
563
|
+
return createClassyStore;
|
|
564
|
+
}
|
|
565
|
+
});
|
|
530
566
|
Object.defineProperty(exports, 'findGetterDescriptor', {
|
|
531
567
|
enumerable: true,
|
|
532
568
|
get: function () {
|
|
@@ -551,16 +587,10 @@ Object.defineProperty(exports, 'snapshot', {
|
|
|
551
587
|
return snapshot;
|
|
552
588
|
}
|
|
553
589
|
});
|
|
554
|
-
Object.defineProperty(exports, 'store', {
|
|
555
|
-
enumerable: true,
|
|
556
|
-
get: function () {
|
|
557
|
-
return store;
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
590
|
Object.defineProperty(exports, 'subscribe', {
|
|
561
591
|
enumerable: true,
|
|
562
592
|
get: function () {
|
|
563
593
|
return subscribe;
|
|
564
594
|
}
|
|
565
595
|
});
|
|
566
|
-
//# sourceMappingURL=snapshot-
|
|
596
|
+
//# sourceMappingURL=snapshot-CbVbxG7s.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-CbVbxG7s.cjs","names":[],"sources":["../src/utils/internal/internal.ts","../src/core/core.ts","../src/snapshot/snapshot.ts"],"sourcesContent":["const objectProto = Object.getPrototypeOf({});\n\n/**\n * Symbol that class instances can use to opt-in to deep proxying.\n * Classes with `static [PROXYABLE] = true` will be wrapped by the store proxy\n * just like plain objects, enabling nested reactivity.\n */\nexport const PROXYABLE = Symbol.for('@codebelt/classy-store.proxyable');\n\n/**\n * Returns `true` if `value` is a plain object (created via `{}` or `new Object()`).\n * Needed by `canProxy()` to distinguish plain objects (which should be deep-proxied)\n * from class instances, Date, Map, etc. (which should not).\n */\nexport function isPlainObject(\n value: unknown,\n): value is Record<string | symbol, unknown> {\n if (typeof value !== 'object' || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === objectProto || proto === null;\n}\n\n/**\n * Central gatekeeper for the proxy system: determines which values get wrapped\n * in child proxies (`core.ts`) or deep-cloned in snapshots (`snapshot.ts`).\n *\n * Returns `true` for arrays, plain objects, and class instances that opt-in\n * via `static [PROXYABLE] = true`. Everything else (Date, Map, Set, class\n * instances without PROXYABLE, primitives) is left as-is.\n */\nexport function canProxy(value: unknown): value is object {\n if (typeof value !== 'object' || value === null) return false;\n if (Array.isArray(value)) return true;\n // Allow class instances that opt-in via the PROXYABLE symbol.\n const ctor = (value as {constructor?: unknown}).constructor;\n if (ctor && (ctor as Record<symbol, unknown>)[PROXYABLE]) {\n return true;\n }\n return isPlainObject(value);\n}\n\n/**\n * Walk the prototype chain of `target` looking for a getter descriptor for `prop`.\n * Returns the first (most-derived) getter found, or `undefined` if none exists.\n *\n * Used by `core.ts` (GET trap) to detect class getters for memoized evaluation,\n * and by `snapshot.ts` to skip own-property copying for getter-backed keys.\n */\nexport function findGetterDescriptor(\n target: object,\n prop: string | symbol,\n): PropertyDescriptor | undefined {\n let proto: object | null = target;\n while (proto) {\n const desc = Object.getOwnPropertyDescriptor(proto, prop);\n if (desc?.get) return desc;\n proto = Object.getPrototypeOf(proto);\n }\n return undefined;\n}\n","import type {DepEntry, StoreInternal} from '../types';\nimport {canProxy, findGetterDescriptor} from '../utils/internal/internal';\n\n// ── Global state ──────────────────────────────────────────────────────────────\n\n/** Global version counter shared across all stores. */\nlet globalVersion = 0;\n\n/** Maps every store proxy → its internal bookkeeping. */\nconst internalsMap = new WeakMap<object, StoreInternal>();\n\n// ── Dependency tracking for computed getters ──────────────────────────────────\n\n/**\n * Stack of active dependency trackers. Each entry records which properties\n * a getter reads during evaluation. A stack (not a single variable) is needed\n * because getter A can read getter B, which pushes a second tracker.\n */\nconst trackerStack: {internal: StoreInternal; deps: DepEntry[]}[] = [];\n\n/** Returns the tracker currently recording deps, or `null` if none is active. */\nfunction activeTracker() {\n return trackerStack.length > 0 ? trackerStack[trackerStack.length - 1] : null;\n}\n\n/**\n * Record a dependency on the active tracker (if any).\n * Called from the GET trap whenever a non-getter, non-method property is read.\n */\nfunction recordDep(\n internal: StoreInternal,\n prop: string | symbol,\n value: unknown,\n): void {\n const tracker = activeTracker();\n if (!tracker) return;\n\n // Only record deps for reads on the SAME proxy that owns the getter.\n // Child-proxy reads don't need explicit tracking because child mutations\n // bubble up via version numbers on the parent's childInternals.\n if (tracker.internal !== internal) return;\n\n const childInternal = internal.childInternals.get(prop);\n if (childInternal) {\n tracker.deps.push({\n kind: 'version',\n internal: childInternal,\n version: childInternal.version,\n parentTarget: internal.target,\n prop,\n });\n } else {\n tracker.deps.push({kind: 'value', target: internal.target, prop, value});\n }\n}\n\n/**\n * Record a computed getter dependency on the active tracker (if any).\n * Unlike `recordDep`, this creates a `'computed'` dep entry that validates\n * through the computed cache rather than re-executing the raw getter.\n */\nfunction recordComputedDep(\n internal: StoreInternal,\n prop: string | symbol,\n value: unknown,\n): void {\n const tracker = activeTracker();\n if (!tracker) return;\n if (tracker.internal !== internal) return;\n\n tracker.deps.push({kind: 'computed', internal, prop, value});\n}\n\n/**\n * Check whether all dependencies from a previous computation are still valid.\n */\nfunction areDepsValid(deps: DepEntry[]): boolean {\n for (const dep of deps) {\n if (dep.kind === 'version') {\n // Verify the parent property still references the same child object.\n // If the property was replaced entirely (e.g., store.items = newArray),\n // the old child internal is detached and its version never changes.\n // This check catches that case.\n if (\n !Object.is(Reflect.get(dep.parentTarget, dep.prop), dep.internal.target)\n )\n return false;\n if (dep.internal.version !== dep.version) return false;\n } else if (dep.kind === 'computed') {\n // Validate through the computed cache — never re-execute the raw getter.\n const cached = dep.internal.computedCache.get(dep.prop);\n if (!cached) return false; // no cache → must recompute\n if (!areDepsValid(cached.deps)) return false; // nested deps changed\n if (!Object.is(cached.value, dep.value)) return false; // value changed\n } else {\n // Check that the property still exists — if it was deleted and\n // dep.value was `undefined`, Object.is(undefined, undefined) would\n // incorrectly pass without this guard.\n if (!Reflect.has(dep.target, dep.prop)) return false;\n if (!Object.is(Reflect.get(dep.target, dep.prop), dep.value))\n return false;\n }\n }\n return true;\n}\n\n/**\n * Evaluate a computed getter with memoization.\n * Returns the cached value if dependencies haven't changed.\n * Otherwise re-evaluates with dependency tracking and caches the result.\n */\nfunction evaluateComputed(\n internal: StoreInternal,\n prop: string | symbol,\n getterFn: () => unknown,\n receiver: object,\n): unknown {\n const cached = internal.computedCache.get(prop);\n if (cached && areDepsValid(cached.deps)) {\n return cached.value;\n }\n\n // Push a new tracker frame for this getter evaluation.\n const frame = {internal, deps: [] as DepEntry[]};\n trackerStack.push(frame);\n try {\n const value = getterFn.call(receiver);\n internal.computedCache.set(prop, {value, deps: frame.deps});\n return value;\n } finally {\n trackerStack.pop();\n }\n}\n\n// ── Public helpers ────────────────────────────────────────────────────────────\n\n/**\n * Retrieve the internal bookkeeping for a store proxy.\n * Throws if the object was not created with `createClassyStore()`.\n */\nexport function getInternal(proxy: object): StoreInternal {\n const internal = internalsMap.get(proxy);\n if (!internal)\n throw new Error('@codebelt/classy-store: object is not a store proxy');\n return internal;\n}\n\n// ── Notification batching ─────────────────────────────────────────────────────\n\n/**\n * Bump version from the mutated node up to the root, invalidate snapshot caches,\n * and schedule a single microtask notification (deduped at the root level).\n *\n * Version propagation is what enables structural sharing in snapshots: unchanged\n * children keep their old version, so the snapshot cache returns the same frozen ref.\n */\nfunction scheduleNotify(internal: StoreInternal): void {\n let current: StoreInternal | null = internal;\n while (current) {\n current.version = ++globalVersion;\n current = current.parent;\n }\n\n const root = getRoot(internal);\n if (!root.notifyScheduled) {\n root.notifyScheduled = true;\n queueMicrotask(() => {\n root.notifyScheduled = false;\n for (const listener of root.listeners) {\n listener();\n }\n });\n }\n}\n\n/** Walk the parent chain to find the root StoreInternal (the notification hub). */\nfunction getRoot(internal: StoreInternal): StoreInternal {\n let current = internal;\n while (current.parent) {\n current = current.parent;\n }\n return current;\n}\n\n// ── Create proxy (recursive) ──────────────────────────────────────────────────\n\n/**\n * Create an ES6 Proxy around `target` with SET/GET/DELETE traps.\n *\n * This is the core of the library's reactivity. The proxy intercepts:\n * - **SET**: compares old/new with `Object.is`, cleans up child proxies on replacement,\n * forwards the write, and schedules a batched notification.\n * - **GET**: detects class getters (memoized), binds methods to the proxy, lazily wraps\n * nested objects/arrays in child proxies, and records dependencies for computed getters.\n * - **DELETE**: cleans up child proxies and schedules notification.\n *\n * Nested objects are recursively wrapped on first access (lazy deep proxy).\n */\nfunction createStoreProxy<T extends object>(\n target: T,\n parent: StoreInternal | null,\n): T {\n const internal: StoreInternal = {\n target,\n version: ++globalVersion,\n listeners: new Set(),\n childProxies: new Map(),\n childInternals: new Map(),\n parent,\n notifyScheduled: false,\n computedCache: new Map(),\n };\n\n /** Cache for bound methods so we return the same reference each time. */\n const boundMethods = new Map<\n string | symbol,\n (...args: unknown[]) => unknown\n >();\n\n const proxy = new Proxy(target, {\n set(_target, prop, value, _receiver) {\n const oldValue = Reflect.get(_target, prop);\n if (Object.is(oldValue, value)) return true; // noop — same value\n\n // If the new value replaces a child proxy, clean it up.\n if (internal.childProxies.has(prop)) {\n internal.childProxies.delete(prop);\n internal.childInternals.delete(prop);\n }\n\n Reflect.set(_target, prop, value);\n scheduleNotify(internal);\n return true;\n },\n\n get(_target, prop, receiver) {\n // 1. Check for getters on the prototype chain (computed values).\n const getterDesc = findGetterDescriptor(_target, prop);\n if (getterDesc?.get) {\n // Memoized: evaluate with dependency tracking, return cached if deps unchanged.\n const value = evaluateComputed(\n internal,\n prop,\n getterDesc.get,\n receiver,\n );\n // Record as a computed dependency for any parent getter currently tracking.\n // Uses 'computed' dep kind so validation goes through the cache, not raw getter.\n recordComputedDep(internal, prop, value);\n return value;\n }\n\n const value = Reflect.get(_target, prop);\n\n // 2. Methods: bind to the proxy so `this.prop = x` goes through SET trap.\n if (typeof value === 'function') {\n // Array prototype methods should not be cached the same way as class methods.\n if (Array.isArray(_target)) {\n return value.bind(receiver);\n }\n const cached = boundMethods.get(prop);\n if (cached) return cached;\n const bound = (value as (...args: unknown[]) => unknown).bind(receiver);\n boundMethods.set(prop, bound);\n return bound;\n }\n\n // 3. Nested plain objects/arrays: lazy-wrap in a child proxy.\n if (canProxy(value)) {\n let childProxy = internal.childProxies.get(prop);\n if (!childProxy) {\n childProxy = createStoreProxy(value as object, internal);\n internal.childProxies.set(prop, childProxy);\n const childInternal = internalsMap.get(childProxy) as StoreInternal;\n internal.childInternals.set(prop, childInternal);\n }\n // Record dependency AFTER ensuring child proxy/internal exists.\n recordDep(internal, prop, value);\n return childProxy;\n }\n\n // 4. Primitives — record dependency and return.\n recordDep(internal, prop, value);\n return value;\n },\n\n deleteProperty(_target, prop) {\n if (internal.childProxies.has(prop)) {\n internal.childProxies.delete(prop);\n internal.childInternals.delete(prop);\n }\n const deleted = Reflect.deleteProperty(_target, prop);\n if (deleted) {\n scheduleNotify(internal);\n }\n return deleted;\n },\n });\n\n internalsMap.set(proxy, internal);\n return proxy;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Wraps a class instance in a reactive proxy.\n *\n * - Mutations (property writes, array push/splice, etc.) are intercepted and\n * batched into a single notification per microtask.\n * - Class getters are automatically memoized — they only recompute when a\n * dependency they read changes.\n * - Methods are automatically bound so `this` mutations go through the proxy.\n *\n * @param instance - A class instance (or plain object) to make reactive.\n * @returns The same object wrapped in a reactive Proxy.\n *\n * @example\n * ```ts\n * const myStore = createClassyStore(new MyClass());\n * ```\n */\nexport function createClassyStore<T extends object>(instance: T): T {\n return createStoreProxy(instance, null);\n}\n\n/**\n * Subscribe to store changes. The callback fires once per batched mutation\n * (coalesced via `queueMicrotask`), not once per individual property write.\n *\n * @param proxy - A reactive proxy created by `createClassyStore()`.\n * @param callback - Invoked after each batched mutation.\n * @returns An unsubscribe function. Call it to stop receiving notifications.\n */\nexport function subscribe(proxy: object, callback: () => void): () => void {\n const internal = getInternal(proxy);\n // Always subscribe on the root so notifications fire regardless of\n // whether the user subscribes to the root proxy or a child proxy.\n const root = getRoot(internal);\n root.listeners.add(callback);\n return () => {\n root.listeners.delete(callback);\n };\n}\n\n/**\n * Returns the current version number of a store proxy.\n *\n * Versions are monotonically increasing and bump on any mutation in the\n * store's subtree (child mutations propagate up to the root). Useful for\n * debugging, custom cache invalidation, or testing whether a store has changed.\n */\nexport function getVersion(proxy: object): number {\n return getInternal(proxy).version;\n}\n","import {getInternal} from '../core/core';\nimport type {Snapshot, StoreInternal} from '../types';\nimport {canProxy, findGetterDescriptor} from '../utils/internal/internal';\n\n// ── Caches ────────────────────────────────────────────────────────────────────\n\n/**\n * Version-stamped snapshot cache for tracked (proxied) sub-trees.\n * Key: raw target object → [version, frozen snapshot].\n */\nconst snapshotCache = new WeakMap<object, [version: number, snap: object]>();\n\n/**\n * Cache for untracked nested objects (never accessed through the proxy).\n * Key: the raw mutable object → frozen deep clone.\n * Safe because untracked objects can't be mutated through the proxy.\n */\nconst untrackedCache = new WeakMap<object, object>();\n\n/**\n * Per-snapshot, per-getter cache. Ensures repeated access to the same getter\n * on the same frozen snapshot returns the identical reference.\n */\nconst snapshotGetterCache = new WeakMap<\n object,\n Map<string | symbol, unknown>\n>();\n\n// ── Cross-snapshot getter memoization ─────────────────────────────────────────\n\n/**\n * A dependency recorded during getter execution.\n * prop: the property name read on `this`.\n * value: the value returned (reference).\n */\ntype GetterDep = {prop: string | symbol; value: unknown};\n\n/**\n * Cached result of a snapshot getter with the dependencies it read,\n * used for cross-snapshot memoization. When a new snapshot is created,\n * we check if the deps are still reference-equal (via structural sharing)\n * and return the cached result if so -- avoiding re-execution of the getter.\n */\ntype GetterMemoEntry = {deps: GetterDep[]; result: unknown};\n\n/**\n * Cross-snapshot getter memoization cache.\n * Keyed by raw class instance (target), maps each getter name to its last\n * deps + result. This enables getter result stability across snapshot\n * boundaries: if `this.items` hasn't changed between snapshots (same\n * reference via structural sharing), the getter returns the same result.\n */\nconst crossSnapshotMemo = new WeakMap<\n object,\n Map<string | symbol, GetterMemoEntry>\n>();\n\n/**\n * Run a getter against the snapshot with dependency tracking.\n *\n * Creates a lightweight Proxy that intercepts `this.prop` reads and records\n * which properties (and their values/references) the getter accessed. The\n * Proxy delegates to the real frozen snapshot for actual values, so getters\n * that read other getters trigger the memoized getter chain correctly.\n */\nfunction computeWithTracking(\n snap: object,\n getterFn: () => unknown,\n): {deps: GetterDep[]; result: unknown} {\n const deps: GetterDep[] = [];\n const readProps = new Set<string | symbol>();\n\n // Use an empty non-frozen target — we delegate everything to `snap`.\n const handler: ProxyHandler<object> = {\n get(_dummyTarget, prop, _receiver) {\n // Delegate to the real snapshot (receiver = snap so installed getters\n // run with `this = snap`, triggering their own memoization chain).\n const value = Reflect.get(snap, prop, snap);\n if (!readProps.has(prop)) {\n readProps.add(prop);\n deps.push({prop, value});\n }\n return value;\n },\n };\n\n const tracked = new Proxy({}, handler);\n const result = getterFn.call(tracked);\n return {deps, result};\n}\n\n/**\n * Check if all previously recorded deps still hold on the current snapshot.\n * For data properties this is a reference comparison (structural sharing\n * guarantees stable refs for unchanged sub-trees). For getter properties\n * this invokes the getter (which is itself memoized), then compares.\n */\nfunction areMemoizedDepsValid(currentSnap: object, deps: GetterDep[]): boolean {\n for (const dep of deps) {\n const currentValue = Reflect.get(currentSnap, dep.prop, currentSnap);\n if (!Object.is(currentValue, dep.value)) return false;\n }\n return true;\n}\n\n/**\n * Evaluate a snapshot getter with two layers of caching:\n *\n * 1. **Per-snapshot cache** — same getter on the same frozen snapshot always\n * returns the same reference.\n * 2. **Cross-snapshot memo** — if the properties the getter read last time are\n * structurally the same (reference equality via structural sharing), the\n * previous result is returned without re-running the getter body.\n */\nfunction evaluateSnapshotGetter(\n currentSnap: object,\n target: object,\n key: string | symbol,\n getterFn: () => unknown,\n): unknown {\n // ── Per-snapshot fast path ──\n const perSnapCache = snapshotGetterCache.get(currentSnap);\n if (perSnapCache?.has(key)) return perSnapCache.get(key);\n\n // ── Cross-snapshot memo ──\n let memoMap = crossSnapshotMemo.get(target);\n const prev = memoMap?.get(key);\n\n let result: unknown;\n\n if (prev && areMemoizedDepsValid(currentSnap, prev.deps)) {\n // Dependencies unchanged → reuse previous result.\n result = prev.result;\n } else {\n // Compute fresh with dep tracking.\n const computation = computeWithTracking(currentSnap, getterFn);\n result = computation.result;\n\n // Save cross-snapshot memo.\n if (!memoMap) {\n memoMap = new Map();\n crossSnapshotMemo.set(target, memoMap);\n }\n memoMap.set(key, {deps: computation.deps, result});\n }\n\n // Save per-snapshot cache.\n let cache = snapshotGetterCache.get(currentSnap);\n if (!cache) {\n cache = new Map();\n snapshotGetterCache.set(currentSnap, cache);\n }\n cache.set(key, result);\n\n return result;\n}\n\n// ── Internal helpers ──────────────────────────────────────────────────────────\n\n/**\n * Resolve a single property value into its snapshot equivalent.\n *\n * - If the key has a tracked child internal → recurse (version-cached, structural sharing).\n * - If the value is a nested plain object/array without tracking → deep-clone & freeze (cached by identity).\n * - Otherwise → return the value as-is (primitive, Date, Map, function, etc.).\n */\nfunction snapshotValue(\n value: unknown,\n parentInternal: StoreInternal,\n key: string | symbol,\n): unknown {\n const childInternal = parentInternal.childInternals.get(key);\n if (childInternal) {\n return createSnapshotRecursive(childInternal.target, childInternal);\n }\n if (canProxy(value)) {\n return deepFreezeClone(value as object);\n }\n return value;\n}\n\n/**\n * Deep-clone and freeze a plain object or array that is NOT tracked by a proxy.\n * Cached by raw object identity for structural sharing across snapshots.\n */\nfunction deepFreezeClone(value: object): object {\n const cached = untrackedCache.get(value);\n if (cached) return cached;\n\n let clone: Record<string | symbol, unknown> | unknown[];\n\n if (Array.isArray(value)) {\n clone = [];\n for (let i = 0; i < value.length; i++) {\n const item = value[i];\n (clone as unknown[])[i] = canProxy(item)\n ? deepFreezeClone(item as object)\n : item;\n }\n } else {\n clone = Object.create(Object.getPrototypeOf(value));\n for (const key of Reflect.ownKeys(value)) {\n const desc = Object.getOwnPropertyDescriptor(value, key);\n if (!desc || !('value' in desc)) continue;\n const item = desc.value;\n (clone as Record<string | symbol, unknown>)[key] = canProxy(item)\n ? deepFreezeClone(item as object)\n : item;\n }\n }\n\n Object.freeze(clone);\n untrackedCache.set(value, clone);\n return clone;\n}\n\n/**\n * Collect all getter descriptors from the prototype chain of `target`.\n * Returns an array of [propertyName, getterFunction] pairs.\n *\n * Only includes getters defined on the prototype (class getters), not on the\n * instance itself. When a getter is overridden in a subclass, the most-derived\n * version wins (we walk from the instance's direct prototype upward and skip\n * keys already seen).\n */\nfunction collectGetters(\n target: object,\n): Array<[string | symbol, () => unknown]> {\n const getters: Array<[string | symbol, () => unknown]> = [];\n const seen = new Set<string | symbol>();\n let proto: object | null = Object.getPrototypeOf(target);\n while (proto && proto !== Object.prototype) {\n for (const key of Reflect.ownKeys(proto)) {\n if (key === 'constructor') continue;\n if (seen.has(key)) continue; // most-derived version already collected\n const desc = Object.getOwnPropertyDescriptor(proto, key);\n if (desc?.get) {\n getters.push([key, desc.get]);\n seen.add(key);\n }\n }\n proto = Object.getPrototypeOf(proto);\n }\n return getters;\n}\n\n/**\n * Install lazy-memoizing getters on a snapshot object.\n *\n * Each getter uses cross-snapshot memoization:\n * - Tracks which `this` properties the getter reads on first evaluation.\n * - On subsequent snapshots, if those properties are structurally the same\n * (thanks to structural sharing), the previous result is returned.\n * - Within the same snapshot, repeated accesses always return the same ref.\n */\nfunction installMemoizedGetters(\n snap: Record<string | symbol, unknown>,\n target: object,\n): void {\n const getters = collectGetters(target);\n for (const [key, getterFn] of getters) {\n Object.defineProperty(snap, key, {\n get() {\n return evaluateSnapshotGetter(this as object, target, key, getterFn);\n },\n enumerable: true,\n configurable: true, // required so Object.freeze can make it non-configurable\n });\n }\n}\n\n/**\n * Recursively creates a frozen snapshot from a tracked (proxied) sub-tree.\n *\n * Each node checks its version-stamped cache first (O(1) hit). On a miss,\n * it builds a new frozen object by recursing into child internals (tracked\n * sub-trees) and deep-cloning untracked nested objects. Unchanged children\n * return the same cached reference, achieving structural sharing -- the key\n * to efficient `Object.is` equality in selectors.\n *\n * For class instances, the prototype chain is preserved and class getters\n * are installed as lazy-memoizing accessors via `installMemoizedGetters`.\n */\nfunction createSnapshotRecursive<T extends object>(\n target: T,\n internal: StoreInternal,\n): T {\n // Cache hit: version unchanged → return the same frozen snapshot reference.\n const cached = snapshotCache.get(target);\n if (cached && cached[0] === internal.version) {\n return cached[1] as T;\n }\n\n let snap: Record<string | symbol, unknown> | unknown[];\n\n if (Array.isArray(target)) {\n snap = [];\n for (let i = 0; i < target.length; i++) {\n (snap as unknown[])[i] = snapshotValue(target[i], internal, String(i));\n }\n } else {\n // Preserve the prototype chain and install memoized getters on the snapshot.\n snap = Object.create(Object.getPrototypeOf(target));\n for (const key of Reflect.ownKeys(target)) {\n // Skip prototype getters — they re-evaluate via the preserved prototype.\n if (findGetterDescriptor(target, key)?.get) continue;\n\n const desc = Object.getOwnPropertyDescriptor(target, key);\n if (!desc || !('value' in desc)) continue;\n\n (snap as Record<string | symbol, unknown>)[key] = snapshotValue(\n desc.value,\n internal,\n key,\n );\n }\n\n // Install lazy-memoizing getters with cross-snapshot caching.\n installMemoizedGetters(snap as Record<string | symbol, unknown>, target);\n }\n\n Object.freeze(snap);\n // Cache AFTER populating + freezing. The reference is stable.\n snapshotCache.set(target, [internal.version, snap]);\n return snap as T;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/**\n * Creates an immutable, deeply-frozen snapshot of the store proxy's current state.\n *\n * **Structural sharing:** unchanged sub-trees reuse the previous snapshot's\n * object reference, so `===` comparison can cheaply detect changes.\n *\n * **Version-cached:** calling `snapshot()` multiple times without intervening\n * mutations returns the identical snapshot object (O(1) cache hit).\n *\n * **Getters:** class getters are automatically memoized — they compute once\n * per snapshot and their results are stable across snapshots when dependencies\n * haven't changed (cross-snapshot memoization).\n *\n * @param proxyStore - A reactive proxy created by `createClassyStore()`.\n * @returns A deeply frozen plain-JS object (Snapshot<T>).\n */\nexport function snapshot<T extends object>(proxyStore: T): Snapshot<T> {\n const internal = getInternal(proxyStore);\n return createSnapshotRecursive(internal.target, internal) as Snapshot<T>;\n}\n"],"mappings":"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"}
|
|
@@ -24,4 +24,4 @@ type SnapshotLeaf = Primitive | AnyFunction | Date | RegExp | Error | Map<unknow
|
|
|
24
24
|
type Snapshot<T> = T extends SnapshotLeaf ? T : T extends Array<infer U> ? ReadonlyArray<Snapshot<U>> : T extends object ? { readonly [K in keyof T]: Snapshot<T[K]> } : T;
|
|
25
25
|
//#endregion
|
|
26
26
|
export { Snapshot as t };
|
|
27
|
-
//# sourceMappingURL=types-
|
|
27
|
+
//# sourceMappingURL=types-Cf8Fp7kA.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-Cf8Fp7kA.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;KAIK,SAAA;;AAAS;;;;KAOT,WAAA,OAAkB,IAAA;AAAI;;;;;;AAAA,KAQtB,YAAA,GACD,SAAA,GACA,WAAA,GACA,IAAA,GACA,MAAA,GACA,KAAA,GACA,GAAA,qBACA,GAAA,YACA,OAAA,oBACA,OAAA,WACA,OAAA;;;;;KAMQ,QAAA,MAAc,CAAA,SAAU,YAAA,GAChC,CAAA,GACA,CAAA,SAAU,KAAA,YACR,aAAA,CAAc,QAAA,CAAS,CAAA,KACvB,CAAA,yCACwB,CAAA,GAAI,QAAA,CAAS,CAAA,CAAE,CAAA,OACrC,CAAA"}
|
package/dist/utils/index.cjs
CHANGED
|
@@ -1,6 +1,138 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_snapshot = require('../snapshot-
|
|
2
|
+
const require_snapshot = require('../snapshot-CbVbxG7s.cjs');
|
|
3
|
+
const require_equality = require('../equality-Cz6riknL.cjs');
|
|
3
4
|
|
|
5
|
+
//#region src/utils/devtools/devtools.ts
|
|
6
|
+
/**
|
|
7
|
+
* Connect a store proxy to Redux DevTools for state inspection and time-travel debugging.
|
|
8
|
+
*
|
|
9
|
+
* Uses `subscribe()` + `snapshot()` to send state on each change.
|
|
10
|
+
* Listens for `DISPATCH` messages (`JUMP_TO_STATE`, `JUMP_TO_ACTION`) and applies
|
|
11
|
+
* the received state back to the store proxy, skipping getters and methods.
|
|
12
|
+
*
|
|
13
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
14
|
+
* @param options - Optional configuration.
|
|
15
|
+
* @returns A dispose function that disconnects from DevTools and unsubscribes.
|
|
16
|
+
*/
|
|
17
|
+
function devtools(proxyStore, options) {
|
|
18
|
+
const { name = "ClassyStore", enabled = true } = options ?? {};
|
|
19
|
+
if (!enabled || typeof window === "undefined" || !window.__REDUX_DEVTOOLS_EXTENSION__) return () => {};
|
|
20
|
+
const connection = window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name });
|
|
21
|
+
connection.init(require_snapshot.snapshot(proxyStore));
|
|
22
|
+
let isTimeTraveling = false;
|
|
23
|
+
const unsubscribeFromStore = require_snapshot.subscribe(proxyStore, () => {
|
|
24
|
+
if (isTimeTraveling) return;
|
|
25
|
+
connection.send({ type: "STORE_UPDATE" }, require_snapshot.snapshot(proxyStore));
|
|
26
|
+
});
|
|
27
|
+
const devToolsUnsub = connection.subscribe((message) => {
|
|
28
|
+
if (message.type === "DISPATCH" && message.state) {
|
|
29
|
+
const payloadType = message.payload?.type;
|
|
30
|
+
if (payloadType === "JUMP_TO_STATE" || payloadType === "JUMP_TO_ACTION") try {
|
|
31
|
+
const newState = JSON.parse(message.state);
|
|
32
|
+
isTimeTraveling = true;
|
|
33
|
+
for (const key of Object.keys(newState)) {
|
|
34
|
+
if (require_snapshot.findGetterDescriptor(proxyStore, key)?.get) continue;
|
|
35
|
+
if (typeof proxyStore[key] === "function") continue;
|
|
36
|
+
proxyStore[key] = newState[key];
|
|
37
|
+
}
|
|
38
|
+
queueMicrotask(() => {
|
|
39
|
+
isTimeTraveling = false;
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
isTimeTraveling = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return () => {
|
|
47
|
+
unsubscribeFromStore();
|
|
48
|
+
if (typeof devToolsUnsub === "function") devToolsUnsub();
|
|
49
|
+
else if (devToolsUnsub && typeof devToolsUnsub.unsubscribe === "function") devToolsUnsub.unsubscribe();
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/utils/history/history.ts
|
|
55
|
+
/**
|
|
56
|
+
* Add undo/redo capability to a store proxy via a snapshot stack.
|
|
57
|
+
*
|
|
58
|
+
* Captures a snapshot on each mutation and maintains a history array with a
|
|
59
|
+
* pointer. `undo()` and `redo()` apply previous/next snapshots back to the
|
|
60
|
+
* store proxy, skipping getters and methods.
|
|
61
|
+
*
|
|
62
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
63
|
+
* @param options - Optional configuration (e.g., history limit).
|
|
64
|
+
* @returns A `HistoryHandle` with undo/redo controls and a dispose function.
|
|
65
|
+
*/
|
|
66
|
+
function withHistory(proxyStore, options) {
|
|
67
|
+
const limit = options?.limit ?? 100;
|
|
68
|
+
const history = [require_snapshot.snapshot(proxyStore)];
|
|
69
|
+
let pointer = 0;
|
|
70
|
+
let paused = false;
|
|
71
|
+
/**
|
|
72
|
+
* Apply a snapshot's data properties back to the store proxy.
|
|
73
|
+
* Skips getters and methods — same pattern as `persist`.
|
|
74
|
+
*/
|
|
75
|
+
function applySnapshot(snap) {
|
|
76
|
+
const snapRecord = snap;
|
|
77
|
+
for (const key of Object.keys(snapRecord)) {
|
|
78
|
+
if (require_snapshot.findGetterDescriptor(proxyStore, key)?.get) continue;
|
|
79
|
+
if (typeof proxyStore[key] === "function") continue;
|
|
80
|
+
proxyStore[key] = snapRecord[key];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const unsubscribeFromStore = require_snapshot.subscribe(proxyStore, () => {
|
|
84
|
+
if (paused) return;
|
|
85
|
+
const snap = require_snapshot.snapshot(proxyStore);
|
|
86
|
+
if (pointer < history.length - 1) history.length = pointer + 1;
|
|
87
|
+
history.push(snap);
|
|
88
|
+
pointer = history.length - 1;
|
|
89
|
+
if (history.length > limit) {
|
|
90
|
+
history.shift();
|
|
91
|
+
pointer = history.length - 1;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
undo() {
|
|
96
|
+
if (pointer <= 0) return;
|
|
97
|
+
pointer--;
|
|
98
|
+
paused = true;
|
|
99
|
+
try {
|
|
100
|
+
applySnapshot(history[pointer]);
|
|
101
|
+
} finally {
|
|
102
|
+
paused = false;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
redo() {
|
|
106
|
+
if (pointer >= history.length - 1) return;
|
|
107
|
+
pointer++;
|
|
108
|
+
paused = true;
|
|
109
|
+
try {
|
|
110
|
+
applySnapshot(history[pointer]);
|
|
111
|
+
} finally {
|
|
112
|
+
paused = false;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
get canUndo() {
|
|
116
|
+
return pointer > 0;
|
|
117
|
+
},
|
|
118
|
+
get canRedo() {
|
|
119
|
+
return pointer < history.length - 1;
|
|
120
|
+
},
|
|
121
|
+
pause() {
|
|
122
|
+
paused = true;
|
|
123
|
+
},
|
|
124
|
+
resume() {
|
|
125
|
+
paused = false;
|
|
126
|
+
},
|
|
127
|
+
dispose() {
|
|
128
|
+
unsubscribeFromStore();
|
|
129
|
+
history.length = 0;
|
|
130
|
+
pointer = 0;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
4
136
|
//#region src/utils/persist/persist.ts
|
|
5
137
|
/** Check if a value is a PropertyTransform descriptor (has `key` + `serialize`). */
|
|
6
138
|
function isTransform(entry) {
|
|
@@ -22,7 +154,7 @@ function resolveProperties(proxyStore, properties) {
|
|
|
22
154
|
const snap = require_snapshot.snapshot(proxyStore);
|
|
23
155
|
const result = [];
|
|
24
156
|
for (const key of Object.keys(snap)) {
|
|
25
|
-
if (require_snapshot.findGetterDescriptor(
|
|
157
|
+
if (require_snapshot.findGetterDescriptor(proxyStore, key)?.get) continue;
|
|
26
158
|
if (typeof proxyStore[key] === "function") continue;
|
|
27
159
|
result.push({ key });
|
|
28
160
|
}
|
|
@@ -54,7 +186,7 @@ function getDefaultStorage() {
|
|
|
54
186
|
* On init (or manual rehydrate), reads from storage and applies the state back
|
|
55
187
|
* to the store proxy.
|
|
56
188
|
*
|
|
57
|
-
* @param proxyStore - A reactive proxy created by `
|
|
189
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
58
190
|
* @param options - Persistence configuration.
|
|
59
191
|
* @returns A handle with lifecycle controls (unsubscribe, save, clear, rehydrate, hydrated).
|
|
60
192
|
*/
|
|
@@ -68,6 +200,7 @@ function persist(proxyStore, options) {
|
|
|
68
200
|
for (const prop of resolvedProps) if (prop.transform) transformMap.set(prop.key, prop.transform);
|
|
69
201
|
const propKeys = resolvedProps.map((p) => p.key);
|
|
70
202
|
let disposed = false;
|
|
203
|
+
let hydrating = false;
|
|
71
204
|
let debounceTimer = null;
|
|
72
205
|
let hydratedFlag = false;
|
|
73
206
|
let expiredFlag = false;
|
|
@@ -102,7 +235,7 @@ function persist(proxyStore, options) {
|
|
|
102
235
|
}
|
|
103
236
|
/** Schedule a debounced write (or write immediately if debounce is 0). */
|
|
104
237
|
function scheduleWrite() {
|
|
105
|
-
if (disposed) return;
|
|
238
|
+
if (disposed || hydrating) return;
|
|
106
239
|
if (debounceMs <= 0) {
|
|
107
240
|
writeToStorage();
|
|
108
241
|
return;
|
|
@@ -124,7 +257,7 @@ function persist(proxyStore, options) {
|
|
|
124
257
|
} catch {
|
|
125
258
|
return;
|
|
126
259
|
}
|
|
127
|
-
if (!envelope || typeof envelope !== "object" || typeof envelope.state !== "object") return;
|
|
260
|
+
if (!envelope || typeof envelope !== "object" || envelope.state === null || typeof envelope.state !== "object") return;
|
|
128
261
|
if (typeof envelope.expiresAt === "number" && Date.now() >= envelope.expiresAt) {
|
|
129
262
|
expiredFlag = true;
|
|
130
263
|
if (clearOnExpire) storage.removeItem(name);
|
|
@@ -141,6 +274,7 @@ function persist(proxyStore, options) {
|
|
|
141
274
|
for (const key of propKeys) currentState[key] = currentSnap[key];
|
|
142
275
|
let merged;
|
|
143
276
|
if (typeof merge === "function") merged = merge(state, currentState);
|
|
277
|
+
else if (merge === "replace") merged = state;
|
|
144
278
|
else merged = {
|
|
145
279
|
...currentState,
|
|
146
280
|
...state
|
|
@@ -150,7 +284,12 @@ function persist(proxyStore, options) {
|
|
|
150
284
|
/** Read from storage and apply to the store. */
|
|
151
285
|
async function hydrateFromStorage() {
|
|
152
286
|
const raw = await storage.getItem(name);
|
|
153
|
-
if (raw !== null)
|
|
287
|
+
if (raw !== null) {
|
|
288
|
+
hydrating = true;
|
|
289
|
+
applyPersistedState(raw);
|
|
290
|
+
await new Promise((r) => queueMicrotask(r));
|
|
291
|
+
hydrating = false;
|
|
292
|
+
}
|
|
154
293
|
}
|
|
155
294
|
const shouldSyncTabs = syncTabsOption !== void 0 ? syncTabsOption : isLocalStorage(storage);
|
|
156
295
|
/** Handler for `window.storage` events. */
|
|
@@ -196,9 +335,11 @@ function persist(proxyStore, options) {
|
|
|
196
335
|
await writeToStorage();
|
|
197
336
|
},
|
|
198
337
|
async clear() {
|
|
338
|
+
if (disposed) return;
|
|
199
339
|
await storage.removeItem(name);
|
|
200
340
|
},
|
|
201
341
|
async rehydrate() {
|
|
342
|
+
expiredFlag = false;
|
|
202
343
|
await hydrateFromStorage();
|
|
203
344
|
if (!hydratedFlag) {
|
|
204
345
|
hydratedFlag = true;
|
|
@@ -209,5 +350,35 @@ function persist(proxyStore, options) {
|
|
|
209
350
|
}
|
|
210
351
|
|
|
211
352
|
//#endregion
|
|
353
|
+
//#region src/utils/subscribe-key/subscribe-key.ts
|
|
354
|
+
/**
|
|
355
|
+
* Subscribe to changes on a single property of a store proxy.
|
|
356
|
+
*
|
|
357
|
+
* Wraps `subscribe()` + `snapshot()` and compares `snapshot()[key]` with the
|
|
358
|
+
* previous value via `Object.is()`. The callback fires only when the watched
|
|
359
|
+
* property actually changes.
|
|
360
|
+
*
|
|
361
|
+
* @param proxyStore - A reactive proxy created by `createClassyStore()`.
|
|
362
|
+
* @param key - The property key to watch.
|
|
363
|
+
* @param callback - Called with `(value, previousValue)` when the property changes.
|
|
364
|
+
* @returns An unsubscribe function.
|
|
365
|
+
*/
|
|
366
|
+
function subscribeKey(proxyStore, key, callback) {
|
|
367
|
+
let previousValue = require_snapshot.snapshot(proxyStore)[key];
|
|
368
|
+
return require_snapshot.subscribe(proxyStore, () => {
|
|
369
|
+
const currentValue = require_snapshot.snapshot(proxyStore)[key];
|
|
370
|
+
if (!Object.is(currentValue, previousValue)) {
|
|
371
|
+
const prev = previousValue;
|
|
372
|
+
previousValue = currentValue;
|
|
373
|
+
callback(currentValue, prev);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
//#endregion
|
|
379
|
+
exports.devtools = devtools;
|
|
212
380
|
exports.persist = persist;
|
|
381
|
+
exports.shallowEqual = require_equality.shallowEqual;
|
|
382
|
+
exports.subscribeKey = subscribeKey;
|
|
383
|
+
exports.withHistory = withHistory;
|
|
213
384
|
//# sourceMappingURL=index.cjs.map
|