@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/README.md CHANGED
@@ -91,60 +91,99 @@ console.log(unref(123)); // 123 (returns non-refs as is)
91
91
  ```
92
92
 
93
93
  ### `computed<T>(getter: () => T): ComputedRef<T>`
94
+ ### `computed<T>(options: { get: () => T, set: (value: T) => void }): WritableComputedRef<T>`
94
95
 
95
- Creates a computed property based on a getter function. The getter tracks reactive dependencies (`ref`s or reactive object properties) and its result is cached. The computed value only recalculates when a dependency changes.
96
- Computed refs are read-only.
96
+ Creates a computed property based on a getter function or a getter/setter pair.
97
+
98
+ - **Getter-only:** The getter tracks reactive dependencies (`ref`s or reactive object properties) and its result is cached. The computed value only recalculates when a dependency changes. Computed refs created this way are **read-only**.
99
+ - **Getter/Setter:** Provides both a getter for deriving the value and a setter for mutating underlying reactive state when the computed ref's `.value` is assigned.
97
100
 
98
101
  ```typescript
99
102
  import { ref, computed, watchEffect, isComputed } from '@yiin/reactive-proxy-state';
100
103
 
104
+ // Read-only computed
101
105
  const firstName = ref('John');
102
106
  const lastName = ref('Doe');
103
107
 
104
- const fullName = computed(() => {
105
- console.log('Computing fullName...');
108
+ const readOnlyFullName = computed(() => {
109
+ console.log('Computing readOnlyFullName...');
106
110
  return `${firstName.value} ${lastName.value}`;
107
111
  });
108
112
 
109
113
  // Accessing .value triggers computation
110
- console.log(fullName.value);
111
- // Output: Computing fullName...
114
+ console.log(readOnlyFullName.value);
115
+ // Output: Computing readOnlyFullName...
112
116
  // Output: John Doe
113
117
 
114
118
  // Accessing again uses the cache
115
- console.log(fullName.value);
119
+ console.log(readOnlyFullName.value);
116
120
  // Output: John Doe
117
121
 
118
122
  watchEffect(() => {
119
- console.log('Full name changed:', fullName.value);
123
+ console.log('Read-only full name changed:', readOnlyFullName.value);
120
124
  });
121
- // Output: Full name changed: John Doe
125
+ // Output: Read-only full name changed: John Doe
122
126
 
123
127
  // Changing a dependency marks computed as dirty
124
128
  firstName.value = 'Jane';
125
129
 
126
130
  // Accessing .value again triggers re-computation and the effect
127
- console.log(fullName.value);
128
- // Output: Computing fullName...
129
- // Output: Full name changed: Jane Doe
131
+ console.log(readOnlyFullName.value);
132
+ // Output: Computing readOnlyFullName...
133
+ // Output: Read-only full name changed: Jane Doe
130
134
  // Output: Jane Doe
131
135
 
132
136
  // Chained computed
133
- const message = computed(() => `User: ${fullName.value}`);
137
+ const message = computed(() => `User: ${readOnlyFullName.value}`);
134
138
  console.log(message.value); // User: Jane Doe
135
139
 
136
140
  lastName.value = 'Smith';
137
- // Output: Computing fullName...
138
- // Output: Full name changed: Jane Smith
141
+ // Output: Computing readOnlyFullName...
142
+ // Output: Read-only full name changed: Jane Smith
139
143
  console.log(message.value); // User: Jane Smith (message recomputed automatically)
140
144
 
141
145
  // Read-only check
146
+ console.warn = () => console.log('Warning triggered!'); // Mock console.warn
142
147
  try {
143
- (fullName as any).value = 'Test'; // Throws warning
148
+ (readOnlyFullName as any).value = 'Test'; // Triggers warning
144
149
  } catch (e) { /* ... */ }
150
+ // Output: Warning triggered!
151
+ console.log(readOnlyFullName.value); // Jane Smith (value unchanged)
152
+
153
+ // Writable computed
154
+ const source = ref(1);
155
+ const plusOne = computed({
156
+ get: () => source.value + 1,
157
+ set: (newValue) => {
158
+ console.log(`Setting source based on new value: ${newValue}`);
159
+ source.value = newValue - 1;
160
+ }
161
+ });
162
+
163
+ console.log(plusOne.value); // 2 (Initial get)
164
+ console.log(source.value); // 1
165
+
166
+ watchEffect(() => {
167
+ console.log('Writable computed changed:', plusOne.value);
168
+ });
169
+ // Output: Writable computed changed: 2
170
+
171
+ // Set the writable computed value
172
+ plusOne.value = 10;
173
+ // Output: Setting source based on new value: 10
174
+ // Output: Writable computed changed: 10
175
+
176
+ console.log(plusOne.value); // 10
177
+ console.log(source.value); // 9 (Source was updated by the setter)
178
+
179
+ // Changing the source ref also updates the computed
180
+ source.value = 20;
181
+ // Output: Writable computed changed: 21
182
+ console.log(plusOne.value); // 21
145
183
 
146
184
  // Helper
147
- console.log(isComputed(fullName)); // true
185
+ console.log(isComputed(readOnlyFullName)); // true
186
+ console.log(isComputed(plusOne)); // true
148
187
  console.log(isComputed(firstName)); // false
149
188
  ```
150
189
 
@@ -6,11 +6,16 @@ export interface ComputedRef<T = any> extends Omit<Ref<T>, 'value'> {
6
6
  readonly [isRefSymbol]: true;
7
7
  }
8
8
  export interface WritableComputedRef<T> extends Ref<T> {
9
+ readonly [isComputedSymbol]: true;
10
+ readonly [isRefSymbol]: true;
9
11
  }
10
12
  type ComputedGetter<T> = () => T;
13
+ type ComputedSetter<T> = (v: T) => void;
14
+ interface WritableComputedOptions<T> {
15
+ get: ComputedGetter<T>;
16
+ set: ComputedSetter<T>;
17
+ }
11
18
  export declare function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>;
12
- /**
13
- * Checks if a value is a computed ref.
14
- */
15
- export declare function isComputed<T>(c: any): c is ComputedRef<T>;
19
+ export declare function computed<T>(options: WritableComputedOptions<T>): WritableComputedRef<T>;
20
+ export declare function isComputed<T>(c: any): c is ComputedRef<T> | WritableComputedRef<T>;
16
21
  export {};
package/dist/computed.js CHANGED
@@ -1,59 +1,58 @@
1
1
  import { watchEffect, track, trigger } from './watchEffect';
2
2
  import { isRefSymbol } from './ref';
3
- // Symbol for marking computed refs
3
+ // symbol for identifying computed refs
4
4
  const isComputedSymbol = Symbol('isComputed');
5
- // Implementation v4 - Using watchEffect with scheduler
6
- export function computed(getter) {
5
+ // implementation using a lazy watchEffect with a custom scheduler for caching
6
+ export function computed(getterOrOptions) {
7
+ let getter;
8
+ let setter;
9
+ const isGetter = typeof getterOrOptions === 'function';
10
+ if (isGetter) {
11
+ getter = getterOrOptions;
12
+ }
13
+ else {
14
+ getter = getterOrOptions.get;
15
+ setter = getterOrOptions.set;
16
+ }
7
17
  let _value;
8
- let _dirty = true; // Start dirty
9
- let computedRef; // Placeholder
10
- // Create a lazy effect with a scheduler
18
+ let _dirty = true; // flag to track if the cached value is stale
19
+ let computedRef; // placeholder to allow self-reference in scheduler
20
+ // create a lazy effect; scheduler intercepts triggers to mark dirty instead of recomputing immediately
11
21
  const stopHandle = watchEffect(getter, {
12
- lazy: true, // Don't run the getter immediately
22
+ lazy: true,
13
23
  scheduler: () => {
14
- // When a dependency changes, don't re-run the getter immediately.
15
- // Instead, mark the computed as dirty and trigger downstream effects.
16
24
  if (!_dirty) {
17
25
  _dirty = true;
18
- // Trigger effects that depend on the computed ref's value
26
+ // trigger effects that depend on this computed ref
19
27
  trigger(computedRef, 'value');
20
28
  }
21
29
  },
22
30
  });
23
- // Access the internal effect runner
24
31
  const effectRunner = stopHandle.effect;
25
32
  computedRef = {
26
33
  [isRefSymbol]: true,
27
34
  [isComputedSymbol]: true,
28
35
  get value() {
29
- // 1. Track access to this computed value for any outer effects
30
36
  track(computedRef, 'value');
31
- // 2. If dirty, run the effect manually. This will:
32
- // - Execute the getter
33
- // - Update _value (via getter's return)
34
- // - Track dependencies for the getter (handled by watchEffect internals)
35
- // - Set _dirty to false
37
+ // if dirty, recompute value by running the getter
36
38
  if (_dirty) {
37
- // console.log('Recomputing computed value via getter access');
38
- _value = effectRunner.run(); // Run the getter, update value, track deps
39
- _dirty = false; // Mark as clean *after* successful run
39
+ _value = effectRunner.run();
40
+ _dirty = false; // mark as clean after successful run
40
41
  }
41
- // 3. Return the (now current) value.
42
42
  return _value;
43
43
  },
44
44
  set value(newValue) {
45
- console.warn('Computed value is read-only');
45
+ if (setter) {
46
+ setter(newValue);
47
+ }
48
+ else {
49
+ console.warn('computed value is read-only');
50
+ }
46
51
  },
47
- // Expose the stop function if needed
48
- // stop: stopHandle
52
+ // stop: stopHandle // potentially expose stop handle
49
53
  };
50
- // Initial computation is lazy, happens on first .value access.
51
- // The scheduler ensures dependency changes only mark it dirty.
52
54
  return computedRef;
53
55
  }
54
- /**
55
- * Checks if a value is a computed ref.
56
- */
57
56
  export function isComputed(c) {
58
57
  return !!(c && c[isComputedSymbol]);
59
58
  }
package/dist/reactive.js CHANGED
@@ -3,18 +3,22 @@ import { wrapArray } from './wrapArray';
3
3
  import { wrapMap } from './wrapMap';
4
4
  import { wrapSet } from './wrapSet';
5
5
  import { track, trigger } from './watchEffect';
6
- // Pre-allocate type check function
6
+ // avoid repeated typeof checks
7
7
  function isObject(v) {
8
8
  return v && typeof v === 'object';
9
9
  }
10
+ // create a reactive proxy for an object
10
11
  export function reactive(obj, emit, path = [], seen = globalSeen) {
12
+ // prevent infinite recursion with circular references
11
13
  if (seen.has(obj))
12
14
  return seen.get(obj);
15
+ // helper to wrap nested values recursively
13
16
  function wrapValue(val, subPath) {
14
17
  if (!isObject(val))
15
- return val;
18
+ return val; // primitives are returned directly
16
19
  if (seen.has(val))
17
- return seen.get(val);
20
+ return seen.get(val); // handle cycles within nested structures
21
+ // delegate wrapping to specific functions based on type
18
22
  if (Array.isArray(val))
19
23
  return wrapArray(val, emit, subPath);
20
24
  if (val instanceof Map)
@@ -22,43 +26,48 @@ export function reactive(obj, emit, path = [], seen = globalSeen) {
22
26
  if (val instanceof Set)
23
27
  return wrapSet(val, emit, subPath);
24
28
  if (val instanceof Date)
25
- return new Date(val.getTime());
29
+ return new Date(val.getTime()); // dates are not proxied, return copy
30
+ // default to reactive for plain objects
26
31
  return reactive(val, emit, subPath, seen);
27
32
  }
28
33
  const proxy = new Proxy(obj, {
29
34
  get(target, prop, receiver) {
30
35
  const value = Reflect.get(target, prop, receiver);
31
- // Track this property access for reactivity
36
+ // track property access for dependency tracking
32
37
  track(target, prop);
33
- // Fast path for non-objects
38
+ // return non-objects directly without wrapping
34
39
  if (!isObject(value))
35
40
  return value;
36
- // Use cached path concatenation
37
- const pathKey = `${path.join('.')}.${String(prop)}`;
41
+ // calculate the path for the nested property, using cache for performance
42
+ const propKey = String(prop);
43
+ const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
38
44
  let newPath = getPathConcat(pathKey);
39
45
  if (newPath === undefined) {
40
- newPath = path.concat(String(prop));
46
+ newPath = path.concat(propKey);
41
47
  setPathConcat(pathKey, newPath);
42
48
  }
49
+ // wrap the nested value if it's an object/collection
43
50
  return wrapValue(value, newPath);
44
51
  },
45
52
  set(target, prop, value, receiver) {
46
53
  const oldValue = target[prop];
47
- // Fast path for primitive equality
54
+ // avoid unnecessary triggers if the value hasn't changed
55
+ // fast path for primitives
48
56
  if (oldValue === value)
49
57
  return true;
50
- // Only do deep equality check for objects
51
- if (isObject(oldValue) && isObject(value) && deepEqual(oldValue, value))
52
- return true;
58
+ // deep equality check for objects/arrays
59
+ if (isObject(oldValue) && isObject(value) && deepEqual(oldValue, value, new WeakMap()))
60
+ return true; // use new WeakMap for deepEqual seen
53
61
  const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
54
62
  const result = Reflect.set(target, prop, value, receiver);
55
- // Only emit if the set was successful and it's not a setter property
63
+ // only emit and trigger if the set was successful and wasn't intercepted by a setter
56
64
  if (result && (!descriptor || !descriptor.set)) {
57
- // Use cached path concatenation
58
- const pathKey = `${path.join('.')}.${String(prop)}`;
65
+ // calculate path, using cache
66
+ const propKey = String(prop);
67
+ const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
59
68
  let newPath = getPathConcat(pathKey);
60
69
  if (newPath === undefined) {
61
- newPath = path.concat(String(prop));
70
+ newPath = path.concat(propKey);
62
71
  setPathConcat(pathKey, newPath);
63
72
  }
64
73
  const event = {
@@ -68,32 +77,38 @@ export function reactive(obj, emit, path = [], seen = globalSeen) {
68
77
  newValue: value
69
78
  };
70
79
  emit(event);
71
- // Trigger effects
80
+ // notify effects watching this property
72
81
  trigger(target, prop);
73
82
  }
74
83
  return result;
75
84
  },
76
85
  deleteProperty(target, prop) {
77
86
  const oldValue = target[prop];
87
+ const hadProperty = Object.prototype.hasOwnProperty.call(target, prop);
78
88
  const result = Reflect.deleteProperty(target, prop);
79
- // Use cached path concatenation
80
- const pathKey = `${path.join('.')}.${String(prop)}`;
81
- let newPath = getPathConcat(pathKey);
82
- if (newPath === undefined) {
83
- newPath = path.concat(String(prop));
84
- setPathConcat(pathKey, newPath);
89
+ // only emit and trigger if the property existed and deletion was successful
90
+ if (hadProperty && result) {
91
+ // calculate path, using cache
92
+ const propKey = String(prop);
93
+ const pathKey = path.length > 0 ? `${path.join('.')}.${propKey}` : propKey;
94
+ let newPath = getPathConcat(pathKey);
95
+ if (newPath === undefined) {
96
+ newPath = path.concat(propKey);
97
+ setPathConcat(pathKey, newPath);
98
+ }
99
+ const event = {
100
+ action: 'delete',
101
+ path: newPath,
102
+ oldValue
103
+ };
104
+ emit(event);
105
+ // notify effects watching this property
106
+ trigger(target, prop);
85
107
  }
86
- const event = {
87
- action: 'delete',
88
- path: newPath,
89
- oldValue
90
- };
91
- emit(event);
92
- // Trigger effects
93
- trigger(target, prop);
94
108
  return result;
95
109
  }
96
110
  });
111
+ // cache the proxy to handle circular references and improve performance
97
112
  seen.set(obj, proxy);
98
113
  return proxy;
99
114
  }
package/dist/ref.d.ts CHANGED
@@ -4,19 +4,20 @@ export interface Ref<T = any> {
4
4
  readonly [isRefSymbol]: true;
5
5
  }
6
6
  /**
7
- * Takes an inner value and returns a reactive and mutable ref object,
8
- * which has a single property `.value` that points to the inner value.
9
- * The ref tracks access and mutations to its `.value` property.
10
- * If the initial value is an object, it is NOT automatically made reactive.
7
+ * creates a reactive reference object.
8
+ * the object has a single `.value` property.
9
+ * reactivity is tracked on access and mutation of the `.value` property.
10
+ * if an object is passed as the initial value, the object itself is *not* made deeply reactive.
11
+ * only the assignment to `.value` is tracked.
11
12
  */
12
13
  export declare function ref<T>(value: T): Ref<T>;
13
14
  export declare function ref<T = undefined>(): Ref<T | undefined>;
14
15
  /**
15
- * Checks if a value is a ref object.
16
+ * checks if a value is a ref object.
16
17
  */
17
18
  export declare function isRef<T>(r: any): r is Ref<T>;
18
19
  /**
19
- * Returns the inner value if the argument is a ref, otherwise returns the
20
- * argument itself.
20
+ * returns the inner value if the argument is a ref,
21
+ * otherwise returns the argument itself. this is a sugar for `isRef(val) ? val.value : val`.
21
22
  */
22
23
  export declare function unref<T>(refValue: T | Ref<T>): T;
package/dist/ref.js CHANGED
@@ -1,51 +1,51 @@
1
1
  import { track, trigger } from './watchEffect';
2
2
  // Removed reactive import as ref doesn't automatically make contained objects reactive
3
3
  // import { reactive } from './reactive';
4
- // Symbol for marking refs
5
- export const isRefSymbol = Symbol('isRef'); // Add export and Simplified symbol description
4
+ // symbol used to identify refs internally and via isRef()
5
+ export const isRefSymbol = Symbol('isRef');
6
6
  export function ref(value) {
7
7
  return createRef(value);
8
8
  }
9
- // Internal function to create refs (no longer shallow distinction needed here)
9
+ // internal factory for creating ref objects
10
10
  function createRef(rawValue) {
11
- // If the value is already a ref, return it directly
11
+ // avoid wrapping if the value is already a ref
12
12
  if (isRef(rawValue)) {
13
- // Cast rawValue back to Ref<T> after type guard
14
13
  return rawValue;
15
14
  }
16
- // The ref holds the raw value directly
17
- let value = rawValue;
18
- // Create the ref object with getter/setter for reactivity
15
+ // store the inner value
16
+ let _value = rawValue;
17
+ // create the ref object with a getter/setter on `.value`
19
18
  const r = {
20
- [isRefSymbol]: true, // Mark as ref
19
+ [isRefSymbol]: true, // mark as a ref using the symbol
21
20
  get value() {
22
- // Track dependency on the 'value' property of this ref object
23
- // Ensure 'r' is treated as the target object for tracking
21
+ // track dependency when `.value` is accessed
22
+ // `r` (the ref object itself) is the target for tracking
24
23
  track(r, 'value');
25
- return value;
24
+ return _value;
26
25
  },
27
26
  set value(newValue) {
28
- // Check if value actually changed (using simple comparison)
29
- // For objects, this means identity change, not deep mutation.
30
- if (value !== newValue) {
31
- value = newValue;
32
- // Trigger effects depending on the 'value' property of this ref object
33
- // Ensure 'r' is treated as the target object for triggering
27
+ // only update and trigger if the value has actually changed
28
+ // this uses strict equality (===), so for objects, it checks reference equality
29
+ if (_value !== newValue) {
30
+ _value = newValue;
31
+ // trigger effects when `.value` is assigned a new value
32
+ // `r` (the ref object itself) is the target for triggering
34
33
  trigger(r, 'value');
35
34
  }
36
35
  },
37
- }; // Explicit cast to ensure type correctness
36
+ }; // cast to ensure the object conforms to the Ref interface
38
37
  return r;
39
38
  }
40
39
  /**
41
- * Checks if a value is a ref object.
40
+ * checks if a value is a ref object.
42
41
  */
43
42
  export function isRef(r) {
43
+ // check for the presence of the internal symbol
44
44
  return !!(r && r[isRefSymbol]);
45
45
  }
46
46
  /**
47
- * Returns the inner value if the argument is a ref, otherwise returns the
48
- * argument itself.
47
+ * returns the inner value if the argument is a ref,
48
+ * otherwise returns the argument itself. this is a sugar for `isRef(val) ? val.value : val`.
49
49
  */
50
50
  export function unref(refValue) {
51
51
  return isRef(refValue) ? refValue.value : refValue;