mobx-vue-bridge 1.3.0 β†’ 1.5.0

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.
@@ -1,7 +1,27 @@
1
1
  import { reactive, onUnmounted, ref } from 'vue';
2
- import { toJS, reaction, observe, isComputedProp, isObservableProp } from 'mobx';
2
+ import { toJS, reaction, observe } from 'mobx';
3
3
  import { deepObserve } from 'mobx-utils';
4
4
  import clone from 'clone';
5
+ import { categorizeMobxMembers } from './utils/memberDetection.js';
6
+ import { isEqual } from './utils/equality.js';
7
+ import { createDeepProxy } from './utils/deepProxy.js';
8
+ import {
9
+ safelyReadInitialValue,
10
+ createReactiveRef,
11
+ separateGetterSetterPairs,
12
+ findGettersOnly,
13
+ findSettersOnly,
14
+ defineReactiveProperty,
15
+ createLazyValidatedSetter,
16
+ createReadOnlySetter,
17
+ createWriteOnlySetter,
18
+ createTwoWayBindingSetter,
19
+ isCurrentlyUpdating,
20
+ observeProperty,
21
+ deepObserveProperty,
22
+ observeGetter,
23
+ safelyDisposeSubscription,
24
+ } from './utils/helpers.js';
5
25
 
6
26
  /**
7
27
  * πŸŒ‰ MobX-Vue Bridge
@@ -49,494 +69,200 @@ export function useMobxBridge(mobxObject, options = {}) {
49
69
 
50
70
  const vueState = reactive({});
51
71
 
52
- // Discover props/methods via MobX introspection (don’t rely on raw descriptors)
53
- const props = Object.getOwnPropertyNames(mobxObject)
54
- .concat(Object.getOwnPropertyNames(Object.getPrototypeOf(mobxObject)))
55
- .filter(p => p !== 'constructor' && !p.startsWith('_'));
72
+ // Use the imported categorization function
73
+ const members = categorizeMobxMembers(mobxObject);
56
74
 
57
- const members = {
58
- getters: props.filter(p => {
59
- try {
60
- // First try to check if it's a computed property via MobX introspection
61
- try {
62
- return isComputedProp(mobxObject, p);
63
- } catch (computedError) {
64
- // If isComputedProp fails (e.g., due to uninitialized nested objects),
65
- // fall back to checking if it has a getter descriptor
66
- const descriptor = Object.getOwnPropertyDescriptor(mobxObject, p) ||
67
- Object.getOwnPropertyDescriptor(Object.getPrototypeOf(mobxObject), p);
68
-
69
- // If it has a getter but no corresponding property, it's likely a computed getter
70
- return descriptor && typeof descriptor.get === 'function' &&
71
- !isObservableProp(mobxObject, p);
72
- }
73
- } catch (error) {
74
- return false;
75
- }
76
- }),
77
- setters: props.filter(p => {
78
- try {
79
- // Check if it has a setter descriptor
80
- const descriptor = Object.getOwnPropertyDescriptor(mobxObject, p) ||
81
- Object.getOwnPropertyDescriptor(Object.getPrototypeOf(mobxObject), p);
82
-
83
- // Must have a setter
84
- if (!descriptor || typeof descriptor.set !== 'function') return false;
85
-
86
- // Exclude methods
87
- if (typeof mobxObject[p] === 'function') return false;
88
-
89
- // For MobX objects with makeAutoObservable, we need to distinguish:
90
- // 1. Regular observable properties (handled separately)
91
- // 2. Computed properties with setters (getter/setter pairs)
92
- // 3. Setter-only properties
93
-
94
- // Include if it's a computed property with a WORKING setter (getter/setter pair)
95
- try {
96
- if (isComputedProp(mobxObject, p)) {
97
- // For computed properties, test if the setter actually works
98
- try {
99
- const originalValue = mobxObject[p];
100
- descriptor.set.call(mobxObject, originalValue); // Try to set to same value
101
- return true; // Setter works, it's a getter/setter pair
102
- } catch (setterError) {
103
- return false; // Setter throws error, it's a computed-only property
104
- }
105
- }
106
- } catch (error) {
107
- // If isComputedProp fails, check if it has a getter and test the setter
108
- if (descriptor.get) {
109
- try {
110
- // Try to get the current value and set it back
111
- const currentValue = mobxObject[p];
112
- descriptor.set.call(mobxObject, currentValue);
113
- return true; // Setter works
114
- } catch (setterError) {
115
- return false; // Setter throws error
116
- }
117
- }
118
- }
119
-
120
- // Include if it's NOT an observable property (setter-only or other cases)
121
- if (!isObservableProp(mobxObject, p)) return true;
122
-
123
- // Exclude regular observable properties (they're handled separately)
124
- return false;
125
- } catch (error) {
126
- return false;
127
- }
128
- }),
129
- properties: props.filter(p => {
130
- try {
131
- // Check if it's an observable property
132
- if (!isObservableProp(mobxObject, p)) return false;
133
-
134
- // Check if it's a function (method)
135
- if (typeof mobxObject[p] === 'function') return false;
136
-
137
- // Check if it's a computed property - if so, it's a getter, not a property
138
- const isComputed = isComputedProp(mobxObject, p);
139
- if (isComputed) return false;
140
-
141
- return true; // Regular observable property
142
- } catch (error) {
143
- return false;
144
- }
145
- }),
146
- methods: props.filter(p => {
147
- try {
148
- return typeof mobxObject[p] === 'function';
149
- } catch (error) {
150
- return false;
151
- }
152
- }),
153
- };
154
-
155
-
156
- // ---- utils: guards + equality --------------------------------------------
75
+ // ---- utils: guards -------------------------------------------------------
157
76
  const updatingFromMobx = new Set();
158
77
  const updatingFromVue = new Set();
159
78
 
160
- /**
161
- * Deep equality comparison with circular reference protection.
162
- * Uses WeakSet to track visited objects and prevent infinite recursion.
163
- *
164
- * @param {any} a - First value to compare
165
- * @param {any} b - Second value to compare
166
- * @param {WeakSet} visited - Set of visited objects to prevent circular references
167
- * @returns {boolean} True if values are deeply equal
168
- */
169
- const isEqual = (a, b, visited = new WeakSet()) => {
170
- if (Object.is(a, b)) return true;
171
-
172
- // Handle null/undefined cases
173
- if (a == null || b == null) return a === b;
174
-
175
- // Different types are not equal
176
- if (typeof a !== typeof b) return false;
177
-
178
- // For primitives, Object.is should have caught them
179
- if (typeof a !== 'object') return false;
180
-
181
- // Check for circular references
182
- if (visited.has(a)) return true;
183
- visited.add(a);
184
-
185
- // Fast array comparison
186
- if (Array.isArray(a) && Array.isArray(b)) {
187
- if (a.length !== b.length) return false;
188
- return a.every((val, i) => isEqual(val, b[i], visited));
189
- }
190
-
191
- // Fast object comparison - check keys first
192
- const aKeys = Object.keys(a);
193
- const bKeys = Object.keys(b);
194
- if (aKeys.length !== bKeys.length) return false;
195
-
196
- // Check if all keys match
197
- if (!aKeys.every(key => bKeys.includes(key))) return false;
198
-
199
- // Check values (recursive)
200
- return aKeys.every(key => isEqual(a[key], b[key], visited));
201
- };
202
-
203
79
  // Warning helpers to reduce duplication
204
- const warnDirectMutation = (prop) => console.warn(`Direct mutation of '${prop}' is disabled`);
205
- const warnSetterMutation = (prop) => console.warn(`Direct mutation of setter '${prop}' is disabled`);
206
80
  const warnMethodAssignment = (prop) => console.warn(`Cannot assign to method '${prop}'`);
207
81
 
208
- /**
209
- * Creates a deep proxy for nested objects/arrays to handle mutations at any level.
210
- * This enables mutations like state.items.push(item) to work correctly.
211
- * Respects the allowDirectMutation configuration for all nesting levels.
212
- *
213
- * Note: Mutations are batched via queueMicrotask to prevent corruption during
214
- * array operations like shift(), unshift(), splice() which modify multiple indices.
215
- * This ensures data correctness at the cost of a microtask delay.
216
- *
217
- * IMPORTANT: The proxy wraps the value stored in propertyRefs[prop].value, which is
218
- * a clone. When nested mutations occur, we update the clone in-place, then trigger
219
- * a sync back to MobX by re-assigning the entire cloned structure.
220
- *
221
- * @param {object|array} value - The nested value to wrap in a proxy
222
- * @param {string} prop - The parent property name for error messages and sync
223
- * @param {function} getRoot - Function that returns the current root value from propertyRef
224
- * @returns {Proxy} Proxied object/array with reactive mutation handling
225
- */
226
- const createDeepProxy = (value, prop, getRoot = null) => {
227
- // Don't proxy built-in objects that should remain unchanged
228
- if (value instanceof Date || value instanceof RegExp || value instanceof Map ||
229
- value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
230
- return value;
231
- }
232
-
233
- // If no getRoot provided, use the default which gets from propertyRefs
234
- if (!getRoot) {
235
- getRoot = () => propertyRefs[prop].value;
236
- }
237
-
238
- // Track pending updates to batch array mutations
239
- let updatePending = false;
240
-
241
- return new Proxy(value, {
242
- get: (target, key) => {
243
- const result = target[key];
244
- // If the result is an object/array, wrap it in a proxy too (but not built-ins)
245
- if (result && typeof result === 'object' &&
246
- !(result instanceof Date || result instanceof RegExp || result instanceof Map ||
247
- result instanceof Set || result instanceof WeakMap || result instanceof WeakSet)) {
248
- return createDeepProxy(result, prop, getRoot);
249
- }
250
- return result;
251
- },
252
- set: (target, key, val) => {
253
- // Check if direct mutation is allowed
254
- if (!allowDirectMutation) {
255
- warnDirectMutation(`${prop}.${String(key)}`);
256
- return true; // Must return true to avoid TypeError in strict mode
257
- }
258
-
259
- // Update the target in-place (this modifies the clone in propertyRefs[prop].value)
260
- target[key] = val;
261
-
262
- // Batch updates to avoid corrupting in-progress array operations
263
- // like shift(), unshift(), splice() which modify multiple indices synchronously
264
- if (!updatePending) {
265
- updatePending = true;
266
- queueMicrotask(() => {
267
- updatePending = false;
268
- // The root value has already been modified in-place above (target[key] = val)
269
- // Now we need to trigger Vue reactivity and sync to MobX
270
-
271
- // Clone the root to create a new reference for Vue reactivity
272
- // This ensures Vue detects the change
273
- const rootValue = getRoot();
274
- const cloned = clone(rootValue);
275
-
276
- // Update the Vue ref to trigger reactivity
277
- propertyRefs[prop].value = cloned;
278
-
279
- // Update MobX immediately with the cloned value
280
- updatingFromVue.add(prop);
281
- try {
282
- mobxObject[prop] = cloned;
283
- } finally {
284
- updatingFromVue.delete(prop);
285
- }
286
- });
287
- }
288
-
289
- return true;
290
- }
291
- });
292
- };
293
-
294
- // ---- properties (two-way) -------------------------------------------------
82
+ // ---- Bridge observable properties (two-way binding) ----
295
83
  const propertyRefs = {};
296
84
 
297
- members.properties.forEach(prop => {
298
- propertyRefs[prop] = ref(toJS(mobxObject[prop]));
85
+ const bridgeObservableProperty = (propertyName) => {
86
+ propertyRefs[propertyName] = createReactiveRef(toJS(mobxObject[propertyName]));
299
87
 
300
- Object.defineProperty(vueState, prop, {
301
- get: () => {
302
- const value = propertyRefs[prop].value;
303
- // For objects/arrays, return a deep proxy that syncs mutations back
304
- if (value && typeof value === 'object') {
305
- return createDeepProxy(value, prop);
306
- }
307
- return value;
308
- },
309
- set: allowDirectMutation
310
- ? (value) => {
311
- // Update Vue ref
312
- const cloned = clone(value);
313
- if (!isEqual(propertyRefs[prop].value, cloned)) {
314
- propertyRefs[prop].value = cloned;
315
- }
316
- // ALSO update MobX immediately (synchronous)
317
- if (!isEqual(mobxObject[prop], cloned)) {
318
- updatingFromVue.add(prop);
319
- try {
320
- mobxObject[prop] = cloned;
321
- } finally {
322
- updatingFromVue.delete(prop);
323
- }
324
- }
325
- }
326
- : () => warnDirectMutation(prop),
327
- enumerable: true,
328
- configurable: true,
88
+ const createDeepProxyForValue = (value) => {
89
+ if (value && typeof value === 'object') {
90
+ return createDeepProxy(
91
+ value,
92
+ propertyName,
93
+ () => propertyRefs[propertyName].value,
94
+ allowDirectMutation,
95
+ updatingFromVue,
96
+ mobxObject,
97
+ propertyRefs
98
+ );
99
+ }
100
+ return value;
101
+ };
102
+
103
+ defineReactiveProperty(vueState, propertyName, {
104
+ get: () => createDeepProxyForValue(propertyRefs[propertyName].value),
105
+ set: createTwoWayBindingSetter({
106
+ propertyName,
107
+ target: mobxObject,
108
+ allowDirectMutation,
109
+ guardSet: updatingFromVue,
110
+ propertyRef: propertyRefs[propertyName],
111
+ // No deepProxyCreator needed - createTwoWayBindingSetter already clones the value
112
+ }),
329
113
  });
114
+ };
330
115
 
331
- });
116
+ members.properties.forEach(bridgeObservableProperty);
332
117
 
333
118
  // ---- getters and setters (handle both computed and two-way binding) ------
334
119
  const getterRefs = {};
335
120
  const setterRefs = {};
121
+ const readOnlyDetected = new Set(); // Track properties detected as read-only on first write
336
122
 
337
- // First, handle properties that have BOTH getters and setters (getter/setter pairs)
338
- const getterSetterPairs = members.getters.filter(prop => members.setters.includes(prop));
339
- const gettersOnly = members.getters.filter(prop => !members.setters.includes(prop));
340
- const settersOnly = members.setters.filter(prop => !members.getters.includes(prop));
123
+ // Categorize properties by their getter/setter combinations
124
+ const getterSetterPairs = separateGetterSetterPairs(members.getters, members.setters);
125
+ const gettersOnly = findGettersOnly(members.getters, members.setters);
126
+ const settersOnly = findSettersOnly(members.setters, members.getters);
341
127
 
342
- // Getter/setter pairs: writable with reactive updates
343
- getterSetterPairs.forEach(prop => {
344
- // Get initial value from getter
345
- let initialValue;
346
- try {
347
- initialValue = toJS(mobxObject[prop]);
348
- } catch (error) {
349
- initialValue = undefined;
350
- }
351
- getterRefs[prop] = ref(initialValue);
352
- setterRefs[prop] = ref(initialValue);
128
+ // ---- Bridge getter/setter pairs (potentially writable computed properties) ----
129
+ // Note: Some may have MobX synthetic setters that throw - we detect this lazily on first write
130
+ const bridgeGetterSetterPair = (propertyName) => {
131
+ const initialValue = safelyReadInitialValue(mobxObject, propertyName);
132
+ getterRefs[propertyName] = createReactiveRef(initialValue);
353
133
 
354
- Object.defineProperty(vueState, prop, {
355
- get: () => getterRefs[prop].value,
356
- set: allowDirectMutation
357
- ? (value) => {
358
- // Update both refs
359
- setterRefs[prop].value = value;
360
- // Call the MobX setter immediately
361
- updatingFromVue.add(prop);
362
- try {
363
- mobxObject[prop] = value;
364
- // The getter ref will be updated by the reaction
365
- } catch (error) {
366
- console.warn(`Failed to set property '${prop}':`, error);
367
- } finally {
368
- updatingFromVue.delete(prop);
369
- }
370
- }
371
- : () => warnDirectMutation(prop),
372
- enumerable: true,
373
- configurable: true,
134
+ defineReactiveProperty(vueState, propertyName, {
135
+ get: () => getterRefs[propertyName].value,
136
+ set: createLazyValidatedSetter({
137
+ propertyName,
138
+ target: mobxObject,
139
+ allowDirectMutation,
140
+ readOnlySet: readOnlyDetected,
141
+ guardSet: updatingFromVue,
142
+ }),
374
143
  });
375
- });
144
+ };
145
+
146
+ getterSetterPairs.forEach(bridgeGetterSetterPair);
376
147
 
377
- // Getter-only properties: read-only computed
378
- gettersOnly.forEach(prop => {
379
- // Safely get initial value of computed property, handle errors gracefully
380
- let initialValue;
381
- try {
382
- initialValue = toJS(mobxObject[prop]);
383
- } catch (error) {
384
- // If computed property throws during initialization (e.g., accessing null.property),
385
- // set initial value to undefined and let the reaction handle updates later
386
- initialValue = undefined;
387
- }
388
- getterRefs[prop] = ref(initialValue);
148
+ // ---- Bridge getter-only properties (read-only computed) ----
149
+ const bridgeGetterOnly = (propertyName) => {
150
+ const initialValue = safelyReadInitialValue(mobxObject, propertyName);
151
+ getterRefs[propertyName] = createReactiveRef(initialValue);
389
152
 
390
- Object.defineProperty(vueState, prop, {
391
- get: () => getterRefs[prop].value,
392
- set: () => {
393
- throw new Error(`Cannot assign to computed property '${prop}'`)
394
- },
395
- enumerable: true,
396
- configurable: true,
153
+ defineReactiveProperty(vueState, propertyName, {
154
+ get: () => getterRefs[propertyName].value,
155
+ set: createReadOnlySetter(propertyName),
397
156
  });
398
- });
157
+ };
399
158
 
400
- // Setter-only properties: write-only
401
- settersOnly.forEach(prop => {
402
- // For setter-only properties, track the last set value
403
- setterRefs[prop] = ref(undefined);
159
+ gettersOnly.forEach(bridgeGetterOnly);
404
160
 
405
- Object.defineProperty(vueState, prop, {
406
- get: () => setterRefs[prop].value,
407
- set: allowDirectMutation
408
- ? (value) => {
409
- // Update the setter ref
410
- setterRefs[prop].value = value;
411
-
412
- // Call the MobX setter immediately
413
- updatingFromVue.add(prop);
414
- try {
415
- mobxObject[prop] = value;
416
- } catch (error) {
417
- console.warn(`Failed to set property '${prop}':`, error);
418
- } finally {
419
- updatingFromVue.delete(prop);
420
- }
421
- }
422
- : () => warnSetterMutation(prop),
423
- enumerable: true,
424
- configurable: true,
161
+ // ---- Bridge setter-only properties (write-only) ----
162
+ const bridgeSetterOnly = (propertyName) => {
163
+ setterRefs[propertyName] = createReactiveRef(undefined);
164
+
165
+ defineReactiveProperty(vueState, propertyName, {
166
+ get: () => setterRefs[propertyName].value,
167
+ set: createWriteOnlySetter({
168
+ propertyName,
169
+ target: mobxObject,
170
+ allowDirectMutation,
171
+ guardSet: updatingFromVue,
172
+ setterRef: setterRefs[propertyName],
173
+ }),
425
174
  });
426
- });
175
+ };
427
176
 
428
- // ---- methods (bound) ------------------------------------------------------
429
- members.methods.forEach(prop => {
177
+ settersOnly.forEach(bridgeSetterOnly);
178
+
179
+ // ---- Bridge methods (bound to MobX context) ----
180
+ const bridgeMethod = (methodName) => {
430
181
  // Cache the bound method to avoid creating new functions on every access
431
- const boundMethod = mobxObject[prop].bind(mobxObject);
432
- Object.defineProperty(vueState, prop, {
182
+ const boundMethod = mobxObject[methodName].bind(mobxObject);
183
+
184
+ defineReactiveProperty(vueState, methodName, {
433
185
  get: () => boundMethod,
434
- set: () => warnMethodAssignment(prop),
435
- enumerable: true,
436
- configurable: true,
186
+ set: () => warnMethodAssignment(methodName),
437
187
  });
438
- });
188
+ };
189
+
190
+ members.methods.forEach(bridgeMethod);
439
191
 
440
192
  // ---- MobX β†’ Vue: property observation ----------------------------------------
441
193
  const subscriptions = [];
194
+ const deepObserveSubscriptions = {}; // Track deep observe subs per property for re-subscription
442
195
 
443
- setupStandardPropertyObservers();
196
+ setupPropertyObservation();
197
+ setupGetterObservation();
444
198
 
445
- // Standard property observation implementation
446
- function setupStandardPropertyObservers() {
447
- // Use individual observe for each property to avoid circular reference issues
448
- members.properties.forEach(prop => {
449
- try {
450
- const sub = observe(mobxObject, prop, (change) => {
451
- if (!propertyRefs[prop]) return;
452
- if (updatingFromVue.has(prop)) return; // avoid echo
453
- updatingFromMobx.add(prop);
454
- try {
455
- const next = toJS(mobxObject[prop]);
456
- if (!isEqual(propertyRefs[prop].value, next)) {
457
- propertyRefs[prop].value = next;
458
- }
459
- } finally {
460
- updatingFromMobx.delete(prop);
461
- }
462
- });
463
- subscriptions.push(sub);
464
- } catch (error) {
465
- // Silently ignore non-observable properties
466
- }
467
- });
199
+ // Observe observable properties for MobX β†’ Vue sync
200
+ function setupPropertyObservation() {
201
+ members.properties.forEach(propertyName => {
202
+ // Helper to setup/re-setup deep observation for a value
203
+ const setupDeepObserve = (value) => {
204
+ // Dispose existing deep observe subscription if any
205
+ if (deepObserveSubscriptions[propertyName]) {
206
+ safelyDisposeSubscription(deepObserveSubscriptions[propertyName]);
207
+ deepObserveSubscriptions[propertyName] = null;
208
+ }
209
+
210
+ // Only deep observe objects and arrays
211
+ if (!value || typeof value !== 'object') {
212
+ return;
213
+ }
468
214
 
469
- // For nested objects and arrays, use deepObserve to handle deep changes
470
- // This handles both object properties and array mutations
471
- members.properties.forEach(prop => {
472
- const value = mobxObject[prop];
473
- if (value && typeof value === 'object') { // Include both objects AND arrays
474
- try {
475
- const sub = deepObserve(value, (change, path) => {
476
- if (!propertyRefs[prop]) return;
477
- if (updatingFromVue.has(prop)) return; // avoid echo
478
- updatingFromMobx.add(prop);
479
- try {
480
- const next = toJS(mobxObject[prop]);
481
- if (!isEqual(propertyRefs[prop].value, next)) {
482
- propertyRefs[prop].value = next;
483
- }
484
- } finally {
485
- updatingFromMobx.delete(prop);
486
- }
487
- });
488
- subscriptions.push(sub);
489
- } catch (error) {
490
- // Silently ignore if deepObserve fails (e.g., circular references in nested objects)
215
+ const deepObserveSub = deepObserveProperty({
216
+ target: mobxObject,
217
+ propertyName,
218
+ refToUpdate: propertyRefs[propertyName],
219
+ echoGuard: updatingFromVue,
220
+ updateGuard: updatingFromMobx,
221
+ });
222
+ if (deepObserveSub) {
223
+ deepObserveSubscriptions[propertyName] = deepObserveSub;
224
+ subscriptions.push(deepObserveSub);
491
225
  }
492
- }
226
+ };
227
+
228
+ // Observe direct property changes
229
+ const observeSub = observeProperty({
230
+ target: mobxObject,
231
+ propertyName,
232
+ refToUpdate: propertyRefs[propertyName],
233
+ echoGuard: updatingFromVue,
234
+ updateGuard: updatingFromMobx,
235
+ onValueChanged: setupDeepObserve, // Re-subscribe deepObserve when value changes
236
+ });
237
+ if (observeSub) subscriptions.push(observeSub);
238
+
239
+ // Initial deep observe setup
240
+ setupDeepObserve(mobxObject[propertyName]);
493
241
  });
494
242
  }
495
243
 
496
- // Getters: keep them in sync via reaction (both getter-only and getter/setter pairs)
497
- [...gettersOnly, ...getterSetterPairs].forEach(prop => {
498
- const sub = reaction(
499
- () => {
500
- try {
501
- return toJS(mobxObject[prop]);
502
- } catch (error) {
503
- // If computed property throws (e.g., accessing null.property), return undefined
504
- return undefined;
505
- }
506
- },
507
- (next) => {
508
- if (!getterRefs[prop]) return;
509
- if (!isEqual(getterRefs[prop].value, next)) {
510
- getterRefs[prop].value = next;
511
- }
512
- }
513
- );
514
- subscriptions.push(sub);
515
- });
244
+ // Observe computed properties (getters) for MobX β†’ Vue sync
245
+ function setupGetterObservation() {
246
+ [...gettersOnly, ...getterSetterPairs].forEach(propertyName => {
247
+ const reactionSub = observeGetter({
248
+ target: mobxObject,
249
+ propertyName,
250
+ refToUpdate: getterRefs[propertyName],
251
+ });
252
+ subscriptions.push(reactionSub);
253
+ });
254
+ }
516
255
 
517
- // Cleanup
256
+ // Cleanup subscriptions when component unmounts
518
257
  onUnmounted(() => {
519
- subscriptions.forEach(unsub => {
520
- try {
521
- if (typeof unsub === 'function') {
522
- unsub();
523
- }
524
- } catch {
525
- // Silently ignore cleanup errors
526
- }
527
- });
258
+ subscriptions.forEach(safelyDisposeSubscription);
528
259
  });
529
260
 
530
261
  return vueState;
531
262
  }
532
263
 
533
264
  /**
534
- * Helper alias for useMobxBridge - commonly used with presenter objects
535
- *
536
- * @param {object} presenter - The MobX presenter object to bridge
537
- * @param {object} options - Configuration options
538
- * @returns {object} Vue reactive state object
265
+ * Alias for useMobxBridge - for users who prefer "presenter" terminology
266
+ * @alias useMobxBridge
539
267
  */
540
- export function usePresenterState(presenter, options = {}) {
541
- return useMobxBridge(presenter, options);
542
- }
268
+ export const usePresenterState = useMobxBridge;