@yiin/reactive-proxy-state 1.0.3 → 1.0.5

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/state.js CHANGED
@@ -1,23 +1,26 @@
1
1
  import { pathCache, setInPathCache } from './utils';
2
- // Pre-allocate helper functions to avoid recreation
2
+ // helper to abstract getting a value from a map or object property
3
3
  function getValue(obj, key) {
4
4
  if (obj instanceof Map)
5
5
  return obj.get(key);
6
6
  return obj[key];
7
7
  }
8
+ // helper to abstract setting a value on a map or object property
8
9
  function setValue(obj, key, value) {
9
10
  if (obj instanceof Map)
10
11
  obj.set(key, value);
11
12
  else
12
13
  obj[key] = value;
13
14
  }
15
+ // helper to abstract deleting a key from a map or object
14
16
  function deleteValue(obj, key) {
15
17
  if (obj instanceof Map)
16
18
  obj.delete(key);
17
19
  else
18
20
  delete obj[key];
19
21
  }
20
- // Cache for action handlers to avoid switch statement overhead
22
+ // dispatch table for applying state events based on action type
23
+ // this avoids a large switch statement and makes adding new actions easier
21
24
  const actionHandlers = {
22
25
  'set': function (parent, key, event) {
23
26
  setValue(parent, key, event.newValue);
@@ -27,36 +30,36 @@ const actionHandlers = {
27
30
  },
28
31
  'array-push': function (targetArray, _keyIgnored, event) {
29
32
  if (!Array.isArray(targetArray)) {
30
- console.warn(`Expected Array at path ${event.path.join('.')}`);
33
+ console.warn(`expected array at path ${event.path.join('.')}`);
31
34
  return;
32
35
  }
33
36
  if (!event.items) {
34
37
  console.warn('array-push event missing items');
35
38
  return;
36
39
  }
37
- // Note: event.key is the starting index, but push always adds to the end.
40
+ // event.key is the starting index, but push always adds to end
38
41
  targetArray.push(...event.items);
39
42
  },
40
43
  'array-pop': function (targetArray, _keyIgnored, event) {
41
44
  if (!Array.isArray(targetArray)) {
42
- console.warn(`Expected Array at path ${event.path.join('.')}`);
45
+ console.warn(`expected array at path ${event.path.join('.')}`);
43
46
  return;
44
47
  }
45
- // We don't need event.key or event.oldValue to perform the pop.
48
+ // event info (key, oldvalue) not needed to perform pop
46
49
  if (targetArray.length > 0) {
47
50
  targetArray.pop();
48
51
  }
49
52
  },
50
53
  'array-splice': function (targetArray, _keyIgnored, event) {
51
54
  if (!Array.isArray(targetArray)) {
52
- console.warn(`Expected Array at path ${event.path.join('.')}`);
55
+ console.warn(`expected array at path ${event.path.join('.')}`);
53
56
  return;
54
57
  }
55
58
  if (event.key === undefined || event.deleteCount === undefined) {
56
- console.warn('array-splice event missing key or deleteCount');
59
+ console.warn('array-splice event missing key or deletecount');
57
60
  return;
58
61
  }
59
- // Call splice with appropriate arguments
62
+ // handle splice with or without items to insert
60
63
  if (event.items && event.items.length > 0) {
61
64
  targetArray.splice(event.key, event.deleteCount, ...event.items);
62
65
  }
@@ -66,142 +69,143 @@ const actionHandlers = {
66
69
  },
67
70
  'array-shift': function (targetArray, _keyIgnored, event) {
68
71
  if (!Array.isArray(targetArray)) {
69
- console.warn(`Expected Array at path ${event.path.join('.')}`);
72
+ console.warn(`expected array at path ${event.path.join('.')}`);
70
73
  return;
71
74
  }
72
- // We don't need event.key or event.oldValue to perform the shift.
75
+ // event info not needed to perform shift
73
76
  if (targetArray.length > 0) {
74
77
  targetArray.shift();
75
78
  }
76
79
  },
77
80
  'array-unshift': function (targetArray, _keyIgnored, event) {
78
81
  if (!Array.isArray(targetArray)) {
79
- console.warn(`Expected Array at path ${event.path.join('.')}`);
82
+ console.warn(`expected array at path ${event.path.join('.')}`);
80
83
  return;
81
84
  }
82
85
  if (!event.items) {
83
86
  console.warn('array-unshift event missing items');
84
87
  return;
85
88
  }
86
- // We don't need event.key to perform unshift
89
+ // event.key (always 0) not needed to perform unshift
87
90
  targetArray.unshift(...event.items);
88
91
  },
89
- 'map-set': function (parent, key, event) {
90
- const target = getValue(parent, key);
91
- if (target instanceof Map) {
92
- target.set(event.key, event.newValue);
93
- }
94
- else {
95
- console.warn(`Expected Map at path ${key}`);
92
+ // note: for map/set operations originating from the proxy wrapper (e.g., map.set(k,v)),
93
+ // the *event path* points to the map itself, and event.key/event.value hold the map's key/value.
94
+ // however, the actionHandlers expect `target` to be the collection and `key` to be the map/set key.
95
+ // the logic in `updateState` below handles finding the correct target collection first.
96
+ 'map-set': function (targetMap, _parentKeyIgnored, event) {
97
+ if (!(targetMap instanceof Map)) {
98
+ console.warn(`expected map at path ${event.path.join('.')}`);
99
+ return;
96
100
  }
101
+ targetMap.set(event.key, event.newValue);
97
102
  },
98
- 'map-delete': function (parent, key, event) {
99
- const target = getValue(parent, key);
100
- if (target instanceof Map) {
101
- target.delete(event.key);
102
- }
103
- else {
104
- console.warn(`Expected Map at path ${key}`);
103
+ 'map-delete': function (targetMap, _parentKeyIgnored, event) {
104
+ if (!(targetMap instanceof Map)) {
105
+ console.warn(`expected map at path ${event.path.join('.')}`);
106
+ return;
105
107
  }
108
+ targetMap.delete(event.key);
106
109
  },
107
- 'map-clear': function (parent, key, event) {
108
- const target = getValue(parent, key);
109
- if (target instanceof Map) {
110
- target.clear();
111
- }
112
- else {
113
- console.warn(`Expected Map at path ${key}`);
110
+ 'map-clear': function (targetMap, _parentKeyIgnored, event) {
111
+ if (!(targetMap instanceof Map)) {
112
+ console.warn(`expected map at path ${event.path.join('.')}`);
113
+ return;
114
114
  }
115
+ targetMap.clear();
115
116
  },
116
117
  'set-add': function (targetSet, _keyIgnored, event) {
117
- if (targetSet instanceof Set) {
118
- targetSet.add(event.value);
119
- }
120
- else {
121
- console.warn(`Expected Set at path ${event.path.join('.')}`);
118
+ if (!(targetSet instanceof Set)) {
119
+ console.warn(`expected set at path ${event.path.join('.')}`);
120
+ return;
122
121
  }
122
+ targetSet.add(event.value);
123
123
  },
124
124
  'set-delete': function (targetSet, _keyIgnored, event) {
125
- if (targetSet instanceof Set) {
126
- targetSet.delete(event.value);
127
- }
128
- else {
129
- console.warn(`Expected Set at path ${event.path.join('.')}`);
125
+ if (!(targetSet instanceof Set)) {
126
+ console.warn(`expected set at path ${event.path.join('.')}`);
127
+ return;
130
128
  }
129
+ targetSet.delete(event.value);
131
130
  },
132
131
  'set-clear': function (targetSet, _keyIgnored, event) {
133
- if (targetSet instanceof Set) {
134
- targetSet.clear();
135
- }
136
- else {
137
- console.warn(`Expected Set at path ${event.path.join('.')}`);
132
+ if (!(targetSet instanceof Set)) {
133
+ console.warn(`expected set at path ${event.path.join('.')}`);
134
+ return;
138
135
  }
136
+ targetSet.clear();
139
137
  }
140
138
  };
139
+ // applies a state change event to a plain javascript object/array (the target state)
141
140
  export function updateState(root, event) {
142
141
  const { action, path } = event;
143
- if (path.length === 0) {
144
- console.warn('Event path is empty');
142
+ if (!path || path.length === 0) {
143
+ console.warn('event path is empty, cannot apply update', event);
145
144
  return;
146
145
  }
147
146
  const handler = actionHandlers[action];
148
147
  if (!handler) {
149
- throw new Error(`Unhandled action: ${action}`);
148
+ // maybe allow custom handlers or ignore unknown actions?
149
+ console.error(`unhandled action type: ${action}`, event);
150
+ return;
151
+ // throw new Error(`Unhandled action: ${action}`);
150
152
  }
151
- // Determine target and key based on action type
153
+ // determine the target object/collection and the specific key (if applicable)
154
+ // based on the action type and the event path.
152
155
  let targetForHandler;
153
- let keyForHandler = null; // Key is only relevant for set/delete/map actions
154
- if (action === 'set' || action === 'delete' || action.startsWith('map-')) {
155
- // Actions where path leads to parent, last element is key
156
+ let keyForHandler = null; // key passed to handler, relevant for set/delete
157
+ // path resolution logic differs based on action type:
158
+ // - set/delete: path leads to the *value* being set/deleted, so we need the parent object and the final path segment (key).
159
+ // - array-*/map-*/set-*: path leads to the *collection* itself.
160
+ if (action === 'set' || action === 'delete') {
161
+ // need the parent object and the final key
156
162
  if (path.length === 1) {
157
- targetForHandler = root;
163
+ targetForHandler = root; // parent is the root object
158
164
  keyForHandler = path[0];
159
165
  }
160
166
  else {
161
- const parentPathKey = path.slice(0, -1).join('.');
167
+ const parentPath = path.slice(0, -1);
168
+ const parentPathKey = parentPath.join('.');
169
+ // try cache first for performance
162
170
  let parent = pathCache.get(root)?.get(parentPathKey);
163
171
  if (parent === undefined) {
164
- parent = path.slice(0, -1).reduce((acc, key) => acc ? getValue(acc, key) : undefined, root);
172
+ // traverse path manually if not in cache
173
+ parent = parentPath.reduce((acc, key) => acc ? getValue(acc, key) : undefined, root);
165
174
  if (parent !== undefined)
166
- setInPathCache(root, parentPathKey, parent);
175
+ setInPathCache(root, parentPathKey, parent); // cache if found
167
176
  }
168
177
  if (parent === undefined) {
169
- console.warn(`Parent path ${parentPathKey} not found for action ${action}`);
178
+ console.warn(`parent path ${parentPathKey} not found for action ${action}`);
170
179
  return;
171
180
  }
172
181
  targetForHandler = parent;
173
182
  keyForHandler = path[path.length - 1];
174
183
  }
175
184
  }
176
- else if (action.startsWith('array-') || action.startsWith('set-')) {
177
- // Actions where path leads directly to the collection (Array or Set)
178
- if (path.length === 1) {
179
- targetForHandler = getValue(root, path[0]);
180
- }
181
- else {
182
- const parentPathKey = path.slice(0, -1).join('.');
183
- let parent = pathCache.get(root)?.get(parentPathKey);
184
- if (parent === undefined) {
185
- parent = path.slice(0, -1).reduce((acc, key) => acc ? getValue(acc, key) : undefined, root);
186
- if (parent !== undefined)
187
- setInPathCache(root, parentPathKey, parent);
188
- }
189
- if (parent === undefined) {
190
- console.warn(`Parent path ${parentPathKey} not found for action ${action}`);
191
- return;
192
- }
193
- targetForHandler = getValue(parent, path[path.length - 1]);
194
- }
195
- if (targetForHandler === undefined) {
196
- console.warn(`Target collection at path ${path.join('.')} not found for action ${action}`);
185
+ else if (action.startsWith('array-') || action.startsWith('map-') || action.startsWith('set-')) {
186
+ // need the collection object itself (array, map, or set)
187
+ const targetPath = path;
188
+ const targetPathKey = targetPath.join('.');
189
+ // try cache first
190
+ let targetCollection = pathCache.get(root)?.get(targetPathKey);
191
+ if (targetCollection === undefined) {
192
+ // traverse path manually if not in cache
193
+ targetCollection = targetPath.reduce((acc, key) => acc ? getValue(acc, key) : undefined, root);
194
+ if (targetCollection !== undefined)
195
+ setInPathCache(root, targetPathKey, targetCollection); // cache if found
196
+ }
197
+ if (targetCollection === undefined) {
198
+ console.warn(`target collection at path ${targetPathKey} not found for action ${action}`);
197
199
  return;
198
200
  }
201
+ targetForHandler = targetCollection;
202
+ // keyForHandler remains null for these actions as the handler operates directly on the collection
199
203
  }
200
204
  else {
201
- // Should not happen if handler exists
202
- console.error(`Unexpected action type passed checks: ${action}`);
205
+ // this should not be reachable if handler lookup succeeded
206
+ console.error(`unexpected action type passed checks: ${action}`);
203
207
  return;
204
208
  }
205
- // Call the handler with the appropriately determined target and key
209
+ // call the appropriate handler with the resolved target and key
206
210
  handler(targetForHandler, keyForHandler, event);
207
211
  }
package/dist/types.d.ts CHANGED
@@ -3,11 +3,10 @@ export type ActionType = 'set' | 'delete' | 'array-push' | 'array-pop' | 'array-
3
3
  export interface StateEvent {
4
4
  action: ActionType;
5
5
  path: Path;
6
- oldValue?: any;
7
6
  newValue?: any;
7
+ oldValue?: any;
8
8
  key?: any;
9
9
  value?: any;
10
- args?: any[];
11
10
  items?: any[];
12
11
  deleteCount?: number;
13
12
  oldValues?: any[];
package/dist/utils.d.ts CHANGED
@@ -8,18 +8,13 @@ export declare function setInPathCache(root: object, pathKey: string, value: any
8
8
  export declare function getPathConcat(path: string): any[] | undefined;
9
9
  export declare function setPathConcat(path: string, value: any[]): void;
10
10
  /**
11
- * Recursively traverses an object to track all nested properties.
12
- * Used for deep watching.
13
- * @param value - The value to traverse.
14
- * @param seen - A Set to handle circular references.
11
+ * recursively traverses an object or array, accessing each property/element.
12
+ * used by `watch` with `deep: true` to establish dependencies on all nested properties.
13
+ * the actual tracking is done by the proxy `get` handlers triggered during traversal.
15
14
  */
16
15
  export declare function traverse(value: any, seen?: Set<any>): any;
17
16
  /**
18
- * Creates a deep clone of a value.
19
- * Handles primitives, Dates, Arrays, Maps, Sets, and plain objects.
20
- * Includes cycle detection.
21
- * @param value The value to clone.
22
- * @param seen A WeakMap to handle circular references during recursion.
23
- * @returns A deep clone of the value.
17
+ * creates a deep clone of a value.
18
+ * includes cycle detection using a weakmap.
24
19
  */
25
20
  export declare function deepClone<T>(value: T, seen?: WeakMap<WeakKey, any>): T;
package/dist/utils.js CHANGED
@@ -1,20 +1,22 @@
1
- // Cache for memoized deepEqual results with cleanup
1
+ // cache for memoized deepEqual results, using weakmap to avoid memory leaks
2
2
  const deepEqualCache = new WeakMap();
3
- const MAX_CACHE_SIZE = 1000; // Prevent unbounded growth
4
- // Path traversal cache with LRU-like behavior
3
+ const MAX_CACHE_SIZE = 1000;
4
+ // path traversal cache (object -> pathString -> value) with lru-like behavior
5
+ // weakmap for the root object ensures the cache entry is garbage collected when the object is
5
6
  export const pathCache = new WeakMap();
6
- const pathCacheSize = new WeakMap();
7
- // Path concatenation cache with size limit
7
+ const pathCacheSize = new WeakMap(); // track individual map sizes
8
+ // path concatenation cache ('a.b.c' -> ['a', 'b', 'c']) with size limit
8
9
  export const pathConcatCache = new Map();
9
10
  const MAX_PATH_CACHE_SIZE = 1000;
10
- // Global seen map for circular reference detection
11
+ // global weakmap for circular reference detection during deep operations like clone or equal
11
12
  export const globalSeen = new WeakMap();
12
- // Global cache for proxy wrappers
13
+ // global cache to reuse proxy wrappers for the same original object
13
14
  export const wrapperCache = new WeakMap();
15
+ // simple lru-like cleanup for individual object path caches
14
16
  function cleanupPathCache(root) {
15
17
  const cache = pathCache.get(root);
16
18
  if (cache && pathCacheSize.get(root) > MAX_CACHE_SIZE) {
17
- // Clear oldest entries (first 20%)
19
+ // clear oldest entries (first 20%)
18
20
  const entriesToRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
19
21
  let count = 0;
20
22
  for (const key of cache.keys()) {
@@ -26,9 +28,10 @@ function cleanupPathCache(root) {
26
28
  pathCacheSize.set(root, MAX_CACHE_SIZE - entriesToRemove);
27
29
  }
28
30
  }
31
+ // simple lru-like cleanup for the global path concatenation cache
29
32
  function cleanupPathConcatCache() {
30
33
  if (pathConcatCache.size > MAX_PATH_CACHE_SIZE) {
31
- // Remove oldest entries (first 20%)
34
+ // remove oldest entries (first 20%)
32
35
  const entriesToRemove = Math.floor(MAX_PATH_CACHE_SIZE * 0.2);
33
36
  let count = 0;
34
37
  for (const key of pathConcatCache.keys()) {
@@ -40,216 +43,176 @@ function cleanupPathConcatCache() {
40
43
  }
41
44
  }
42
45
  export function deepEqual(a, b, seen = globalSeen) {
43
- // Fast path for primitive equality
44
46
  if (a === b)
45
47
  return true;
46
- // Fast path for null/undefined
47
48
  if (a == null || b == null)
48
49
  return a === b;
49
- // Fast path for different types
50
50
  if (typeof a !== typeof b)
51
51
  return false;
52
- // Fast path for Date objects
53
52
  if (a instanceof Date && b instanceof Date)
54
53
  return a.getTime() === b.getTime();
55
- // Fast path for non-objects
56
54
  if (typeof a !== 'object')
57
55
  return false;
58
- // Fast path for different array types
59
56
  if (Array.isArray(a) !== Array.isArray(b))
60
57
  return false;
61
- // Check for circular references
58
+ // handle circular references
62
59
  if (seen.has(a))
63
60
  return seen.get(a) === b;
64
61
  seen.set(a, b);
65
- // Check cache for memoized results
62
+ // check memoization cache before diving deeper
66
63
  if (deepEqualCache.has(a) && deepEqualCache.get(a)?.has(b)) {
67
64
  return deepEqualCache.get(a).get(b);
68
65
  }
69
- // Initialize cache for this object if needed
70
66
  if (!deepEqualCache.has(a)) {
71
67
  deepEqualCache.set(a, new WeakMap());
72
68
  }
73
69
  let result;
74
- // Compare arrays
75
70
  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
- }
71
+ result = a.length === b.length && a.every((val, idx) => deepEqual(val, b[idx], seen));
82
72
  }
83
73
  else {
84
- // Compare objects
85
74
  const keysA = Object.keys(a);
86
75
  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
- }
76
+ result = keysA.length === keysB.length && keysA.every(key => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key], seen));
93
77
  }
94
- // Cache the result
78
+ // cache the result before returning
95
79
  deepEqualCache.get(a).set(b, result);
96
80
  return result;
97
81
  }
98
- // Helper to safely get from path cache with cleanup
99
82
  export function getFromPathCache(root, pathKey) {
100
83
  const cache = pathCache.get(root);
101
84
  if (!cache)
102
85
  return undefined;
103
86
  const result = cache.get(pathKey);
104
87
  if (result !== undefined) {
105
- // Move to end (most recently used)
88
+ // simulate lru by deleting and re-setting the key
106
89
  cache.delete(pathKey);
107
90
  cache.set(pathKey, result);
108
91
  }
109
92
  return result;
110
93
  }
111
- // Helper to safely set in path cache with cleanup
112
94
  export function setInPathCache(root, pathKey, value) {
113
95
  if (!pathCache.has(root)) {
114
96
  pathCache.set(root, new Map());
115
97
  pathCacheSize.set(root, 0);
116
98
  }
117
99
  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);
100
+ // adjust size only if the key is new
101
+ if (!cache.has(pathKey)) {
102
+ pathCacheSize.set(root, pathCacheSize.get(root) + 1);
103
+ }
104
+ else {
105
+ cache.delete(pathKey); // remove old entry before adding to end
122
106
  }
123
107
  cache.set(pathKey, value);
124
- pathCacheSize.set(root, size + 1);
125
- cleanupPathCache(root);
108
+ cleanupPathCache(root); // check if cleanup is needed after adding
126
109
  }
127
- // Helper to safely get/set path concatenation with cleanup
128
110
  export function getPathConcat(path) {
129
111
  const result = pathConcatCache.get(path);
130
112
  if (result !== undefined) {
131
- // Move to end (most recently used)
113
+ // simulate lru
132
114
  pathConcatCache.delete(path);
133
115
  pathConcatCache.set(path, result);
134
116
  }
135
117
  return result;
136
118
  }
137
119
  export function setPathConcat(path, value) {
120
+ // delete first to ensure it's added at the end (most recent)
121
+ if (pathConcatCache.has(path)) {
122
+ pathConcatCache.delete(path);
123
+ }
138
124
  pathConcatCache.set(path, value);
139
- cleanupPathConcatCache();
125
+ cleanupPathConcatCache(); // check size limit
140
126
  }
141
- // Helper to check if a value is an object (and not null)
142
127
  function isObject(val) {
143
128
  return val !== null && typeof val === 'object';
144
129
  }
145
130
  /**
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.
131
+ * recursively traverses an object or array, accessing each property/element.
132
+ * used by `watch` with `deep: true` to establish dependencies on all nested properties.
133
+ * the actual tracking is done by the proxy `get` handlers triggered during traversal.
150
134
  */
151
135
  export function traverse(value, seen = new Set()) {
152
136
  if (!isObject(value) || seen.has(value)) {
153
137
  return value;
154
138
  }
155
139
  seen.add(value);
156
- // Traverse arrays
157
140
  if (Array.isArray(value)) {
158
- // Access length for tracking
159
- value.length;
141
+ value.length; // track length
160
142
  for (let i = 0; i < value.length; i++) {
161
143
  traverse(value[i], seen);
162
144
  }
163
145
  }
164
- // Traverse Sets and Maps
165
146
  else if (value instanceof Set || value instanceof Map) {
166
147
  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
148
+ if (Array.isArray(v)) { // map entries [key, value]
149
+ traverse(v[0], seen); // key
150
+ traverse(v[1], seen); // value
171
151
  }
172
- else {
173
- traverse(v, seen); // Value for Set
152
+ else { // set values
153
+ traverse(v, seen);
174
154
  }
175
155
  }
176
- // --- Stop after handling Map/Set ---
177
- return value;
156
+ return value; // no need to iterate plain object keys for map/set
178
157
  }
179
- // Traverse plain objects (Only if not Array, Map, or Set)
180
158
  else {
181
159
  for (const key in value) {
182
- // Access the property to trigger tracking via the proxy's get handler
183
160
  traverse(value[key], seen);
184
161
  }
185
162
  }
186
163
  return value;
187
164
  }
188
165
  /**
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.
166
+ * creates a deep clone of a value.
167
+ * includes cycle detection using a weakmap.
195
168
  */
196
169
  export function deepClone(value, seen = new WeakMap()) {
197
- // Primitives and null are returned directly
198
170
  if (value === null || typeof value !== 'object') {
199
171
  return value;
200
172
  }
201
- // Handle Dates
202
173
  if (value instanceof Date) {
203
174
  return new Date(value.getTime());
204
175
  }
205
- // Handle cycles
206
176
  if (seen.has(value)) {
207
177
  return seen.get(value);
208
178
  }
209
- // Handle Arrays
210
179
  if (Array.isArray(value)) {
211
180
  const newArray = [];
212
- seen.set(value, newArray);
181
+ seen.set(value, newArray); // store ref before recursing
213
182
  for (let i = 0; i < value.length; i++) {
214
183
  newArray[i] = deepClone(value[i], seen);
215
184
  }
216
185
  return newArray;
217
186
  }
218
- // Handle Maps
219
187
  if (value instanceof Map) {
220
188
  const newMap = new Map();
221
- seen.set(value, newMap);
189
+ seen.set(value, newMap); // store ref before recursing
222
190
  value.forEach((val, key) => {
223
- // Clone both key and value in case they are objects
224
191
  newMap.set(deepClone(key, seen), deepClone(val, seen));
225
192
  });
226
193
  return newMap;
227
194
  }
228
- // Handle Sets
229
195
  if (value instanceof Set) {
230
196
  const newSet = new Set();
231
- seen.set(value, newSet);
197
+ seen.set(value, newSet); // store ref before recursing
232
198
  value.forEach(val => {
233
199
  newSet.add(deepClone(val, seen));
234
200
  });
235
201
  return newSet;
236
202
  }
237
- // Handle plain objects
238
- // Create object with same prototype
203
+ // handle plain objects
239
204
  const newObject = Object.create(Object.getPrototypeOf(value));
240
- seen.set(value, newObject);
241
- // Copy properties
205
+ seen.set(value, newObject); // store ref before recursing
242
206
  for (const key in value) {
243
207
  if (Object.prototype.hasOwnProperty.call(value, key)) {
244
208
  newObject[key] = deepClone(value[key], seen);
245
209
  }
246
210
  }
247
- // Copy symbol properties (if any)
211
+ // copy symbol properties
248
212
  const symbolKeys = Object.getOwnPropertySymbols(value);
249
213
  for (const symbolKey of symbolKeys) {
250
- // Check if property descriptor exists and has get/set or is value
251
214
  const descriptor = Object.getOwnPropertyDescriptor(value, symbolKey);
252
- if (descriptor && (descriptor.value !== undefined || descriptor.get || descriptor.set)) {
215
+ if (descriptor && Object.prototype.propertyIsEnumerable.call(value, symbolKey)) {
253
216
  newObject[symbolKey] = deepClone(value[symbolKey], seen);
254
217
  }
255
218
  }
package/dist/watch.d.ts CHANGED
@@ -7,13 +7,8 @@ export interface WatchOptions {
7
7
  deep?: boolean;
8
8
  }
9
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
10
+ * watches a reactive source (getter function or reactive object/ref)
11
+ * and runs a callback when the source's value changes.
16
12
  */
17
- export declare function watch<T = any>(sourceInput: WatchSourceInput<T>, // Renamed parameter
18
- callback: WatchCallback<T>, options?: WatchOptions): WatchStopHandle;
13
+ export declare function watch<T = any>(sourceInput: WatchSourceInput<T>, callback: WatchCallback<T>, options?: WatchOptions): WatchStopHandle;
19
14
  export {};