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.
- package/CHANGELOG.md +98 -0
- package/README.md +47 -0
- package/package.json +1 -1
- package/src/mobxVueBridge.js +169 -443
- package/src/utils/deepProxy.js +109 -0
- package/src/utils/equality.js +61 -0
- package/src/utils/helpers.js +395 -0
- package/src/utils/memberDetection.js +195 -0
package/src/mobxVueBridge.js
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { reactive, onUnmounted, ref } from 'vue';
|
|
2
|
-
import { toJS, reaction, observe
|
|
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
|
-
//
|
|
53
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
298
|
-
propertyRefs[
|
|
85
|
+
const bridgeObservableProperty = (propertyName) => {
|
|
86
|
+
propertyRefs[propertyName] = createReactiveRef(toJS(mobxObject[propertyName]));
|
|
299
87
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
//
|
|
338
|
-
const getterSetterPairs = members.getters
|
|
339
|
-
const gettersOnly = members.getters
|
|
340
|
-
const settersOnly = members.setters
|
|
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
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
355
|
-
get: () => getterRefs[
|
|
356
|
-
set:
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
391
|
-
get: () => getterRefs[
|
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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[
|
|
432
|
-
|
|
182
|
+
const boundMethod = mobxObject[methodName].bind(mobxObject);
|
|
183
|
+
|
|
184
|
+
defineReactiveProperty(vueState, methodName, {
|
|
433
185
|
get: () => boundMethod,
|
|
434
|
-
set: () => warnMethodAssignment(
|
|
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
|
-
|
|
196
|
+
setupPropertyObservation();
|
|
197
|
+
setupGetterObservation();
|
|
444
198
|
|
|
445
|
-
//
|
|
446
|
-
function
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
//
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
541
|
-
return useMobxBridge(presenter, options);
|
|
542
|
-
}
|
|
268
|
+
export const usePresenterState = useMobxBridge;
|