@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/README.md +13 -13
- package/dist/computed.d.ts +16 -0
- package/dist/computed.js +59 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/reactive.d.ts +2 -0
- package/dist/reactive.js +99 -0
- package/dist/ref.d.ts +22 -0
- package/dist/ref.js +60 -0
- package/dist/state.d.ts +2 -0
- package/dist/state.js +207 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +25 -0
- package/dist/utils.js +257 -0
- package/dist/watch.d.ts +19 -0
- package/dist/watch.js +74 -0
- package/dist/watchEffect.d.ts +49 -0
- package/dist/watchEffect.js +139 -0
- package/dist/wrapArray.d.ts +2 -0
- package/dist/wrapArray.js +237 -0
- package/dist/wrapMap.d.ts +2 -0
- package/dist/wrapMap.js +241 -0
- package/dist/wrapSet.d.ts +2 -0
- package/dist/wrapSet.js +184 -0
- package/package.json +1 -1
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 `
|
|
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
|
-
### `
|
|
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 {
|
|
30
|
+
import { reactive } from '@yiin/reactive-proxy-state';
|
|
31
31
|
|
|
32
|
-
const state =
|
|
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 `
|
|
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 {
|
|
160
|
+
import { reactive, ref, watchEffect } from '@yiin/reactive-proxy-state';
|
|
161
161
|
|
|
162
|
-
// ... existing watchEffect example using
|
|
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 `
|
|
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 {
|
|
185
|
+
import { reactive, ref, watch } from '@yiin/reactive-proxy-state';
|
|
186
186
|
|
|
187
|
-
// ... existing watch examples using
|
|
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
|
-
`
|
|
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 {
|
|
212
|
+
import { reactive, watchEffect } from '@yiin/reactive-proxy-state';
|
|
213
213
|
|
|
214
|
-
const state =
|
|
214
|
+
const state = reactive({
|
|
215
215
|
list: [1, 2],
|
|
216
216
|
data: new Map<string, number>(),
|
|
217
217
|
tags: new Set<string>()
|
|
@@ -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 {};
|
package/dist/computed.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
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';
|
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';
|
package/dist/reactive.js
ADDED
|
@@ -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)
|
package/dist/state.d.ts
ADDED
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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 {};
|
package/dist/utils.d.ts
ADDED
|
@@ -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;
|