@yiin/reactive-proxy-state 1.0.4 → 1.0.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/dist/watch.js CHANGED
@@ -1,74 +1,44 @@
1
1
  import { watchEffect } from './watchEffect';
2
2
  import { traverse, deepClone } from './utils';
3
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
4
+ * watches a reactive source (getter function or reactive object/ref)
5
+ * and runs a callback when the source's value changes.
10
6
  */
11
- export function watch(sourceInput, // Renamed parameter
12
- callback, options = {}) {
13
- // Default deep to true unless explicitly false
7
+ export function watch(sourceInput, callback, options = {}) {
14
8
  const { immediate = false, deep = true } = options;
15
- // Normalize sourceInput to always be a function
9
+ // normalize source to always be a getter function
16
10
  const source = typeof sourceInput === 'function'
17
11
  ? sourceInput
18
12
  : () => sourceInput;
19
13
  let oldValue;
20
14
  let initialized = false;
21
- // Use watchEffect to track dependencies and re-run when they change
15
+ // use watchEffect internally to handle dependency tracking
22
16
  const stopEffect = watchEffect(() => {
23
- // 1. Run the source function to get the current value
24
17
  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
18
+ // if deep watching, traverse the current value to track nested dependencies
19
+ if (deep) {
20
+ traverse(currentValue);
40
21
  }
41
- // 3. Compare the actual currentValue with the oldValue
42
22
  if (initialized) {
43
23
  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
- }
24
+ // for deep watches, the effect running implies a dependency changed.
25
+ // for shallow, explicitly check reference equality.
26
+ hasChanged = deep || currentValue !== oldValue;
54
27
  if (hasChanged) {
55
- // Get the value to pass as the previous oldValue to the callback
56
28
  const prevOldValue = oldValue;
57
- // Update the stored oldValue. Clone *only if* deep watching is enabled.
29
+ // store a clone for deep watches to pass as the correct oldValue next time
58
30
  oldValue = deep ? deepClone(currentValue) : currentValue;
59
31
  callback(currentValue, prevOldValue);
60
32
  }
61
33
  }
62
34
  else {
63
- // First run: establish initial oldValue (cloned if deep) and handle immediate call
35
+ // first run: store initial value (cloned if deep) and run immediate callback if requested
64
36
  oldValue = deep ? deepClone(currentValue) : currentValue;
65
37
  initialized = true;
66
38
  if (immediate) {
67
- // Pass undefined as oldValue for immediate calls
68
- callback(currentValue, undefined);
39
+ callback(currentValue, undefined); // pass undefined as oldValue for immediate
69
40
  }
70
41
  }
71
- });
72
- // Return the stop handle from watchEffect
42
+ }, { lazy: false }); // run immediately (watchEffect handles `immediate` option internally)
73
43
  return stopEffect;
74
44
  }
@@ -14,15 +14,19 @@ export interface TrackedEffect<T = any> {
14
14
  export declare let activeEffect: TrackedEffect<any> | null;
15
15
  export declare function setActiveEffect(effect: TrackedEffect<any> | null): void;
16
16
  /**
17
- * Clean up dependencies for a specific effect
17
+ * removes an effect from all dependency sets it belongs to.
18
+ * this is crucial to prevent memory leaks and unnecessary updates when an effect is stopped or re-run.
18
19
  */
19
20
  export declare function cleanupEffect(effect: TrackedEffect<any>): void;
20
21
  /**
21
- * Track a property access for the active effect
22
+ * establishes a dependency between the currently active effect and a specific object property.
23
+ * called by proxy getters or ref getters.
22
24
  */
23
25
  export declare function track(target: object, key: string | symbol): void;
24
26
  /**
25
- * Trigger effects associated with a property (Synchronous Only)
27
+ * triggers all active effects associated with a specific object property.
28
+ * called by proxy setters/deleters or ref setters.
29
+ * currently runs effects synchronously.
26
30
  */
27
31
  export declare function trigger(target: object, key: string | symbol): void;
28
32
  export interface WatchEffectOptions {
@@ -42,8 +46,9 @@ export interface WatchEffectOptions {
42
46
  }) => void;
43
47
  }
44
48
  /**
45
- * Runs a function and re-runs it when its reactive dependencies change.
46
- * Returns a stop handle that also exposes the effect instance.
49
+ * runs a function immediately, tracks its reactive dependencies, and re-runs it
50
+ * synchronously whenever any of those dependencies change.
51
+ * returns a stop handle to manually stop the effect.
47
52
  */
48
53
  export declare function watchEffect<T>(effectCallback: EffectCallback<T>, options?: WatchEffectOptions): WatchEffectStopHandle<T>;
49
54
  export {};
@@ -1,139 +1,154 @@
1
- // Track currently running effect
2
- // Note: activeEffect can hold effects with different return types
1
+ // tracks the currently executing effect to establish dependencies
3
2
  export let activeEffect = null;
4
- // Setter function for activeEffect
3
+ // allows setting the active effect, used internally by the effect runner
5
4
  export function setActiveEffect(effect) {
6
5
  activeEffect = effect;
7
6
  }
8
- // Store for tracking dependencies: target -> key -> Set<TrackedEffect>
9
- // The Sets will hold effects of potentially different types
7
+ // storage for dependencies: target object -> property key -> set of effects that depend on this key
10
8
  const targetMap = new WeakMap();
11
9
  /**
12
- * Clean up dependencies for a specific effect
10
+ * removes an effect from all dependency sets it belongs to.
11
+ * this is crucial to prevent memory leaks and unnecessary updates when an effect is stopped or re-run.
13
12
  */
14
13
  export function cleanupEffect(effect) {
15
14
  if (effect.dependencies) {
16
15
  effect.dependencies.forEach(dep => {
16
+ // remove this effect from the dependency set associated with a specific target/key
17
17
  dep.delete(effect);
18
18
  });
19
- effect.dependencies.clear(); // Clear the set for the next run
19
+ // clear the effect's own list of dependencies for the next run
20
+ effect.dependencies.clear();
20
21
  }
21
22
  }
22
23
  /**
23
- * Track a property access for the active effect
24
+ * establishes a dependency between the currently active effect and a specific object property.
25
+ * called by proxy getters or ref getters.
24
26
  */
25
27
  export function track(target, key) {
26
- // Only track if there's an active effect that should be running
28
+ // do nothing if there is no active effect or if the effect is stopped
27
29
  if (!activeEffect || !activeEffect.active)
28
30
  return;
29
- // Get the dependency map for this target
31
+ // get or create the dependency map for the target object
30
32
  let depsMap = targetMap.get(target);
31
33
  if (!depsMap) {
32
34
  depsMap = new Map();
33
35
  targetMap.set(target, depsMap);
34
36
  }
35
- // Get the set of effects for this property
37
+ // get or create the set of effects for the specific property key
36
38
  let dep = depsMap.get(key);
37
39
  if (!dep) {
38
40
  dep = new Set();
39
41
  depsMap.set(key, dep);
40
42
  }
41
- // Add the active effect to the set if not already present
42
- // Ensure we are adding the correct type to the Set
43
+ // add the current effect to the dependency set if it's not already there
43
44
  const effectToAdd = activeEffect;
44
45
  if (!dep.has(effectToAdd)) {
45
46
  dep.add(effectToAdd);
46
- // Add this dep set to the effect's own dependency list for cleanup
47
+ // also add this dependency set to the effect's own tracking list for cleanup purposes
47
48
  if (!effectToAdd.dependencies) {
48
49
  effectToAdd.dependencies = new Set();
49
50
  }
50
51
  effectToAdd.dependencies.add(dep);
51
- // Trigger onTrack if available
52
+ // trigger the onTrack debug hook if provided
52
53
  if (effectToAdd.options?.onTrack) {
53
- // Pass the raw user callback, not the internal effect object
54
+ // pass the original user callback to the hook, not the internal wrapper
54
55
  effectToAdd.options.onTrack({ effect: effectToAdd._rawCallback, target, key, type: 'track' });
55
56
  }
56
57
  }
57
58
  }
58
59
  /**
59
- * Trigger effects associated with a property (Synchronous Only)
60
+ * triggers all active effects associated with a specific object property.
61
+ * called by proxy setters/deleters or ref setters.
62
+ * currently runs effects synchronously.
60
63
  */
61
64
  export function trigger(target, key) {
62
65
  const depsMap = targetMap.get(target);
63
66
  if (!depsMap)
64
- return;
65
- // Use a Set to avoid duplicate runs within the same trigger
67
+ return; // no effects tracked for this target
68
+ // use a set to collect effects to run, avoiding duplicate executions within the same trigger cycle
66
69
  const effectsToRun = new Set();
70
+ // helper to add effects from a specific dependency set to the run queue
67
71
  const addEffects = (depKey) => {
68
72
  const dep = depsMap.get(depKey);
69
73
  if (dep) {
70
74
  dep.forEach(effect => {
71
- // Avoid infinite loops by not scheduling the currently running effect
72
- // Also check if effect is active
75
+ // avoid triggering the effect if it's the one currently running (prevents infinite loops)
76
+ // also ensure the effect hasn't been stopped
73
77
  if (effect !== activeEffect && effect.active) {
74
78
  effectsToRun.add(effect);
75
79
  }
76
80
  });
77
81
  }
78
82
  };
83
+ // add effects associated with the specific key that changed
79
84
  addEffects(key);
80
- // Schedule or run effects
85
+ // todo: consider adding effects associated with iteration keys (like Symbol.iterator or 'length' for arrays) if applicable
86
+ // schedule or run the collected effects
81
87
  effectsToRun.forEach(effect => {
82
- // Trigger onTrigger if available
88
+ // trigger the onTrigger debug hook if provided
83
89
  if (effect.options?.onTrigger) {
84
90
  effect.options.onTrigger({ effect: effect._rawCallback, target, key, type: 'trigger' });
85
91
  }
86
- // Use scheduler if available, otherwise run directly
92
+ // use a custom scheduler if provided, otherwise run the effect synchronously
87
93
  if (effect.options?.scheduler) {
88
94
  effect.options.scheduler(effect.run);
89
95
  }
90
96
  else {
91
- effect.run(); // Run the effect's wrapper
97
+ effect.run(); // execute the effect's wrapper function (`run`)
92
98
  }
93
99
  });
94
100
  }
95
101
  /**
96
- * Runs a function and re-runs it when its reactive dependencies change.
97
- * Returns a stop handle that also exposes the effect instance.
102
+ * runs a function immediately, tracks its reactive dependencies, and re-runs it
103
+ * synchronously whenever any of those dependencies change.
104
+ * returns a stop handle to manually stop the effect.
98
105
  */
99
106
  export function watchEffect(effectCallback, options = {}) {
100
- // The core runner function that handles cleanup, activeEffect setting, and execution
107
+ // the wrapper function that manages the effect lifecycle (cleanup, tracking, execution)
101
108
  const run = () => {
102
- if (!effectFn.active)
103
- return effectCallback(); // If stopped, just return value (though likely undefined)
109
+ if (!effectFn.active) {
110
+ // if stopped, potentially run the callback once without tracking, though behavior might be undefined
111
+ // vue's behavior here might differ, review needed if exact compatibility matters
112
+ try {
113
+ return effectCallback();
114
+ }
115
+ catch (e) {
116
+ console.error("error in stopped watchEffect callback:", e);
117
+ // decide on return value for stopped effects that error
118
+ return undefined; // or rethrow?
119
+ }
120
+ }
104
121
  const previousEffect = activeEffect;
105
122
  try {
106
- cleanupEffect(effectFn);
107
- setActiveEffect(effectFn);
108
- return effectCallback(); // Execute the user's function
123
+ cleanupEffect(effectFn); // clean up dependencies from the previous run
124
+ setActiveEffect(effectFn); // set this effect as the one currently tracking
125
+ return effectCallback(); // execute the user's function, triggering tracks
109
126
  }
110
127
  finally {
111
- setActiveEffect(previousEffect);
128
+ setActiveEffect(previousEffect); // restore the previous active effect
112
129
  }
113
130
  };
114
- // The effect object itself
131
+ // create the internal effect object
115
132
  const effectFn = {
116
133
  run: run,
117
- dependencies: new Set(),
134
+ dependencies: new Set(), // initialize empty dependencies
118
135
  options: options,
119
- active: true,
120
- _rawCallback: effectCallback
136
+ active: true, // start as active
137
+ _rawCallback: effectCallback // store the original callback
121
138
  };
122
- // Run effect immediately unless lazy
139
+ // run the effect immediately unless the `lazy` option is true
123
140
  if (!options.lazy) {
124
141
  effectFn.run();
125
142
  }
126
- // Create the stop handle function
143
+ // create the function that stops the effect
127
144
  const stopHandle = () => {
128
145
  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;
146
+ cleanupEffect(effectFn); // remove from dependency lists
147
+ effectFn.active = false; // mark as inactive
148
+ // potentially clear other properties like dependencies/options if desired, but keeping them might allow restart? TBD.
134
149
  }
135
150
  };
136
- // Attach the effect instance to the stop handle
151
+ // attach the effect instance to the stop handle for potential advanced usage
137
152
  stopHandle.effect = effectFn;
138
153
  return stopHandle;
139
154
  }
package/dist/wrapArray.js CHANGED
@@ -3,20 +3,19 @@ import { reactive } from './reactive';
3
3
  import { wrapMap } from './wrapMap';
4
4
  import { wrapSet } from './wrapSet';
5
5
  import { track, trigger } from './watchEffect';
6
- // Pre-allocate type check function
6
+ // avoid repeated typeof checks
7
7
  function isObject(v) {
8
8
  return v && typeof v === 'object';
9
9
  }
10
10
  export function wrapArray(arr, emit, path) {
11
- // Check wrapper cache first
11
+ // reuse existing proxy if available for performance
12
12
  const cachedProxy = wrapperCache.get(arr);
13
13
  if (cachedProxy)
14
14
  return cachedProxy;
15
15
  const proxy = new Proxy(arr, {
16
16
  get(target, prop, receiver) {
17
- // Original track call - might be redundant if handled below but keep for now
18
17
  track(target, prop);
19
- // Handle specific array mutation methods first
18
+ // handle specific array mutation methods that require custom logic and event emission
20
19
  switch (prop) {
21
20
  case 'push':
22
21
  track(target, 'length');
@@ -28,7 +27,7 @@ export function wrapArray(arr, emit, path) {
28
27
  const event = {
29
28
  action: 'array-push',
30
29
  path: path,
31
- key: oldLength, // Start index was the old length
30
+ key: oldLength, // start index was the old length
32
31
  items: items
33
32
  };
34
33
  emit(event);
@@ -132,15 +131,11 @@ export function wrapArray(arr, emit, path) {
132
131
  }
133
132
  return result;
134
133
  };
135
- // Handle iteration methods
134
+ // handle methods that rely on iteration state
136
135
  case Symbol.iterator:
137
- case 'values': // values() returns an iterator
138
- case 'keys': // keys() returns an iterator
139
- case 'entries': // entries() returns an iterator
140
- // Track dependency on iteration
141
- track(target, Symbol.iterator);
142
- // Fall through to Reflect.get and bind below
143
- break;
136
+ case 'values':
137
+ case 'keys':
138
+ case 'entries':
144
139
  case 'forEach':
145
140
  case 'map':
146
141
  case 'filter':
@@ -150,38 +145,34 @@ export function wrapArray(arr, emit, path) {
150
145
  case 'findIndex':
151
146
  case 'every':
152
147
  case 'some':
153
- case 'join': // join depends on iteration
154
- // These methods depend on iteration
148
+ case 'join':
155
149
  track(target, Symbol.iterator);
156
- // Fall through to Reflect.get and bind below
150
+ // fall through to default behavior (usually binding)
157
151
  break;
158
152
  case 'length':
159
- // Explicitly track length access
160
153
  track(target, 'length');
161
154
  return Reflect.get(target, prop, receiver);
162
155
  }
163
- // Fallback for index access and other properties
164
156
  const value = Reflect.get(target, prop, receiver);
165
- // Handle index access: wrap retrieved element if it's an object
157
+ // determine if the property access is numeric array index access
166
158
  const isNumericIndex = typeof prop === 'number' || (typeof prop === 'string' && !isNaN(parseInt(prop, 10)));
167
159
  if (isNumericIndex) {
168
- // Track access to specific index
169
160
  track(target, String(prop));
170
161
  if (!isObject(value))
171
162
  return value;
172
- // Check wrapper cache for the element
163
+ // reuse existing proxy for nested object/array if available
173
164
  const cachedValueProxy = wrapperCache.get(value);
174
165
  if (cachedValueProxy)
175
166
  return cachedValueProxy;
176
- // Calculate path for the element
167
+ // calculate the nested path for the element, optimizing with caching
177
168
  const propKey = String(prop);
178
- const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey; // Fix pathKey generation for index 0
169
+ const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
179
170
  let newPath = getPathConcat(pathKey);
180
171
  if (newPath === undefined) {
181
172
  newPath = path.concat(propKey);
182
173
  setPathConcat(pathKey, newPath);
183
174
  }
184
- // Wrap based on type (no longer passing seen)
175
+ // recursively wrap nested structures
185
176
  if (Array.isArray(value))
186
177
  return wrapArray(value, emit, newPath);
187
178
  if (value instanceof Map)
@@ -189,12 +180,10 @@ export function wrapArray(arr, emit, path) {
189
180
  if (value instanceof Set)
190
181
  return wrapSet(value, emit, newPath);
191
182
  if (value instanceof Date)
192
- return new Date(value.getTime()); // Dates are not proxied
193
- // Default to reactive for plain objects
183
+ return new Date(value.getTime()); // dates are not proxied, return a copy
194
184
  return reactive(value, emit, newPath);
195
185
  }
196
- // For non-numeric properties or properties that aren't objects, return value directly
197
- // Also handle functions bound to the target
186
+ // ensure functions accessed directly are bound to the original target
198
187
  if (typeof value === 'function') {
199
188
  return value.bind(target);
200
189
  }
@@ -202,18 +191,19 @@ export function wrapArray(arr, emit, path) {
202
191
  },
203
192
  set(target, prop, value, receiver) {
204
193
  const oldValue = target[prop];
205
- // Fast path for primitive equality
194
+ // avoid unnecessary triggers if value hasn't changed
206
195
  if (oldValue === value)
207
196
  return true;
208
- // Deep equality check with new WeakMap
209
197
  if (isObject(oldValue) && isObject(value) && deepEqual(oldValue, value, new WeakMap()))
210
198
  return true;
211
199
  const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
212
200
  const result = Reflect.set(target, prop, value, receiver);
213
201
  const isNumericIndex = typeof prop === 'number' || (typeof prop === 'string' && !isNaN(parseInt(String(prop))));
202
+ // emit event and trigger effects only if the set was successful and wasn't intercepted by a setter
203
+ // (unless it's a direct numeric index set, which doesn't have a descriptor.set)
214
204
  if (result && (!descriptor || !descriptor.set || isNumericIndex)) {
215
205
  const propKey = String(prop);
216
- const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey; // Fix pathKey generation for index 0
206
+ const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
217
207
  let newPath = getPathConcat(pathKey);
218
208
  if (newPath === undefined) {
219
209
  newPath = path.concat(propKey);
@@ -231,7 +221,7 @@ export function wrapArray(arr, emit, path) {
231
221
  return result;
232
222
  }
233
223
  });
234
- // Cache the newly created proxy before returning
224
+ // cache the newly created proxy before returning
235
225
  wrapperCache.set(arr, proxy);
236
226
  return proxy;
237
227
  }