@yiin/reactive-proxy-state 1.0.4 → 1.0.6

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/wrapMap.js CHANGED
@@ -4,26 +4,26 @@ import { wrapArray } from './wrapArray';
4
4
  import { wrapSet } from './wrapSet';
5
5
  import { track, trigger } from './watchEffect';
6
6
  export function wrapMap(map, emit, path) {
7
- // Check wrapper cache first
7
+ // reuse existing proxy if available for performance
8
8
  const cachedProxy = wrapperCache.get(map);
9
9
  if (cachedProxy)
10
10
  return cachedProxy;
11
11
  const proxy = new Proxy(map, {
12
12
  get(target, prop, receiver) {
13
- // Original track call - Keep for properties not explicitly handled
14
13
  track(target, prop);
15
- // --- Specific Method Handlers ---
16
14
  if (prop === 'set') {
17
15
  return function (key, value) {
18
16
  const existed = target.has(key);
19
17
  const oldValue = target.get(key);
20
18
  const oldSize = target.size;
19
+ // avoid unnecessary work if value hasn't changed
21
20
  if (oldValue === value)
22
21
  return receiver;
23
22
  if (oldValue && typeof oldValue === 'object' && value && typeof value === 'object' && deepEqual(oldValue, value, new WeakMap()))
24
23
  return receiver;
25
24
  target.set(key, value);
26
25
  const newSize = target.size;
26
+ // optimize path calculation by caching concatenated paths
27
27
  const pathKey = path.join('.');
28
28
  let cachedPath = getPathConcat(pathKey);
29
29
  if (cachedPath === undefined) {
@@ -38,6 +38,7 @@ export function wrapMap(map, emit, path) {
38
38
  newValue: value
39
39
  };
40
40
  emit(event);
41
+ // trigger effects based on whether it was an add or update
41
42
  if (!existed) {
42
43
  trigger(target, Symbol.iterator);
43
44
  if (oldSize !== newSize) {
@@ -59,7 +60,7 @@ export function wrapMap(map, emit, path) {
59
60
  const oldSize = target.size;
60
61
  const result = target.delete(key);
61
62
  const newSize = target.size;
62
- if (result) {
63
+ if (result) { // only emit and trigger if delete was successful
63
64
  const pathKey = path.join('.');
64
65
  let cachedPath = getPathConcat(pathKey);
65
66
  if (cachedPath === undefined) {
@@ -94,7 +95,6 @@ export function wrapMap(map, emit, path) {
94
95
  key: null
95
96
  };
96
97
  emit(event);
97
- // Clear triggers both iterator and size
98
98
  trigger(target, Symbol.iterator);
99
99
  if (oldSize !== newSize) {
100
100
  trigger(target, 'size');
@@ -102,18 +102,15 @@ export function wrapMap(map, emit, path) {
102
102
  };
103
103
  }
104
104
  if (prop === 'get') {
105
- // Return function that tracks the specific key upon execution
105
+ // return a function that tracks the specific key only when called
106
106
  return function (key) {
107
- // Track access to this specific key when 'get' is called
108
107
  track(target, String(key));
109
108
  const value = target.get(key);
110
109
  if (!value || typeof value !== 'object')
111
- return value; // Fast path for non-objects
112
- // Check wrapper cache for the value
110
+ return value;
113
111
  const cachedValueProxy = wrapperCache.get(value);
114
112
  if (cachedValueProxy)
115
113
  return cachedValueProxy;
116
- // Calculate path for the value
117
114
  const keyString = String(key);
118
115
  const pathKey = path.length > 0 ? `${path.join('.')}.${keyString}` : keyString;
119
116
  let newPath = getPathConcat(pathKey);
@@ -121,7 +118,7 @@ export function wrapMap(map, emit, path) {
121
118
  newPath = path.concat(keyString);
122
119
  setPathConcat(pathKey, newPath);
123
120
  }
124
- // Wrap based on type (no longer passing seen)
121
+ // recursively wrap nested structures
125
122
  if (value instanceof Map)
126
123
  return wrapMap(value, emit, newPath);
127
124
  if (value instanceof Set)
@@ -129,60 +126,58 @@ export function wrapMap(map, emit, path) {
129
126
  if (Array.isArray(value))
130
127
  return wrapArray(value, emit, newPath);
131
128
  if (value instanceof Date)
132
- return new Date(value.getTime()); // Dates are not proxied
133
- // Default to reactive for plain objects
129
+ return new Date(value.getTime()); // dates are not proxied, return a copy
134
130
  return reactive(value, emit, newPath);
135
131
  };
136
132
  }
137
133
  if (prop === 'has') {
138
- // Track dependency on iteration/structure when 'has' is accessed
139
134
  track(target, Symbol.iterator);
140
135
  return function (key) {
141
- // Track specific key when 'has' is called
136
+ // track the specific key only when 'has' is called
142
137
  track(target, String(key));
143
138
  return target.has(key);
144
- }.bind(target); // Bind to original target for correct 'this'
139
+ }.bind(target);
145
140
  }
146
- // --- Iteration Methods ---
141
+ // handle iteration methods
147
142
  if (prop === Symbol.iterator || prop === 'entries' || prop === 'values' || prop === 'keys' || prop === 'forEach') {
148
- // Track dependency on iteration when these methods are accessed
149
143
  track(target, Symbol.iterator);
150
144
  const originalMethod = Reflect.get(target, prop, receiver);
151
- // Return custom iterators/forEach that wrap values
145
+ // return custom iterators/foreach that wrap values during iteration
152
146
  if (prop === 'forEach') {
153
147
  return (callbackfn, thisArg) => {
154
- // Use proxied .entries() to get wrapped values + tracking
148
+ // use the proxied .entries() to ensure values passed to callback are wrapped and tracked
155
149
  const entriesIterator = proxy.entries();
156
150
  for (const [key, value] of entriesIterator) {
157
151
  callbackfn.call(thisArg, value, key, proxy);
158
152
  }
159
153
  };
160
154
  }
161
- // Handle Symbol.iterator, entries, values, keys
155
+ // handle symbol.iterator, entries, values, keys by creating generator functions
162
156
  return function* (...args) {
163
157
  const iterator = originalMethod.apply(target, args);
164
158
  for (const entry of iterator) {
165
- let keyToWrap = entry; // Default for keys()
166
- let valueToWrap = entry; // Default for values()
159
+ let keyToWrap = entry;
160
+ let valueToWrap = entry;
167
161
  let isEntry = false;
168
162
  if (prop === 'entries' || prop === Symbol.iterator) {
169
163
  keyToWrap = entry[0];
170
164
  valueToWrap = entry[1];
171
165
  isEntry = true;
172
166
  }
173
- // Wrap key if object
167
+ // wrap key if it's an object
168
+ // note: reactivity on map keys can be complex/unexpected
174
169
  let wrappedKey = keyToWrap;
175
170
  if (isEntry && keyToWrap && typeof keyToWrap === 'object') {
176
- const pathKey = path.length > 0 ? `${path.join('.')}.${String(keyToWrap)}` : String(keyToWrap); // Or find better key path?
177
- let keyPath = getPathConcat(pathKey); // Reuse paths where possible
171
+ const pathKey = path.length > 0 ? `${path.join('.')}.${String(keyToWrap)}` : String(keyToWrap);
172
+ let keyPath = getPathConcat(pathKey);
178
173
  if (keyPath === undefined) {
179
- keyPath = path.concat(String(keyToWrap)); // Simplification for key path
174
+ keyPath = path.concat(String(keyToWrap));
180
175
  setPathConcat(pathKey, keyPath);
181
176
  }
182
- // TODO: Decide if Map keys should be deeply reactive
177
+ // todo: decide if map keys should be deeply reactive
183
178
  wrappedKey = reactive(keyToWrap, emit, keyPath);
184
179
  }
185
- // Wrap value if object
180
+ // wrap value if it's an object
186
181
  let wrappedValue = valueToWrap;
187
182
  if (valueToWrap && typeof valueToWrap === 'object') {
188
183
  const cachedValueProxy = wrapperCache.get(valueToWrap);
@@ -190,7 +185,7 @@ export function wrapMap(map, emit, path) {
190
185
  wrappedValue = cachedValueProxy;
191
186
  }
192
187
  else {
193
- const keyString = String(keyToWrap);
188
+ const keyString = String(keyToWrap); // use original key for path
194
189
  const pathKey = path.length > 0 ? `${path.join('.')}.${keyString}` : keyString;
195
190
  let newPath = getPathConcat(pathKey);
196
191
  if (newPath === undefined) {
@@ -209,7 +204,6 @@ export function wrapMap(map, emit, path) {
209
204
  wrappedValue = reactive(valueToWrap, emit, newPath);
210
205
  }
211
206
  }
212
- // Yield based on iterator type
213
207
  if (prop === 'entries' || prop === Symbol.iterator) {
214
208
  yield [wrappedKey, wrappedValue];
215
209
  }
@@ -222,20 +216,15 @@ export function wrapMap(map, emit, path) {
222
216
  }
223
217
  };
224
218
  }
225
- // --- Size Property ---
226
219
  if (prop === 'size') {
227
- // Explicitly track size access
228
220
  track(target, 'size');
229
- // Return the size directly from the target to avoid potential 'this' issues with Reflect.get
230
221
  return target.size;
231
222
  }
232
- // --- Fallback for other properties/methods ---
233
223
  const value = Reflect.get(target, prop, receiver);
234
- // For non-function properties or bound functions, return the value as is.
235
224
  return value;
236
225
  }
237
226
  });
238
- // Cache the newly created proxy before returning
227
+ // cache the newly created proxy before returning
239
228
  wrapperCache.set(map, proxy);
240
229
  return proxy;
241
230
  }
package/dist/wrapSet.js CHANGED
@@ -4,18 +4,18 @@ import { wrapArray } from './wrapArray';
4
4
  import { wrapMap } from './wrapMap';
5
5
  import { track, trigger } from './watchEffect';
6
6
  export function wrapSet(set, emit, path) {
7
- // Check wrapper cache first
7
+ // reuse existing proxy if available for performance
8
8
  const cachedProxy = wrapperCache.get(set);
9
9
  if (cachedProxy)
10
10
  return cachedProxy;
11
11
  const proxy = new Proxy(set, {
12
12
  get(target, prop, receiver) {
13
- // Original track call
14
13
  track(target, prop);
15
14
  if (prop === 'add') {
16
15
  return function (value) {
17
16
  const existed = target.has(value);
18
17
  const oldSize = target.size;
18
+ // only add and trigger if the value doesn't already exist
19
19
  if (!existed) {
20
20
  target.add(value);
21
21
  const newSize = target.size;
@@ -30,7 +30,7 @@ export function wrapSet(set, emit, path) {
30
30
  trigger(target, 'size');
31
31
  }
32
32
  }
33
- return receiver;
33
+ return receiver; // return the proxy itself for chaining
34
34
  };
35
35
  }
36
36
  if (prop === 'delete') {
@@ -38,15 +38,15 @@ export function wrapSet(set, emit, path) {
38
38
  const existed = target.has(value);
39
39
  const oldSize = target.size;
40
40
  if (existed) {
41
- const oldValue = value; // The value being deleted is the oldValue
41
+ const oldValue = value;
42
42
  const result = target.delete(value);
43
43
  const newSize = target.size;
44
- if (result) {
44
+ if (result) { // only emit and trigger if delete was successful
45
45
  const event = {
46
46
  action: 'set-delete',
47
47
  path: path,
48
- value: value, // value being deleted
49
- oldValue: oldValue // Add oldValue to the event
48
+ value: value,
49
+ oldValue: oldValue
50
50
  };
51
51
  emit(event);
52
52
  trigger(target, Symbol.iterator);
@@ -67,12 +67,11 @@ export function wrapSet(set, emit, path) {
67
67
  target.clear();
68
68
  const newSize = target.size;
69
69
  const event = {
70
- action: 'set-clear', // Create 'set-clear' action
70
+ action: 'set-clear',
71
71
  path: path,
72
72
  value: null
73
73
  };
74
74
  emit(event);
75
- // Clear triggers both iterator and size
76
75
  trigger(target, Symbol.iterator);
77
76
  if (oldSize !== newSize) {
78
77
  trigger(target, 'size');
@@ -80,64 +79,58 @@ export function wrapSet(set, emit, path) {
80
79
  };
81
80
  }
82
81
  if (prop === 'has') {
83
- // 'has' depends on the specific value and iteration/size
84
- track(target, Symbol.iterator); // Track iteration implicitly
82
+ track(target, Symbol.iterator);
85
83
  return function (value) {
86
- // Also track the specific value when 'has' is called
84
+ // track specific primitive value when 'has' is called
85
+ // tracking object values for existence is complex and less common, handled by iteration track
87
86
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'symbol') {
88
87
  track(target, String(value));
89
88
  }
90
- // How to track non-primitive values?
91
- // We might need a different strategy for object keys in Sets/Maps if deep reactivity on keys is needed
92
89
  return target.has(value);
93
- }.bind(target); // Bind to original target
90
+ }.bind(target);
94
91
  }
95
- // Handle iteration methods - custom implementation already tracks
92
+ // handle iteration methods
96
93
  if (prop === 'values' || prop === Symbol.iterator || prop === 'entries' || prop === 'keys' || prop === 'forEach') {
97
- // Track dependency on iteration
98
94
  track(target, Symbol.iterator);
99
95
  const originalMethod = Reflect.get(target, prop, receiver);
100
- // Return a function that yields wrapped values (or calls forEach)
96
+ // return custom iterators/foreach that wrap values during iteration
101
97
  if (prop === 'forEach') {
102
98
  return (callbackfn, thisArg) => {
103
- // Simplified forEach based on values iterator
104
- const valuesIterator = proxy.values(); // Use the proxied values() to get wrapped values + tracking
99
+ // use the proxied values() to ensure values passed to callback are wrapped and tracked
100
+ const valuesIterator = proxy.values();
105
101
  for (const value of valuesIterator) {
106
102
  callbackfn.call(thisArg, value, value, proxy);
107
103
  }
108
104
  };
109
105
  }
110
- // Handle Symbol.iterator, values, keys, entries
106
+ // handle symbol.iterator, values, keys, entries by creating generator functions
111
107
  return function* (...args) {
112
- let index = 0; // Index for path generation
108
+ let index = 0; // use index for path generation if value is not primitive
113
109
  const iterator = originalMethod.apply(target, args);
114
110
  for (const entry of iterator) {
115
- let valueToWrap = entry; // Default for values(), keys()
116
- let mapKey = undefined; // For entries()
117
- // For entries(), the entry is [key, value]
111
+ let valueToWrap = entry;
112
+ let mapKey = undefined; // key for entries() which yields [value, value]
118
113
  if (prop === 'entries') {
119
- mapKey = entry[0];
114
+ mapKey = entry[0]; // for Set.entries(), key and value are the same
120
115
  valueToWrap = entry[1];
121
116
  }
122
- // Track access to each element during iteration
123
117
  track(target, String(index));
124
118
  let wrappedValue = valueToWrap;
125
119
  if (valueToWrap && typeof valueToWrap === 'object') {
126
- // Check wrapper cache first
127
120
  const cachedValueProxy = wrapperCache.get(valueToWrap);
128
121
  if (cachedValueProxy) {
129
122
  wrappedValue = cachedValueProxy;
130
123
  }
131
124
  else {
132
- // Calculate path using index (or mapKey if available and primitive)
133
- const keyForPath = (mapKey !== undefined && (typeof mapKey !== 'object') ? String(mapKey) : String(index));
125
+ // calculate path using index as key, as set values don't have inherent keys
126
+ const keyForPath = String(index);
134
127
  const pathKey = path.length > 0 ? `${path.join('.')}.${keyForPath}` : keyForPath;
135
128
  let newPath = getPathConcat(pathKey);
136
129
  if (newPath === undefined) {
137
130
  newPath = path.concat(keyForPath);
138
131
  setPathConcat(pathKey, newPath);
139
132
  }
140
- // Wrap based on type
133
+ // recursively wrap nested structures
141
134
  if (valueToWrap instanceof Map)
142
135
  wrappedValue = wrapMap(valueToWrap, emit, newPath);
143
136
  else if (valueToWrap instanceof Set)
@@ -145,15 +138,13 @@ export function wrapSet(set, emit, path) {
145
138
  else if (Array.isArray(valueToWrap))
146
139
  wrappedValue = wrapArray(valueToWrap, emit, newPath);
147
140
  else if (valueToWrap instanceof Date)
148
- wrappedValue = new Date(valueToWrap.getTime());
141
+ wrappedValue = new Date(valueToWrap.getTime()); // dates are not proxied, return copy
149
142
  else
150
143
  wrappedValue = reactive(valueToWrap, emit, newPath);
151
- // Note: We don't cache the *wrapped* value here, wrap functions handle caching
152
144
  }
153
145
  }
154
- // Yield the original or wrapped value/entry
155
146
  if (prop === 'entries') {
156
- yield [mapKey, wrappedValue];
147
+ yield [wrappedValue, wrappedValue]; // set entries yield [value, value]
157
148
  }
158
149
  else {
159
150
  yield wrappedValue;
@@ -163,22 +154,14 @@ export function wrapSet(set, emit, path) {
163
154
  };
164
155
  }
165
156
  if (prop === 'size') {
166
- // Explicitly track size access
167
157
  track(target, 'size');
168
- // Return the size directly from the target to avoid potential 'this' issues with Reflect.get
169
158
  return target.size;
170
159
  }
171
- // Fallback for other properties (should be minimal for Set)
172
160
  const value = Reflect.get(target, prop, receiver);
173
- // REMOVED - Tracking handled above
174
- // if (prop === 'size') { ... }
175
- // If the property is a function (and not handled above), return it directly.
176
- // Reflect.get preserves the correct 'this' binding (receiver).
177
- // REMOVED: return value.bind(target);
178
161
  return value;
179
162
  }
180
163
  });
181
- // Cache the newly created proxy before returning
164
+ // cache the newly created proxy before returning
182
165
  wrapperCache.set(set, proxy);
183
166
  return proxy;
184
167
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yiin/reactive-proxy-state",
3
3
  "private": false,
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "description": "A simple, standalone reactivity library using Proxies",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -20,7 +20,10 @@
20
20
  "build": "tsc",
21
21
  "test": "bun test",
22
22
  "test:watch": "bun test --watch",
23
- "test:bench": "bun --bun ./benchmarks/state-sync.bench.ts"
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"
24
27
  },
25
28
  "keywords": [
26
29
  "state",
@@ -47,6 +50,8 @@
47
50
  },
48
51
  "devDependencies": {
49
52
  "bun-types": "^1.2.8",
50
- "typescript": "^5.0.4"
53
+ "typescript": "^5.0.4",
54
+ "vitepress": "^1.6.3",
55
+ "vue": "^3.5.13"
51
56
  }
52
57
  }