mycelia-kernel-plugin 1.1.0 → 1.3.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/README.md +50 -3
- package/bin/cli.js +508 -3
- package/package.json +10 -3
- package/src/react/README.md +174 -0
- package/src/react/index.js +401 -0
- package/src/svelte/builders.js +43 -0
- package/src/svelte/index.js +183 -0
- package/src/svelte/listeners.js +96 -0
- package/src/svelte/queues.js +114 -0
- package/src/svelte/stores.js +36 -0
- package/src/vue/builders.js +40 -0
- package/src/vue/composables.js +37 -0
- package/src/vue/index.js +252 -0
- package/src/vue/listeners.js +78 -0
- package/src/vue/queues.js +113 -0
package/src/vue/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mycelia Plugin System - Vue Bindings
|
|
3
|
+
*
|
|
4
|
+
* Vue utilities that make the Mycelia Plugin System feel natural
|
|
5
|
+
* inside Vue 3 applications using the Composition API.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```js
|
|
9
|
+
* import { MyceliaPlugin, useFacet, useListener } from 'mycelia-kernel-plugin/vue';
|
|
10
|
+
* import { createApp } from 'vue';
|
|
11
|
+
*
|
|
12
|
+
* const buildSystem = () => useBase('app')
|
|
13
|
+
* .use(useDatabase)
|
|
14
|
+
* .build();
|
|
15
|
+
*
|
|
16
|
+
* const app = createApp(App);
|
|
17
|
+
* app.use(MyceliaPlugin, { build: buildSystem });
|
|
18
|
+
*
|
|
19
|
+
* // In component
|
|
20
|
+
* export default {
|
|
21
|
+
* setup() {
|
|
22
|
+
* const db = useFacet('database');
|
|
23
|
+
* useListener('user:created', (msg) => console.log(msg));
|
|
24
|
+
* }
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { provide, inject, ref, onUnmounted, watch, getCurrentInstance, onBeforeUnmount } from 'vue';
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Core Bindings: Plugin + Basic Composables
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const MyceliaKey = Symbol('mycelia');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* MyceliaPlugin - Vue plugin that provides Mycelia system to the app
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} app - Vue app instance
|
|
41
|
+
* @param {Object} options - Plugin options
|
|
42
|
+
* @param {Function} options.build - Async function that returns a built system
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```js
|
|
46
|
+
* import { createApp } from 'vue';
|
|
47
|
+
* import { MyceliaPlugin } from 'mycelia-kernel-plugin/vue';
|
|
48
|
+
*
|
|
49
|
+
* const buildSystem = () => useBase('app').use(useDatabase).build();
|
|
50
|
+
*
|
|
51
|
+
* const app = createApp(App);
|
|
52
|
+
* app.use(MyceliaPlugin, { build: buildSystem });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export const MyceliaPlugin = {
|
|
56
|
+
async install(app, options) {
|
|
57
|
+
const { build } = options;
|
|
58
|
+
const system = ref(null);
|
|
59
|
+
const error = ref(null);
|
|
60
|
+
const loading = ref(true);
|
|
61
|
+
let currentSystem = null;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
currentSystem = await build();
|
|
65
|
+
system.value = currentSystem;
|
|
66
|
+
loading.value = false;
|
|
67
|
+
error.value = null;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
error.value = err;
|
|
70
|
+
loading.value = false;
|
|
71
|
+
system.value = null;
|
|
72
|
+
// Re-throw to let Vue handle it
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Provide system to all components
|
|
77
|
+
const context = {
|
|
78
|
+
system,
|
|
79
|
+
loading,
|
|
80
|
+
error
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
app.provide(MyceliaKey, context);
|
|
84
|
+
|
|
85
|
+
// Store cleanup function on app config for manual cleanup
|
|
86
|
+
// Vue 3 doesn't provide a plugin unmount hook, so cleanup should be
|
|
87
|
+
// handled manually when unmounting the app:
|
|
88
|
+
// const dispose = app.config.globalProperties.$myceliaDispose;
|
|
89
|
+
// if (dispose) await dispose();
|
|
90
|
+
// app.unmount();
|
|
91
|
+
app.config.globalProperties.$myceliaDispose = async () => {
|
|
92
|
+
if (currentSystem && typeof currentSystem.dispose === 'function') {
|
|
93
|
+
await currentSystem.dispose().catch(() => {
|
|
94
|
+
// Ignore disposal errors
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* useMycelia - Get the Mycelia system from inject
|
|
103
|
+
*
|
|
104
|
+
* @returns {Object} The Mycelia system instance
|
|
105
|
+
* @throws {Error} If used outside MyceliaPlugin
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```js
|
|
109
|
+
* import { useMycelia } from 'mycelia-kernel-plugin/vue';
|
|
110
|
+
*
|
|
111
|
+
* export default {
|
|
112
|
+
* setup() {
|
|
113
|
+
* const system = useMycelia();
|
|
114
|
+
* // Use system.find(), system.listeners, etc.
|
|
115
|
+
* }
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function useMycelia() {
|
|
120
|
+
const context = inject(MyceliaKey);
|
|
121
|
+
if (!context) {
|
|
122
|
+
throw new Error('useMycelia must be used within MyceliaPlugin');
|
|
123
|
+
}
|
|
124
|
+
return context.system.value;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* useMyceliaContext - Get the full Mycelia context (system, loading, error)
|
|
129
|
+
*
|
|
130
|
+
* @returns {Object} Context object with system, loading, and error refs
|
|
131
|
+
* @throws {Error} If used outside MyceliaPlugin
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```js
|
|
135
|
+
* import { useMyceliaContext } from 'mycelia-kernel-plugin/vue';
|
|
136
|
+
*
|
|
137
|
+
* export default {
|
|
138
|
+
* setup() {
|
|
139
|
+
* const { system, loading, error } = useMyceliaContext();
|
|
140
|
+
* if (loading.value) return { loading: true };
|
|
141
|
+
* if (error.value) return { error: error.value };
|
|
142
|
+
* return { system: system.value };
|
|
143
|
+
* }
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function useMyceliaContext() {
|
|
148
|
+
const context = inject(MyceliaKey);
|
|
149
|
+
if (!context) {
|
|
150
|
+
throw new Error('useMyceliaContext must be used within MyceliaPlugin');
|
|
151
|
+
}
|
|
152
|
+
return context;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* useFacet - Get a facet by kind from the system with reactivity
|
|
157
|
+
*
|
|
158
|
+
* @param {string} kind - Facet kind identifier
|
|
159
|
+
* @returns {import('vue').Ref<Object|null>} Reactive ref to the facet instance, or null if not found
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```js
|
|
163
|
+
* import { useFacet } from 'mycelia-kernel-plugin/vue';
|
|
164
|
+
*
|
|
165
|
+
* export default {
|
|
166
|
+
* setup() {
|
|
167
|
+
* const db = useFacet('database');
|
|
168
|
+
* // Use db.value.query(), etc.
|
|
169
|
+
* }
|
|
170
|
+
* }
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export function useFacet(kind) {
|
|
174
|
+
const system = useMycelia();
|
|
175
|
+
const facet = ref(system?.find?.(kind) ?? null);
|
|
176
|
+
|
|
177
|
+
// Watch for system changes (e.g., after reload)
|
|
178
|
+
watch(() => system, (newSystem) => {
|
|
179
|
+
facet.value = newSystem?.find?.(kind) ?? null;
|
|
180
|
+
}, { immediate: true });
|
|
181
|
+
|
|
182
|
+
return facet;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* useMyceliaCleanup - Automatically handle system cleanup on component unmount
|
|
187
|
+
*
|
|
188
|
+
* This composable automatically disposes the Mycelia system when the component
|
|
189
|
+
* unmounts. It's useful for root components or components that manage app lifecycle.
|
|
190
|
+
*
|
|
191
|
+
* @param {Object} [options={}] - Options
|
|
192
|
+
* @param {boolean} [options.auto=true] - If true, automatically cleanup on unmount. If false, only return cleanup function.
|
|
193
|
+
* @returns {Function} Cleanup function that can be called manually
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```vue
|
|
197
|
+
* <script setup>
|
|
198
|
+
* import { useMyceliaCleanup } from 'mycelia-kernel-plugin/vue';
|
|
199
|
+
*
|
|
200
|
+
* // Automatic cleanup on component unmount
|
|
201
|
+
* useMyceliaCleanup();
|
|
202
|
+
* </script>
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```vue
|
|
207
|
+
* <script setup>
|
|
208
|
+
* import { useMyceliaCleanup } from 'mycelia-kernel-plugin/vue';
|
|
209
|
+
*
|
|
210
|
+
* // Get cleanup function for manual use
|
|
211
|
+
* const dispose = useMyceliaCleanup({ auto: false });
|
|
212
|
+
*
|
|
213
|
+
* const handleLogout = async () => {
|
|
214
|
+
* await dispose();
|
|
215
|
+
* // Continue with logout logic
|
|
216
|
+
* };
|
|
217
|
+
* </script>
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
export function useMyceliaCleanup(options = {}) {
|
|
221
|
+
const { auto = true } = options;
|
|
222
|
+
const instance = getCurrentInstance();
|
|
223
|
+
const app = instance?.appContext.app;
|
|
224
|
+
|
|
225
|
+
const cleanup = async () => {
|
|
226
|
+
const dispose = app?.config.globalProperties.$myceliaDispose;
|
|
227
|
+
if (dispose) {
|
|
228
|
+
await dispose();
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (auto) {
|
|
233
|
+
onBeforeUnmount(async () => {
|
|
234
|
+
await cleanup();
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return cleanup;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Re-export listener helpers
|
|
242
|
+
export { useListener, useEventStream } from './listeners.js';
|
|
243
|
+
|
|
244
|
+
// Re-export queue helpers
|
|
245
|
+
export { useQueueStatus, useQueueDrain } from './queues.js';
|
|
246
|
+
|
|
247
|
+
// Re-export builder helpers
|
|
248
|
+
export { createVueSystemBuilder } from './builders.js';
|
|
249
|
+
|
|
250
|
+
// Re-export composable generator
|
|
251
|
+
export { createFacetComposable } from './composables.js';
|
|
252
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mycelia Plugin System - Vue Listener Helpers
|
|
3
|
+
*
|
|
4
|
+
* Vue composables for event listener management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ref, onUnmounted } from 'vue';
|
|
8
|
+
import { useMycelia } from './index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* useListener - Register an event listener with automatic cleanup
|
|
12
|
+
*
|
|
13
|
+
* @param {string} eventName - Event name/path to listen for
|
|
14
|
+
* @param {Function} handler - Handler function: (message) => void
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```js
|
|
18
|
+
* import { useListener } from 'mycelia-kernel-plugin/vue';
|
|
19
|
+
*
|
|
20
|
+
* export default {
|
|
21
|
+
* setup() {
|
|
22
|
+
* useListener('user:created', (msg) => {
|
|
23
|
+
* console.log('User created:', msg.body);
|
|
24
|
+
* });
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useListener(eventName, handler) {
|
|
30
|
+
const system = useMycelia();
|
|
31
|
+
const listeners = system?.listeners; // useListeners facet
|
|
32
|
+
|
|
33
|
+
if (!listeners || !listeners.hasListeners?.()) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
listeners.on(eventName, handler);
|
|
38
|
+
|
|
39
|
+
onUnmounted(() => {
|
|
40
|
+
listeners.off?.(eventName, handler);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* useEventStream - Subscribe to events and keep them in reactive state
|
|
46
|
+
*
|
|
47
|
+
* @param {string} eventName - Event name/path to listen for
|
|
48
|
+
* @param {Object} [options={}] - Options
|
|
49
|
+
* @param {boolean} [options.accumulate=false] - If true, accumulate events in array
|
|
50
|
+
* @returns {import('vue').Ref<any|any[]|null>} Reactive ref to latest event value, array of events, or null
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```js
|
|
54
|
+
* import { useEventStream } from 'mycelia-kernel-plugin/vue';
|
|
55
|
+
*
|
|
56
|
+
* export default {
|
|
57
|
+
* setup() {
|
|
58
|
+
* const events = useEventStream('todo:created', { accumulate: true });
|
|
59
|
+
* return { events };
|
|
60
|
+
* }
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function useEventStream(eventName, options = {}) {
|
|
65
|
+
const { accumulate = false } = options;
|
|
66
|
+
const value = ref(accumulate ? [] : null);
|
|
67
|
+
|
|
68
|
+
useListener(eventName, (msg) => {
|
|
69
|
+
if (accumulate) {
|
|
70
|
+
value.value = [...value.value, msg.body];
|
|
71
|
+
} else {
|
|
72
|
+
value.value = msg.body;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mycelia Plugin System - Vue Queue Helpers
|
|
3
|
+
*
|
|
4
|
+
* Vue composables for queue management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ref, onMounted, onUnmounted } from 'vue';
|
|
8
|
+
import { useFacet } from './index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* useQueueStatus - Get queue status with reactive updates
|
|
12
|
+
*
|
|
13
|
+
* @returns {import('vue').Ref<Object>} Reactive ref to queue status object
|
|
14
|
+
* @returns {number} size - Current queue size
|
|
15
|
+
* @returns {number} capacity - Maximum queue capacity
|
|
16
|
+
* @returns {number} utilization - Utilization ratio (0-1)
|
|
17
|
+
* @returns {boolean} isFull - Whether queue is full
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```js
|
|
21
|
+
* import { useQueueStatus } from 'mycelia-kernel-plugin/vue';
|
|
22
|
+
*
|
|
23
|
+
* export default {
|
|
24
|
+
* setup() {
|
|
25
|
+
* const status = useQueueStatus();
|
|
26
|
+
* return { status };
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useQueueStatus() {
|
|
32
|
+
const queue = useFacet('queue');
|
|
33
|
+
const status = ref({
|
|
34
|
+
size: 0,
|
|
35
|
+
capacity: 0,
|
|
36
|
+
utilization: 0,
|
|
37
|
+
isFull: false
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let intervalId = null;
|
|
41
|
+
|
|
42
|
+
const updateStatus = () => {
|
|
43
|
+
if (queue.value?.getQueueStatus) {
|
|
44
|
+
const newStatus = queue.value.getQueueStatus();
|
|
45
|
+
status.value = {
|
|
46
|
+
size: newStatus.size || 0,
|
|
47
|
+
capacity: newStatus.maxSize || 0,
|
|
48
|
+
utilization: newStatus.utilization || 0,
|
|
49
|
+
isFull: newStatus.isFull || false
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
onMounted(() => {
|
|
55
|
+
updateStatus();
|
|
56
|
+
intervalId = setInterval(updateStatus, 100); // Poll every 100ms
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
onUnmounted(() => {
|
|
60
|
+
if (intervalId) {
|
|
61
|
+
clearInterval(intervalId);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return status;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* useQueueDrain - Automatically drain queue on mount
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} [options={}] - Options
|
|
72
|
+
* @param {number} [options.interval=100] - Polling interval in ms
|
|
73
|
+
* @param {Function} [options.onMessage] - Callback for each message: (msg, options) => void
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```js
|
|
77
|
+
* import { useQueueDrain } from 'mycelia-kernel-plugin/vue';
|
|
78
|
+
*
|
|
79
|
+
* export default {
|
|
80
|
+
* setup() {
|
|
81
|
+
* useQueueDrain({
|
|
82
|
+
* interval: 50,
|
|
83
|
+
* onMessage: (msg) => console.log('Processed:', msg)
|
|
84
|
+
* });
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export function useQueueDrain(options = {}) {
|
|
90
|
+
const { interval = 100, onMessage } = options;
|
|
91
|
+
const queue = useFacet('queue');
|
|
92
|
+
let processInterval = null;
|
|
93
|
+
|
|
94
|
+
onMounted(() => {
|
|
95
|
+
if (!queue.value || !queue.value.hasMessagesToProcess) return;
|
|
96
|
+
|
|
97
|
+
processInterval = setInterval(() => {
|
|
98
|
+
if (queue.value?.hasMessagesToProcess()) {
|
|
99
|
+
const next = queue.value.selectNextMessage();
|
|
100
|
+
if (next && onMessage) {
|
|
101
|
+
onMessage(next.msg, next.options);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, interval);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
onUnmounted(() => {
|
|
108
|
+
if (processInterval) {
|
|
109
|
+
clearInterval(processInterval);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|