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 +8 -1
- package/dist/core/proxy.d.ts +1 -0
- package/dist/core/proxy.d.ts.map +1 -1
- package/dist/core/snapshot.d.ts +9 -0
- package/dist/core/snapshot.d.ts.map +1 -0
- package/dist/hooks/useScope.d.ts +8 -7
- package/dist/hooks/useScope.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +108 -95
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +108 -95
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -353,6 +353,7 @@ function logSubscriptionRemoved(path) {
|
|
|
353
353
|
|
|
354
354
|
// Track proxy to path mapping for type-safe activation
|
|
355
355
|
const proxyPathMap = new WeakMap();
|
|
356
|
+
const proxyTargetMap = new WeakMap();
|
|
356
357
|
// Store a deep clone of the initial store state for use with $reset
|
|
357
358
|
let initialStoreState$1 = {};
|
|
358
359
|
// Path usage tracking
|
|
@@ -570,6 +571,11 @@ function createAdvancedProxy(target, path = [], depth = 0) {
|
|
|
570
571
|
if (typeof prop === 'symbol') {
|
|
571
572
|
return Reflect.get(obj, prop, receiver);
|
|
572
573
|
}
|
|
574
|
+
const value = Reflect.get(obj, prop, receiver);
|
|
575
|
+
// Functions are commands/helpers, not reactive data dependencies.
|
|
576
|
+
if (typeof value === 'function') {
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
573
579
|
const currentPropPath = [...path, prop.toString()];
|
|
574
580
|
const propPathKey = currentPropPath.join('.');
|
|
575
581
|
// Track path access during dependency tracking
|
|
@@ -580,9 +586,12 @@ function createAdvancedProxy(target, path = [], depth = 0) {
|
|
|
580
586
|
}
|
|
581
587
|
// Track path access for dependency tracking (inline to avoid circular dependency)
|
|
582
588
|
trackPathAccess(currentPropPath);
|
|
583
|
-
const value = obj[prop];
|
|
584
589
|
// For objects, create proxies for nested values
|
|
585
590
|
if (value && typeof value === 'object' && path.length < proxyConfig.maxDepth) {
|
|
591
|
+
// If value is already a proxy, return it directly to prevent double-wrapping
|
|
592
|
+
if (proxyPathMap.has(value)) {
|
|
593
|
+
return value;
|
|
594
|
+
}
|
|
586
595
|
const shouldProxy = !proxyConfig.lazyProxyDeepObjects ||
|
|
587
596
|
pathUsageStats.accessedPaths.has(propPathKey) ||
|
|
588
597
|
pathUsageStats.modifiedPaths.has(propPathKey) ||
|
|
@@ -594,27 +603,16 @@ function createAdvancedProxy(target, path = [], depth = 0) {
|
|
|
594
603
|
}
|
|
595
604
|
return value;
|
|
596
605
|
},
|
|
597
|
-
set(obj, prop, value
|
|
606
|
+
set(obj, prop, value) {
|
|
598
607
|
if (typeof prop === 'symbol') {
|
|
599
|
-
return Reflect.set(obj, prop, value
|
|
608
|
+
return Reflect.set(obj, prop, value);
|
|
600
609
|
}
|
|
601
610
|
const propPath = [...path, prop.toString()];
|
|
602
611
|
const propPathKey = propPath.join('.');
|
|
603
|
-
// Track modification
|
|
604
612
|
if (proxyConfig.trackPathUsage) {
|
|
605
613
|
pathUsageStats.modifiedPaths.add(propPathKey);
|
|
606
614
|
}
|
|
607
|
-
|
|
608
|
-
if (value && typeof value === 'object' && !proxyCache.has(value)) {
|
|
609
|
-
const newPath = [...path, prop.toString()];
|
|
610
|
-
const proxiedValue = createAdvancedProxy(value, newPath, 0);
|
|
611
|
-
const result = Reflect.set(obj, prop, proxiedValue, receiver);
|
|
612
|
-
// Notify the specific property path and all parent/child paths
|
|
613
|
-
notifyListeners(propPath);
|
|
614
|
-
return result;
|
|
615
|
-
}
|
|
616
|
-
const result = Reflect.set(obj, prop, value, receiver);
|
|
617
|
-
// Notify the specific property path (this will also notify parent/child paths)
|
|
615
|
+
const result = Reflect.set(obj, prop, value);
|
|
618
616
|
notifyListeners(propPath);
|
|
619
617
|
return result;
|
|
620
618
|
},
|
|
@@ -640,6 +638,7 @@ function createAdvancedProxy(target, path = [], depth = 0) {
|
|
|
640
638
|
proxyCacheLRU.add(target, proxy);
|
|
641
639
|
// Track the path for this proxy
|
|
642
640
|
proxyPathMap.set(proxy, [...path]);
|
|
641
|
+
proxyTargetMap.set(proxy, target);
|
|
643
642
|
return proxy;
|
|
644
643
|
}
|
|
645
644
|
/**
|
|
@@ -656,16 +655,7 @@ function addObjectMethods(target, path) {
|
|
|
656
655
|
const propPath = [...currentPath, key].join('.');
|
|
657
656
|
pathUsageStats.modifiedPaths.add(propPath);
|
|
658
657
|
}
|
|
659
|
-
|
|
660
|
-
if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
|
|
661
|
-
const newPath = [...currentPath, key];
|
|
662
|
-
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
663
|
-
Reflect.set(this, key, createAdvancedProxy(newValue, newPath, 0), this);
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
667
|
-
Reflect.set(this, key, newValue, this);
|
|
668
|
-
}
|
|
658
|
+
Reflect.set(this, key, newProps[key], this);
|
|
669
659
|
});
|
|
670
660
|
return this;
|
|
671
661
|
},
|
|
@@ -676,28 +666,17 @@ function addObjectMethods(target, path) {
|
|
|
676
666
|
methodsToDefine.$set = {
|
|
677
667
|
value: function (newProps) {
|
|
678
668
|
const currentPath = proxyPathMap.get(this) || path;
|
|
679
|
-
// Clear existing properties
|
|
680
669
|
Object.keys(this).forEach(key => {
|
|
681
670
|
if (typeof this[key] !== 'function') {
|
|
682
671
|
Reflect.deleteProperty(this, key);
|
|
683
672
|
}
|
|
684
673
|
});
|
|
685
|
-
// Set new properties
|
|
686
674
|
Object.keys(newProps || {}).forEach(key => {
|
|
687
675
|
if (proxyConfig.trackPathUsage) {
|
|
688
676
|
const propPath = [...currentPath, key].join('.');
|
|
689
677
|
pathUsageStats.modifiedPaths.add(propPath);
|
|
690
678
|
}
|
|
691
|
-
|
|
692
|
-
if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
|
|
693
|
-
const newPath = [...currentPath, key];
|
|
694
|
-
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
695
|
-
Reflect.set(this, key, createAdvancedProxy(newValue, newPath, 0), this);
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
699
|
-
Reflect.set(this, key, newValue, this);
|
|
700
|
-
}
|
|
679
|
+
Reflect.set(this, key, newProps[key], this);
|
|
701
680
|
});
|
|
702
681
|
return this;
|
|
703
682
|
},
|
|
@@ -727,12 +706,7 @@ function addObjectMethods(target, path) {
|
|
|
727
706
|
pathUsageStats.modifiedPaths.add(propPath);
|
|
728
707
|
}
|
|
729
708
|
const currentValue = this[key];
|
|
730
|
-
|
|
731
|
-
if (newValue && typeof newValue === 'object' && !proxyCache.has(newValue)) {
|
|
732
|
-
const newPath = [...currentPath, key];
|
|
733
|
-
newValue = createAdvancedProxy(newValue, newPath, 0);
|
|
734
|
-
}
|
|
735
|
-
// Use Reflect.set with this proxy as the receiver to trigger the set handler
|
|
709
|
+
const newValue = updater(currentValue);
|
|
736
710
|
Reflect.set(this, key, newValue, this);
|
|
737
711
|
return this;
|
|
738
712
|
},
|
|
@@ -790,20 +764,13 @@ function addObjectMethods(target, path) {
|
|
|
790
764
|
* Add custom methods to array targets
|
|
791
765
|
*/
|
|
792
766
|
function addArrayMethods(target, path) {
|
|
793
|
-
const originalPush =
|
|
794
|
-
const originalSplice =
|
|
767
|
+
const originalPush = Array.prototype.push;
|
|
768
|
+
const originalSplice = Array.prototype.splice;
|
|
795
769
|
// Override push
|
|
796
770
|
Object.defineProperty(target, 'push', {
|
|
797
771
|
value: function (...items) {
|
|
798
772
|
const currentPath = proxyPathMap.get(this) || path;
|
|
799
|
-
const
|
|
800
|
-
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
801
|
-
const itemPath = [...currentPath, '_item'];
|
|
802
|
-
return createAdvancedProxy(item, itemPath, 0);
|
|
803
|
-
}
|
|
804
|
-
return item;
|
|
805
|
-
});
|
|
806
|
-
const result = originalPush.apply(this, processedItems);
|
|
773
|
+
const result = originalPush.apply(this, items);
|
|
807
774
|
if (currentPath.length > 0) {
|
|
808
775
|
if (proxyConfig.trackPathUsage) {
|
|
809
776
|
pathUsageStats.modifiedPaths.add(currentPath.join('.'));
|
|
@@ -820,22 +787,13 @@ function addArrayMethods(target, path) {
|
|
|
820
787
|
value: function (start, deleteCount, ...items) {
|
|
821
788
|
const currentPath = proxyPathMap.get(this) || path;
|
|
822
789
|
const arrayLength = this.length;
|
|
823
|
-
const processedItems = items.map((item, index) => {
|
|
824
|
-
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
825
|
-
const itemPath = [...currentPath, (start + index).toString()];
|
|
826
|
-
return createAdvancedProxy(item, itemPath, 0);
|
|
827
|
-
}
|
|
828
|
-
return item;
|
|
829
|
-
});
|
|
830
790
|
const actualDeleteCount = deleteCount === undefined ? (arrayLength - start) : deleteCount;
|
|
831
|
-
const result = originalSplice.apply(this, [start, actualDeleteCount, ...
|
|
791
|
+
const result = originalSplice.apply(this, [start, actualDeleteCount, ...items]);
|
|
832
792
|
if (currentPath.length > 0) {
|
|
833
793
|
if (proxyConfig.trackPathUsage) {
|
|
834
794
|
pathUsageStats.modifiedPaths.add(currentPath.join('.'));
|
|
835
795
|
}
|
|
836
|
-
// Notify the array itself
|
|
837
796
|
notifyListeners(currentPath);
|
|
838
|
-
// Also notify about each index that was affected
|
|
839
797
|
for (let i = start; i < arrayLength; i++) {
|
|
840
798
|
const indexPath = [...currentPath, i.toString()];
|
|
841
799
|
notifyListeners(indexPath);
|
|
@@ -856,14 +814,7 @@ function addArrayMethods(target, path) {
|
|
|
856
814
|
}
|
|
857
815
|
const currentPath = proxyPathMap.get(this) || path;
|
|
858
816
|
this.length = 0;
|
|
859
|
-
|
|
860
|
-
if (item && typeof item === 'object' && !proxyCache.has(item)) {
|
|
861
|
-
const itemPath = [...currentPath, index.toString()];
|
|
862
|
-
return createAdvancedProxy(item, itemPath, 0);
|
|
863
|
-
}
|
|
864
|
-
return item;
|
|
865
|
-
});
|
|
866
|
-
originalPush.apply(this, processedItems);
|
|
817
|
+
originalPush.apply(this, newArray);
|
|
867
818
|
if (currentPath.length > 0) {
|
|
868
819
|
if (proxyConfig.trackPathUsage) {
|
|
869
820
|
pathUsageStats.modifiedPaths.add(currentPath.join('.'));
|
|
@@ -895,15 +846,8 @@ function addArrayMethods(target, path) {
|
|
|
895
846
|
initialValue = [];
|
|
896
847
|
}
|
|
897
848
|
this.length = 0;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const itemPath = [...currentPath, index.toString()];
|
|
901
|
-
return createAdvancedProxy(item, itemPath, 0);
|
|
902
|
-
}
|
|
903
|
-
return item;
|
|
904
|
-
});
|
|
905
|
-
if (processedItems.length > 0) {
|
|
906
|
-
originalPush.apply(this, processedItems);
|
|
849
|
+
if (initialValue.length > 0) {
|
|
850
|
+
originalPush.apply(this, JSON.parse(JSON.stringify(initialValue)));
|
|
907
851
|
}
|
|
908
852
|
if (currentPath.length > 0) {
|
|
909
853
|
notifyListeners(currentPath);
|
|
@@ -1020,6 +964,52 @@ function trackPathAccess(path) {
|
|
|
1020
964
|
}
|
|
1021
965
|
}
|
|
1022
966
|
|
|
967
|
+
function isPlainObject(value) {
|
|
968
|
+
const prototype = Object.getPrototypeOf(value);
|
|
969
|
+
return prototype === Object.prototype || prototype === null;
|
|
970
|
+
}
|
|
971
|
+
function unwrapProxyTarget(value) {
|
|
972
|
+
if (value && typeof value === 'object' && proxyTargetMap.has(value)) {
|
|
973
|
+
return proxyTargetMap.get(value);
|
|
974
|
+
}
|
|
975
|
+
return value;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Create a read-only snapshot suitable for rendering.
|
|
979
|
+
*
|
|
980
|
+
* Snapshots are plain arrays/objects with no proxy methods, which makes them
|
|
981
|
+
* safe for React Compiler memoization while keeping `$` as the mutable API.
|
|
982
|
+
*/
|
|
983
|
+
function createReadonlySnapshot(value, seen = new WeakMap()) {
|
|
984
|
+
if (value === null || typeof value !== 'object') {
|
|
985
|
+
return value;
|
|
986
|
+
}
|
|
987
|
+
const source = unwrapProxyTarget(value);
|
|
988
|
+
if (seen.has(source)) {
|
|
989
|
+
return seen.get(source);
|
|
990
|
+
}
|
|
991
|
+
if (Array.isArray(source)) {
|
|
992
|
+
const snapshot = [];
|
|
993
|
+
seen.set(source, snapshot);
|
|
994
|
+
source.forEach(item => {
|
|
995
|
+
snapshot.push(createReadonlySnapshot(item, seen));
|
|
996
|
+
});
|
|
997
|
+
return snapshot;
|
|
998
|
+
}
|
|
999
|
+
if (!isPlainObject(source)) {
|
|
1000
|
+
return source;
|
|
1001
|
+
}
|
|
1002
|
+
const snapshot = {};
|
|
1003
|
+
seen.set(source, snapshot);
|
|
1004
|
+
Object.keys(source).forEach(key => {
|
|
1005
|
+
const propertyValue = source[key];
|
|
1006
|
+
if (typeof propertyValue !== 'function') {
|
|
1007
|
+
snapshot[key] = createReadonlySnapshot(propertyValue, seen);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
return snapshot;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1023
1013
|
/**
|
|
1024
1014
|
* Hook to subscribe to the global store and re-render when specific data changes.
|
|
1025
1015
|
*
|
|
@@ -1027,15 +1017,16 @@ function trackPathAccess(path) {
|
|
|
1027
1017
|
* re-renders the component when those specific paths change. This provides
|
|
1028
1018
|
* fine-grained reactivity without unnecessary renders.
|
|
1029
1019
|
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1020
|
+
* The returned value is a read-only snapshot of the selected state. Mutate the
|
|
1021
|
+
* store through the main `$` proxy (or a proxy created explicitly for commands),
|
|
1022
|
+
* and use the hook return value only for rendering.
|
|
1032
1023
|
*
|
|
1033
1024
|
* @example
|
|
1034
1025
|
* // Subscribe to user data
|
|
1035
1026
|
* const user = useScope(() => $.user);
|
|
1036
1027
|
*
|
|
1037
|
-
* //
|
|
1038
|
-
* user.$merge({ name: 'New Name' });
|
|
1028
|
+
* // Mutate through the main store proxy
|
|
1029
|
+
* $.user.$merge({ name: 'New Name' });
|
|
1039
1030
|
*
|
|
1040
1031
|
* // Subscribe to a specific property
|
|
1041
1032
|
* const userName = useScope(() => $.user.name);
|
|
@@ -1044,22 +1035,24 @@ function trackPathAccess(path) {
|
|
|
1044
1035
|
* const isAdmin = useScope(() => $.user.role === 'admin');
|
|
1045
1036
|
*
|
|
1046
1037
|
* @param selector - Function that returns the data you want to subscribe to
|
|
1047
|
-
* @returns
|
|
1038
|
+
* @returns A read-only snapshot of the selected data
|
|
1048
1039
|
*/
|
|
1049
1040
|
function useScope(selector) {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1041
|
+
const snapshotCacheRef = react.useRef({
|
|
1042
|
+
revision: -1,
|
|
1043
|
+
source: undefined,
|
|
1044
|
+
snapshot: undefined,
|
|
1045
|
+
});
|
|
1046
|
+
// Track dependencies and get the selected value from the store
|
|
1047
|
+
const { value: selectedValue, paths: trackedPaths } = trackDependencies(selector);
|
|
1055
1048
|
// Add tracked paths to selector paths for ultra-selective proxying
|
|
1056
1049
|
trackedPaths.forEach(path => {
|
|
1057
1050
|
selectorPaths.add(path);
|
|
1058
1051
|
pathUsageStats.subscribedPaths.add(path);
|
|
1059
1052
|
});
|
|
1060
|
-
// Use a counter to
|
|
1061
|
-
//
|
|
1062
|
-
const [, forceUpdate] = react.useState(0);
|
|
1053
|
+
// Use a counter to invalidate the cached snapshot only when this hook
|
|
1054
|
+
// receives a relevant store notification.
|
|
1055
|
+
const [revision, forceUpdate] = react.useState(0);
|
|
1063
1056
|
// Create stable update handler that forces re-render
|
|
1064
1057
|
const handleChange = react.useCallback(() => {
|
|
1065
1058
|
try {
|
|
@@ -1070,6 +1063,16 @@ function useScope(selector) {
|
|
|
1070
1063
|
}
|
|
1071
1064
|
}, []);
|
|
1072
1065
|
react.useEffect(() => {
|
|
1066
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
1067
|
+
requestAnimationFrame(() => {
|
|
1068
|
+
handleChange();
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
else {
|
|
1072
|
+
setTimeout(() => {
|
|
1073
|
+
handleChange();
|
|
1074
|
+
}, 16);
|
|
1075
|
+
}
|
|
1073
1076
|
// Create path keys for subscription using the original approach
|
|
1074
1077
|
// If trackedPaths is ['user', 'name'], create subscriptions for ['user', 'user.name']
|
|
1075
1078
|
const pathKeys = trackedPaths.length > 0
|
|
@@ -1086,8 +1089,18 @@ function useScope(selector) {
|
|
|
1086
1089
|
unsubscribeFunctions.forEach(unsubscribe => unsubscribe());
|
|
1087
1090
|
};
|
|
1088
1091
|
}, [trackedPaths.join(','), handleChange]); // Stable dependencies
|
|
1089
|
-
|
|
1090
|
-
|
|
1092
|
+
if (selectedValue === null || typeof selectedValue !== 'object') {
|
|
1093
|
+
return selectedValue;
|
|
1094
|
+
}
|
|
1095
|
+
if (snapshotCacheRef.current.revision !== revision ||
|
|
1096
|
+
snapshotCacheRef.current.source !== selectedValue) {
|
|
1097
|
+
snapshotCacheRef.current = {
|
|
1098
|
+
revision,
|
|
1099
|
+
source: selectedValue,
|
|
1100
|
+
snapshot: createReadonlySnapshot(selectedValue),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
return snapshotCacheRef.current.snapshot;
|
|
1091
1104
|
}
|
|
1092
1105
|
|
|
1093
1106
|
/**
|