scope-state 0.1.4 → 0.1.6

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/README.md CHANGED
@@ -170,7 +170,10 @@ const TodoList = () => {
170
170
  ### Core Functions
171
171
 
172
172
  #### `useScope(selector)`
173
- Subscribe to reactive state changes.
173
+ Subscribe to reactive state changes and receive a read-only snapshot for rendering.
174
+
175
+ Mutate state through the main `$` proxy. The value returned from `useScope()` is
176
+ intended for reads only.
174
177
 
175
178
  ```tsx
176
179
  // Subscribe to entire object
@@ -184,6 +187,10 @@ const isAdmin = useScope(() => $.user.role === 'admin');
184
187
 
185
188
  // Subscribe to array
186
189
  const todos = useScope(() => $.todos);
190
+
191
+ // Mutate through the main store proxy
192
+ $.user.$merge({ name: 'John' });
193
+ $.todos.push({ id: 1, text: 'Ship feature', completed: false });
187
194
  ```
188
195
 
189
196
  #### `configure(options)`
@@ -1,5 +1,6 @@
1
1
  import type { CustomMethods, CustomArrayMethods } from '../types';
2
2
  export declare const proxyPathMap: WeakMap<object, string[]>;
3
+ export declare const proxyTargetMap: WeakMap<object, object>;
3
4
  export declare const pathUsageStats: {
4
5
  accessedPaths: Set<string>;
5
6
  modifiedPaths: Set<string>;
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/core/proxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAIlE,eAAO,MAAM,YAAY,2BAAkC,CAAC;AAM5D,eAAO,MAAM,cAAc;;;;CAI1B,CAAC;AAGF,eAAO,MAAM,aAAa,aAAoB,CAAC;AAM/C,eAAO,MAAM,eAAe;;;;;CAK3B,CAAC;AAiGF,eAAO,MAAM,cAAc;;;;;;2BA4CF,MAAM;CAS9B,CAAC;AAEF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAErD;AAYD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,EAClD,MAAM,EAAE,CAAC,EACT,IAAI,GAAE,MAAM,EAAO,EACnB,KAAK,GAAE,MAAU,GAChB,CAAC,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CA4JrE;AA8UD;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAUtC;AAED;;GAEG;AACH,wBAAgB,kBAAkB;;;;;;;EAMjC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,UAAQ,GAAG,GAAG,CA4B3D"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/core/proxy.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAIlE,eAAO,MAAM,YAAY,2BAAkC,CAAC;AAC5D,eAAO,MAAM,cAAc,yBAAgC,CAAC;AAM5D,eAAO,MAAM,cAAc;;;;CAI1B,CAAC;AAGF,eAAO,MAAM,aAAa,aAAoB,CAAC;AAM/C,eAAO,MAAM,eAAe;;;;;CAK3B,CAAC;AAiGF,eAAO,MAAM,cAAc;;;;;;2BA4CF,MAAM;CAS9B,CAAC;AAEF;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAErD;AAYD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,MAAM,EAClD,MAAM,EAAE,CAAC,EACT,IAAI,GAAE,MAAM,EAAO,EACnB,KAAK,GAAE,MAAU,GAChB,CAAC,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAwJrE;AAqRD;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAUtC;AAED;;GAEG;AACH,wBAAgB,kBAAkB;;;;;;;EAMjC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,UAAQ,GAAG,GAAG,CA4B3D"}
@@ -0,0 +1,9 @@
1
+ import type { ScopeSnapshot } from '../types';
2
+ /**
3
+ * Create a read-only snapshot suitable for rendering.
4
+ *
5
+ * Snapshots are plain arrays/objects with no proxy methods, which makes them
6
+ * safe for React Compiler memoization while keeping `$` as the mutable API.
7
+ */
8
+ export declare function createReadonlySnapshot<T>(value: T, seen?: WeakMap<object, unknown>): ScopeSnapshot<T>;
9
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/core/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAgB9C;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EACtC,KAAK,EAAE,CAAC,EACR,IAAI,2BAAiC,GACpC,aAAa,CAAC,CAAC,CAAC,CAsClB"}
@@ -1,4 +1,4 @@
1
- import type { CustomMethods, CustomArrayMethods } from '../types';
1
+ import type { ScopeSnapshot } from '../types';
2
2
  /**
3
3
  * Hook to subscribe to the global store and re-render when specific data changes.
4
4
  *
@@ -6,15 +6,16 @@ import type { CustomMethods, CustomArrayMethods } from '../types';
6
6
  * re-renders the component when those specific paths change. This provides
7
7
  * fine-grained reactivity without unnecessary renders.
8
8
  *
9
- * For objects, the returned value includes custom methods ($merge, $set, $delete, $update)
10
- * that allow you to modify the data directly and trigger reactive updates.
9
+ * The returned value is a read-only snapshot of the selected state. Mutate the
10
+ * store through the main `$` proxy (or a proxy created explicitly for commands),
11
+ * and use the hook return value only for rendering.
11
12
  *
12
13
  * @example
13
14
  * // Subscribe to user data
14
15
  * const user = useScope(() => $.user);
15
16
  *
16
- * // Update user data directly (triggers re-render only for components using $.user)
17
- * user.$merge({ name: 'New Name' });
17
+ * // Mutate through the main store proxy
18
+ * $.user.$merge({ name: 'New Name' });
18
19
  *
19
20
  * // Subscribe to a specific property
20
21
  * const userName = useScope(() => $.user.name);
@@ -23,7 +24,7 @@ import type { CustomMethods, CustomArrayMethods } from '../types';
23
24
  * const isAdmin = useScope(() => $.user.role === 'admin');
24
25
  *
25
26
  * @param selector - Function that returns the data you want to subscribe to
26
- * @returns The selected data, with custom methods attached if it's an object
27
+ * @returns A read-only snapshot of the selected data
27
28
  */
28
- export declare function useScope<T>(selector: () => T): T extends object ? T & CustomMethods<T> : T extends Array<infer U> ? U[] & CustomArrayMethods<U> : T;
29
+ export declare function useScope<T>(selector: () => T): ScopeSnapshot<T>;
29
30
  //# sourceMappingURL=useScope.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useScope.d.ts","sourceRoot":"","sources":["../../src/hooks/useScope.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,GAChB,CAAC,SAAS,MAAM,GACf,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,GACpB,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,CAAC,GACxB,CAAC,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC,GAC3B,CAAC,CAsDJ"}
1
+ {"version":3,"file":"useScope.d.ts","sourceRoot":"","sources":["../../src/hooks/useScope.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,GAChB,aAAa,CAAC,CAAC,CAAC,CAiFlB"}
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { persistenceAPI, hydrateState, mergePersistedIntoState } from './persist
7
7
  export { createLocalStorageAdapter, createMemoryAdapter, createCachedAdapter, getDefaultAdapter } from './persistence/adapters';
8
8
  export { setStorageAdapter, getStorageAdapter } from './persistence/storage';
9
9
  export { setInitialStoreState, createAdvancedProxy, pathUsageStats, selectorPaths, clearProxyCache, getProxyCacheStats, optimizeMemoryUsage, proxyPathMap } from './core/proxy';
10
- export type { ScopeConfig, ProxyConfig, MonitoringConfig, PersistenceConfig, StorageAdapter, MaybePromise, CustomMethods, CustomArrayMethods, StoreType, MonitoringStats, ProxyCacheStats, PathUsageStats } from './types';
10
+ export type { ScopeConfig, ProxyConfig, MonitoringConfig, PersistenceConfig, StorageAdapter, MaybePromise, CustomMethods, CustomArrayMethods, ScopeSnapshot, StoreType, MonitoringStats, ProxyCacheStats, PathUsageStats } from './types';
11
11
  import type { StoreType, CustomMethods, CustomArrayMethods, ScopeConfig } from './types';
12
12
  /**
13
13
  * Configure Scope with custom settings and return a properly typed store
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAIrE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChI,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACb,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,SAAS,EACT,eAAe,EACf,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAC;AASjB,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAYzF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrD,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,SAAS,CAAC,CAAC,CAAC,CAwEd;AAGD,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAqB7F;AASD,eAAO,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAoB,CAAC;AAGlD,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAE5H;AAGD,eAAO,MAAM,MAAM,uBAAiB,CAAC;AAGrC,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAE5C;AAGD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAyBrC;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAEtC;AAED,wBAAgB,QAAQ,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC,CAsClE;AAED,wBAAgB,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAK5D;AAYD,eAAO,MAAM,SAAS;;;;CAUrB,CAAC;AAGF,eAAO,MAAM,QAAQ,KAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAIrE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChI,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EACL,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,YAAY,EACb,MAAM,cAAc,CAAC;AAGtB,YAAY,EACV,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,SAAS,EACT,eAAe,EACf,eAAe,EACf,cAAc,EACf,MAAM,SAAS,CAAC;AASjB,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAYzF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACrD,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GACrB,SAAS,CAAC,CAAC,CAAC,CAwEd;AAGD,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAqB7F;AASD,eAAO,MAAM,CAAC,EAAE,SAAS,CAAC,GAAG,CAAoB,CAAC;AAGlD,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,GAAG,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAE5H;AAGD,eAAO,MAAM,MAAM,uBAAiB,CAAC;AAGrC,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAE5C;AAGD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAyBrC;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAEtC;AAED,wBAAgB,QAAQ,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC,CAsClE;AAED,wBAAgB,IAAI,CAAC,CAAC,GAAG,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAK5D;AAYD,eAAO,MAAM,SAAS;;;;CAUrB,CAAC;AAGF,eAAO,MAAM,QAAQ,KAAc,CAAC"}
package/dist/index.esm.js CHANGED
@@ -351,6 +351,7 @@ function logSubscriptionRemoved(path) {
351
351
 
352
352
  // Track proxy to path mapping for type-safe activation
353
353
  const proxyPathMap = new WeakMap();
354
+ const proxyTargetMap = new WeakMap();
354
355
  // Store a deep clone of the initial store state for use with $reset
355
356
  let initialStoreState$1 = {};
356
357
  // Path usage tracking
@@ -568,6 +569,11 @@ function createAdvancedProxy(target, path = [], depth = 0) {
568
569
  if (typeof prop === 'symbol') {
569
570
  return Reflect.get(obj, prop, receiver);
570
571
  }
572
+ const value = Reflect.get(obj, prop, receiver);
573
+ // Functions are commands/helpers, not reactive data dependencies.
574
+ if (typeof value === 'function') {
575
+ return value;
576
+ }
571
577
  const currentPropPath = [...path, prop.toString()];
572
578
  const propPathKey = currentPropPath.join('.');
573
579
  // Track path access during dependency tracking
@@ -578,9 +584,12 @@ function createAdvancedProxy(target, path = [], depth = 0) {
578
584
  }
579
585
  // Track path access for dependency tracking (inline to avoid circular dependency)
580
586
  trackPathAccess(currentPropPath);
581
- const value = obj[prop];
582
587
  // For objects, create proxies for nested values
583
588
  if (value && typeof value === 'object' && path.length < proxyConfig.maxDepth) {
589
+ // If value is already a proxy, return it directly to prevent double-wrapping
590
+ if (proxyPathMap.has(value)) {
591
+ return value;
592
+ }
584
593
  const shouldProxy = !proxyConfig.lazyProxyDeepObjects ||
585
594
  pathUsageStats.accessedPaths.has(propPathKey) ||
586
595
  pathUsageStats.modifiedPaths.has(propPathKey) ||
@@ -592,27 +601,16 @@ function createAdvancedProxy(target, path = [], depth = 0) {
592
601
  }
593
602
  return value;
594
603
  },
595
- set(obj, prop, value, receiver) {
604
+ set(obj, prop, value) {
596
605
  if (typeof prop === 'symbol') {
597
- return Reflect.set(obj, prop, value, receiver);
606
+ return Reflect.set(obj, prop, value);
598
607
  }
599
608
  const propPath = [...path, prop.toString()];
600
609
  const propPathKey = propPath.join('.');
601
- // Track modification
602
610
  if (proxyConfig.trackPathUsage) {
603
611
  pathUsageStats.modifiedPaths.add(propPathKey);
604
612
  }
605
- // Handle object assignment with proxying
606
- if (value && typeof value === 'object' && !proxyCache.has(value)) {
607
- const newPath = [...path, prop.toString()];
608
- const proxiedValue = createAdvancedProxy(value, newPath, 0);
609
- const result = Reflect.set(obj, prop, proxiedValue, receiver);
610
- // Notify the specific property path and all parent/child paths
611
- notifyListeners(propPath);
612
- return result;
613
- }
614
- const result = Reflect.set(obj, prop, value, receiver);
615
- // Notify the specific property path (this will also notify parent/child paths)
613
+ const result = Reflect.set(obj, prop, value);
616
614
  notifyListeners(propPath);
617
615
  return result;
618
616
  },
@@ -638,6 +636,7 @@ function createAdvancedProxy(target, path = [], depth = 0) {
638
636
  proxyCacheLRU.add(target, proxy);
639
637
  // Track the path for this proxy
640
638
  proxyPathMap.set(proxy, [...path]);
639
+ proxyTargetMap.set(proxy, target);
641
640
  return proxy;
642
641
  }
643
642
  /**
@@ -654,16 +653,7 @@ function addObjectMethods(target, path) {
654
653
  const propPath = [...currentPath, key].join('.');
655
654
  pathUsageStats.modifiedPaths.add(propPath);
656
655
  }
657
- const newValue = newProps[key];
658
- if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
659
- const newPath = [...currentPath, key];
660
- // Use Reflect.set with this proxy as the receiver to trigger the set handler
661
- Reflect.set(this, key, createAdvancedProxy(newValue, newPath, 0), this);
662
- }
663
- else {
664
- // Use Reflect.set with this proxy as the receiver to trigger the set handler
665
- Reflect.set(this, key, newValue, this);
666
- }
656
+ Reflect.set(this, key, newProps[key], this);
667
657
  });
668
658
  return this;
669
659
  },
@@ -674,28 +664,17 @@ function addObjectMethods(target, path) {
674
664
  methodsToDefine.$set = {
675
665
  value: function (newProps) {
676
666
  const currentPath = proxyPathMap.get(this) || path;
677
- // Clear existing properties
678
667
  Object.keys(this).forEach(key => {
679
668
  if (typeof this[key] !== 'function') {
680
669
  Reflect.deleteProperty(this, key);
681
670
  }
682
671
  });
683
- // Set new properties
684
672
  Object.keys(newProps || {}).forEach(key => {
685
673
  if (proxyConfig.trackPathUsage) {
686
674
  const propPath = [...currentPath, key].join('.');
687
675
  pathUsageStats.modifiedPaths.add(propPath);
688
676
  }
689
- const newValue = newProps[key];
690
- if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
691
- const newPath = [...currentPath, key];
692
- // Use Reflect.set with this proxy as the receiver to trigger the set handler
693
- Reflect.set(this, key, createAdvancedProxy(newValue, newPath, 0), this);
694
- }
695
- else {
696
- // Use Reflect.set with this proxy as the receiver to trigger the set handler
697
- Reflect.set(this, key, newValue, this);
698
- }
677
+ Reflect.set(this, key, newProps[key], this);
699
678
  });
700
679
  return this;
701
680
  },
@@ -725,12 +704,7 @@ function addObjectMethods(target, path) {
725
704
  pathUsageStats.modifiedPaths.add(propPath);
726
705
  }
727
706
  const currentValue = this[key];
728
- let newValue = updater(currentValue);
729
- if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
730
- const newPath = [...currentPath, key];
731
- newValue = createAdvancedProxy(newValue, newPath, 0);
732
- }
733
- // Use Reflect.set with this proxy as the receiver to trigger the set handler
707
+ const newValue = updater(currentValue);
734
708
  Reflect.set(this, key, newValue, this);
735
709
  return this;
736
710
  },
@@ -788,20 +762,13 @@ function addObjectMethods(target, path) {
788
762
  * Add custom methods to array targets
789
763
  */
790
764
  function addArrayMethods(target, path) {
791
- const originalPush = target.push;
792
- const originalSplice = target.splice;
765
+ const originalPush = Array.prototype.push;
766
+ const originalSplice = Array.prototype.splice;
793
767
  // Override push
794
768
  Object.defineProperty(target, 'push', {
795
769
  value: function (...items) {
796
770
  const currentPath = proxyPathMap.get(this) || path;
797
- const processedItems = items.map(item => {
798
- if (item && typeof item === 'object' && !proxyCache.has(item)) {
799
- const itemPath = [...currentPath, '_item'];
800
- return createAdvancedProxy(item, itemPath, 0);
801
- }
802
- return item;
803
- });
804
- const result = originalPush.apply(this, processedItems);
771
+ const result = originalPush.apply(this, items);
805
772
  if (currentPath.length > 0) {
806
773
  if (proxyConfig.trackPathUsage) {
807
774
  pathUsageStats.modifiedPaths.add(currentPath.join('.'));
@@ -818,22 +785,13 @@ function addArrayMethods(target, path) {
818
785
  value: function (start, deleteCount, ...items) {
819
786
  const currentPath = proxyPathMap.get(this) || path;
820
787
  const arrayLength = this.length;
821
- const processedItems = items.map((item, index) => {
822
- if (item && typeof item === 'object' && !proxyCache.has(item)) {
823
- const itemPath = [...currentPath, (start + index).toString()];
824
- return createAdvancedProxy(item, itemPath, 0);
825
- }
826
- return item;
827
- });
828
788
  const actualDeleteCount = deleteCount === undefined ? (arrayLength - start) : deleteCount;
829
- const result = originalSplice.apply(this, [start, actualDeleteCount, ...processedItems]);
789
+ const result = originalSplice.apply(this, [start, actualDeleteCount, ...items]);
830
790
  if (currentPath.length > 0) {
831
791
  if (proxyConfig.trackPathUsage) {
832
792
  pathUsageStats.modifiedPaths.add(currentPath.join('.'));
833
793
  }
834
- // Notify the array itself
835
794
  notifyListeners(currentPath);
836
- // Also notify about each index that was affected
837
795
  for (let i = start; i < arrayLength; i++) {
838
796
  const indexPath = [...currentPath, i.toString()];
839
797
  notifyListeners(indexPath);
@@ -854,14 +812,7 @@ function addArrayMethods(target, path) {
854
812
  }
855
813
  const currentPath = proxyPathMap.get(this) || path;
856
814
  this.length = 0;
857
- const processedItems = newArray.map((item, index) => {
858
- if (item && typeof item === 'object' && !proxyCache.has(item)) {
859
- const itemPath = [...currentPath, index.toString()];
860
- return createAdvancedProxy(item, itemPath, 0);
861
- }
862
- return item;
863
- });
864
- originalPush.apply(this, processedItems);
815
+ originalPush.apply(this, newArray);
865
816
  if (currentPath.length > 0) {
866
817
  if (proxyConfig.trackPathUsage) {
867
818
  pathUsageStats.modifiedPaths.add(currentPath.join('.'));
@@ -893,15 +844,8 @@ function addArrayMethods(target, path) {
893
844
  initialValue = [];
894
845
  }
895
846
  this.length = 0;
896
- const processedItems = initialValue.map((item, index) => {
897
- if (item && typeof item === 'object' && !proxyCache.has(item)) {
898
- const itemPath = [...currentPath, index.toString()];
899
- return createAdvancedProxy(item, itemPath, 0);
900
- }
901
- return item;
902
- });
903
- if (processedItems.length > 0) {
904
- originalPush.apply(this, processedItems);
847
+ if (initialValue.length > 0) {
848
+ originalPush.apply(this, JSON.parse(JSON.stringify(initialValue)));
905
849
  }
906
850
  if (currentPath.length > 0) {
907
851
  notifyListeners(currentPath);
@@ -1018,6 +962,52 @@ function trackPathAccess(path) {
1018
962
  }
1019
963
  }
1020
964
 
965
+ function isPlainObject(value) {
966
+ const prototype = Object.getPrototypeOf(value);
967
+ return prototype === Object.prototype || prototype === null;
968
+ }
969
+ function unwrapProxyTarget(value) {
970
+ if (value && typeof value === 'object' && proxyTargetMap.has(value)) {
971
+ return proxyTargetMap.get(value);
972
+ }
973
+ return value;
974
+ }
975
+ /**
976
+ * Create a read-only snapshot suitable for rendering.
977
+ *
978
+ * Snapshots are plain arrays/objects with no proxy methods, which makes them
979
+ * safe for React Compiler memoization while keeping `$` as the mutable API.
980
+ */
981
+ function createReadonlySnapshot(value, seen = new WeakMap()) {
982
+ if (value === null || typeof value !== 'object') {
983
+ return value;
984
+ }
985
+ const source = unwrapProxyTarget(value);
986
+ if (seen.has(source)) {
987
+ return seen.get(source);
988
+ }
989
+ if (Array.isArray(source)) {
990
+ const snapshot = [];
991
+ seen.set(source, snapshot);
992
+ source.forEach(item => {
993
+ snapshot.push(createReadonlySnapshot(item, seen));
994
+ });
995
+ return snapshot;
996
+ }
997
+ if (!isPlainObject(source)) {
998
+ return source;
999
+ }
1000
+ const snapshot = {};
1001
+ seen.set(source, snapshot);
1002
+ Object.keys(source).forEach(key => {
1003
+ const propertyValue = source[key];
1004
+ if (typeof propertyValue !== 'function') {
1005
+ snapshot[key] = createReadonlySnapshot(propertyValue, seen);
1006
+ }
1007
+ });
1008
+ return snapshot;
1009
+ }
1010
+
1021
1011
  /**
1022
1012
  * Hook to subscribe to the global store and re-render when specific data changes.
1023
1013
  *
@@ -1025,15 +1015,16 @@ function trackPathAccess(path) {
1025
1015
  * re-renders the component when those specific paths change. This provides
1026
1016
  * fine-grained reactivity without unnecessary renders.
1027
1017
  *
1028
- * For objects, the returned value includes custom methods ($merge, $set, $delete, $update)
1029
- * that allow you to modify the data directly and trigger reactive updates.
1018
+ * The returned value is a read-only snapshot of the selected state. Mutate the
1019
+ * store through the main `$` proxy (or a proxy created explicitly for commands),
1020
+ * and use the hook return value only for rendering.
1030
1021
  *
1031
1022
  * @example
1032
1023
  * // Subscribe to user data
1033
1024
  * const user = useScope(() => $.user);
1034
1025
  *
1035
- * // Update user data directly (triggers re-render only for components using $.user)
1036
- * user.$merge({ name: 'New Name' });
1026
+ * // Mutate through the main store proxy
1027
+ * $.user.$merge({ name: 'New Name' });
1037
1028
  *
1038
1029
  * // Subscribe to a specific property
1039
1030
  * const userName = useScope(() => $.user.name);
@@ -1042,22 +1033,24 @@ function trackPathAccess(path) {
1042
1033
  * const isAdmin = useScope(() => $.user.role === 'admin');
1043
1034
  *
1044
1035
  * @param selector - Function that returns the data you want to subscribe to
1045
- * @returns The selected data, with custom methods attached if it's an object
1036
+ * @returns A read-only snapshot of the selected data
1046
1037
  */
1047
1038
  function useScope(selector) {
1048
- // Use ref to store the latest selector to avoid stale closures
1049
- const selectorRef = useRef(selector);
1050
- selectorRef.current = selector;
1051
- // Track dependencies and get initial value with advanced tracking
1052
- const { value: initialValue, paths: trackedPaths } = trackDependencies(selector);
1039
+ const snapshotCacheRef = useRef({
1040
+ revision: -1,
1041
+ source: undefined,
1042
+ snapshot: undefined,
1043
+ });
1044
+ // Track dependencies and get the selected value from the store
1045
+ const { value: selectedValue, paths: trackedPaths } = trackDependencies(selector);
1053
1046
  // Add tracked paths to selector paths for ultra-selective proxying
1054
1047
  trackedPaths.forEach(path => {
1055
1048
  selectorPaths.add(path);
1056
1049
  pathUsageStats.subscribedPaths.add(path);
1057
1050
  });
1058
- // Use a counter to force re-renders instead of storing the value
1059
- // This way we always return the fresh proxy object from the selector
1060
- const [, forceUpdate] = useState(0);
1051
+ // Use a counter to invalidate the cached snapshot only when this hook
1052
+ // receives a relevant store notification.
1053
+ const [revision, forceUpdate] = useState(0);
1061
1054
  // Create stable update handler that forces re-render
1062
1055
  const handleChange = useCallback(() => {
1063
1056
  try {
@@ -1068,6 +1061,16 @@ function useScope(selector) {
1068
1061
  }
1069
1062
  }, []);
1070
1063
  useEffect(() => {
1064
+ if (typeof requestAnimationFrame === 'function') {
1065
+ requestAnimationFrame(() => {
1066
+ handleChange();
1067
+ });
1068
+ }
1069
+ else {
1070
+ setTimeout(() => {
1071
+ handleChange();
1072
+ }, 16);
1073
+ }
1071
1074
  // Create path keys for subscription using the original approach
1072
1075
  // If trackedPaths is ['user', 'name'], create subscriptions for ['user', 'user.name']
1073
1076
  const pathKeys = trackedPaths.length > 0
@@ -1084,8 +1087,18 @@ function useScope(selector) {
1084
1087
  unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
1085
1088
  };
1086
1089
  }, [trackedPaths.join(','), handleChange]); // Stable dependencies
1087
- // Always return the fresh result from the selector (preserves proxy and methods)
1088
- return selectorRef.current();
1090
+ if (selectedValue === null || typeof selectedValue !== 'object') {
1091
+ return selectedValue;
1092
+ }
1093
+ if (snapshotCacheRef.current.revision !== revision ||
1094
+ snapshotCacheRef.current.source !== selectedValue) {
1095
+ snapshotCacheRef.current = {
1096
+ revision,
1097
+ source: selectedValue,
1098
+ snapshot: createReadonlySnapshot(selectedValue),
1099
+ };
1100
+ }
1101
+ return snapshotCacheRef.current.snapshot;
1089
1102
  }
1090
1103
 
1091
1104
  /**