@yiin/reactive-proxy-state 1.0.12 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils.js DELETED
@@ -1,220 +0,0 @@
1
- // cache for memoized deepEqual results, using weakmap to avoid memory leaks
2
- const deepEqualCache = new WeakMap();
3
- const MAX_CACHE_SIZE = 1000;
4
- // path traversal cache (object -> pathString -> value) with lru-like behavior
5
- // weakmap for the root object ensures the cache entry is garbage collected when the object is
6
- export const pathCache = new WeakMap();
7
- const pathCacheSize = new WeakMap(); // track individual map sizes
8
- // path concatenation cache ('a.b.c' -> ['a', 'b', 'c']) with size limit
9
- export const pathConcatCache = new Map();
10
- const MAX_PATH_CACHE_SIZE = 1000;
11
- // global weakmap for circular reference detection during deep operations like clone or equal
12
- export const globalSeen = new WeakMap();
13
- // global cache to reuse proxy wrappers for the same original object
14
- export const wrapperCache = new WeakMap();
15
- // simple lru-like cleanup for individual object path caches
16
- function cleanupPathCache(root) {
17
- const cache = pathCache.get(root);
18
- if (cache && pathCacheSize.get(root) > MAX_CACHE_SIZE) {
19
- // clear oldest entries (first 20%)
20
- const entriesToRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
21
- let count = 0;
22
- for (const key of cache.keys()) {
23
- if (count >= entriesToRemove)
24
- break;
25
- cache.delete(key);
26
- count++;
27
- }
28
- pathCacheSize.set(root, MAX_CACHE_SIZE - entriesToRemove);
29
- }
30
- }
31
- // simple lru-like cleanup for the global path concatenation cache
32
- function cleanupPathConcatCache() {
33
- if (pathConcatCache.size > MAX_PATH_CACHE_SIZE) {
34
- // remove oldest entries (first 20%)
35
- const entriesToRemove = Math.floor(MAX_PATH_CACHE_SIZE * 0.2);
36
- let count = 0;
37
- for (const key of pathConcatCache.keys()) {
38
- if (count >= entriesToRemove)
39
- break;
40
- pathConcatCache.delete(key);
41
- count++;
42
- }
43
- }
44
- }
45
- export function deepEqual(a, b, seen = globalSeen) {
46
- if (a === b)
47
- return true;
48
- if (a == null || b == null)
49
- return a === b;
50
- if (typeof a !== typeof b)
51
- return false;
52
- if (a instanceof Date && b instanceof Date)
53
- return a.getTime() === b.getTime();
54
- if (typeof a !== 'object')
55
- return false;
56
- if (Array.isArray(a) !== Array.isArray(b))
57
- return false;
58
- // handle circular references
59
- if (seen.has(a))
60
- return seen.get(a) === b;
61
- seen.set(a, b);
62
- // check memoization cache before diving deeper
63
- if (deepEqualCache.has(a) && deepEqualCache.get(a)?.has(b)) {
64
- return deepEqualCache.get(a).get(b);
65
- }
66
- if (!deepEqualCache.has(a)) {
67
- deepEqualCache.set(a, new WeakMap());
68
- }
69
- let result;
70
- if (Array.isArray(a)) {
71
- result = a.length === b.length && a.every((val, idx) => deepEqual(val, b[idx], seen));
72
- }
73
- else {
74
- const keysA = Object.keys(a);
75
- const keysB = Object.keys(b);
76
- result = keysA.length === keysB.length && keysA.every(key => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key], seen));
77
- }
78
- // cache the result before returning
79
- deepEqualCache.get(a).set(b, result);
80
- return result;
81
- }
82
- export function getFromPathCache(root, pathKey) {
83
- const cache = pathCache.get(root);
84
- if (!cache)
85
- return undefined;
86
- const result = cache.get(pathKey);
87
- if (result !== undefined) {
88
- // simulate lru by deleting and re-setting the key
89
- cache.delete(pathKey);
90
- cache.set(pathKey, result);
91
- }
92
- return result;
93
- }
94
- export function setInPathCache(root, pathKey, value) {
95
- if (!pathCache.has(root)) {
96
- pathCache.set(root, new Map());
97
- pathCacheSize.set(root, 0);
98
- }
99
- const cache = pathCache.get(root);
100
- // adjust size only if the key is new
101
- if (!cache.has(pathKey)) {
102
- pathCacheSize.set(root, pathCacheSize.get(root) + 1);
103
- }
104
- else {
105
- cache.delete(pathKey); // remove old entry before adding to end
106
- }
107
- cache.set(pathKey, value);
108
- cleanupPathCache(root); // check if cleanup is needed after adding
109
- }
110
- export function getPathConcat(path) {
111
- const result = pathConcatCache.get(path);
112
- if (result !== undefined) {
113
- // simulate lru
114
- pathConcatCache.delete(path);
115
- pathConcatCache.set(path, result);
116
- }
117
- return result;
118
- }
119
- export function setPathConcat(path, value) {
120
- // delete first to ensure it's added at the end (most recent)
121
- if (pathConcatCache.has(path)) {
122
- pathConcatCache.delete(path);
123
- }
124
- pathConcatCache.set(path, value);
125
- cleanupPathConcatCache(); // check size limit
126
- }
127
- function isObject(val) {
128
- return val !== null && typeof val === 'object';
129
- }
130
- /**
131
- * recursively traverses an object or array, accessing each property/element.
132
- * used by `watch` with `deep: true` to establish dependencies on all nested properties.
133
- * the actual tracking is done by the proxy `get` handlers triggered during traversal.
134
- */
135
- export function traverse(value, seen = new Set()) {
136
- if (!isObject(value) || seen.has(value)) {
137
- return value;
138
- }
139
- seen.add(value);
140
- if (Array.isArray(value)) {
141
- value.length; // track length
142
- for (let i = 0; i < value.length; i++) {
143
- traverse(value[i], seen);
144
- }
145
- }
146
- else if (value instanceof Set || value instanceof Map) {
147
- for (const v of value) {
148
- if (Array.isArray(v)) { // map entries [key, value]
149
- traverse(v[0], seen); // key
150
- traverse(v[1], seen); // value
151
- }
152
- else { // set values
153
- traverse(v, seen);
154
- }
155
- }
156
- return value; // no need to iterate plain object keys for map/set
157
- }
158
- else {
159
- for (const key in value) {
160
- traverse(value[key], seen);
161
- }
162
- }
163
- return value;
164
- }
165
- /**
166
- * creates a deep clone of a value.
167
- * includes cycle detection using a weakmap.
168
- */
169
- export function deepClone(value, seen = new WeakMap()) {
170
- if (value === null || typeof value !== 'object') {
171
- return value;
172
- }
173
- if (value instanceof Date) {
174
- return new Date(value.getTime());
175
- }
176
- if (seen.has(value)) {
177
- return seen.get(value);
178
- }
179
- if (Array.isArray(value)) {
180
- const newArray = [];
181
- seen.set(value, newArray); // store ref before recursing
182
- for (let i = 0; i < value.length; i++) {
183
- newArray[i] = deepClone(value[i], seen);
184
- }
185
- return newArray;
186
- }
187
- if (value instanceof Map) {
188
- const newMap = new Map();
189
- seen.set(value, newMap); // store ref before recursing
190
- value.forEach((val, key) => {
191
- newMap.set(deepClone(key, seen), deepClone(val, seen));
192
- });
193
- return newMap;
194
- }
195
- if (value instanceof Set) {
196
- const newSet = new Set();
197
- seen.set(value, newSet); // store ref before recursing
198
- value.forEach(val => {
199
- newSet.add(deepClone(val, seen));
200
- });
201
- return newSet;
202
- }
203
- // handle plain objects
204
- const newObject = Object.create(Object.getPrototypeOf(value));
205
- seen.set(value, newObject); // store ref before recursing
206
- for (const key in value) {
207
- if (Object.prototype.hasOwnProperty.call(value, key)) {
208
- newObject[key] = deepClone(value[key], seen);
209
- }
210
- }
211
- // copy symbol properties
212
- const symbolKeys = Object.getOwnPropertySymbols(value);
213
- for (const symbolKey of symbolKeys) {
214
- const descriptor = Object.getOwnPropertyDescriptor(value, symbolKey);
215
- if (descriptor && Object.prototype.propertyIsEnumerable.call(value, symbolKey)) {
216
- newObject[symbolKey] = deepClone(value[symbolKey], seen);
217
- }
218
- }
219
- return newObject;
220
- }
@@ -1,154 +0,0 @@
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 DELETED
@@ -1,44 +0,0 @@
1
- import { watchEffect } from './watch-effect';
2
- import { traverse, deepClone } from './utils';
3
- /**
4
- * watches a reactive source (getter function or reactive object/ref)
5
- * and runs a callback when the source's value changes.
6
- */
7
- export function watch(sourceInput, callback, options = {}) {
8
- const { immediate = false, deep = true } = options;
9
- // normalize source to always be a getter function
10
- const source = typeof sourceInput === 'function'
11
- ? sourceInput
12
- : () => sourceInput;
13
- let oldValue;
14
- let initialized = false;
15
- // use watchEffect internally to handle dependency tracking
16
- const stopEffect = watchEffect(() => {
17
- const currentValue = source();
18
- // if deep watching, traverse the current value to track nested dependencies
19
- if (deep) {
20
- traverse(currentValue);
21
- }
22
- if (initialized) {
23
- let hasChanged = false;
24
- // for deep watches, the effect running implies a dependency changed.
25
- // for shallow, explicitly check reference equality.
26
- hasChanged = deep || currentValue !== oldValue;
27
- if (hasChanged) {
28
- const prevOldValue = oldValue;
29
- // store a clone for deep watches to pass as the correct oldValue next time
30
- oldValue = deep ? deepClone(currentValue) : currentValue;
31
- callback(currentValue, prevOldValue);
32
- }
33
- }
34
- else {
35
- // first run: store initial value (cloned if deep) and run immediate callback if requested
36
- oldValue = deep ? deepClone(currentValue) : currentValue;
37
- initialized = true;
38
- if (immediate) {
39
- callback(currentValue, undefined); // pass undefined as oldValue for immediate
40
- }
41
- }
42
- }, { lazy: false }); // run immediately (watchEffect handles `immediate` option internally)
43
- return stopEffect;
44
- }
@@ -1,54 +0,0 @@
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 {};
@@ -1,154 +0,0 @@
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
- }