mobx-vue-bridge 1.0.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 ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2025-09-29
9
+
10
+ ### Added
11
+ - Initial release of mobx-vue-bridge
12
+ - Two-way data binding between MobX observables and Vue 3 reactivity
13
+ - Support for properties, getters, setters, and methods
14
+ - Deep object/array observation with proper reactivity
15
+ - Configurable mutation behavior via `allowDirectMutation` option
16
+ - TypeScript definitions for better developer experience
17
+ - Comprehensive test suite covering various scenarios
18
+ - Error handling for edge cases and circular references
19
+ - Performance optimizations with intelligent change detection
20
+
21
+ ### Features
22
+ - `useMobxBridge()` - Main bridge function
23
+ - `usePresenterState()` - Alias for presenter pattern usage
24
+ - Automatic property type detection (observable, computed, methods)
25
+ - Intelligent synchronization preventing infinite loops
26
+ - Support for nested objects and arrays
27
+ - Graceful handling of uninitialized computed properties
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Visar Uruqi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # 🌉 MobX-Vue Bridge
2
+
3
+ A seamless bridge between MobX observables and Vue 3's reactivity system, enabling effortless two-way data binding and state synchronization.
4
+
5
+ [![npm version](https://badge.fury.io/js/mobx-vue-bridge.svg)](https://www.npmjs.com/package/mobx-vue-bridge)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## ✨ Features
9
+
10
+ - 🔄 **Two-way data binding** between MobX observables and Vue reactive state
11
+ - 🎯 **Automatic property detection** (properties, getters, setters, methods)
12
+ - 🏗️ **Deep object/array observation** with proper reactivity
13
+ - ⚙️ **Configurable mutation behavior**
14
+ - 🔒 **Type-safe bridging** between reactive systems
15
+ - 🚀 **Optimized performance** with intelligent change detection
16
+ - 🛡️ **Error handling** for edge cases and circular references
17
+
18
+ ## 📦 Installation
19
+
20
+ ```bash
21
+ npm install mobx-vue-bridge
22
+ ```
23
+
24
+ **Peer Dependencies:**
25
+ - Vue 3.x
26
+ - MobX 6.x
27
+
28
+ ```bash
29
+ npm install vue mobx
30
+ ```
31
+
32
+ ## 🚀 Quick Start
33
+
34
+ ```javascript
35
+ import { useMobxBridge } from 'mobx-vue-bridge'
36
+ import { makeAutoObservable } from 'mobx'
37
+
38
+ // Your MobX store/presenter
39
+ class UserPresenter {
40
+ constructor() {
41
+ this.name = 'John'
42
+ this.age = 25
43
+ this.emails = []
44
+
45
+ makeAutoObservable(this)
46
+ }
47
+
48
+ get displayName() {
49
+ return `${this.name} (${this.age})`
50
+ }
51
+
52
+ addEmail(email) {
53
+ this.emails.push(email)
54
+ }
55
+ }
56
+
57
+ // In your Vue component
58
+
59
+ // Option 1: Modern <script setup> syntax (recommended)
60
+ <script setup>
61
+ import { useMobxBridge } from 'mobx-vue-bridge'
62
+ import { makeAutoObservable } from 'mobx'
63
+
64
+ // Your MobX presenter
65
+ class UserPresenter {
66
+ constructor() {
67
+ this.name = 'John'
68
+ this.age = 25
69
+ this.emails = []
70
+
71
+ makeAutoObservable(this)
72
+ }
73
+
74
+ get displayName() {
75
+ return `${this.name} (${this.age})`
76
+ }
77
+
78
+ addEmail(email) {
79
+ this.emails.push(email)
80
+ }
81
+ }
82
+
83
+ const userPresenter = new UserPresenter()
84
+
85
+ // Bridge MobX observable to Vue reactive state
86
+ const state = useMobxBridge(userPresenter)
87
+ </script>
88
+
89
+ // Option 2: Traditional Composition API
90
+ <script>
91
+ import { useMobxBridge } from 'mobx-vue-bridge'
92
+ import { makeAutoObservable } from 'mobx'
93
+
94
+ class UserPresenter {
95
+ constructor() {
96
+ this.name = 'John'
97
+ this.age = 25
98
+ this.emails = []
99
+
100
+ makeAutoObservable(this)
101
+ }
102
+
103
+ get displayName() {
104
+ return `${this.name} (${this.age})`
105
+ }
106
+
107
+ addEmail(email) {
108
+ this.emails.push(email)
109
+ }
110
+ }
111
+
112
+ export default {
113
+ setup() {
114
+ const userPresenter = new UserPresenter()
115
+
116
+ // Bridge MobX observable to Vue reactive state
117
+ const state = useMobxBridge(userPresenter)
118
+
119
+ return {
120
+ state
121
+ }
122
+ }
123
+ }
124
+ </script>
125
+ ```
126
+
127
+ ```vue
128
+ <template>
129
+ <div>
130
+ <!-- Two-way binding works seamlessly -->
131
+ <input v-model="state.name" />
132
+ <input v-model.number="state.age" />
133
+
134
+ <!-- Computed properties are reactive -->
135
+ <p>{{ state.displayName }}</p>
136
+
137
+ <!-- Methods are properly bound -->
138
+ <button @click="state.addEmail('new@email.com')">
139
+ Add Email
140
+ </button>
141
+
142
+ <!-- Arrays/objects are deeply reactive -->
143
+ <ul>
144
+ <li v-for="email in state.emails" :key="email">
145
+ {{ email }}
146
+ </li>
147
+ </ul>
148
+ </div>
149
+ </template>
150
+ ```
151
+
152
+ ## 📚 API Reference
153
+
154
+ ### `useMobxBridge(mobxObject, options?)`
155
+
156
+ Bridges a MobX observable object with Vue's reactivity system.
157
+
158
+ **Parameters:**
159
+ - `mobxObject` - The MobX observable object to bridge
160
+ - `options` - Configuration options (optional)
161
+
162
+ **Options:**
163
+ - `allowDirectMutation` (boolean, default: `true`) - Whether to allow direct mutation of properties
164
+
165
+ **Returns:** Vue reactive state object
166
+
167
+ ```javascript
168
+ // With configuration
169
+ const state = useMobxBridge(store, {
170
+ allowDirectMutation: false // Prevents direct mutations
171
+ })
172
+ ```
173
+
174
+ ### `usePresenterState(presenter, options?)`
175
+
176
+ Alias for `useMobxBridge` - commonly used with presenter pattern.
177
+
178
+ ```javascript
179
+ const state = usePresenterState(presenter, options)
180
+ ```
181
+
182
+ ## 🎯 Use Cases
183
+
184
+ ### Presenter Pattern
185
+ ```javascript
186
+ class TodoPresenter {
187
+ constructor(todoService) {
188
+ this.todoService = todoService
189
+ this.todos = []
190
+ this.filter = 'all'
191
+ this.loading = false
192
+
193
+ makeAutoObservable(this)
194
+ }
195
+
196
+ get filteredTodos() {
197
+ switch (this.filter) {
198
+ case 'active': return this.todos.filter(t => !t.completed)
199
+ case 'completed': return this.todos.filter(t => t.completed)
200
+ default: return this.todos
201
+ }
202
+ }
203
+
204
+ async loadTodos() {
205
+ this.loading = true
206
+ try {
207
+ this.todos = await this.todoService.fetchTodos()
208
+ } finally {
209
+ this.loading = false
210
+ }
211
+ }
212
+ }
213
+
214
+ // In component
215
+ const presenter = new TodoPresenter(todoService)
216
+ const state = usePresenterState(presenter)
217
+ ```
218
+
219
+ ### Store Integration
220
+ ```javascript
221
+ // MobX store
222
+ class AppStore {
223
+ constructor() {
224
+ this.user = null
225
+ this.theme = 'light'
226
+ this.notifications = []
227
+
228
+ makeAutoObservable(this)
229
+ }
230
+
231
+ get isAuthenticated() {
232
+ return !!this.user
233
+ }
234
+
235
+ setTheme(theme) {
236
+ this.theme = theme
237
+ }
238
+ }
239
+
240
+ // Bridge in component
241
+ const state = useMobxBridge(appStore)
242
+ ```
243
+
244
+ ## 🔧 Advanced Features
245
+
246
+ ### Deep Reactivity
247
+ The bridge automatically handles deep changes in objects and arrays:
248
+
249
+ ```javascript
250
+ // These mutations are automatically synced
251
+ state.user.profile.name = 'New Name' // Object mutation
252
+ state.todos.push(newTodo) // Array mutation
253
+ state.settings.colors[0] = '#FF0000' // Nested array mutation
254
+ ```
255
+
256
+ ### Error Handling
257
+ The bridge gracefully handles edge cases:
258
+
259
+ - Uninitialized computed properties
260
+ - Circular references
261
+ - Failed setter operations
262
+ - Missing dependencies
263
+
264
+ ### Performance Optimization
265
+ - Intelligent change detection prevents unnecessary updates
266
+ - Efficient shallow/deep equality checks
267
+ - Minimal overhead for large object graphs
268
+
269
+ ## 🧪 Testing
270
+
271
+ ```bash
272
+ npm test # Run tests
273
+ npm run test:watch # Watch mode
274
+ npm run test:coverage # Coverage report
275
+ ```
276
+
277
+ ## 🤝 Contributing
278
+
279
+ Contributions are welcome! Please feel free to submit a Pull Request.
280
+
281
+ ## 📄 License
282
+
283
+ MIT © [Visar Uruqi](https://github.com/visaruruqi)
284
+
285
+ ## 🔗 Links
286
+
287
+ - [GitHub Repository](https://github.com/visaruruqi/mobx-vue-bridge)
288
+ - [NPM Package](https://www.npmjs.com/package/mobx-vue-bridge)
289
+ - [Issues](https://github.com/visaruruqi/mobx-vue-bridge/issues)
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "mobx-vue-bridge",
3
+ "version": "1.0.0",
4
+ "description": "A bridge between MobX observables and Vue 3 reactivity system, enabling seamless two-way data binding and state synchronization.",
5
+ "main": "src/mobxVueBridge.js",
6
+ "module": "src/mobxVueBridge.js",
7
+ "types": "src/mobxVueBridge.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/mobxVueBridge.js",
12
+ "require": "./src/mobxVueBridge.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "src/",
17
+ "LICENSE",
18
+ "README.md",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "vitest",
23
+ "test:watch": "vitest --watch",
24
+ "test:coverage": "vitest --coverage",
25
+ "prepublishOnly": "npm test && node scripts/pre-publish.js",
26
+ "publish:dry": "npm publish --dry-run"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/visaruruqi/mobx-vue-bridge.git"
31
+ },
32
+ "keywords": [
33
+ "mobx",
34
+ "vue",
35
+ "vue3",
36
+ "reactivity",
37
+ "state-management",
38
+ "bridge",
39
+ "observable",
40
+ "reactive",
41
+ "two-way-binding",
42
+ "composition-api"
43
+ ],
44
+ "author": "Visar Uruqi",
45
+ "license": "MIT",
46
+ "bugs": {
47
+ "url": "https://github.com/visaruruqi/mobx-vue-bridge/issues"
48
+ },
49
+ "homepage": "https://github.com/visaruruqi/mobx-vue-bridge#readme",
50
+ "peerDependencies": {
51
+ "vue": "^3.0.0",
52
+ "mobx": "^6.0.0"
53
+ },
54
+ "dependencies": {
55
+ "mobx-utils": "^6.0.0",
56
+ "clone": "^2.1.2"
57
+ },
58
+ "devDependencies": {
59
+ "vitest": "^1.0.0",
60
+ "@vitest/coverage-v8": "^1.0.0"
61
+ },
62
+ "engines": {
63
+ "node": ">=16.0.0"
64
+ }
65
+ }
@@ -0,0 +1,33 @@
1
+ import { Ref, UnwrapRef } from 'vue'
2
+
3
+ export interface MobxBridgeOptions {
4
+ /**
5
+ * Whether to allow direct mutation of properties
6
+ * @default true
7
+ */
8
+ allowDirectMutation?: boolean
9
+ }
10
+
11
+ /**
12
+ * Bridge between MobX observables and Vue 3 reactivity system
13
+ *
14
+ * @param mobxObject - The MobX observable object to bridge
15
+ * @param options - Configuration options
16
+ * @returns Vue reactive state object
17
+ */
18
+ export function useMobxBridge<T extends object>(
19
+ mobxObject: T,
20
+ options?: MobxBridgeOptions
21
+ ): UnwrapRef<T>
22
+
23
+ /**
24
+ * Helper alias for useMobxBridge - commonly used with presenter objects
25
+ *
26
+ * @param presenter - The MobX presenter object to bridge
27
+ * @param options - Configuration options
28
+ * @returns Vue reactive state object
29
+ */
30
+ export function usePresenterState<T extends object>(
31
+ presenter: T,
32
+ options?: MobxBridgeOptions
33
+ ): UnwrapRef<T>
@@ -0,0 +1,432 @@
1
+ import { reactive, onUnmounted, ref } from 'vue';
2
+ import { toJS, reaction, observe, isComputedProp, isObservableProp } from 'mobx';
3
+ import { deepObserve } from 'mobx-utils';
4
+ import clone from 'clone';
5
+
6
+ /**
7
+ * 🌉 MobX-Vue Bridge
8
+ *
9
+ * @param {object} mobxObject - The MobX observable object to bridge
10
+ * @param {object} options - Configuration options
11
+ * @param {boolean} options.allowDirectMutation - Whether to allow direct mutation of properties
12
+ * @returns {object} Vue reactive state object
13
+ */
14
+ export function useMobxBridge(mobxObject, options = {}) {
15
+ const safeOptions = options || {};
16
+ // Use explicit boolean conversion to handle truthy/falsy values properly
17
+ const allowDirectMutation = safeOptions.allowDirectMutation !== undefined
18
+ ? Boolean(safeOptions.allowDirectMutation)
19
+ : true; // Keep the original default of true
20
+ const vueState = reactive({});
21
+
22
+ // Discover props/methods via MobX introspection (don’t rely on raw descriptors)
23
+ const props = Object.getOwnPropertyNames(mobxObject)
24
+ .concat(Object.getOwnPropertyNames(Object.getPrototypeOf(mobxObject)))
25
+ .filter(p => p !== 'constructor' && !p.startsWith('_'));
26
+
27
+ const members = {
28
+ getters: props.filter(p => {
29
+ try {
30
+ // First try to check if it's a computed property via MobX introspection
31
+ try {
32
+ return isComputedProp(mobxObject, p);
33
+ } catch (computedError) {
34
+ // If isComputedProp fails (e.g., due to uninitialized nested objects),
35
+ // fall back to checking if it has a getter descriptor
36
+ const descriptor = Object.getOwnPropertyDescriptor(mobxObject, p) ||
37
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(mobxObject), p);
38
+
39
+ // If it has a getter but no corresponding property, it's likely a computed getter
40
+ return descriptor && typeof descriptor.get === 'function' &&
41
+ !isObservableProp(mobxObject, p);
42
+ }
43
+ } catch (error) {
44
+ return false;
45
+ }
46
+ }),
47
+ setters: props.filter(p => {
48
+ try {
49
+ // Check if it has a setter descriptor
50
+ const descriptor = Object.getOwnPropertyDescriptor(mobxObject, p) ||
51
+ Object.getOwnPropertyDescriptor(Object.getPrototypeOf(mobxObject), p);
52
+
53
+ // Must have a setter
54
+ if (!descriptor || typeof descriptor.set !== 'function') return false;
55
+
56
+ // Exclude methods
57
+ if (typeof mobxObject[p] === 'function') return false;
58
+
59
+ // For MobX objects with makeAutoObservable, we need to distinguish:
60
+ // 1. Regular observable properties (handled separately)
61
+ // 2. Computed properties with setters (getter/setter pairs)
62
+ // 3. Setter-only properties
63
+
64
+ // Include if it's a computed property with a WORKING setter (getter/setter pair)
65
+ try {
66
+ if (isComputedProp(mobxObject, p)) {
67
+ // For computed properties, test if the setter actually works
68
+ try {
69
+ const originalValue = mobxObject[p];
70
+ descriptor.set.call(mobxObject, originalValue); // Try to set to same value
71
+ return true; // Setter works, it's a getter/setter pair
72
+ } catch (setterError) {
73
+ return false; // Setter throws error, it's a computed-only property
74
+ }
75
+ }
76
+ } catch (error) {
77
+ // If isComputedProp fails, check if it has a getter and test the setter
78
+ if (descriptor.get) {
79
+ try {
80
+ // Try to get the current value and set it back
81
+ const currentValue = mobxObject[p];
82
+ descriptor.set.call(mobxObject, currentValue);
83
+ return true; // Setter works
84
+ } catch (setterError) {
85
+ return false; // Setter throws error
86
+ }
87
+ }
88
+ }
89
+
90
+ // Include if it's NOT an observable property (setter-only or other cases)
91
+ if (!isObservableProp(mobxObject, p)) return true;
92
+
93
+ // Exclude regular observable properties (they're handled separately)
94
+ return false;
95
+ } catch (error) {
96
+ return false;
97
+ }
98
+ }),
99
+ properties: props.filter(p => {
100
+ try {
101
+ // Check if it's an observable property
102
+ if (!isObservableProp(mobxObject, p)) return false;
103
+
104
+ // Check if it's a function (method)
105
+ if (typeof mobxObject[p] === 'function') return false;
106
+
107
+ // Check if it's a computed property - if so, it's a getter, not a property
108
+ const isComputed = isComputedProp(mobxObject, p);
109
+ if (isComputed) return false;
110
+
111
+ return true; // Regular observable property
112
+ } catch (error) {
113
+ return false;
114
+ }
115
+ }),
116
+ methods: props.filter(p => {
117
+ try {
118
+ return typeof mobxObject[p] === 'function';
119
+ } catch (error) {
120
+ return false;
121
+ }
122
+ }),
123
+ };
124
+
125
+
126
+ // ---- utils: guards + equality --------------------------------------------
127
+ const updatingFromMobx = new Set();
128
+ const updatingFromVue = new Set();
129
+
130
+ const isEqual = (a, b) => {
131
+ if (Object.is(a, b)) return true;
132
+
133
+ // Handle null/undefined cases
134
+ if (a == null || b == null) return a === b;
135
+
136
+ // Different types are not equal
137
+ if (typeof a !== typeof b) return false;
138
+
139
+ // For primitives, Object.is should have caught them
140
+ if (typeof a !== 'object') return false;
141
+
142
+ // Fast array comparison
143
+ if (Array.isArray(a) && Array.isArray(b)) {
144
+ if (a.length !== b.length) return false;
145
+ return a.every((val, i) => isEqual(val, b[i]));
146
+ }
147
+
148
+ // Fast object comparison - check keys first
149
+ const aKeys = Object.keys(a);
150
+ const bKeys = Object.keys(b);
151
+ if (aKeys.length !== bKeys.length) return false;
152
+
153
+ // Check if all keys match
154
+ if (!aKeys.every(key => bKeys.includes(key))) return false;
155
+
156
+ // Check values (recursive)
157
+ return aKeys.every(key => isEqual(a[key], b[key]));
158
+ };
159
+
160
+ // Warning helpers to reduce duplication
161
+ const warnDirectMutation = (prop) => console.warn(`Direct mutation of '${prop}' is disabled`);
162
+ const warnSetterMutation = (prop) => console.warn(`Direct mutation of setter '${prop}' is disabled`);
163
+ const warnMethodAssignment = (prop) => console.warn(`Cannot assign to method '${prop}'`);
164
+
165
+ // Helper to create deep proxies for nested objects and arrays
166
+ const createDeepProxy = (value, prop) => {
167
+ // Don't proxy built-in objects that should remain unchanged
168
+ if (value instanceof Date || value instanceof RegExp || value instanceof Map ||
169
+ value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) {
170
+ return value;
171
+ }
172
+
173
+ return new Proxy(value, {
174
+ get: (target, key) => {
175
+ const result = target[key];
176
+ // If the result is an object/array, wrap it in a proxy too (but not built-ins)
177
+ if (result && typeof result === 'object' &&
178
+ !(result instanceof Date || result instanceof RegExp || result instanceof Map ||
179
+ result instanceof Set || result instanceof WeakMap || result instanceof WeakSet)) {
180
+ return createDeepProxy(result, prop);
181
+ }
182
+ return result;
183
+ },
184
+ set: (target, key, val) => {
185
+ target[key] = val;
186
+ // Update the Vue ref to trigger reactivity
187
+ propertyRefs[prop].value = clone(propertyRefs[prop].value);
188
+ // Update MobX immediately
189
+ mobxObject[prop] = clone(propertyRefs[prop].value);
190
+ return true;
191
+ }
192
+ });
193
+ };
194
+
195
+ // ---- properties (two-way) -------------------------------------------------
196
+ const propertyRefs = {};
197
+
198
+ members.properties.forEach(prop => {
199
+ propertyRefs[prop] = ref(toJS(mobxObject[prop]));
200
+
201
+ Object.defineProperty(vueState, prop, {
202
+ get: () => {
203
+ const value = propertyRefs[prop].value;
204
+ // For objects/arrays, return a deep proxy that syncs mutations back
205
+ if (value && typeof value === 'object') {
206
+ return createDeepProxy(value, prop);
207
+ }
208
+ return value;
209
+ },
210
+ set: allowDirectMutation
211
+ ? (value) => {
212
+ // Update Vue ref
213
+ const cloned = clone(value);
214
+ if (!isEqual(propertyRefs[prop].value, cloned)) {
215
+ propertyRefs[prop].value = cloned;
216
+ }
217
+ // ALSO update MobX immediately (synchronous)
218
+ if (!isEqual(mobxObject[prop], cloned)) {
219
+ mobxObject[prop] = cloned;
220
+ }
221
+ }
222
+ : () => warnDirectMutation(prop),
223
+ enumerable: true,
224
+ configurable: true,
225
+ });
226
+
227
+ });
228
+
229
+ // ---- getters and setters (handle both computed and two-way binding) ------
230
+ const getterRefs = {};
231
+ const setterRefs = {};
232
+
233
+ // First, handle properties that have BOTH getters and setters (getter/setter pairs)
234
+ const getterSetterPairs = members.getters.filter(prop => members.setters.includes(prop));
235
+ const gettersOnly = members.getters.filter(prop => !members.setters.includes(prop));
236
+ const settersOnly = members.setters.filter(prop => !members.getters.includes(prop));
237
+
238
+ // Getter/setter pairs: writable with reactive updates
239
+ getterSetterPairs.forEach(prop => {
240
+ // Get initial value from getter
241
+ let initialValue;
242
+ try {
243
+ initialValue = toJS(mobxObject[prop]);
244
+ } catch (error) {
245
+ initialValue = undefined;
246
+ }
247
+ getterRefs[prop] = ref(initialValue);
248
+ setterRefs[prop] = ref(initialValue);
249
+
250
+ Object.defineProperty(vueState, prop, {
251
+ get: () => getterRefs[prop].value,
252
+ set: allowDirectMutation
253
+ ? (value) => {
254
+ // Update both refs
255
+ setterRefs[prop].value = value;
256
+ // Call the MobX setter immediately
257
+ try {
258
+ mobxObject[prop] = value;
259
+ // The getter ref will be updated by the reaction
260
+ } catch (error) {
261
+ console.warn(`Failed to set property '${prop}':`, error);
262
+ }
263
+ }
264
+ : () => warnDirectMutation(prop),
265
+ enumerable: true,
266
+ configurable: true,
267
+ });
268
+ });
269
+
270
+ // Getter-only properties: read-only computed
271
+ gettersOnly.forEach(prop => {
272
+ // Safely get initial value of computed property, handle errors gracefully
273
+ let initialValue;
274
+ try {
275
+ initialValue = toJS(mobxObject[prop]);
276
+ } catch (error) {
277
+ // If computed property throws during initialization (e.g., accessing null.property),
278
+ // set initial value to undefined and let the reaction handle updates later
279
+ initialValue = undefined;
280
+ }
281
+ getterRefs[prop] = ref(initialValue);
282
+
283
+ Object.defineProperty(vueState, prop, {
284
+ get: () => getterRefs[prop].value,
285
+ set: () => {
286
+ throw new Error(`Cannot assign to computed property '${prop}'`)
287
+ },
288
+ enumerable: true,
289
+ configurable: true,
290
+ });
291
+ });
292
+
293
+ // Setter-only properties: write-only
294
+ settersOnly.forEach(prop => {
295
+ // For setter-only properties, track the last set value
296
+ setterRefs[prop] = ref(undefined);
297
+
298
+ Object.defineProperty(vueState, prop, {
299
+ get: () => setterRefs[prop].value,
300
+ set: allowDirectMutation
301
+ ? (value) => {
302
+ // Update the setter ref
303
+ setterRefs[prop].value = value;
304
+
305
+ // Call the MobX setter immediately
306
+ try {
307
+ mobxObject[prop] = value;
308
+ } catch (error) {
309
+ console.warn(`Failed to set property '${prop}':`, error);
310
+ }
311
+ }
312
+ : () => warnSetterMutation(prop),
313
+ enumerable: true,
314
+ configurable: true,
315
+ });
316
+ });
317
+
318
+ // ---- methods (bound) ------------------------------------------------------
319
+ members.methods.forEach(prop => {
320
+ // Cache the bound method to avoid creating new functions on every access
321
+ const boundMethod = mobxObject[prop].bind(mobxObject);
322
+ Object.defineProperty(vueState, prop, {
323
+ get: () => boundMethod,
324
+ set: () => warnMethodAssignment(prop),
325
+ enumerable: true,
326
+ configurable: true,
327
+ });
328
+ });
329
+
330
+ // ---- MobX → Vue: property observation ----------------------------------------
331
+ const subscriptions = [];
332
+
333
+ setupStandardPropertyObservers();
334
+
335
+ // Standard property observation implementation
336
+ function setupStandardPropertyObservers() {
337
+ // Use individual observe for each property to avoid circular reference issues
338
+ members.properties.forEach(prop => {
339
+ try {
340
+ const sub = observe(mobxObject, prop, (change) => {
341
+ if (!propertyRefs[prop]) return;
342
+ if (updatingFromVue.has(prop)) return; // avoid echo
343
+ updatingFromMobx.add(prop);
344
+ try {
345
+ const next = toJS(mobxObject[prop]);
346
+ if (!isEqual(propertyRefs[prop].value, next)) {
347
+ propertyRefs[prop].value = next;
348
+ }
349
+ } finally {
350
+ updatingFromMobx.delete(prop);
351
+ }
352
+ });
353
+ subscriptions.push(sub);
354
+ } catch (error) {
355
+ // Silently ignore non-observable properties
356
+ }
357
+ });
358
+
359
+ // For nested objects and arrays, use deepObserve to handle deep changes
360
+ // This handles both object properties and array mutations
361
+ members.properties.forEach(prop => {
362
+ const value = mobxObject[prop];
363
+ if (value && typeof value === 'object') { // Include both objects AND arrays
364
+ try {
365
+ const sub = deepObserve(value, (change, path) => {
366
+ if (!propertyRefs[prop]) return;
367
+ if (updatingFromVue.has(prop)) return; // avoid echo
368
+ updatingFromMobx.add(prop);
369
+ try {
370
+ const next = toJS(mobxObject[prop]);
371
+ if (!isEqual(propertyRefs[prop].value, next)) {
372
+ propertyRefs[prop].value = next;
373
+ }
374
+ } finally {
375
+ updatingFromMobx.delete(prop);
376
+ }
377
+ });
378
+ subscriptions.push(sub);
379
+ } catch (error) {
380
+ // Silently ignore if deepObserve fails (e.g., circular references in nested objects)
381
+ }
382
+ }
383
+ });
384
+ }
385
+
386
+ // Getters: keep them in sync via reaction (both getter-only and getter/setter pairs)
387
+ [...gettersOnly, ...getterSetterPairs].forEach(prop => {
388
+ const sub = reaction(
389
+ () => {
390
+ try {
391
+ return toJS(mobxObject[prop]);
392
+ } catch (error) {
393
+ // If computed property throws (e.g., accessing null.property), return undefined
394
+ return undefined;
395
+ }
396
+ },
397
+ (next) => {
398
+ if (!getterRefs[prop]) return;
399
+ if (!isEqual(getterRefs[prop].value, next)) {
400
+ getterRefs[prop].value = next;
401
+ }
402
+ }
403
+ );
404
+ subscriptions.push(sub);
405
+ });
406
+
407
+ // Cleanup
408
+ onUnmounted(() => {
409
+ subscriptions.forEach(unsub => {
410
+ try {
411
+ if (typeof unsub === 'function') {
412
+ unsub();
413
+ }
414
+ } catch {
415
+ // Silently ignore cleanup errors
416
+ }
417
+ });
418
+ });
419
+
420
+ return vueState;
421
+ }
422
+
423
+ /**
424
+ * Helper alias for useMobxBridge - commonly used with presenter objects
425
+ *
426
+ * @param {object} presenter - The MobX presenter object to bridge
427
+ * @param {object} options - Configuration options
428
+ * @returns {object} Vue reactive state object
429
+ */
430
+ export function usePresenterState(presenter, options = {}) {
431
+ return useMobxBridge(presenter, options);
432
+ }