@yiin/reactive-proxy-state 1.0.2 → 1.0.3

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/utils.js ADDED
@@ -0,0 +1,257 @@
1
+ // Cache for memoized deepEqual results with cleanup
2
+ const deepEqualCache = new WeakMap();
3
+ const MAX_CACHE_SIZE = 1000; // Prevent unbounded growth
4
+ // Path traversal cache with LRU-like behavior
5
+ export const pathCache = new WeakMap();
6
+ const pathCacheSize = new WeakMap();
7
+ // Path concatenation cache with size limit
8
+ export const pathConcatCache = new Map();
9
+ const MAX_PATH_CACHE_SIZE = 1000;
10
+ // Global seen map for circular reference detection
11
+ export const globalSeen = new WeakMap();
12
+ // Global cache for proxy wrappers
13
+ export const wrapperCache = new WeakMap();
14
+ function cleanupPathCache(root) {
15
+ const cache = pathCache.get(root);
16
+ if (cache && pathCacheSize.get(root) > MAX_CACHE_SIZE) {
17
+ // Clear oldest entries (first 20%)
18
+ const entriesToRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
19
+ let count = 0;
20
+ for (const key of cache.keys()) {
21
+ if (count >= entriesToRemove)
22
+ break;
23
+ cache.delete(key);
24
+ count++;
25
+ }
26
+ pathCacheSize.set(root, MAX_CACHE_SIZE - entriesToRemove);
27
+ }
28
+ }
29
+ function cleanupPathConcatCache() {
30
+ if (pathConcatCache.size > MAX_PATH_CACHE_SIZE) {
31
+ // Remove oldest entries (first 20%)
32
+ const entriesToRemove = Math.floor(MAX_PATH_CACHE_SIZE * 0.2);
33
+ let count = 0;
34
+ for (const key of pathConcatCache.keys()) {
35
+ if (count >= entriesToRemove)
36
+ break;
37
+ pathConcatCache.delete(key);
38
+ count++;
39
+ }
40
+ }
41
+ }
42
+ export function deepEqual(a, b, seen = globalSeen) {
43
+ // Fast path for primitive equality
44
+ if (a === b)
45
+ return true;
46
+ // Fast path for null/undefined
47
+ if (a == null || b == null)
48
+ return a === b;
49
+ // Fast path for different types
50
+ if (typeof a !== typeof b)
51
+ return false;
52
+ // Fast path for Date objects
53
+ if (a instanceof Date && b instanceof Date)
54
+ return a.getTime() === b.getTime();
55
+ // Fast path for non-objects
56
+ if (typeof a !== 'object')
57
+ return false;
58
+ // Fast path for different array types
59
+ if (Array.isArray(a) !== Array.isArray(b))
60
+ return false;
61
+ // Check for circular references
62
+ if (seen.has(a))
63
+ return seen.get(a) === b;
64
+ seen.set(a, b);
65
+ // Check cache for memoized results
66
+ if (deepEqualCache.has(a) && deepEqualCache.get(a)?.has(b)) {
67
+ return deepEqualCache.get(a).get(b);
68
+ }
69
+ // Initialize cache for this object if needed
70
+ if (!deepEqualCache.has(a)) {
71
+ deepEqualCache.set(a, new WeakMap());
72
+ }
73
+ let result;
74
+ // Compare arrays
75
+ if (Array.isArray(a)) {
76
+ if (a.length !== b.length) {
77
+ result = false;
78
+ }
79
+ else {
80
+ result = a.every((val, idx) => deepEqual(val, b[idx], seen));
81
+ }
82
+ }
83
+ else {
84
+ // Compare objects
85
+ const keysA = Object.keys(a);
86
+ const keysB = Object.keys(b);
87
+ if (keysA.length !== keysB.length) {
88
+ result = false;
89
+ }
90
+ else {
91
+ result = keysA.every(key => deepEqual(a[key], b[key], seen));
92
+ }
93
+ }
94
+ // Cache the result
95
+ deepEqualCache.get(a).set(b, result);
96
+ return result;
97
+ }
98
+ // Helper to safely get from path cache with cleanup
99
+ export function getFromPathCache(root, pathKey) {
100
+ const cache = pathCache.get(root);
101
+ if (!cache)
102
+ return undefined;
103
+ const result = cache.get(pathKey);
104
+ if (result !== undefined) {
105
+ // Move to end (most recently used)
106
+ cache.delete(pathKey);
107
+ cache.set(pathKey, result);
108
+ }
109
+ return result;
110
+ }
111
+ // Helper to safely set in path cache with cleanup
112
+ export function setInPathCache(root, pathKey, value) {
113
+ if (!pathCache.has(root)) {
114
+ pathCache.set(root, new Map());
115
+ pathCacheSize.set(root, 0);
116
+ }
117
+ const cache = pathCache.get(root);
118
+ const size = pathCacheSize.get(root);
119
+ // If key exists, remove it first (will be added at end)
120
+ if (cache.has(pathKey)) {
121
+ cache.delete(pathKey);
122
+ }
123
+ cache.set(pathKey, value);
124
+ pathCacheSize.set(root, size + 1);
125
+ cleanupPathCache(root);
126
+ }
127
+ // Helper to safely get/set path concatenation with cleanup
128
+ export function getPathConcat(path) {
129
+ const result = pathConcatCache.get(path);
130
+ if (result !== undefined) {
131
+ // Move to end (most recently used)
132
+ pathConcatCache.delete(path);
133
+ pathConcatCache.set(path, result);
134
+ }
135
+ return result;
136
+ }
137
+ export function setPathConcat(path, value) {
138
+ pathConcatCache.set(path, value);
139
+ cleanupPathConcatCache();
140
+ }
141
+ // Helper to check if a value is an object (and not null)
142
+ function isObject(val) {
143
+ return val !== null && typeof val === 'object';
144
+ }
145
+ /**
146
+ * Recursively traverses an object to track all nested properties.
147
+ * Used for deep watching.
148
+ * @param value - The value to traverse.
149
+ * @param seen - A Set to handle circular references.
150
+ */
151
+ export function traverse(value, seen = new Set()) {
152
+ if (!isObject(value) || seen.has(value)) {
153
+ return value;
154
+ }
155
+ seen.add(value);
156
+ // Traverse arrays
157
+ if (Array.isArray(value)) {
158
+ // Access length for tracking
159
+ value.length;
160
+ for (let i = 0; i < value.length; i++) {
161
+ traverse(value[i], seen);
162
+ }
163
+ }
164
+ // Traverse Sets and Maps
165
+ else if (value instanceof Set || value instanceof Map) {
166
+ for (const v of value) {
167
+ // For Maps, traverse both keys (if objects) and values
168
+ if (Array.isArray(v)) {
169
+ traverse(v[0], seen); // Key
170
+ traverse(v[1], seen); // Value
171
+ }
172
+ else {
173
+ traverse(v, seen); // Value for Set
174
+ }
175
+ }
176
+ // --- Stop after handling Map/Set ---
177
+ return value;
178
+ }
179
+ // Traverse plain objects (Only if not Array, Map, or Set)
180
+ else {
181
+ for (const key in value) {
182
+ // Access the property to trigger tracking via the proxy's get handler
183
+ traverse(value[key], seen);
184
+ }
185
+ }
186
+ return value;
187
+ }
188
+ /**
189
+ * Creates a deep clone of a value.
190
+ * Handles primitives, Dates, Arrays, Maps, Sets, and plain objects.
191
+ * Includes cycle detection.
192
+ * @param value The value to clone.
193
+ * @param seen A WeakMap to handle circular references during recursion.
194
+ * @returns A deep clone of the value.
195
+ */
196
+ export function deepClone(value, seen = new WeakMap()) {
197
+ // Primitives and null are returned directly
198
+ if (value === null || typeof value !== 'object') {
199
+ return value;
200
+ }
201
+ // Handle Dates
202
+ if (value instanceof Date) {
203
+ return new Date(value.getTime());
204
+ }
205
+ // Handle cycles
206
+ if (seen.has(value)) {
207
+ return seen.get(value);
208
+ }
209
+ // Handle Arrays
210
+ if (Array.isArray(value)) {
211
+ const newArray = [];
212
+ seen.set(value, newArray);
213
+ for (let i = 0; i < value.length; i++) {
214
+ newArray[i] = deepClone(value[i], seen);
215
+ }
216
+ return newArray;
217
+ }
218
+ // Handle Maps
219
+ if (value instanceof Map) {
220
+ const newMap = new Map();
221
+ seen.set(value, newMap);
222
+ value.forEach((val, key) => {
223
+ // Clone both key and value in case they are objects
224
+ newMap.set(deepClone(key, seen), deepClone(val, seen));
225
+ });
226
+ return newMap;
227
+ }
228
+ // Handle Sets
229
+ if (value instanceof Set) {
230
+ const newSet = new Set();
231
+ seen.set(value, newSet);
232
+ value.forEach(val => {
233
+ newSet.add(deepClone(val, seen));
234
+ });
235
+ return newSet;
236
+ }
237
+ // Handle plain objects
238
+ // Create object with same prototype
239
+ const newObject = Object.create(Object.getPrototypeOf(value));
240
+ seen.set(value, newObject);
241
+ // Copy properties
242
+ for (const key in value) {
243
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
244
+ newObject[key] = deepClone(value[key], seen);
245
+ }
246
+ }
247
+ // Copy symbol properties (if any)
248
+ const symbolKeys = Object.getOwnPropertySymbols(value);
249
+ for (const symbolKey of symbolKeys) {
250
+ // Check if property descriptor exists and has get/set or is value
251
+ const descriptor = Object.getOwnPropertyDescriptor(value, symbolKey);
252
+ if (descriptor && (descriptor.value !== undefined || descriptor.get || descriptor.set)) {
253
+ newObject[symbolKey] = deepClone(value[symbolKey], seen);
254
+ }
255
+ }
256
+ return newObject;
257
+ }
@@ -0,0 +1,19 @@
1
+ type WatchCallback<T = any> = (newValue: T, oldValue: T | undefined) => void;
2
+ type WatchSource<T = any> = () => T;
3
+ type WatchSourceInput<T = any> = WatchSource<T> | T;
4
+ type WatchStopHandle = () => void;
5
+ export interface WatchOptions {
6
+ immediate?: boolean;
7
+ deep?: boolean;
8
+ }
9
+ /**
10
+ * Watches a reactive source and runs a callback when it changes
11
+ *
12
+ * @param source - A function that returns the value to watch
13
+ * @param callback - Function to call when the source changes
14
+ * @param options - Watch options (immediate, deep)
15
+ * @returns A function to stop watching
16
+ */
17
+ export declare function watch<T = any>(sourceInput: WatchSourceInput<T>, // Renamed parameter
18
+ callback: WatchCallback<T>, options?: WatchOptions): WatchStopHandle;
19
+ export {};
package/dist/watch.js ADDED
@@ -0,0 +1,74 @@
1
+ import { watchEffect } from './watchEffect';
2
+ import { traverse, deepClone } from './utils';
3
+ /**
4
+ * Watches a reactive source and runs a callback when it changes
5
+ *
6
+ * @param source - A function that returns the value to watch
7
+ * @param callback - Function to call when the source changes
8
+ * @param options - Watch options (immediate, deep)
9
+ * @returns A function to stop watching
10
+ */
11
+ export function watch(sourceInput, // Renamed parameter
12
+ callback, options = {}) {
13
+ // Default deep to true unless explicitly false
14
+ const { immediate = false, deep = true } = options;
15
+ // Normalize sourceInput to always be a function
16
+ const source = typeof sourceInput === 'function'
17
+ ? sourceInput
18
+ : () => sourceInput;
19
+ let oldValue;
20
+ let initialized = false;
21
+ // Use watchEffect to track dependencies and re-run when they change
22
+ const stopEffect = watchEffect(() => {
23
+ // 1. Run the source function to get the current value
24
+ const currentValue = source();
25
+ // Determine if deep watching is needed for tracking
26
+ // Use the defaulted 'deep' value
27
+ let needsDeepTracking = deep === true;
28
+ // This check becomes redundant if deep defaults to true, but keep for potential explicit {deep: false} on collections?
29
+ // Maybe simplify: if deep is true, always traverse.
30
+ /*
31
+ if (!needsDeepTracking && currentValue && typeof currentValue === 'object') {
32
+ if (Array.isArray(currentValue) || currentValue instanceof Map || currentValue instanceof Set) {
33
+ needsDeepTracking = true; // Track collections even if deep:false? Vue does shallow on collections by default.
34
+ }
35
+ }
36
+ */
37
+ // 2. If deep tracking needed, traverse the value *for tracking purposes only*
38
+ if (needsDeepTracking) {
39
+ traverse(currentValue); // Discard result, only needed for effect tracking
40
+ }
41
+ // 3. Compare the actual currentValue with the oldValue
42
+ if (initialized) {
43
+ let hasChanged = false;
44
+ // Use the defaulted 'deep' value for comparison logic
45
+ if (!deep) {
46
+ hasChanged = currentValue !== oldValue;
47
+ }
48
+ else {
49
+ // For deep watches, the effect running *implies* a relevant change occurred.
50
+ // The traverse() call ensures dependencies were tracked. If the effect
51
+ // runs, we assume a change happened without needing deepEqual.
52
+ hasChanged = true;
53
+ }
54
+ if (hasChanged) {
55
+ // Get the value to pass as the previous oldValue to the callback
56
+ const prevOldValue = oldValue;
57
+ // Update the stored oldValue. Clone *only if* deep watching is enabled.
58
+ oldValue = deep ? deepClone(currentValue) : currentValue;
59
+ callback(currentValue, prevOldValue);
60
+ }
61
+ }
62
+ else {
63
+ // First run: establish initial oldValue (cloned if deep) and handle immediate call
64
+ oldValue = deep ? deepClone(currentValue) : currentValue;
65
+ initialized = true;
66
+ if (immediate) {
67
+ // Pass undefined as oldValue for immediate calls
68
+ callback(currentValue, undefined);
69
+ }
70
+ }
71
+ });
72
+ // Return the stop handle from watchEffect
73
+ return stopEffect;
74
+ }
@@ -0,0 +1,49 @@
1
+ type EffectCallback<T = any> = () => T;
2
+ type Scheduler = (job: () => void) => void;
3
+ export interface WatchEffectStopHandle<T = any> {
4
+ (): void;
5
+ effect: TrackedEffect<T>;
6
+ }
7
+ export interface TrackedEffect<T = any> {
8
+ run: () => T;
9
+ dependencies?: Set<Set<TrackedEffect<any>>>;
10
+ options?: WatchEffectOptions;
11
+ active?: boolean;
12
+ _rawCallback: EffectCallback<T>;
13
+ }
14
+ export declare let activeEffect: TrackedEffect<any> | null;
15
+ export declare function setActiveEffect(effect: TrackedEffect<any> | null): void;
16
+ /**
17
+ * Clean up dependencies for a specific effect
18
+ */
19
+ export declare function cleanupEffect(effect: TrackedEffect<any>): void;
20
+ /**
21
+ * Track a property access for the active effect
22
+ */
23
+ export declare function track(target: object, key: string | symbol): void;
24
+ /**
25
+ * Trigger effects associated with a property (Synchronous Only)
26
+ */
27
+ export declare function trigger(target: object, key: string | symbol): void;
28
+ export interface WatchEffectOptions {
29
+ lazy?: boolean;
30
+ scheduler?: Scheduler;
31
+ onTrack?: (event: {
32
+ effect: EffectCallback<any>;
33
+ target: object;
34
+ key: string | symbol;
35
+ type: 'track';
36
+ }) => void;
37
+ onTrigger?: (event: {
38
+ effect: EffectCallback<any>;
39
+ target: object;
40
+ key: string | symbol;
41
+ type: 'trigger';
42
+ }) => void;
43
+ }
44
+ /**
45
+ * Runs a function and re-runs it when its reactive dependencies change.
46
+ * Returns a stop handle that also exposes the effect instance.
47
+ */
48
+ export declare function watchEffect<T>(effectCallback: EffectCallback<T>, options?: WatchEffectOptions): WatchEffectStopHandle<T>;
49
+ export {};
@@ -0,0 +1,139 @@
1
+ // Track currently running effect
2
+ // Note: activeEffect can hold effects with different return types
3
+ export let activeEffect = null;
4
+ // Setter function for activeEffect
5
+ export function setActiveEffect(effect) {
6
+ activeEffect = effect;
7
+ }
8
+ // Store for tracking dependencies: target -> key -> Set<TrackedEffect>
9
+ // The Sets will hold effects of potentially different types
10
+ const targetMap = new WeakMap();
11
+ /**
12
+ * Clean up dependencies for a specific effect
13
+ */
14
+ export function cleanupEffect(effect) {
15
+ if (effect.dependencies) {
16
+ effect.dependencies.forEach(dep => {
17
+ dep.delete(effect);
18
+ });
19
+ effect.dependencies.clear(); // Clear the set for the next run
20
+ }
21
+ }
22
+ /**
23
+ * Track a property access for the active effect
24
+ */
25
+ export function track(target, key) {
26
+ // Only track if there's an active effect that should be running
27
+ if (!activeEffect || !activeEffect.active)
28
+ return;
29
+ // Get the dependency map for this target
30
+ let depsMap = targetMap.get(target);
31
+ if (!depsMap) {
32
+ depsMap = new Map();
33
+ targetMap.set(target, depsMap);
34
+ }
35
+ // Get the set of effects for this property
36
+ let dep = depsMap.get(key);
37
+ if (!dep) {
38
+ dep = new Set();
39
+ depsMap.set(key, dep);
40
+ }
41
+ // Add the active effect to the set if not already present
42
+ // Ensure we are adding the correct type to the Set
43
+ const effectToAdd = activeEffect;
44
+ if (!dep.has(effectToAdd)) {
45
+ dep.add(effectToAdd);
46
+ // Add this dep set to the effect's own dependency list for cleanup
47
+ if (!effectToAdd.dependencies) {
48
+ effectToAdd.dependencies = new Set();
49
+ }
50
+ effectToAdd.dependencies.add(dep);
51
+ // Trigger onTrack if available
52
+ if (effectToAdd.options?.onTrack) {
53
+ // Pass the raw user callback, not the internal effect object
54
+ effectToAdd.options.onTrack({ effect: effectToAdd._rawCallback, target, key, type: 'track' });
55
+ }
56
+ }
57
+ }
58
+ /**
59
+ * Trigger effects associated with a property (Synchronous Only)
60
+ */
61
+ export function trigger(target, key) {
62
+ const depsMap = targetMap.get(target);
63
+ if (!depsMap)
64
+ return;
65
+ // Use a Set to avoid duplicate runs within the same trigger
66
+ const effectsToRun = new Set();
67
+ const addEffects = (depKey) => {
68
+ const dep = depsMap.get(depKey);
69
+ if (dep) {
70
+ dep.forEach(effect => {
71
+ // Avoid infinite loops by not scheduling the currently running effect
72
+ // Also check if effect is active
73
+ if (effect !== activeEffect && effect.active) {
74
+ effectsToRun.add(effect);
75
+ }
76
+ });
77
+ }
78
+ };
79
+ addEffects(key);
80
+ // Schedule or run effects
81
+ effectsToRun.forEach(effect => {
82
+ // Trigger onTrigger if available
83
+ if (effect.options?.onTrigger) {
84
+ effect.options.onTrigger({ effect: effect._rawCallback, target, key, type: 'trigger' });
85
+ }
86
+ // Use scheduler if available, otherwise run directly
87
+ if (effect.options?.scheduler) {
88
+ effect.options.scheduler(effect.run);
89
+ }
90
+ else {
91
+ effect.run(); // Run the effect's wrapper
92
+ }
93
+ });
94
+ }
95
+ /**
96
+ * Runs a function and re-runs it when its reactive dependencies change.
97
+ * Returns a stop handle that also exposes the effect instance.
98
+ */
99
+ export function watchEffect(effectCallback, options = {}) {
100
+ // The core runner function that handles cleanup, activeEffect setting, and execution
101
+ const run = () => {
102
+ if (!effectFn.active)
103
+ return effectCallback(); // If stopped, just return value (though likely undefined)
104
+ const previousEffect = activeEffect;
105
+ try {
106
+ cleanupEffect(effectFn);
107
+ setActiveEffect(effectFn);
108
+ return effectCallback(); // Execute the user's function
109
+ }
110
+ finally {
111
+ setActiveEffect(previousEffect);
112
+ }
113
+ };
114
+ // The effect object itself
115
+ const effectFn = {
116
+ run: run,
117
+ dependencies: new Set(),
118
+ options: options,
119
+ active: true,
120
+ _rawCallback: effectCallback
121
+ };
122
+ // Run effect immediately unless lazy
123
+ if (!options.lazy) {
124
+ effectFn.run();
125
+ }
126
+ // Create the stop handle function
127
+ const stopHandle = () => {
128
+ if (effectFn.active) {
129
+ cleanupEffect(effectFn);
130
+ effectFn.active = false;
131
+ // Clear references
132
+ // effectFn.dependencies = undefined; // Keep dependencies for potential re-activation?
133
+ // effectFn.options = undefined;
134
+ }
135
+ };
136
+ // Attach the effect instance to the stop handle
137
+ stopHandle.effect = effectFn;
138
+ return stopHandle;
139
+ }
@@ -0,0 +1,2 @@
1
+ import { EmitFunction, Path } from './types';
2
+ export declare function wrapArray<T extends any[]>(arr: T, emit: EmitFunction, path: Path): T;