@yiin/reactive-proxy-state 1.0.1 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yiin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -16,20 +16,20 @@ bun add @yiin/reactive-proxy-state
16
16
 
17
17
  ## Core Concepts
18
18
 
19
- 1. **Reactive State**: Create reactive versions of your objects using `wrapState`. Any mutations to these wrapped objects will be tracked.
19
+ 1. **Reactive State**: Create reactive versions of your objects using `reactive`. Any mutations to these wrapped objects will be tracked.
20
20
  2. **Dependency Tracking**: When code inside a `watchEffect` reads a property of a reactive object, a dependency is established.
21
21
  3. **Effect Triggering**: When a tracked property is mutated, any dependent effects (`watchEffect` or `watch` callbacks) are re-run **synchronously**.
22
22
 
23
23
  ## API
24
24
 
25
- ### `wrapState<T extends object>(obj: T): T`
25
+ ### `reactive<T extends object>(obj: T): T`
26
26
 
27
27
  Creates a reactive proxy for the given object, Array, Map, or Set. Nested objects/collections are also recursively wrapped.
28
28
 
29
29
  ```typescript
30
- import { wrapState } from '@yiin/reactive-proxy-state';
30
+ import { reactive } from '@yiin/reactive-proxy-state';
31
31
 
32
- const state = wrapState({
32
+ const state = reactive({
33
33
  count: 0,
34
34
  user: { name: 'Alice' },
35
35
  items: ['a', 'b'],
@@ -49,7 +49,7 @@ state.ids.add(3);
49
49
 
50
50
  Creates a reactive "reference" object for any value type (primitive or object). The value is accessed and mutated through the `.value` property. Reactivity is tracked on the `.value` property itself.
51
51
 
52
- **Note:** If a plain object is passed to `ref`, the object *itself* is not made deeply reactive. Only assignment to the `.value` property is tracked. Use `wrapState` for deep object reactivity.
52
+ **Note:** If a plain object is passed to `ref`, the object *itself* is not made deeply reactive. Only assignment to the `.value` property is tracked. Use `reactive` for deep object reactivity.
53
53
 
54
54
  ```typescript
55
55
  import { ref, watchEffect, isRef, unref } from '@yiin/reactive-proxy-state';
@@ -157,9 +157,9 @@ Runs a function immediately, tracks its reactive dependencies, and re-runs it sy
157
157
  * `onTrigger?(event)`: Debug hook called when the effect is triggered by a mutation.
158
158
 
159
159
  ```typescript
160
- import { wrapState, ref, watchEffect } from '@yiin/reactive-proxy-state';
160
+ import { reactive, ref, watchEffect } from '@yiin/reactive-proxy-state';
161
161
 
162
- // ... existing watchEffect example using wrapState ...
162
+ // ... existing watchEffect example using reactive ...
163
163
 
164
164
  // Using watchEffect with refs
165
165
  const counter = ref(10);
@@ -173,7 +173,7 @@ counter.value--;
173
173
 
174
174
  ### `watch<T>(source: WatchSource<T> | T, callback: (newValue: T, oldValue: T | undefined) => void, options?: WatchOptions)`
175
175
 
176
- Watches a specific reactive source (either a getter function, a direct reactive object/value created by `wrapState`, or a `ref`) and runs a callback when the source's value changes.
176
+ Watches a specific reactive source (either a getter function, a direct reactive object/value created by `reactive`, or a `ref`) and runs a callback when the source's value changes.
177
177
 
178
178
  `WatchSource<T>`: A function that returns the value to watch, or a `ref`.
179
179
  `callback`: Function executed on change. Receives the new value and the old value.
@@ -182,9 +182,9 @@ Watches a specific reactive source (either a getter function, a direct reactive
182
182
  * `deep?: boolean`: If `true`, deeply traverses the source for dependency tracking and uses deep comparison logic. **Defaults to `true`**. Set to `false` for shallow watching (only triggers on direct assignment or identity change).
183
183
 
184
184
  ```typescript
185
- import { wrapState, ref, watch } from '@yiin/reactive-proxy-state';
185
+ import { reactive, ref, watch } from '@yiin/reactive-proxy-state';
186
186
 
187
- // ... existing watch examples using wrapState ...
187
+ // ... existing watch examples using reactive ...
188
188
 
189
189
  // Watching a ref
190
190
  const count = ref(0);
@@ -206,12 +206,12 @@ doubleCount.value = 110; // Output: Double changed from 200 to 220
206
206
 
207
207
  ## Collections (Arrays, Maps, Sets)
208
208
 
209
- `wrapState` automatically handles Arrays, Maps, and Sets. Mutations via standard methods (`push`, `pop`, `splice`, `set`, `delete`, `add`, `clear`, etc.) are reactive and will trigger effects that depend on the collection or its contents (if watched deeply).
209
+ `reactive` automatically handles Arrays, Maps, and Sets. Mutations via standard methods (`push`, `pop`, `splice`, `set`, `delete`, `add`, `clear`, etc.) are reactive and will trigger effects that depend on the collection or its contents (if watched deeply).
210
210
 
211
211
  ```typescript
212
- import { wrapState, watchEffect } from '@yiin/reactive-proxy-state';
212
+ import { reactive, watchEffect } from '@yiin/reactive-proxy-state';
213
213
 
214
- const state = wrapState({
214
+ const state = reactive({
215
215
  list: [1, 2],
216
216
  data: new Map<string, number>(),
217
217
  tags: new Set<string>()
@@ -227,7 +227,3 @@ state.tags.add('important'); // Output: Tags: important
227
227
  state.data.delete('foo'); // Output: Data has "foo": false
228
228
  state.tags.add('urgent'); // Output: Tags: important, urgent
229
229
  ```
230
-
231
- ## License
232
-
233
- <!-- Add your license information here, e.g., MIT -->
@@ -0,0 +1,16 @@
1
+ import { Ref, isRefSymbol } from './ref';
2
+ declare const isComputedSymbol: unique symbol;
3
+ export interface ComputedRef<T = any> extends Omit<Ref<T>, 'value'> {
4
+ readonly value: T;
5
+ readonly [isComputedSymbol]: true;
6
+ readonly [isRefSymbol]: true;
7
+ }
8
+ export interface WritableComputedRef<T> extends Ref<T> {
9
+ }
10
+ type ComputedGetter<T> = () => T;
11
+ export declare function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>;
12
+ /**
13
+ * Checks if a value is a computed ref.
14
+ */
15
+ export declare function isComputed<T>(c: any): c is ComputedRef<T>;
16
+ export {};
@@ -0,0 +1,59 @@
1
+ import { watchEffect, track, trigger } from './watchEffect';
2
+ import { isRefSymbol } from './ref';
3
+ // Symbol for marking computed refs
4
+ const isComputedSymbol = Symbol('isComputed');
5
+ // Implementation v4 - Using watchEffect with scheduler
6
+ export function computed(getter) {
7
+ let _value;
8
+ let _dirty = true; // Start dirty
9
+ let computedRef; // Placeholder
10
+ // Create a lazy effect with a scheduler
11
+ const stopHandle = watchEffect(getter, {
12
+ lazy: true, // Don't run the getter immediately
13
+ scheduler: () => {
14
+ // When a dependency changes, don't re-run the getter immediately.
15
+ // Instead, mark the computed as dirty and trigger downstream effects.
16
+ if (!_dirty) {
17
+ _dirty = true;
18
+ // Trigger effects that depend on the computed ref's value
19
+ trigger(computedRef, 'value');
20
+ }
21
+ },
22
+ });
23
+ // Access the internal effect runner
24
+ const effectRunner = stopHandle.effect;
25
+ computedRef = {
26
+ [isRefSymbol]: true,
27
+ [isComputedSymbol]: true,
28
+ get value() {
29
+ // 1. Track access to this computed value for any outer effects
30
+ track(computedRef, 'value');
31
+ // 2. If dirty, run the effect manually. This will:
32
+ // - Execute the getter
33
+ // - Update _value (via getter's return)
34
+ // - Track dependencies for the getter (handled by watchEffect internals)
35
+ // - Set _dirty to false
36
+ if (_dirty) {
37
+ // console.log('Recomputing computed value via getter access');
38
+ _value = effectRunner.run(); // Run the getter, update value, track deps
39
+ _dirty = false; // Mark as clean *after* successful run
40
+ }
41
+ // 3. Return the (now current) value.
42
+ return _value;
43
+ },
44
+ set value(newValue) {
45
+ console.warn('Computed value is read-only');
46
+ },
47
+ // Expose the stop function if needed
48
+ // stop: stopHandle
49
+ };
50
+ // Initial computation is lazy, happens on first .value access.
51
+ // The scheduler ensures dependency changes only mark it dirty.
52
+ return computedRef;
53
+ }
54
+ /**
55
+ * Checks if a value is a computed ref.
56
+ */
57
+ export function isComputed(c) {
58
+ return !!(c && c[isComputedSymbol]);
59
+ }
@@ -0,0 +1,11 @@
1
+ export * from './types';
2
+ export * from './utils';
3
+ export * from './state';
4
+ export * from './reactive';
5
+ export * from './wrapArray';
6
+ export * from './wrapMap';
7
+ export * from './wrapSet';
8
+ export * from './watch';
9
+ export * from './watchEffect';
10
+ export * from './ref';
11
+ export * from './computed';
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ export * from './types';
2
+ export * from './utils';
3
+ export * from './state';
4
+ export * from './reactive';
5
+ export * from './wrapArray';
6
+ export * from './wrapMap';
7
+ export * from './wrapSet';
8
+ export * from './watch';
9
+ export * from './watchEffect';
10
+ export * from './ref';
11
+ export * from './computed';
@@ -0,0 +1,2 @@
1
+ import { EmitFunction, Path } from './types';
2
+ export declare function reactive<T extends object>(obj: T, emit: EmitFunction, path?: Path, seen?: WeakMap<any, any>): T;
@@ -0,0 +1,99 @@
1
+ import { deepEqual, globalSeen, getPathConcat, setPathConcat } from './utils';
2
+ import { wrapArray } from './wrapArray';
3
+ import { wrapMap } from './wrapMap';
4
+ import { wrapSet } from './wrapSet';
5
+ import { track, trigger } from './watchEffect';
6
+ // Pre-allocate type check function
7
+ function isObject(v) {
8
+ return v && typeof v === 'object';
9
+ }
10
+ export function reactive(obj, emit, path = [], seen = globalSeen) {
11
+ if (seen.has(obj))
12
+ return seen.get(obj);
13
+ function wrapValue(val, subPath) {
14
+ if (!isObject(val))
15
+ return val;
16
+ if (seen.has(val))
17
+ return seen.get(val);
18
+ if (Array.isArray(val))
19
+ return wrapArray(val, emit, subPath);
20
+ if (val instanceof Map)
21
+ return wrapMap(val, emit, subPath);
22
+ if (val instanceof Set)
23
+ return wrapSet(val, emit, subPath);
24
+ if (val instanceof Date)
25
+ return new Date(val.getTime());
26
+ return reactive(val, emit, subPath, seen);
27
+ }
28
+ const proxy = new Proxy(obj, {
29
+ get(target, prop, receiver) {
30
+ const value = Reflect.get(target, prop, receiver);
31
+ // Track this property access for reactivity
32
+ track(target, prop);
33
+ // Fast path for non-objects
34
+ if (!isObject(value))
35
+ return value;
36
+ // Use cached path concatenation
37
+ const pathKey = `${path.join('.')}.${String(prop)}`;
38
+ let newPath = getPathConcat(pathKey);
39
+ if (newPath === undefined) {
40
+ newPath = path.concat(String(prop));
41
+ setPathConcat(pathKey, newPath);
42
+ }
43
+ return wrapValue(value, newPath);
44
+ },
45
+ set(target, prop, value, receiver) {
46
+ const oldValue = target[prop];
47
+ // Fast path for primitive equality
48
+ if (oldValue === value)
49
+ return true;
50
+ // Only do deep equality check for objects
51
+ if (isObject(oldValue) && isObject(value) && deepEqual(oldValue, value))
52
+ return true;
53
+ const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
54
+ const result = Reflect.set(target, prop, value, receiver);
55
+ // Only emit if the set was successful and it's not a setter property
56
+ if (result && (!descriptor || !descriptor.set)) {
57
+ // Use cached path concatenation
58
+ const pathKey = `${path.join('.')}.${String(prop)}`;
59
+ let newPath = getPathConcat(pathKey);
60
+ if (newPath === undefined) {
61
+ newPath = path.concat(String(prop));
62
+ setPathConcat(pathKey, newPath);
63
+ }
64
+ const event = {
65
+ action: 'set',
66
+ path: newPath,
67
+ oldValue,
68
+ newValue: value
69
+ };
70
+ emit(event);
71
+ // Trigger effects
72
+ trigger(target, prop);
73
+ }
74
+ return result;
75
+ },
76
+ deleteProperty(target, prop) {
77
+ const oldValue = target[prop];
78
+ const result = Reflect.deleteProperty(target, prop);
79
+ // Use cached path concatenation
80
+ const pathKey = `${path.join('.')}.${String(prop)}`;
81
+ let newPath = getPathConcat(pathKey);
82
+ if (newPath === undefined) {
83
+ newPath = path.concat(String(prop));
84
+ setPathConcat(pathKey, newPath);
85
+ }
86
+ const event = {
87
+ action: 'delete',
88
+ path: newPath,
89
+ oldValue
90
+ };
91
+ emit(event);
92
+ // Trigger effects
93
+ trigger(target, prop);
94
+ return result;
95
+ }
96
+ });
97
+ seen.set(obj, proxy);
98
+ return proxy;
99
+ }
package/dist/ref.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export declare const isRefSymbol: unique symbol;
2
+ export interface Ref<T = any> {
3
+ value: T;
4
+ readonly [isRefSymbol]: true;
5
+ }
6
+ /**
7
+ * Takes an inner value and returns a reactive and mutable ref object,
8
+ * which has a single property `.value` that points to the inner value.
9
+ * The ref tracks access and mutations to its `.value` property.
10
+ * If the initial value is an object, it is NOT automatically made reactive.
11
+ */
12
+ export declare function ref<T>(value: T): Ref<T>;
13
+ export declare function ref<T = undefined>(): Ref<T | undefined>;
14
+ /**
15
+ * Checks if a value is a ref object.
16
+ */
17
+ export declare function isRef<T>(r: any): r is Ref<T>;
18
+ /**
19
+ * Returns the inner value if the argument is a ref, otherwise returns the
20
+ * argument itself.
21
+ */
22
+ export declare function unref<T>(refValue: T | Ref<T>): T;
package/dist/ref.js ADDED
@@ -0,0 +1,60 @@
1
+ import { track, trigger } from './watchEffect';
2
+ // Removed reactive import as ref doesn't automatically make contained objects reactive
3
+ // import { reactive } from './reactive';
4
+ // Symbol for marking refs
5
+ export const isRefSymbol = Symbol('isRef'); // Add export and Simplified symbol description
6
+ export function ref(value) {
7
+ return createRef(value);
8
+ }
9
+ // Internal function to create refs (no longer shallow distinction needed here)
10
+ function createRef(rawValue) {
11
+ // If the value is already a ref, return it directly
12
+ if (isRef(rawValue)) {
13
+ // Cast rawValue back to Ref<T> after type guard
14
+ return rawValue;
15
+ }
16
+ // The ref holds the raw value directly
17
+ let value = rawValue;
18
+ // Create the ref object with getter/setter for reactivity
19
+ const r = {
20
+ [isRefSymbol]: true, // Mark as ref
21
+ get value() {
22
+ // Track dependency on the 'value' property of this ref object
23
+ // Ensure 'r' is treated as the target object for tracking
24
+ track(r, 'value');
25
+ return value;
26
+ },
27
+ set value(newValue) {
28
+ // Check if value actually changed (using simple comparison)
29
+ // For objects, this means identity change, not deep mutation.
30
+ if (value !== newValue) {
31
+ value = newValue;
32
+ // Trigger effects depending on the 'value' property of this ref object
33
+ // Ensure 'r' is treated as the target object for triggering
34
+ trigger(r, 'value');
35
+ }
36
+ },
37
+ }; // Explicit cast to ensure type correctness
38
+ return r;
39
+ }
40
+ /**
41
+ * Checks if a value is a ref object.
42
+ */
43
+ export function isRef(r) {
44
+ return !!(r && r[isRefSymbol]);
45
+ }
46
+ /**
47
+ * Returns the inner value if the argument is a ref, otherwise returns the
48
+ * argument itself.
49
+ */
50
+ export function unref(refValue) {
51
+ return isRef(refValue) ? refValue.value : refValue;
52
+ }
53
+ // Basic triggerRef function (may need refinement if used)
54
+ /*
55
+ export function triggerRef(ref: Ref<any>): void {
56
+ trigger(ref, 'value');
57
+ }
58
+ */
59
+ // TODO: Implement shallowRef if needed (uses createRef(value, true))
60
+ // TODO: Implement customRef if needed (more advanced)
@@ -0,0 +1,2 @@
1
+ import { StateEvent } from './types';
2
+ export declare function updateState(root: any, event: StateEvent): void;
package/dist/state.js ADDED
@@ -0,0 +1,207 @@
1
+ import { pathCache, setInPathCache } from './utils';
2
+ // Pre-allocate helper functions to avoid recreation
3
+ function getValue(obj, key) {
4
+ if (obj instanceof Map)
5
+ return obj.get(key);
6
+ return obj[key];
7
+ }
8
+ function setValue(obj, key, value) {
9
+ if (obj instanceof Map)
10
+ obj.set(key, value);
11
+ else
12
+ obj[key] = value;
13
+ }
14
+ function deleteValue(obj, key) {
15
+ if (obj instanceof Map)
16
+ obj.delete(key);
17
+ else
18
+ delete obj[key];
19
+ }
20
+ // Cache for action handlers to avoid switch statement overhead
21
+ const actionHandlers = {
22
+ 'set': function (parent, key, event) {
23
+ setValue(parent, key, event.newValue);
24
+ },
25
+ 'delete': function (parent, key) {
26
+ deleteValue(parent, key);
27
+ },
28
+ 'array-push': function (targetArray, _keyIgnored, event) {
29
+ if (!Array.isArray(targetArray)) {
30
+ console.warn(`Expected Array at path ${event.path.join('.')}`);
31
+ return;
32
+ }
33
+ if (!event.items) {
34
+ console.warn('array-push event missing items');
35
+ return;
36
+ }
37
+ // Note: event.key is the starting index, but push always adds to the end.
38
+ targetArray.push(...event.items);
39
+ },
40
+ 'array-pop': function (targetArray, _keyIgnored, event) {
41
+ if (!Array.isArray(targetArray)) {
42
+ console.warn(`Expected Array at path ${event.path.join('.')}`);
43
+ return;
44
+ }
45
+ // We don't need event.key or event.oldValue to perform the pop.
46
+ if (targetArray.length > 0) {
47
+ targetArray.pop();
48
+ }
49
+ },
50
+ 'array-splice': function (targetArray, _keyIgnored, event) {
51
+ if (!Array.isArray(targetArray)) {
52
+ console.warn(`Expected Array at path ${event.path.join('.')}`);
53
+ return;
54
+ }
55
+ if (event.key === undefined || event.deleteCount === undefined) {
56
+ console.warn('array-splice event missing key or deleteCount');
57
+ return;
58
+ }
59
+ // Call splice with appropriate arguments
60
+ if (event.items && event.items.length > 0) {
61
+ targetArray.splice(event.key, event.deleteCount, ...event.items);
62
+ }
63
+ else {
64
+ targetArray.splice(event.key, event.deleteCount);
65
+ }
66
+ },
67
+ 'array-shift': function (targetArray, _keyIgnored, event) {
68
+ if (!Array.isArray(targetArray)) {
69
+ console.warn(`Expected Array at path ${event.path.join('.')}`);
70
+ return;
71
+ }
72
+ // We don't need event.key or event.oldValue to perform the shift.
73
+ if (targetArray.length > 0) {
74
+ targetArray.shift();
75
+ }
76
+ },
77
+ 'array-unshift': function (targetArray, _keyIgnored, event) {
78
+ if (!Array.isArray(targetArray)) {
79
+ console.warn(`Expected Array at path ${event.path.join('.')}`);
80
+ return;
81
+ }
82
+ if (!event.items) {
83
+ console.warn('array-unshift event missing items');
84
+ return;
85
+ }
86
+ // We don't need event.key to perform unshift
87
+ targetArray.unshift(...event.items);
88
+ },
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}`);
96
+ }
97
+ },
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}`);
105
+ }
106
+ },
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}`);
114
+ }
115
+ },
116
+ '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('.')}`);
122
+ }
123
+ },
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('.')}`);
130
+ }
131
+ },
132
+ '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('.')}`);
138
+ }
139
+ }
140
+ };
141
+ export function updateState(root, event) {
142
+ const { action, path } = event;
143
+ if (path.length === 0) {
144
+ console.warn('Event path is empty');
145
+ return;
146
+ }
147
+ const handler = actionHandlers[action];
148
+ if (!handler) {
149
+ throw new Error(`Unhandled action: ${action}`);
150
+ }
151
+ // Determine target and key based on action type
152
+ 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
+ if (path.length === 1) {
157
+ targetForHandler = root;
158
+ keyForHandler = path[0];
159
+ }
160
+ else {
161
+ const parentPathKey = path.slice(0, -1).join('.');
162
+ let parent = pathCache.get(root)?.get(parentPathKey);
163
+ if (parent === undefined) {
164
+ parent = path.slice(0, -1).reduce((acc, key) => acc ? getValue(acc, key) : undefined, root);
165
+ if (parent !== undefined)
166
+ setInPathCache(root, parentPathKey, parent);
167
+ }
168
+ if (parent === undefined) {
169
+ console.warn(`Parent path ${parentPathKey} not found for action ${action}`);
170
+ return;
171
+ }
172
+ targetForHandler = parent;
173
+ keyForHandler = path[path.length - 1];
174
+ }
175
+ }
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}`);
197
+ return;
198
+ }
199
+ }
200
+ else {
201
+ // Should not happen if handler exists
202
+ console.error(`Unexpected action type passed checks: ${action}`);
203
+ return;
204
+ }
205
+ // Call the handler with the appropriately determined target and key
206
+ handler(targetForHandler, keyForHandler, event);
207
+ }
@@ -0,0 +1,15 @@
1
+ export type Path = (string | number | symbol)[];
2
+ export type ActionType = 'set' | 'delete' | 'array-push' | 'array-pop' | 'array-splice' | 'array-shift' | 'array-unshift' | 'map-set' | 'map-delete' | 'map-clear' | 'set-add' | 'set-delete' | 'set-clear';
3
+ export interface StateEvent {
4
+ action: ActionType;
5
+ path: Path;
6
+ oldValue?: any;
7
+ newValue?: any;
8
+ key?: any;
9
+ value?: any;
10
+ args?: any[];
11
+ items?: any[];
12
+ deleteCount?: number;
13
+ oldValues?: any[];
14
+ }
15
+ export type EmitFunction = (event: StateEvent) => void;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ export declare const pathCache: WeakMap<object, Map<string, any>>;
2
+ export declare const pathConcatCache: Map<string, any[]>;
3
+ export declare const globalSeen: WeakMap<any, any>;
4
+ export declare const wrapperCache: WeakMap<object, object>;
5
+ export declare function deepEqual(a: any, b: any, seen?: WeakMap<any, any>): boolean;
6
+ export declare function getFromPathCache(root: object, pathKey: string): any | undefined;
7
+ export declare function setInPathCache(root: object, pathKey: string, value: any): void;
8
+ export declare function getPathConcat(path: string): any[] | undefined;
9
+ export declare function setPathConcat(path: string, value: any[]): void;
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.
15
+ */
16
+ export declare function traverse(value: any, seen?: Set<any>): any;
17
+ /**
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.
24
+ */
25
+ export declare function deepClone<T>(value: T, seen?: WeakMap<WeakKey, any>): T;