@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 +21 -0
- package/README.md +13 -17
- 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/dist/utils.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Cache for memoized deepEqual results with cleanup
|
|
2
|
+
const deepEqualCache = new WeakMap();
|
|
3
|
+
const MAX_CACHE_SIZE = 1000; // Prevent unbounded growth
|
|
4
|
+
// Path traversal cache with LRU-like behavior
|
|
5
|
+
export const pathCache = new WeakMap();
|
|
6
|
+
const pathCacheSize = new WeakMap();
|
|
7
|
+
// Path concatenation cache with size limit
|
|
8
|
+
export const pathConcatCache = new Map();
|
|
9
|
+
const MAX_PATH_CACHE_SIZE = 1000;
|
|
10
|
+
// Global seen map for circular reference detection
|
|
11
|
+
export const globalSeen = new WeakMap();
|
|
12
|
+
// Global cache for proxy wrappers
|
|
13
|
+
export const wrapperCache = new WeakMap();
|
|
14
|
+
function cleanupPathCache(root) {
|
|
15
|
+
const cache = pathCache.get(root);
|
|
16
|
+
if (cache && pathCacheSize.get(root) > MAX_CACHE_SIZE) {
|
|
17
|
+
// Clear oldest entries (first 20%)
|
|
18
|
+
const entriesToRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
|
|
19
|
+
let count = 0;
|
|
20
|
+
for (const key of cache.keys()) {
|
|
21
|
+
if (count >= entriesToRemove)
|
|
22
|
+
break;
|
|
23
|
+
cache.delete(key);
|
|
24
|
+
count++;
|
|
25
|
+
}
|
|
26
|
+
pathCacheSize.set(root, MAX_CACHE_SIZE - entriesToRemove);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function cleanupPathConcatCache() {
|
|
30
|
+
if (pathConcatCache.size > MAX_PATH_CACHE_SIZE) {
|
|
31
|
+
// Remove oldest entries (first 20%)
|
|
32
|
+
const entriesToRemove = Math.floor(MAX_PATH_CACHE_SIZE * 0.2);
|
|
33
|
+
let count = 0;
|
|
34
|
+
for (const key of pathConcatCache.keys()) {
|
|
35
|
+
if (count >= entriesToRemove)
|
|
36
|
+
break;
|
|
37
|
+
pathConcatCache.delete(key);
|
|
38
|
+
count++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function deepEqual(a, b, seen = globalSeen) {
|
|
43
|
+
// Fast path for primitive equality
|
|
44
|
+
if (a === b)
|
|
45
|
+
return true;
|
|
46
|
+
// Fast path for null/undefined
|
|
47
|
+
if (a == null || b == null)
|
|
48
|
+
return a === b;
|
|
49
|
+
// Fast path for different types
|
|
50
|
+
if (typeof a !== typeof b)
|
|
51
|
+
return false;
|
|
52
|
+
// Fast path for Date objects
|
|
53
|
+
if (a instanceof Date && b instanceof Date)
|
|
54
|
+
return a.getTime() === b.getTime();
|
|
55
|
+
// Fast path for non-objects
|
|
56
|
+
if (typeof a !== 'object')
|
|
57
|
+
return false;
|
|
58
|
+
// Fast path for different array types
|
|
59
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
60
|
+
return false;
|
|
61
|
+
// Check for circular references
|
|
62
|
+
if (seen.has(a))
|
|
63
|
+
return seen.get(a) === b;
|
|
64
|
+
seen.set(a, b);
|
|
65
|
+
// Check cache for memoized results
|
|
66
|
+
if (deepEqualCache.has(a) && deepEqualCache.get(a)?.has(b)) {
|
|
67
|
+
return deepEqualCache.get(a).get(b);
|
|
68
|
+
}
|
|
69
|
+
// Initialize cache for this object if needed
|
|
70
|
+
if (!deepEqualCache.has(a)) {
|
|
71
|
+
deepEqualCache.set(a, new WeakMap());
|
|
72
|
+
}
|
|
73
|
+
let result;
|
|
74
|
+
// Compare arrays
|
|
75
|
+
if (Array.isArray(a)) {
|
|
76
|
+
if (a.length !== b.length) {
|
|
77
|
+
result = false;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result = a.every((val, idx) => deepEqual(val, b[idx], seen));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Compare objects
|
|
85
|
+
const keysA = Object.keys(a);
|
|
86
|
+
const keysB = Object.keys(b);
|
|
87
|
+
if (keysA.length !== keysB.length) {
|
|
88
|
+
result = false;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
result = keysA.every(key => deepEqual(a[key], b[key], seen));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Cache the result
|
|
95
|
+
deepEqualCache.get(a).set(b, result);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
// Helper to safely get from path cache with cleanup
|
|
99
|
+
export function getFromPathCache(root, pathKey) {
|
|
100
|
+
const cache = pathCache.get(root);
|
|
101
|
+
if (!cache)
|
|
102
|
+
return undefined;
|
|
103
|
+
const result = cache.get(pathKey);
|
|
104
|
+
if (result !== undefined) {
|
|
105
|
+
// Move to end (most recently used)
|
|
106
|
+
cache.delete(pathKey);
|
|
107
|
+
cache.set(pathKey, result);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
// Helper to safely set in path cache with cleanup
|
|
112
|
+
export function setInPathCache(root, pathKey, value) {
|
|
113
|
+
if (!pathCache.has(root)) {
|
|
114
|
+
pathCache.set(root, new Map());
|
|
115
|
+
pathCacheSize.set(root, 0);
|
|
116
|
+
}
|
|
117
|
+
const cache = pathCache.get(root);
|
|
118
|
+
const size = pathCacheSize.get(root);
|
|
119
|
+
// If key exists, remove it first (will be added at end)
|
|
120
|
+
if (cache.has(pathKey)) {
|
|
121
|
+
cache.delete(pathKey);
|
|
122
|
+
}
|
|
123
|
+
cache.set(pathKey, value);
|
|
124
|
+
pathCacheSize.set(root, size + 1);
|
|
125
|
+
cleanupPathCache(root);
|
|
126
|
+
}
|
|
127
|
+
// Helper to safely get/set path concatenation with cleanup
|
|
128
|
+
export function getPathConcat(path) {
|
|
129
|
+
const result = pathConcatCache.get(path);
|
|
130
|
+
if (result !== undefined) {
|
|
131
|
+
// Move to end (most recently used)
|
|
132
|
+
pathConcatCache.delete(path);
|
|
133
|
+
pathConcatCache.set(path, result);
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
export function setPathConcat(path, value) {
|
|
138
|
+
pathConcatCache.set(path, value);
|
|
139
|
+
cleanupPathConcatCache();
|
|
140
|
+
}
|
|
141
|
+
// Helper to check if a value is an object (and not null)
|
|
142
|
+
function isObject(val) {
|
|
143
|
+
return val !== null && typeof val === 'object';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Recursively traverses an object to track all nested properties.
|
|
147
|
+
* Used for deep watching.
|
|
148
|
+
* @param value - The value to traverse.
|
|
149
|
+
* @param seen - A Set to handle circular references.
|
|
150
|
+
*/
|
|
151
|
+
export function traverse(value, seen = new Set()) {
|
|
152
|
+
if (!isObject(value) || seen.has(value)) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
seen.add(value);
|
|
156
|
+
// Traverse arrays
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
// Access length for tracking
|
|
159
|
+
value.length;
|
|
160
|
+
for (let i = 0; i < value.length; i++) {
|
|
161
|
+
traverse(value[i], seen);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Traverse Sets and Maps
|
|
165
|
+
else if (value instanceof Set || value instanceof Map) {
|
|
166
|
+
for (const v of value) {
|
|
167
|
+
// For Maps, traverse both keys (if objects) and values
|
|
168
|
+
if (Array.isArray(v)) {
|
|
169
|
+
traverse(v[0], seen); // Key
|
|
170
|
+
traverse(v[1], seen); // Value
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
traverse(v, seen); // Value for Set
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// --- Stop after handling Map/Set ---
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
// Traverse plain objects (Only if not Array, Map, or Set)
|
|
180
|
+
else {
|
|
181
|
+
for (const key in value) {
|
|
182
|
+
// Access the property to trigger tracking via the proxy's get handler
|
|
183
|
+
traverse(value[key], seen);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Creates a deep clone of a value.
|
|
190
|
+
* Handles primitives, Dates, Arrays, Maps, Sets, and plain objects.
|
|
191
|
+
* Includes cycle detection.
|
|
192
|
+
* @param value The value to clone.
|
|
193
|
+
* @param seen A WeakMap to handle circular references during recursion.
|
|
194
|
+
* @returns A deep clone of the value.
|
|
195
|
+
*/
|
|
196
|
+
export function deepClone(value, seen = new WeakMap()) {
|
|
197
|
+
// Primitives and null are returned directly
|
|
198
|
+
if (value === null || typeof value !== 'object') {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
// Handle Dates
|
|
202
|
+
if (value instanceof Date) {
|
|
203
|
+
return new Date(value.getTime());
|
|
204
|
+
}
|
|
205
|
+
// Handle cycles
|
|
206
|
+
if (seen.has(value)) {
|
|
207
|
+
return seen.get(value);
|
|
208
|
+
}
|
|
209
|
+
// Handle Arrays
|
|
210
|
+
if (Array.isArray(value)) {
|
|
211
|
+
const newArray = [];
|
|
212
|
+
seen.set(value, newArray);
|
|
213
|
+
for (let i = 0; i < value.length; i++) {
|
|
214
|
+
newArray[i] = deepClone(value[i], seen);
|
|
215
|
+
}
|
|
216
|
+
return newArray;
|
|
217
|
+
}
|
|
218
|
+
// Handle Maps
|
|
219
|
+
if (value instanceof Map) {
|
|
220
|
+
const newMap = new Map();
|
|
221
|
+
seen.set(value, newMap);
|
|
222
|
+
value.forEach((val, key) => {
|
|
223
|
+
// Clone both key and value in case they are objects
|
|
224
|
+
newMap.set(deepClone(key, seen), deepClone(val, seen));
|
|
225
|
+
});
|
|
226
|
+
return newMap;
|
|
227
|
+
}
|
|
228
|
+
// Handle Sets
|
|
229
|
+
if (value instanceof Set) {
|
|
230
|
+
const newSet = new Set();
|
|
231
|
+
seen.set(value, newSet);
|
|
232
|
+
value.forEach(val => {
|
|
233
|
+
newSet.add(deepClone(val, seen));
|
|
234
|
+
});
|
|
235
|
+
return newSet;
|
|
236
|
+
}
|
|
237
|
+
// Handle plain objects
|
|
238
|
+
// Create object with same prototype
|
|
239
|
+
const newObject = Object.create(Object.getPrototypeOf(value));
|
|
240
|
+
seen.set(value, newObject);
|
|
241
|
+
// Copy properties
|
|
242
|
+
for (const key in value) {
|
|
243
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
244
|
+
newObject[key] = deepClone(value[key], seen);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Copy symbol properties (if any)
|
|
248
|
+
const symbolKeys = Object.getOwnPropertySymbols(value);
|
|
249
|
+
for (const symbolKey of symbolKeys) {
|
|
250
|
+
// Check if property descriptor exists and has get/set or is value
|
|
251
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, symbolKey);
|
|
252
|
+
if (descriptor && (descriptor.value !== undefined || descriptor.get || descriptor.set)) {
|
|
253
|
+
newObject[symbolKey] = deepClone(value[symbolKey], seen);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return newObject;
|
|
257
|
+
}
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type WatchCallback<T = any> = (newValue: T, oldValue: T | undefined) => void;
|
|
2
|
+
type WatchSource<T = any> = () => T;
|
|
3
|
+
type WatchSourceInput<T = any> = WatchSource<T> | T;
|
|
4
|
+
type WatchStopHandle = () => void;
|
|
5
|
+
export interface WatchOptions {
|
|
6
|
+
immediate?: boolean;
|
|
7
|
+
deep?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Watches a reactive source and runs a callback when it changes
|
|
11
|
+
*
|
|
12
|
+
* @param source - A function that returns the value to watch
|
|
13
|
+
* @param callback - Function to call when the source changes
|
|
14
|
+
* @param options - Watch options (immediate, deep)
|
|
15
|
+
* @returns A function to stop watching
|
|
16
|
+
*/
|
|
17
|
+
export declare function watch<T = any>(sourceInput: WatchSourceInput<T>, // Renamed parameter
|
|
18
|
+
callback: WatchCallback<T>, options?: WatchOptions): WatchStopHandle;
|
|
19
|
+
export {};
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { watchEffect } from './watchEffect';
|
|
2
|
+
import { traverse, deepClone } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* Watches a reactive source and runs a callback when it changes
|
|
5
|
+
*
|
|
6
|
+
* @param source - A function that returns the value to watch
|
|
7
|
+
* @param callback - Function to call when the source changes
|
|
8
|
+
* @param options - Watch options (immediate, deep)
|
|
9
|
+
* @returns A function to stop watching
|
|
10
|
+
*/
|
|
11
|
+
export function watch(sourceInput, // Renamed parameter
|
|
12
|
+
callback, options = {}) {
|
|
13
|
+
// Default deep to true unless explicitly false
|
|
14
|
+
const { immediate = false, deep = true } = options;
|
|
15
|
+
// Normalize sourceInput to always be a function
|
|
16
|
+
const source = typeof sourceInput === 'function'
|
|
17
|
+
? sourceInput
|
|
18
|
+
: () => sourceInput;
|
|
19
|
+
let oldValue;
|
|
20
|
+
let initialized = false;
|
|
21
|
+
// Use watchEffect to track dependencies and re-run when they change
|
|
22
|
+
const stopEffect = watchEffect(() => {
|
|
23
|
+
// 1. Run the source function to get the current value
|
|
24
|
+
const currentValue = source();
|
|
25
|
+
// Determine if deep watching is needed for tracking
|
|
26
|
+
// Use the defaulted 'deep' value
|
|
27
|
+
let needsDeepTracking = deep === true;
|
|
28
|
+
// This check becomes redundant if deep defaults to true, but keep for potential explicit {deep: false} on collections?
|
|
29
|
+
// Maybe simplify: if deep is true, always traverse.
|
|
30
|
+
/*
|
|
31
|
+
if (!needsDeepTracking && currentValue && typeof currentValue === 'object') {
|
|
32
|
+
if (Array.isArray(currentValue) || currentValue instanceof Map || currentValue instanceof Set) {
|
|
33
|
+
needsDeepTracking = true; // Track collections even if deep:false? Vue does shallow on collections by default.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
*/
|
|
37
|
+
// 2. If deep tracking needed, traverse the value *for tracking purposes only*
|
|
38
|
+
if (needsDeepTracking) {
|
|
39
|
+
traverse(currentValue); // Discard result, only needed for effect tracking
|
|
40
|
+
}
|
|
41
|
+
// 3. Compare the actual currentValue with the oldValue
|
|
42
|
+
if (initialized) {
|
|
43
|
+
let hasChanged = false;
|
|
44
|
+
// Use the defaulted 'deep' value for comparison logic
|
|
45
|
+
if (!deep) {
|
|
46
|
+
hasChanged = currentValue !== oldValue;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// For deep watches, the effect running *implies* a relevant change occurred.
|
|
50
|
+
// The traverse() call ensures dependencies were tracked. If the effect
|
|
51
|
+
// runs, we assume a change happened without needing deepEqual.
|
|
52
|
+
hasChanged = true;
|
|
53
|
+
}
|
|
54
|
+
if (hasChanged) {
|
|
55
|
+
// Get the value to pass as the previous oldValue to the callback
|
|
56
|
+
const prevOldValue = oldValue;
|
|
57
|
+
// Update the stored oldValue. Clone *only if* deep watching is enabled.
|
|
58
|
+
oldValue = deep ? deepClone(currentValue) : currentValue;
|
|
59
|
+
callback(currentValue, prevOldValue);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// First run: establish initial oldValue (cloned if deep) and handle immediate call
|
|
64
|
+
oldValue = deep ? deepClone(currentValue) : currentValue;
|
|
65
|
+
initialized = true;
|
|
66
|
+
if (immediate) {
|
|
67
|
+
// Pass undefined as oldValue for immediate calls
|
|
68
|
+
callback(currentValue, undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Return the stop handle from watchEffect
|
|
73
|
+
return stopEffect;
|
|
74
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
* Clean up dependencies for a specific effect
|
|
18
|
+
*/
|
|
19
|
+
export declare function cleanupEffect(effect: TrackedEffect<any>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Track a property access for the active effect
|
|
22
|
+
*/
|
|
23
|
+
export declare function track(target: object, key: string | symbol): void;
|
|
24
|
+
/**
|
|
25
|
+
* Trigger effects associated with a property (Synchronous Only)
|
|
26
|
+
*/
|
|
27
|
+
export declare function trigger(target: object, key: string | symbol): void;
|
|
28
|
+
export interface WatchEffectOptions {
|
|
29
|
+
lazy?: boolean;
|
|
30
|
+
scheduler?: Scheduler;
|
|
31
|
+
onTrack?: (event: {
|
|
32
|
+
effect: EffectCallback<any>;
|
|
33
|
+
target: object;
|
|
34
|
+
key: string | symbol;
|
|
35
|
+
type: 'track';
|
|
36
|
+
}) => void;
|
|
37
|
+
onTrigger?: (event: {
|
|
38
|
+
effect: EffectCallback<any>;
|
|
39
|
+
target: object;
|
|
40
|
+
key: string | symbol;
|
|
41
|
+
type: 'trigger';
|
|
42
|
+
}) => void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Runs a function and re-runs it when its reactive dependencies change.
|
|
46
|
+
* Returns a stop handle that also exposes the effect instance.
|
|
47
|
+
*/
|
|
48
|
+
export declare function watchEffect<T>(effectCallback: EffectCallback<T>, options?: WatchEffectOptions): WatchEffectStopHandle<T>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// Track currently running effect
|
|
2
|
+
// Note: activeEffect can hold effects with different return types
|
|
3
|
+
export let activeEffect = null;
|
|
4
|
+
// Setter function for activeEffect
|
|
5
|
+
export function setActiveEffect(effect) {
|
|
6
|
+
activeEffect = effect;
|
|
7
|
+
}
|
|
8
|
+
// Store for tracking dependencies: target -> key -> Set<TrackedEffect>
|
|
9
|
+
// The Sets will hold effects of potentially different types
|
|
10
|
+
const targetMap = new WeakMap();
|
|
11
|
+
/**
|
|
12
|
+
* Clean up dependencies for a specific effect
|
|
13
|
+
*/
|
|
14
|
+
export function cleanupEffect(effect) {
|
|
15
|
+
if (effect.dependencies) {
|
|
16
|
+
effect.dependencies.forEach(dep => {
|
|
17
|
+
dep.delete(effect);
|
|
18
|
+
});
|
|
19
|
+
effect.dependencies.clear(); // Clear the set for the next run
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Track a property access for the active effect
|
|
24
|
+
*/
|
|
25
|
+
export function track(target, key) {
|
|
26
|
+
// Only track if there's an active effect that should be running
|
|
27
|
+
if (!activeEffect || !activeEffect.active)
|
|
28
|
+
return;
|
|
29
|
+
// Get the dependency map for this target
|
|
30
|
+
let depsMap = targetMap.get(target);
|
|
31
|
+
if (!depsMap) {
|
|
32
|
+
depsMap = new Map();
|
|
33
|
+
targetMap.set(target, depsMap);
|
|
34
|
+
}
|
|
35
|
+
// Get the set of effects for this property
|
|
36
|
+
let dep = depsMap.get(key);
|
|
37
|
+
if (!dep) {
|
|
38
|
+
dep = new Set();
|
|
39
|
+
depsMap.set(key, dep);
|
|
40
|
+
}
|
|
41
|
+
// Add the active effect to the set if not already present
|
|
42
|
+
// Ensure we are adding the correct type to the Set
|
|
43
|
+
const effectToAdd = activeEffect;
|
|
44
|
+
if (!dep.has(effectToAdd)) {
|
|
45
|
+
dep.add(effectToAdd);
|
|
46
|
+
// Add this dep set to the effect's own dependency list for cleanup
|
|
47
|
+
if (!effectToAdd.dependencies) {
|
|
48
|
+
effectToAdd.dependencies = new Set();
|
|
49
|
+
}
|
|
50
|
+
effectToAdd.dependencies.add(dep);
|
|
51
|
+
// Trigger onTrack if available
|
|
52
|
+
if (effectToAdd.options?.onTrack) {
|
|
53
|
+
// Pass the raw user callback, not the internal effect object
|
|
54
|
+
effectToAdd.options.onTrack({ effect: effectToAdd._rawCallback, target, key, type: 'track' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Trigger effects associated with a property (Synchronous Only)
|
|
60
|
+
*/
|
|
61
|
+
export function trigger(target, key) {
|
|
62
|
+
const depsMap = targetMap.get(target);
|
|
63
|
+
if (!depsMap)
|
|
64
|
+
return;
|
|
65
|
+
// Use a Set to avoid duplicate runs within the same trigger
|
|
66
|
+
const effectsToRun = new Set();
|
|
67
|
+
const addEffects = (depKey) => {
|
|
68
|
+
const dep = depsMap.get(depKey);
|
|
69
|
+
if (dep) {
|
|
70
|
+
dep.forEach(effect => {
|
|
71
|
+
// Avoid infinite loops by not scheduling the currently running effect
|
|
72
|
+
// Also check if effect is active
|
|
73
|
+
if (effect !== activeEffect && effect.active) {
|
|
74
|
+
effectsToRun.add(effect);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
addEffects(key);
|
|
80
|
+
// Schedule or run effects
|
|
81
|
+
effectsToRun.forEach(effect => {
|
|
82
|
+
// Trigger onTrigger if available
|
|
83
|
+
if (effect.options?.onTrigger) {
|
|
84
|
+
effect.options.onTrigger({ effect: effect._rawCallback, target, key, type: 'trigger' });
|
|
85
|
+
}
|
|
86
|
+
// Use scheduler if available, otherwise run directly
|
|
87
|
+
if (effect.options?.scheduler) {
|
|
88
|
+
effect.options.scheduler(effect.run);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
effect.run(); // Run the effect's wrapper
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Runs a function and re-runs it when its reactive dependencies change.
|
|
97
|
+
* Returns a stop handle that also exposes the effect instance.
|
|
98
|
+
*/
|
|
99
|
+
export function watchEffect(effectCallback, options = {}) {
|
|
100
|
+
// The core runner function that handles cleanup, activeEffect setting, and execution
|
|
101
|
+
const run = () => {
|
|
102
|
+
if (!effectFn.active)
|
|
103
|
+
return effectCallback(); // If stopped, just return value (though likely undefined)
|
|
104
|
+
const previousEffect = activeEffect;
|
|
105
|
+
try {
|
|
106
|
+
cleanupEffect(effectFn);
|
|
107
|
+
setActiveEffect(effectFn);
|
|
108
|
+
return effectCallback(); // Execute the user's function
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
setActiveEffect(previousEffect);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// The effect object itself
|
|
115
|
+
const effectFn = {
|
|
116
|
+
run: run,
|
|
117
|
+
dependencies: new Set(),
|
|
118
|
+
options: options,
|
|
119
|
+
active: true,
|
|
120
|
+
_rawCallback: effectCallback
|
|
121
|
+
};
|
|
122
|
+
// Run effect immediately unless lazy
|
|
123
|
+
if (!options.lazy) {
|
|
124
|
+
effectFn.run();
|
|
125
|
+
}
|
|
126
|
+
// Create the stop handle function
|
|
127
|
+
const stopHandle = () => {
|
|
128
|
+
if (effectFn.active) {
|
|
129
|
+
cleanupEffect(effectFn);
|
|
130
|
+
effectFn.active = false;
|
|
131
|
+
// Clear references
|
|
132
|
+
// effectFn.dependencies = undefined; // Keep dependencies for potential re-activation?
|
|
133
|
+
// effectFn.options = undefined;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
// Attach the effect instance to the stop handle
|
|
137
|
+
stopHandle.effect = effectFn;
|
|
138
|
+
return stopHandle;
|
|
139
|
+
}
|