@yiin/reactive-proxy-state 1.0.8 → 1.0.10
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 +88 -0
- package/dist/computed.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/reactive.js +4 -4
- package/dist/ref.d.ts +16 -0
- package/dist/ref.js +32 -1
- package/dist/watch-effect.d.ts +54 -0
- package/dist/watch-effect.js +154 -0
- package/dist/watch.js +1 -1
- package/dist/wrap-array.d.ts +2 -0
- package/dist/wrap-array.js +237 -0
- package/dist/wrap-map.d.ts +2 -0
- package/dist/wrap-map.js +252 -0
- package/dist/wrap-set.d.ts +2 -0
- package/dist/wrap-set.js +182 -0
- package/dist/wrapArray.js +3 -3
- package/dist/wrapMap.js +3 -3
- package/dist/wrapSet.js +3 -3
- package/package.json +28 -28
package/README.md
CHANGED
|
@@ -24,6 +24,94 @@ bun add @yiin/reactive-proxy-state
|
|
|
24
24
|
2. **Dependency Tracking**: When code inside a `watchEffect` reads a property of a reactive object, a dependency is established.
|
|
25
25
|
3. **Effect Triggering**: When a tracked property is mutated, any dependent effects (`watchEffect` or `watch` callbacks) are re-run **synchronously**.
|
|
26
26
|
|
|
27
|
+
## State Synchronization with `updateState`
|
|
28
|
+
|
|
29
|
+
A key feature is `updateState`, which allows applying changes from a plain JavaScript object (often received from serialization) onto an existing reactive state object. It intelligently updates properties, adds/removes array elements, and modifies Maps/Sets to match the target structure, triggering reactive effects only where necessary.
|
|
30
|
+
|
|
31
|
+
This is typically used with the `emit` callback of `reactive` for state synchronization:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { reactive, updateState, watchEffect } from '@yiin/reactive-proxy-state';
|
|
35
|
+
|
|
36
|
+
// Assume these functions exist:
|
|
37
|
+
// - getInitialStateFromServer(): Fetches the initial state snapshot.
|
|
38
|
+
// - sendEventToClient(event): Sends a state change event to the client.
|
|
39
|
+
// - listenForServerEvents(callback): Sets up a listener for events from the server.
|
|
40
|
+
|
|
41
|
+
// --- Source State (e.g., Server) ---
|
|
42
|
+
const sourceData = {
|
|
43
|
+
counter: 0,
|
|
44
|
+
user: { name: 'Alice' },
|
|
45
|
+
items: ['a']
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// 1. Server creates reactive state & emits deltas via sendEventToClient
|
|
49
|
+
const sourceState = reactive(sourceData, sendEventToClient);
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// --- Client Initialization & Sync ---
|
|
53
|
+
// 2. Client creates its reactive state holder *before* data arrives
|
|
54
|
+
const targetState = reactive({});
|
|
55
|
+
console.log('Client: Initial empty target state created:', targetState);
|
|
56
|
+
|
|
57
|
+
// 3. Client watches its local state for changes (e.g., for UI updates)
|
|
58
|
+
watchEffect(() => {
|
|
59
|
+
console.log('Client: Target state updated:', targetState);
|
|
60
|
+
});
|
|
61
|
+
// Initial output: Client: Target state updated: {}
|
|
62
|
+
|
|
63
|
+
// 4. Client fetches the initial state snapshot
|
|
64
|
+
const initialSnapshot = getInitialStateFromServer(); // Assume fetches { counter: 0, ... }
|
|
65
|
+
console.log('\n--- Client Received Initial Snapshot ---');
|
|
66
|
+
console.log(initialSnapshot);
|
|
67
|
+
|
|
68
|
+
// 5. Client applies the initial snapshot using a 'replace' action
|
|
69
|
+
// (Requires updateState implementation to support action: 'replace')
|
|
70
|
+
updateState(targetState, {
|
|
71
|
+
action: 'replace',
|
|
72
|
+
path: [], // Apply to the root
|
|
73
|
+
newValue: initialSnapshot
|
|
74
|
+
});
|
|
75
|
+
// Output after 'replace':
|
|
76
|
+
// Client: Target state updated: { counter: 0, user: { name: 'Alice' }, items: [ 'a' ] }
|
|
77
|
+
|
|
78
|
+
// 6. Client starts listening for subsequent delta events from the server
|
|
79
|
+
listenForServerEvents((event) => {
|
|
80
|
+
console.log(`Client: Received delta event <- Server:`, event);
|
|
81
|
+
// Apply the delta event normally
|
|
82
|
+
updateState(targetState, event);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// --- Subsequent Server Modifications ---
|
|
87
|
+
// 7. Server state is modified *after* the client has initialized
|
|
88
|
+
console.log('\n--- Server Modifying State (Post-Init) ---');
|
|
89
|
+
sourceState.counter++;
|
|
90
|
+
// Output: (sendEventToClient called with { action: 'set', path: [ 'counter' ], ... })
|
|
91
|
+
sourceState.user.name = 'Charlie';
|
|
92
|
+
// Output: (sendEventToClient called with { action: 'set', path: [ 'user', 'name' ], ... })
|
|
93
|
+
sourceState.items.push('b');
|
|
94
|
+
// Output: (sendEventToClient called with { action: 'array-push', path: [ 'items' ], ... })
|
|
95
|
+
|
|
96
|
+
// --- Delta Events arrive and are processed asynchronously by the client ---
|
|
97
|
+
|
|
98
|
+
// Example Console Output Order (assuming async processing):
|
|
99
|
+
// Client: Initial empty target state created: {}
|
|
100
|
+
// Client: Target state updated: {}
|
|
101
|
+
// --- Client Received Initial Snapshot ---
|
|
102
|
+
// { counter: 0, user: { name: 'Alice' }, items: ['a'] }
|
|
103
|
+
// Client: Target state updated: { counter: 0, user: { name: 'Alice' }, items: [ 'a' ] } // From 'replace'
|
|
104
|
+
// --- Server Modifying State (Post-Init) ---
|
|
105
|
+
// Client: Received delta event <- Server: { action: 'set', path: [ 'counter' ], oldValue: 0, newValue: 1 }
|
|
106
|
+
// Client: Target state updated: { counter: 1, user: { name: 'Alice' }, items: [ 'a' ] }
|
|
107
|
+
// Client: Received delta event <- Server: { action: 'set', path: [ 'user', 'name' ], oldValue: 'Alice', newValue: 'Charlie' }
|
|
108
|
+
// Client: Target state updated: { counter: 1, user: { name: 'Charlie' }, items: [ 'a' ] }
|
|
109
|
+
// Client: Received delta event <- Server: { action: 'array-push', path: [ 'items' ], key: 1, items: [ 'b' ] }
|
|
110
|
+
// Client: Target state updated: { counter: 1, user: { name: 'Charlie' }, items: [ 'a', 'b' ] }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
See the [`updateState` documentation](./docs/api/update-state.md) and [`reactive` documentation](./docs/api/reactive.md) for more details on event emission and application.
|
|
114
|
+
|
|
27
115
|
## API
|
|
28
116
|
|
|
29
117
|
### `reactive<T extends object>(obj: T): T`
|
package/dist/computed.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ export * from './types';
|
|
|
2
2
|
export * from './utils';
|
|
3
3
|
export * from './state';
|
|
4
4
|
export * from './reactive';
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
5
|
+
export * from './wrap-array';
|
|
6
|
+
export * from './wrap-map';
|
|
7
|
+
export * from './wrap-set';
|
|
8
8
|
export * from './watch';
|
|
9
|
-
export * from './
|
|
9
|
+
export * from './watch-effect';
|
|
10
10
|
export * from './ref';
|
|
11
11
|
export * from './computed';
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@ export * from './types';
|
|
|
2
2
|
export * from './utils';
|
|
3
3
|
export * from './state';
|
|
4
4
|
export * from './reactive';
|
|
5
|
-
export * from './
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
5
|
+
export * from './wrap-array';
|
|
6
|
+
export * from './wrap-map';
|
|
7
|
+
export * from './wrap-set';
|
|
8
8
|
export * from './watch';
|
|
9
|
-
export * from './
|
|
9
|
+
export * from './watch-effect';
|
|
10
10
|
export * from './ref';
|
|
11
11
|
export * from './computed';
|
package/dist/reactive.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { deepEqual, globalSeen, getPathConcat, setPathConcat } from './utils';
|
|
2
|
-
import { wrapArray } from './
|
|
3
|
-
import { wrapMap } from './
|
|
4
|
-
import { wrapSet } from './
|
|
5
|
-
import { track, trigger } from './
|
|
2
|
+
import { wrapArray } from './wrap-array';
|
|
3
|
+
import { wrapMap } from './wrap-map';
|
|
4
|
+
import { wrapSet } from './wrap-set';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
6
|
// avoid repeated typeof checks
|
|
7
7
|
function isObject(v) {
|
|
8
8
|
return v && typeof v === 'object';
|
package/dist/ref.d.ts
CHANGED
|
@@ -21,3 +21,19 @@ export declare function isRef<T>(r: any): r is Ref<T>;
|
|
|
21
21
|
* otherwise returns the argument itself. this is a sugar for `isRef(val) ? val.value : val`.
|
|
22
22
|
*/
|
|
23
23
|
export declare function unref<T>(refValue: T | Ref<T>): T;
|
|
24
|
+
/**
|
|
25
|
+
* Converts an object's properties to reactive refs.
|
|
26
|
+
* This is useful when you want to destructure reactive objects but maintain reactivity.
|
|
27
|
+
* @param object The reactive object to convert to refs
|
|
28
|
+
* @returns An object with the same properties, where each property is a ref connected to the original object
|
|
29
|
+
*/
|
|
30
|
+
export declare function toRefs<T extends object>(object: T): {
|
|
31
|
+
[K in keyof T]: Ref<T[K]>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Creates a ref that is connected to a property on an object.
|
|
35
|
+
* @param object The source object
|
|
36
|
+
* @param key The property key
|
|
37
|
+
* @returns A ref connected to the object's property
|
|
38
|
+
*/
|
|
39
|
+
export declare function toRef<T extends object, K extends keyof T>(object: T, key: K): Ref<T[K]>;
|
package/dist/ref.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { track, trigger } from './
|
|
1
|
+
import { track, trigger } from './watch-effect';
|
|
2
2
|
// Removed reactive import as ref doesn't automatically make contained objects reactive
|
|
3
3
|
// import { reactive } from './reactive';
|
|
4
4
|
// symbol used to identify refs internally and via isRef()
|
|
@@ -50,6 +50,37 @@ export function isRef(r) {
|
|
|
50
50
|
export function unref(refValue) {
|
|
51
51
|
return isRef(refValue) ? refValue.value : refValue;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Converts an object's properties to reactive refs.
|
|
55
|
+
* This is useful when you want to destructure reactive objects but maintain reactivity.
|
|
56
|
+
* @param object The reactive object to convert to refs
|
|
57
|
+
* @returns An object with the same properties, where each property is a ref connected to the original object
|
|
58
|
+
*/
|
|
59
|
+
export function toRefs(object) {
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const key in object) {
|
|
62
|
+
result[key] = toRef(object, key);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Creates a ref that is connected to a property on an object.
|
|
68
|
+
* @param object The source object
|
|
69
|
+
* @param key The property key
|
|
70
|
+
* @returns A ref connected to the object's property
|
|
71
|
+
*/
|
|
72
|
+
export function toRef(object, key) {
|
|
73
|
+
return {
|
|
74
|
+
[isRefSymbol]: true,
|
|
75
|
+
get value() {
|
|
76
|
+
track(this, 'value');
|
|
77
|
+
return object[key];
|
|
78
|
+
},
|
|
79
|
+
set value(newVal) {
|
|
80
|
+
object[key] = newVal;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
53
84
|
// Basic triggerRef function (may need refinement if used)
|
|
54
85
|
/*
|
|
55
86
|
export function triggerRef(ref: Ref<any>): void {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
type EffectCallback<T = any> = () => T;
|
|
2
|
+
type Scheduler = (job: () => void) => void;
|
|
3
|
+
export interface WatchEffectStopHandle<T = any> {
|
|
4
|
+
(): void;
|
|
5
|
+
effect: TrackedEffect<T>;
|
|
6
|
+
}
|
|
7
|
+
export interface TrackedEffect<T = any> {
|
|
8
|
+
run: () => T;
|
|
9
|
+
dependencies?: Set<Set<TrackedEffect<any>>>;
|
|
10
|
+
options?: WatchEffectOptions;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
_rawCallback: EffectCallback<T>;
|
|
13
|
+
}
|
|
14
|
+
export declare let activeEffect: TrackedEffect<any> | null;
|
|
15
|
+
export declare function setActiveEffect(effect: TrackedEffect<any> | null): void;
|
|
16
|
+
/**
|
|
17
|
+
* 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.
|
|
19
|
+
*/
|
|
20
|
+
export declare function cleanupEffect(effect: TrackedEffect<any>): void;
|
|
21
|
+
/**
|
|
22
|
+
* establishes a dependency between the currently active effect and a specific object property.
|
|
23
|
+
* called by proxy getters or ref getters.
|
|
24
|
+
*/
|
|
25
|
+
export declare function track(target: object, key: string | symbol): void;
|
|
26
|
+
/**
|
|
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.
|
|
30
|
+
*/
|
|
31
|
+
export declare function trigger(target: object, key: string | symbol): void;
|
|
32
|
+
export interface WatchEffectOptions {
|
|
33
|
+
lazy?: boolean;
|
|
34
|
+
scheduler?: Scheduler;
|
|
35
|
+
onTrack?: (event: {
|
|
36
|
+
effect: EffectCallback<any>;
|
|
37
|
+
target: object;
|
|
38
|
+
key: string | symbol;
|
|
39
|
+
type: 'track';
|
|
40
|
+
}) => void;
|
|
41
|
+
onTrigger?: (event: {
|
|
42
|
+
effect: EffectCallback<any>;
|
|
43
|
+
target: object;
|
|
44
|
+
key: string | symbol;
|
|
45
|
+
type: 'trigger';
|
|
46
|
+
}) => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
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.
|
|
52
|
+
*/
|
|
53
|
+
export declare function watchEffect<T>(effectCallback: EffectCallback<T>, options?: WatchEffectOptions): WatchEffectStopHandle<T>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// tracks the currently executing effect to establish dependencies
|
|
2
|
+
export let activeEffect = null;
|
|
3
|
+
// allows setting the active effect, used internally by the effect runner
|
|
4
|
+
export function setActiveEffect(effect) {
|
|
5
|
+
activeEffect = effect;
|
|
6
|
+
}
|
|
7
|
+
// storage for dependencies: target object -> property key -> set of effects that depend on this key
|
|
8
|
+
const targetMap = new WeakMap();
|
|
9
|
+
/**
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export function cleanupEffect(effect) {
|
|
14
|
+
if (effect.dependencies) {
|
|
15
|
+
effect.dependencies.forEach(dep => {
|
|
16
|
+
// remove this effect from the dependency set associated with a specific target/key
|
|
17
|
+
dep.delete(effect);
|
|
18
|
+
});
|
|
19
|
+
// clear the effect's own list of dependencies for the next run
|
|
20
|
+
effect.dependencies.clear();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* establishes a dependency between the currently active effect and a specific object property.
|
|
25
|
+
* called by proxy getters or ref getters.
|
|
26
|
+
*/
|
|
27
|
+
export function track(target, key) {
|
|
28
|
+
// do nothing if there is no active effect or if the effect is stopped
|
|
29
|
+
if (!activeEffect || !activeEffect.active)
|
|
30
|
+
return;
|
|
31
|
+
// get or create the dependency map for the target object
|
|
32
|
+
let depsMap = targetMap.get(target);
|
|
33
|
+
if (!depsMap) {
|
|
34
|
+
depsMap = new Map();
|
|
35
|
+
targetMap.set(target, depsMap);
|
|
36
|
+
}
|
|
37
|
+
// get or create the set of effects for the specific property key
|
|
38
|
+
let dep = depsMap.get(key);
|
|
39
|
+
if (!dep) {
|
|
40
|
+
dep = new Set();
|
|
41
|
+
depsMap.set(key, dep);
|
|
42
|
+
}
|
|
43
|
+
// add the current effect to the dependency set if it's not already there
|
|
44
|
+
const effectToAdd = activeEffect;
|
|
45
|
+
if (!dep.has(effectToAdd)) {
|
|
46
|
+
dep.add(effectToAdd);
|
|
47
|
+
// also add this dependency set to the effect's own tracking list for cleanup purposes
|
|
48
|
+
if (!effectToAdd.dependencies) {
|
|
49
|
+
effectToAdd.dependencies = new Set();
|
|
50
|
+
}
|
|
51
|
+
effectToAdd.dependencies.add(dep);
|
|
52
|
+
// trigger the onTrack debug hook if provided
|
|
53
|
+
if (effectToAdd.options?.onTrack) {
|
|
54
|
+
// pass the original user callback to the hook, not the internal wrapper
|
|
55
|
+
effectToAdd.options.onTrack({ effect: effectToAdd._rawCallback, target, key, type: 'track' });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
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.
|
|
63
|
+
*/
|
|
64
|
+
export function trigger(target, key) {
|
|
65
|
+
const depsMap = targetMap.get(target);
|
|
66
|
+
if (!depsMap)
|
|
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
|
|
69
|
+
const effectsToRun = new Set();
|
|
70
|
+
// helper to add effects from a specific dependency set to the run queue
|
|
71
|
+
const addEffects = (depKey) => {
|
|
72
|
+
const dep = depsMap.get(depKey);
|
|
73
|
+
if (dep) {
|
|
74
|
+
dep.forEach(effect => {
|
|
75
|
+
// avoid triggering the effect if it's the one currently running (prevents infinite loops)
|
|
76
|
+
// also ensure the effect hasn't been stopped
|
|
77
|
+
if (effect !== activeEffect && effect.active) {
|
|
78
|
+
effectsToRun.add(effect);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
// add effects associated with the specific key that changed
|
|
84
|
+
addEffects(key);
|
|
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
|
|
87
|
+
effectsToRun.forEach(effect => {
|
|
88
|
+
// trigger the onTrigger debug hook if provided
|
|
89
|
+
if (effect.options?.onTrigger) {
|
|
90
|
+
effect.options.onTrigger({ effect: effect._rawCallback, target, key, type: 'trigger' });
|
|
91
|
+
}
|
|
92
|
+
// use a custom scheduler if provided, otherwise run the effect synchronously
|
|
93
|
+
if (effect.options?.scheduler) {
|
|
94
|
+
effect.options.scheduler(effect.run);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
effect.run(); // execute the effect's wrapper function (`run`)
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
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.
|
|
105
|
+
*/
|
|
106
|
+
export function watchEffect(effectCallback, options = {}) {
|
|
107
|
+
// the wrapper function that manages the effect lifecycle (cleanup, tracking, execution)
|
|
108
|
+
const run = () => {
|
|
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
|
+
}
|
|
121
|
+
const previousEffect = activeEffect;
|
|
122
|
+
try {
|
|
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
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
setActiveEffect(previousEffect); // restore the previous active effect
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// create the internal effect object
|
|
132
|
+
const effectFn = {
|
|
133
|
+
run: run,
|
|
134
|
+
dependencies: new Set(), // initialize empty dependencies
|
|
135
|
+
options: options,
|
|
136
|
+
active: true, // start as active
|
|
137
|
+
_rawCallback: effectCallback // store the original callback
|
|
138
|
+
};
|
|
139
|
+
// run the effect immediately unless the `lazy` option is true
|
|
140
|
+
if (!options.lazy) {
|
|
141
|
+
effectFn.run();
|
|
142
|
+
}
|
|
143
|
+
// create the function that stops the effect
|
|
144
|
+
const stopHandle = () => {
|
|
145
|
+
if (effectFn.active) {
|
|
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.
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
// attach the effect instance to the stop handle for potential advanced usage
|
|
152
|
+
stopHandle.effect = effectFn;
|
|
153
|
+
return stopHandle;
|
|
154
|
+
}
|
package/dist/watch.js
CHANGED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { deepEqual, getPathConcat, setPathConcat, wrapperCache } from './utils';
|
|
2
|
+
import { reactive } from './reactive';
|
|
3
|
+
import { wrapMap } from './wrap-map';
|
|
4
|
+
import { wrapSet } from './wrap-set';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
|
+
// avoid repeated typeof checks
|
|
7
|
+
function isObject(v) {
|
|
8
|
+
return v && typeof v === 'object';
|
|
9
|
+
}
|
|
10
|
+
export function wrapArray(arr, emit, path) {
|
|
11
|
+
// reuse existing proxy if available for performance
|
|
12
|
+
const cachedProxy = wrapperCache.get(arr);
|
|
13
|
+
if (cachedProxy)
|
|
14
|
+
return cachedProxy;
|
|
15
|
+
// cache for wrapped methods to avoid re-creating them on each call
|
|
16
|
+
const methodCache = {};
|
|
17
|
+
const proxy = new Proxy(arr, {
|
|
18
|
+
get(target, prop, receiver) {
|
|
19
|
+
track(target, prop);
|
|
20
|
+
if (methodCache[prop]) {
|
|
21
|
+
return methodCache[prop];
|
|
22
|
+
}
|
|
23
|
+
// handle specific array mutation methods that require custom logic and event emission
|
|
24
|
+
switch (prop) {
|
|
25
|
+
case 'push':
|
|
26
|
+
track(target, 'length');
|
|
27
|
+
methodCache[prop] = function (...items) {
|
|
28
|
+
const oldLength = target.length;
|
|
29
|
+
const result = target.push(...items);
|
|
30
|
+
const newLength = target.length;
|
|
31
|
+
if (items.length > 0) {
|
|
32
|
+
const event = {
|
|
33
|
+
action: 'array-push',
|
|
34
|
+
path: path,
|
|
35
|
+
key: oldLength, // start index was the old length
|
|
36
|
+
items: items
|
|
37
|
+
};
|
|
38
|
+
emit(event);
|
|
39
|
+
trigger(target, Symbol.iterator);
|
|
40
|
+
if (oldLength !== newLength) {
|
|
41
|
+
trigger(target, 'length');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
return methodCache[prop];
|
|
47
|
+
case 'pop':
|
|
48
|
+
track(target, 'length');
|
|
49
|
+
methodCache[prop] = function () {
|
|
50
|
+
if (target.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const oldLength = target.length;
|
|
53
|
+
const poppedIndex = oldLength - 1;
|
|
54
|
+
const oldValue = target[poppedIndex];
|
|
55
|
+
const result = target.pop();
|
|
56
|
+
const newLength = target.length;
|
|
57
|
+
const event = {
|
|
58
|
+
action: 'array-pop',
|
|
59
|
+
path: path,
|
|
60
|
+
key: poppedIndex,
|
|
61
|
+
oldValue: oldValue
|
|
62
|
+
};
|
|
63
|
+
emit(event);
|
|
64
|
+
trigger(target, Symbol.iterator);
|
|
65
|
+
if (oldLength !== newLength) {
|
|
66
|
+
trigger(target, 'length');
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
return methodCache[prop];
|
|
71
|
+
case 'shift':
|
|
72
|
+
track(target, 'length');
|
|
73
|
+
methodCache[prop] = function () {
|
|
74
|
+
if (target.length === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
const oldLength = target.length;
|
|
77
|
+
const oldValue = target[0];
|
|
78
|
+
const result = target.shift();
|
|
79
|
+
const newLength = target.length;
|
|
80
|
+
const event = {
|
|
81
|
+
action: 'array-shift',
|
|
82
|
+
path: path,
|
|
83
|
+
key: 0,
|
|
84
|
+
oldValue: oldValue
|
|
85
|
+
};
|
|
86
|
+
emit(event);
|
|
87
|
+
trigger(target, Symbol.iterator);
|
|
88
|
+
if (oldLength !== newLength) {
|
|
89
|
+
trigger(target, 'length');
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
};
|
|
93
|
+
return methodCache[prop];
|
|
94
|
+
case 'unshift':
|
|
95
|
+
track(target, 'length');
|
|
96
|
+
methodCache[prop] = function (...items) {
|
|
97
|
+
const oldLength = target.length;
|
|
98
|
+
const result = target.unshift(...items);
|
|
99
|
+
const newLength = target.length;
|
|
100
|
+
if (items.length > 0) {
|
|
101
|
+
const event = {
|
|
102
|
+
action: 'array-unshift',
|
|
103
|
+
path: path,
|
|
104
|
+
key: 0,
|
|
105
|
+
items: items
|
|
106
|
+
};
|
|
107
|
+
emit(event);
|
|
108
|
+
trigger(target, Symbol.iterator);
|
|
109
|
+
if (oldLength !== newLength) {
|
|
110
|
+
trigger(target, 'length');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
return methodCache[prop];
|
|
116
|
+
case 'splice':
|
|
117
|
+
track(target, 'length');
|
|
118
|
+
methodCache[prop] = function (start, deleteCount, ...items) {
|
|
119
|
+
const oldLength = target.length;
|
|
120
|
+
const actualStart = start < 0 ? Math.max(target.length + start, 0) : Math.min(start, target.length);
|
|
121
|
+
const deleteCountNum = deleteCount === undefined ? target.length - actualStart : Number(deleteCount);
|
|
122
|
+
const actualDeleteCount = Math.min(deleteCountNum, target.length - actualStart);
|
|
123
|
+
const deletedItems = target.slice(actualStart, actualStart + actualDeleteCount);
|
|
124
|
+
const result = target.splice(start, deleteCountNum, ...items);
|
|
125
|
+
const newLength = target.length;
|
|
126
|
+
if (actualDeleteCount > 0 || items.length > 0) {
|
|
127
|
+
const event = {
|
|
128
|
+
action: 'array-splice',
|
|
129
|
+
path: path,
|
|
130
|
+
key: actualStart,
|
|
131
|
+
deleteCount: actualDeleteCount,
|
|
132
|
+
items: items.length > 0 ? items : undefined,
|
|
133
|
+
oldValues: deletedItems.length > 0 ? deletedItems : undefined
|
|
134
|
+
};
|
|
135
|
+
emit(event);
|
|
136
|
+
trigger(target, Symbol.iterator);
|
|
137
|
+
if (oldLength !== newLength) {
|
|
138
|
+
trigger(target, 'length');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
};
|
|
143
|
+
return methodCache[prop];
|
|
144
|
+
// handle methods that rely on iteration state
|
|
145
|
+
case Symbol.iterator:
|
|
146
|
+
case 'values':
|
|
147
|
+
case 'keys':
|
|
148
|
+
case 'entries':
|
|
149
|
+
case 'forEach':
|
|
150
|
+
case 'map':
|
|
151
|
+
case 'filter':
|
|
152
|
+
case 'reduce':
|
|
153
|
+
case 'reduceRight':
|
|
154
|
+
case 'find':
|
|
155
|
+
case 'findIndex':
|
|
156
|
+
case 'every':
|
|
157
|
+
case 'some':
|
|
158
|
+
case 'join':
|
|
159
|
+
track(target, Symbol.iterator);
|
|
160
|
+
// fall through to default behavior (usually binding)
|
|
161
|
+
break;
|
|
162
|
+
case 'length':
|
|
163
|
+
track(target, 'length');
|
|
164
|
+
return Reflect.get(target, prop, receiver);
|
|
165
|
+
}
|
|
166
|
+
const value = Reflect.get(target, prop, receiver);
|
|
167
|
+
// determine if the property access is numeric array index access
|
|
168
|
+
const isNumericIndex = typeof prop === 'number' || (typeof prop === 'string' && !isNaN(parseInt(prop, 10)));
|
|
169
|
+
if (isNumericIndex) {
|
|
170
|
+
track(target, String(prop));
|
|
171
|
+
if (!isObject(value))
|
|
172
|
+
return value;
|
|
173
|
+
// reuse existing proxy for nested object/array if available
|
|
174
|
+
const cachedValueProxy = wrapperCache.get(value);
|
|
175
|
+
if (cachedValueProxy)
|
|
176
|
+
return cachedValueProxy;
|
|
177
|
+
// calculate the nested path for the element, optimizing with caching
|
|
178
|
+
const propKey = String(prop);
|
|
179
|
+
const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
|
|
180
|
+
let newPath = getPathConcat(pathKey);
|
|
181
|
+
if (newPath === undefined) {
|
|
182
|
+
newPath = path.concat(propKey);
|
|
183
|
+
setPathConcat(pathKey, newPath);
|
|
184
|
+
}
|
|
185
|
+
// recursively wrap nested structures
|
|
186
|
+
if (Array.isArray(value))
|
|
187
|
+
return wrapArray(value, emit, newPath);
|
|
188
|
+
if (value instanceof Map)
|
|
189
|
+
return wrapMap(value, emit, newPath);
|
|
190
|
+
if (value instanceof Set)
|
|
191
|
+
return wrapSet(value, emit, newPath);
|
|
192
|
+
if (value instanceof Date)
|
|
193
|
+
return new Date(value.getTime()); // dates are not proxied, return a copy
|
|
194
|
+
return reactive(value, emit, newPath);
|
|
195
|
+
}
|
|
196
|
+
// ensure functions accessed directly are bound to the original target
|
|
197
|
+
if (typeof value === 'function') {
|
|
198
|
+
return value.bind(target);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
},
|
|
202
|
+
set(target, prop, value, receiver) {
|
|
203
|
+
const oldValue = target[prop];
|
|
204
|
+
// avoid unnecessary triggers if value hasn't changed
|
|
205
|
+
if (oldValue === value)
|
|
206
|
+
return true;
|
|
207
|
+
if (isObject(oldValue) && isObject(value) && deepEqual(oldValue, value, new WeakMap()))
|
|
208
|
+
return true;
|
|
209
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
210
|
+
const result = Reflect.set(target, prop, value, receiver);
|
|
211
|
+
const isNumericIndex = typeof prop === 'number' || (typeof prop === 'string' && !isNaN(parseInt(String(prop))));
|
|
212
|
+
// emit event and trigger effects only if the set was successful and wasn't intercepted by a setter
|
|
213
|
+
// (unless it's a direct numeric index set, which doesn't have a descriptor.set)
|
|
214
|
+
if (result && (!descriptor || !descriptor.set || isNumericIndex)) {
|
|
215
|
+
const propKey = String(prop);
|
|
216
|
+
const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
|
|
217
|
+
let newPath = getPathConcat(pathKey);
|
|
218
|
+
if (newPath === undefined) {
|
|
219
|
+
newPath = path.concat(propKey);
|
|
220
|
+
setPathConcat(pathKey, newPath);
|
|
221
|
+
}
|
|
222
|
+
const event = {
|
|
223
|
+
action: 'set',
|
|
224
|
+
path: newPath,
|
|
225
|
+
oldValue,
|
|
226
|
+
newValue: value
|
|
227
|
+
};
|
|
228
|
+
emit(event);
|
|
229
|
+
trigger(target, prop);
|
|
230
|
+
}
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// cache the newly created proxy before returning
|
|
235
|
+
wrapperCache.set(arr, proxy);
|
|
236
|
+
return proxy;
|
|
237
|
+
}
|
package/dist/wrap-map.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { deepEqual, getPathConcat, setPathConcat, wrapperCache } from './utils';
|
|
2
|
+
import { reactive } from './reactive';
|
|
3
|
+
import { wrapArray } from './wrap-array';
|
|
4
|
+
import { wrapSet } from './wrap-set';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
|
+
export function wrapMap(map, emit, path) {
|
|
7
|
+
// reuse existing proxy if available for performance
|
|
8
|
+
const cachedProxy = wrapperCache.get(map);
|
|
9
|
+
if (cachedProxy)
|
|
10
|
+
return cachedProxy;
|
|
11
|
+
// cache for wrapped methods to avoid re-creating them on each call
|
|
12
|
+
const methodCache = {};
|
|
13
|
+
const proxy = new Proxy(map, {
|
|
14
|
+
get(target, prop, receiver) {
|
|
15
|
+
track(target, prop);
|
|
16
|
+
// iteration methods need to track the iterator symbol on every access
|
|
17
|
+
// to re-establish dependency after effect cleanup, even when cached
|
|
18
|
+
if (prop === Symbol.iterator || prop === 'entries' || prop === 'values' || prop === 'keys' || prop === 'forEach') {
|
|
19
|
+
track(target, Symbol.iterator);
|
|
20
|
+
}
|
|
21
|
+
if (methodCache[prop]) {
|
|
22
|
+
return methodCache[prop];
|
|
23
|
+
}
|
|
24
|
+
if (prop === 'set') {
|
|
25
|
+
methodCache[prop] = function (key, value) {
|
|
26
|
+
const existed = target.has(key);
|
|
27
|
+
const oldValue = target.get(key);
|
|
28
|
+
const oldSize = target.size;
|
|
29
|
+
// avoid unnecessary work if value hasn't changed
|
|
30
|
+
if (oldValue === value)
|
|
31
|
+
return receiver;
|
|
32
|
+
if (oldValue && typeof oldValue === 'object' && value && typeof value === 'object' && deepEqual(oldValue, value, new WeakMap()))
|
|
33
|
+
return receiver;
|
|
34
|
+
target.set(key, value);
|
|
35
|
+
const newSize = target.size;
|
|
36
|
+
// optimize path calculation by caching concatenated paths
|
|
37
|
+
const pathKey = path.join('.');
|
|
38
|
+
let cachedPath = getPathConcat(pathKey);
|
|
39
|
+
if (cachedPath === undefined) {
|
|
40
|
+
cachedPath = path;
|
|
41
|
+
setPathConcat(pathKey, cachedPath);
|
|
42
|
+
}
|
|
43
|
+
const event = {
|
|
44
|
+
action: 'map-set',
|
|
45
|
+
path: cachedPath,
|
|
46
|
+
key,
|
|
47
|
+
oldValue,
|
|
48
|
+
newValue: value
|
|
49
|
+
};
|
|
50
|
+
emit(event);
|
|
51
|
+
// trigger effects based on whether it was an add or update
|
|
52
|
+
if (!existed) {
|
|
53
|
+
trigger(target, Symbol.iterator);
|
|
54
|
+
if (oldSize !== newSize) {
|
|
55
|
+
trigger(target, 'size');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
trigger(target, String(key));
|
|
60
|
+
}
|
|
61
|
+
return receiver;
|
|
62
|
+
};
|
|
63
|
+
return methodCache[prop];
|
|
64
|
+
}
|
|
65
|
+
if (prop === 'delete') {
|
|
66
|
+
methodCache[prop] = function (key) {
|
|
67
|
+
const existed = target.has(key);
|
|
68
|
+
if (!existed)
|
|
69
|
+
return false;
|
|
70
|
+
const oldValue = target.get(key);
|
|
71
|
+
const oldSize = target.size;
|
|
72
|
+
const result = target.delete(key);
|
|
73
|
+
const newSize = target.size;
|
|
74
|
+
if (result) { // only emit and trigger if delete was successful
|
|
75
|
+
const pathKey = path.join('.');
|
|
76
|
+
let cachedPath = getPathConcat(pathKey);
|
|
77
|
+
if (cachedPath === undefined) {
|
|
78
|
+
cachedPath = path;
|
|
79
|
+
setPathConcat(pathKey, cachedPath);
|
|
80
|
+
}
|
|
81
|
+
const event = {
|
|
82
|
+
action: 'map-delete',
|
|
83
|
+
path: cachedPath,
|
|
84
|
+
key,
|
|
85
|
+
oldValue
|
|
86
|
+
};
|
|
87
|
+
emit(event);
|
|
88
|
+
trigger(target, Symbol.iterator);
|
|
89
|
+
if (oldSize !== newSize) {
|
|
90
|
+
trigger(target, 'size');
|
|
91
|
+
}
|
|
92
|
+
trigger(target, String(key));
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
};
|
|
96
|
+
return methodCache[prop];
|
|
97
|
+
}
|
|
98
|
+
if (prop === 'clear') {
|
|
99
|
+
methodCache[prop] = function () {
|
|
100
|
+
const oldSize = target.size;
|
|
101
|
+
if (oldSize === 0)
|
|
102
|
+
return;
|
|
103
|
+
target.clear();
|
|
104
|
+
const newSize = target.size;
|
|
105
|
+
const event = {
|
|
106
|
+
action: 'map-clear',
|
|
107
|
+
path: path,
|
|
108
|
+
key: null
|
|
109
|
+
};
|
|
110
|
+
emit(event);
|
|
111
|
+
trigger(target, Symbol.iterator);
|
|
112
|
+
if (oldSize !== newSize) {
|
|
113
|
+
trigger(target, 'size');
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return methodCache[prop];
|
|
117
|
+
}
|
|
118
|
+
if (prop === 'get') {
|
|
119
|
+
// return a function that tracks the specific key only when called
|
|
120
|
+
methodCache[prop] = function (key) {
|
|
121
|
+
track(target, String(key));
|
|
122
|
+
const value = target.get(key);
|
|
123
|
+
if (!value || typeof value !== 'object')
|
|
124
|
+
return value;
|
|
125
|
+
const cachedValueProxy = wrapperCache.get(value);
|
|
126
|
+
if (cachedValueProxy)
|
|
127
|
+
return cachedValueProxy;
|
|
128
|
+
const keyString = String(key);
|
|
129
|
+
const pathKey = path.length > 0 ? `${path.join('.')}.${keyString}` : keyString;
|
|
130
|
+
let newPath = getPathConcat(pathKey);
|
|
131
|
+
if (newPath === undefined) {
|
|
132
|
+
newPath = path.concat(keyString);
|
|
133
|
+
setPathConcat(pathKey, newPath);
|
|
134
|
+
}
|
|
135
|
+
// recursively wrap nested structures
|
|
136
|
+
if (value instanceof Map)
|
|
137
|
+
return wrapMap(value, emit, newPath);
|
|
138
|
+
if (value instanceof Set)
|
|
139
|
+
return wrapSet(value, emit, newPath);
|
|
140
|
+
if (Array.isArray(value))
|
|
141
|
+
return wrapArray(value, emit, newPath);
|
|
142
|
+
if (value instanceof Date)
|
|
143
|
+
return new Date(value.getTime()); // dates are not proxied, return a copy
|
|
144
|
+
return reactive(value, emit, newPath);
|
|
145
|
+
};
|
|
146
|
+
return methodCache[prop];
|
|
147
|
+
}
|
|
148
|
+
if (prop === 'has') {
|
|
149
|
+
track(target, Symbol.iterator);
|
|
150
|
+
methodCache[prop] = function (key) {
|
|
151
|
+
// track the specific key only when 'has' is called
|
|
152
|
+
track(target, String(key));
|
|
153
|
+
return target.has(key);
|
|
154
|
+
}.bind(target); // bind is still okay here, doesn't interfere with caching
|
|
155
|
+
return methodCache[prop];
|
|
156
|
+
}
|
|
157
|
+
// handle iteration methods
|
|
158
|
+
if (prop === Symbol.iterator || prop === 'entries' || prop === 'values' || prop === 'keys' || prop === 'forEach') {
|
|
159
|
+
track(target, Symbol.iterator);
|
|
160
|
+
const originalMethod = Reflect.get(target, prop, receiver);
|
|
161
|
+
// return custom iterators/foreach that wrap values during iteration
|
|
162
|
+
if (prop === 'forEach') {
|
|
163
|
+
methodCache[prop] = (callbackfn, thisArg) => {
|
|
164
|
+
// use the proxied .entries() to ensure values passed to callback are wrapped and tracked
|
|
165
|
+
const entriesIterator = proxy.entries();
|
|
166
|
+
for (const [key, value] of entriesIterator) {
|
|
167
|
+
callbackfn.call(thisArg, value, key, proxy);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
return methodCache[prop];
|
|
171
|
+
}
|
|
172
|
+
// handle symbol.iterator, entries, values, keys by creating generator functions
|
|
173
|
+
methodCache[prop] = function* (...args) {
|
|
174
|
+
const iterator = originalMethod.apply(target, args);
|
|
175
|
+
for (const entry of iterator) {
|
|
176
|
+
let keyToWrap = entry;
|
|
177
|
+
let valueToWrap = entry;
|
|
178
|
+
let isEntry = false;
|
|
179
|
+
if (prop === 'entries' || prop === Symbol.iterator) {
|
|
180
|
+
keyToWrap = entry[0];
|
|
181
|
+
valueToWrap = entry[1];
|
|
182
|
+
isEntry = true;
|
|
183
|
+
}
|
|
184
|
+
// wrap key if it's an object
|
|
185
|
+
// note: reactivity on map keys can be complex/unexpected
|
|
186
|
+
let wrappedKey = keyToWrap;
|
|
187
|
+
if (isEntry && keyToWrap && typeof keyToWrap === 'object') {
|
|
188
|
+
const pathKey = path.length > 0 ? `${path.join('.')}.${String(keyToWrap)}` : String(keyToWrap);
|
|
189
|
+
let keyPath = getPathConcat(pathKey);
|
|
190
|
+
if (keyPath === undefined) {
|
|
191
|
+
keyPath = path.concat(String(keyToWrap));
|
|
192
|
+
setPathConcat(pathKey, keyPath);
|
|
193
|
+
}
|
|
194
|
+
// todo: decide if map keys should be deeply reactive
|
|
195
|
+
wrappedKey = reactive(keyToWrap, emit, keyPath);
|
|
196
|
+
}
|
|
197
|
+
// wrap value if it's an object
|
|
198
|
+
let wrappedValue = valueToWrap;
|
|
199
|
+
if (valueToWrap && typeof valueToWrap === 'object') {
|
|
200
|
+
const cachedValueProxy = wrapperCache.get(valueToWrap);
|
|
201
|
+
if (cachedValueProxy) {
|
|
202
|
+
wrappedValue = cachedValueProxy;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const keyString = String(keyToWrap); // use original key for path
|
|
206
|
+
const pathKey = path.length > 0 ? `${path.join('.')}.${keyString}` : keyString;
|
|
207
|
+
let newPath = getPathConcat(pathKey);
|
|
208
|
+
if (newPath === undefined) {
|
|
209
|
+
newPath = path.concat(keyString);
|
|
210
|
+
setPathConcat(pathKey, newPath);
|
|
211
|
+
}
|
|
212
|
+
if (valueToWrap instanceof Map)
|
|
213
|
+
wrappedValue = wrapMap(valueToWrap, emit, newPath);
|
|
214
|
+
else if (valueToWrap instanceof Set)
|
|
215
|
+
wrappedValue = wrapSet(valueToWrap, emit, newPath);
|
|
216
|
+
else if (Array.isArray(valueToWrap))
|
|
217
|
+
wrappedValue = wrapArray(valueToWrap, emit, newPath);
|
|
218
|
+
else if (valueToWrap instanceof Date)
|
|
219
|
+
wrappedValue = new Date(valueToWrap.getTime());
|
|
220
|
+
else
|
|
221
|
+
wrappedValue = reactive(valueToWrap, emit, newPath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (prop === 'entries' || prop === Symbol.iterator) {
|
|
225
|
+
yield [wrappedKey, wrappedValue];
|
|
226
|
+
}
|
|
227
|
+
else if (prop === 'values') {
|
|
228
|
+
yield wrappedValue;
|
|
229
|
+
}
|
|
230
|
+
else { // keys
|
|
231
|
+
yield wrappedKey;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
return methodCache[prop];
|
|
236
|
+
}
|
|
237
|
+
if (prop === 'size') {
|
|
238
|
+
track(target, 'size');
|
|
239
|
+
return target.size;
|
|
240
|
+
}
|
|
241
|
+
const value = Reflect.get(target, prop, receiver);
|
|
242
|
+
// Bind plain functions accessed directly (e.g., toString)
|
|
243
|
+
if (typeof value === 'function') {
|
|
244
|
+
return value.bind(target);
|
|
245
|
+
}
|
|
246
|
+
return value;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// cache the newly created proxy before returning
|
|
250
|
+
wrapperCache.set(map, proxy);
|
|
251
|
+
return proxy;
|
|
252
|
+
}
|
package/dist/wrap-set.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { getPathConcat, setPathConcat, wrapperCache } from './utils';
|
|
2
|
+
import { reactive } from './reactive';
|
|
3
|
+
import { wrapArray } from './wrap-array';
|
|
4
|
+
import { wrapMap } from './wrap-map';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
|
+
export function wrapSet(set, emit, path) {
|
|
7
|
+
// reuse existing proxy if available for performance
|
|
8
|
+
const cachedProxy = wrapperCache.get(set);
|
|
9
|
+
if (cachedProxy)
|
|
10
|
+
return cachedProxy;
|
|
11
|
+
// cache for wrapped methods to avoid re-creating them on each call
|
|
12
|
+
const methodCache = {};
|
|
13
|
+
const proxy = new Proxy(set, {
|
|
14
|
+
get(target, prop, receiver) {
|
|
15
|
+
track(target, prop);
|
|
16
|
+
if (methodCache[prop]) {
|
|
17
|
+
return methodCache[prop];
|
|
18
|
+
}
|
|
19
|
+
if (prop === 'add') {
|
|
20
|
+
methodCache[prop] = function (value) {
|
|
21
|
+
const existed = target.has(value);
|
|
22
|
+
const oldSize = target.size;
|
|
23
|
+
// only add and trigger if the value doesn't already exist
|
|
24
|
+
if (!existed) {
|
|
25
|
+
target.add(value);
|
|
26
|
+
const newSize = target.size;
|
|
27
|
+
const event = {
|
|
28
|
+
action: 'set-add',
|
|
29
|
+
path: path,
|
|
30
|
+
value: value
|
|
31
|
+
};
|
|
32
|
+
emit(event);
|
|
33
|
+
trigger(target, Symbol.iterator);
|
|
34
|
+
if (oldSize !== newSize) {
|
|
35
|
+
trigger(target, 'size');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return receiver; // return the proxy itself for chaining
|
|
39
|
+
};
|
|
40
|
+
return methodCache[prop];
|
|
41
|
+
}
|
|
42
|
+
if (prop === 'delete') {
|
|
43
|
+
methodCache[prop] = function (value) {
|
|
44
|
+
const existed = target.has(value);
|
|
45
|
+
const oldSize = target.size;
|
|
46
|
+
if (existed) {
|
|
47
|
+
const oldValue = value;
|
|
48
|
+
const result = target.delete(value);
|
|
49
|
+
const newSize = target.size;
|
|
50
|
+
if (result) { // only emit and trigger if delete was successful
|
|
51
|
+
const event = {
|
|
52
|
+
action: 'set-delete',
|
|
53
|
+
path: path,
|
|
54
|
+
value: value,
|
|
55
|
+
oldValue: oldValue
|
|
56
|
+
};
|
|
57
|
+
emit(event);
|
|
58
|
+
trigger(target, Symbol.iterator);
|
|
59
|
+
if (oldSize !== newSize) {
|
|
60
|
+
trigger(target, 'size');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
return methodCache[prop];
|
|
68
|
+
}
|
|
69
|
+
if (prop === 'clear') {
|
|
70
|
+
methodCache[prop] = function () {
|
|
71
|
+
const oldSize = target.size;
|
|
72
|
+
if (oldSize === 0)
|
|
73
|
+
return;
|
|
74
|
+
target.clear();
|
|
75
|
+
const newSize = target.size;
|
|
76
|
+
const event = {
|
|
77
|
+
action: 'set-clear',
|
|
78
|
+
path: path,
|
|
79
|
+
value: null
|
|
80
|
+
};
|
|
81
|
+
emit(event);
|
|
82
|
+
trigger(target, Symbol.iterator);
|
|
83
|
+
if (oldSize !== newSize) {
|
|
84
|
+
trigger(target, 'size');
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return methodCache[prop];
|
|
88
|
+
}
|
|
89
|
+
if (prop === 'has') {
|
|
90
|
+
track(target, Symbol.iterator);
|
|
91
|
+
methodCache[prop] = function (value) {
|
|
92
|
+
// track specific primitive value when 'has' is called
|
|
93
|
+
// tracking object values for existence is complex and less common, handled by iteration track
|
|
94
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol') {
|
|
95
|
+
track(target, String(value));
|
|
96
|
+
}
|
|
97
|
+
return target.has(value);
|
|
98
|
+
}.bind(target); // bind is still okay here, doesn't interfere with caching
|
|
99
|
+
return methodCache[prop];
|
|
100
|
+
}
|
|
101
|
+
// handle iteration methods
|
|
102
|
+
if (prop === 'values' || prop === Symbol.iterator || prop === 'entries' || prop === 'keys' || prop === 'forEach') {
|
|
103
|
+
track(target, Symbol.iterator);
|
|
104
|
+
const originalMethod = Reflect.get(target, prop, receiver);
|
|
105
|
+
// return custom iterators/foreach that wrap values during iteration
|
|
106
|
+
if (prop === 'forEach') {
|
|
107
|
+
methodCache[prop] = (callbackfn, thisArg) => {
|
|
108
|
+
// use the proxied values() to ensure values passed to callback are wrapped and tracked
|
|
109
|
+
const valuesIterator = proxy.values();
|
|
110
|
+
for (const value of valuesIterator) {
|
|
111
|
+
callbackfn.call(thisArg, value, value, proxy);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return methodCache[prop];
|
|
115
|
+
}
|
|
116
|
+
// handle symbol.iterator, values, keys, entries by creating generator functions
|
|
117
|
+
methodCache[prop] = function* (...args) {
|
|
118
|
+
let index = 0; // use index for path generation if value is not primitive
|
|
119
|
+
const iterator = originalMethod.apply(target, args);
|
|
120
|
+
for (const entry of iterator) {
|
|
121
|
+
let valueToWrap = entry;
|
|
122
|
+
let mapKey = undefined; // key for entries() which yields [value, value]
|
|
123
|
+
if (prop === 'entries') {
|
|
124
|
+
mapKey = entry[0]; // for Set.entries(), key and value are the same
|
|
125
|
+
valueToWrap = entry[1];
|
|
126
|
+
}
|
|
127
|
+
track(target, String(index));
|
|
128
|
+
let wrappedValue = valueToWrap;
|
|
129
|
+
if (valueToWrap && typeof valueToWrap === 'object') {
|
|
130
|
+
const cachedValueProxy = wrapperCache.get(valueToWrap);
|
|
131
|
+
if (cachedValueProxy) {
|
|
132
|
+
wrappedValue = cachedValueProxy;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// calculate path using index as key, as set values don't have inherent keys
|
|
136
|
+
const keyForPath = String(index);
|
|
137
|
+
const pathKey = path.length > 0 ? `${path.join('.')}.${keyForPath}` : keyForPath;
|
|
138
|
+
let newPath = getPathConcat(pathKey);
|
|
139
|
+
if (newPath === undefined) {
|
|
140
|
+
newPath = path.concat(keyForPath);
|
|
141
|
+
setPathConcat(pathKey, newPath);
|
|
142
|
+
}
|
|
143
|
+
// recursively wrap nested structures
|
|
144
|
+
if (valueToWrap instanceof Map)
|
|
145
|
+
wrappedValue = wrapMap(valueToWrap, emit, newPath);
|
|
146
|
+
else if (valueToWrap instanceof Set)
|
|
147
|
+
wrappedValue = wrapSet(valueToWrap, emit, newPath);
|
|
148
|
+
else if (Array.isArray(valueToWrap))
|
|
149
|
+
wrappedValue = wrapArray(valueToWrap, emit, newPath);
|
|
150
|
+
else if (valueToWrap instanceof Date)
|
|
151
|
+
wrappedValue = new Date(valueToWrap.getTime()); // dates are not proxied, return copy
|
|
152
|
+
else
|
|
153
|
+
wrappedValue = reactive(valueToWrap, emit, newPath);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (prop === 'entries') {
|
|
157
|
+
yield [wrappedValue, wrappedValue]; // set entries yield [value, value]
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
yield wrappedValue;
|
|
161
|
+
}
|
|
162
|
+
index++;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
return methodCache[prop];
|
|
166
|
+
}
|
|
167
|
+
if (prop === 'size') {
|
|
168
|
+
track(target, 'size');
|
|
169
|
+
return target.size;
|
|
170
|
+
}
|
|
171
|
+
const value = Reflect.get(target, prop, receiver);
|
|
172
|
+
// Bind plain functions accessed directly (e.g., toString)
|
|
173
|
+
if (typeof value === 'function') {
|
|
174
|
+
return value.bind(target);
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// cache the newly created proxy before returning
|
|
180
|
+
wrapperCache.set(set, proxy);
|
|
181
|
+
return proxy;
|
|
182
|
+
}
|
package/dist/wrapArray.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { deepEqual, getPathConcat, setPathConcat, wrapperCache } from './utils';
|
|
2
2
|
import { reactive } from './reactive';
|
|
3
|
-
import { wrapMap } from './
|
|
4
|
-
import { wrapSet } from './
|
|
5
|
-
import { track, trigger } from './
|
|
3
|
+
import { wrapMap } from './wrap-map';
|
|
4
|
+
import { wrapSet } from './wrap-set';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
6
|
// avoid repeated typeof checks
|
|
7
7
|
function isObject(v) {
|
|
8
8
|
return v && typeof v === 'object';
|
package/dist/wrapMap.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { deepEqual, getPathConcat, setPathConcat, wrapperCache } from './utils';
|
|
2
2
|
import { reactive } from './reactive';
|
|
3
|
-
import { wrapArray } from './
|
|
4
|
-
import { wrapSet } from './
|
|
5
|
-
import { track, trigger } from './
|
|
3
|
+
import { wrapArray } from './wrap-array';
|
|
4
|
+
import { wrapSet } from './wrap-set';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
6
|
export function wrapMap(map, emit, path) {
|
|
7
7
|
// reuse existing proxy if available for performance
|
|
8
8
|
const cachedProxy = wrapperCache.get(map);
|
package/dist/wrapSet.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { getPathConcat, setPathConcat, wrapperCache } from './utils';
|
|
2
2
|
import { reactive } from './reactive';
|
|
3
|
-
import { wrapArray } from './
|
|
4
|
-
import { wrapMap } from './
|
|
5
|
-
import { track, trigger } from './
|
|
3
|
+
import { wrapArray } from './wrap-array';
|
|
4
|
+
import { wrapMap } from './wrap-map';
|
|
5
|
+
import { track, trigger } from './watch-effect';
|
|
6
6
|
export function wrapSet(set, emit, path) {
|
|
7
7
|
// reuse existing proxy if available for performance
|
|
8
8
|
const cachedProxy = wrapperCache.get(set);
|
package/package.json
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yiin/reactive-proxy-state",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"author": "Yiin <stanislovas@yiin.lt>",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Yiin/reactive-proxy-state.git"
|
|
8
|
+
},
|
|
6
9
|
"main": "dist/index.js",
|
|
7
|
-
"
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"bun-types": "^1.2.8",
|
|
12
|
+
"typescript": "^5.0.4",
|
|
13
|
+
"vitepress": "^1.6.3",
|
|
14
|
+
"vue": "^3.5.13"
|
|
15
|
+
},
|
|
8
16
|
"exports": {
|
|
9
17
|
".": {
|
|
10
18
|
"types": "./dist/index.d.ts",
|
|
11
19
|
"require": "./dist/index.js"
|
|
12
20
|
}
|
|
13
21
|
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Yiin/reactive-proxy-state/issues"
|
|
24
|
+
},
|
|
25
|
+
"description": "A simple, standalone reactivity library using Proxies",
|
|
14
26
|
"files": [
|
|
15
27
|
"dist",
|
|
16
28
|
"README.md",
|
|
17
29
|
"LICENSE"
|
|
18
30
|
],
|
|
19
|
-
"
|
|
20
|
-
"build": "tsc",
|
|
21
|
-
"test": "bun test",
|
|
22
|
-
"test:watch": "bun test --watch",
|
|
23
|
-
"test:bench": "bun --bun ./benchmarks/state-sync.bench.ts",
|
|
24
|
-
"docs:dev": "vitepress dev docs",
|
|
25
|
-
"docs:build": "vitepress build docs",
|
|
26
|
-
"docs:preview": "vitepress preview docs"
|
|
27
|
-
},
|
|
31
|
+
"homepage": "https://Yiin.github.io/reactive-proxy-state/",
|
|
28
32
|
"keywords": [
|
|
29
33
|
"state",
|
|
30
34
|
"sync",
|
|
@@ -35,23 +39,19 @@
|
|
|
35
39
|
"vue",
|
|
36
40
|
"reactivity"
|
|
37
41
|
],
|
|
38
|
-
"author": "Yiin <stanislovas@yiin.lt>",
|
|
39
42
|
"license": "MIT",
|
|
40
|
-
"
|
|
41
|
-
"type": "git",
|
|
42
|
-
"url": "git+https://github.com/Yiin/reactive-proxy-state.git"
|
|
43
|
-
},
|
|
44
|
-
"bugs": {
|
|
45
|
-
"url": "https://github.com/Yiin/reactive-proxy-state/issues"
|
|
46
|
-
},
|
|
47
|
-
"homepage": "https://Yiin.github.io/reactive-proxy-state/",
|
|
43
|
+
"private": false,
|
|
48
44
|
"publishConfig": {
|
|
49
45
|
"access": "public"
|
|
50
46
|
},
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"test": "bun test",
|
|
50
|
+
"test:watch": "bun test --watch",
|
|
51
|
+
"test:bench": "bun --bun ./benchmarks/state-sync.bench.ts",
|
|
52
|
+
"docs:dev": "vitepress dev docs",
|
|
53
|
+
"docs:build": "vitepress build docs",
|
|
54
|
+
"docs:preview": "vitepress preview docs"
|
|
55
|
+
},
|
|
56
|
+
"types": "dist/index.d.ts"
|
|
57
57
|
}
|