mycelia-kernel-plugin 1.2.0 → 1.4.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,11 +1,12 @@
1
1
  import { StandalonePluginSystem } from '../system/standalone-plugin-system.js';
2
+ import { BaseSubsystem } from '../system/base-subsystem.js';
2
3
  import { deepMerge } from '../builder/context-resolver.js';
3
4
 
4
5
  /**
5
- * useBase - Fluent API Builder for StandalonePluginSystem
6
+ * useBase - Fluent API Builder for StandalonePluginSystem / BaseSubsystem
6
7
  *
7
8
  * Provides a convenient, chainable API for creating and configuring
8
- * StandalonePluginSystem instances.
9
+ * StandalonePluginSystem or BaseSubsystem instances via a fluent API.
9
10
  *
10
11
  * @param {string} name - Unique name for the plugin system
11
12
  * @param {Object} [options={}] - Initial configuration options
@@ -14,6 +15,9 @@ import { deepMerge } from '../builder/context-resolver.js';
14
15
  * @param {Array} [options.defaultHooks=[]] - Optional default hooks to install
15
16
  * @returns {UseBaseBuilder} Builder instance with fluent API
16
17
  *
18
+ * By default it uses StandalonePluginSystem; call .setBase(BaseSubsystem)
19
+ * to build a subsystem instead.
20
+ *
17
21
  * @example
18
22
  * ```javascript
19
23
  * import { useBase } from 'mycelia-kernel-plugin';
@@ -35,24 +39,73 @@ export function useBase(name, options = {}) {
35
39
  throw new Error('useBase: name must be a non-empty string');
36
40
  }
37
41
 
38
- // Create the system instance
39
- const system = new StandalonePluginSystem(name, options);
40
-
41
- // Create builder with fluent API
42
- return new UseBaseBuilder(system);
42
+ // Create builder with fluent API (system will be created lazily)
43
+ return new UseBaseBuilder(name, options);
43
44
  }
44
45
 
45
46
  /**
46
- * UseBaseBuilder - Fluent API builder for StandalonePluginSystem
47
+ * UseBaseBuilder - Fluent API builder for StandalonePluginSystem or BaseSubsystem
47
48
  *
48
49
  * Provides chainable methods for configuring and building the system.
49
50
  */
50
51
  class UseBaseBuilder {
51
- #system;
52
+ #name;
53
+ #options;
54
+ #BaseClass = StandalonePluginSystem; // Default to StandalonePluginSystem
55
+ #system = null; // Lazy initialization
52
56
  #pendingConfig = {};
53
57
 
54
- constructor(system) {
55
- this.#system = system;
58
+ constructor(name, options) {
59
+ this.#name = name;
60
+ this.#options = options;
61
+ }
62
+
63
+ /**
64
+ * Get or create the system instance (lazy initialization)
65
+ * @private
66
+ */
67
+ #getSystem() {
68
+ if (!this.#system) {
69
+ this.#system = new this.#BaseClass(this.#name, this.#options);
70
+ }
71
+ return this.#system;
72
+ }
73
+
74
+ /**
75
+ * Set the base class for the system
76
+ *
77
+ * @param {Function} BaseClass - The base class to use. Must be BaseSubsystem or any class that extends BaseSubsystem
78
+ * @returns {UseBaseBuilder} This builder for chaining
79
+ *
80
+ * @example
81
+ * ```javascript
82
+ * import { BaseSubsystem } from 'mycelia-kernel-plugin';
83
+ *
84
+ * builder.setBase(BaseSubsystem);
85
+ * ```
86
+ *
87
+ * @example
88
+ * ```javascript
89
+ * // Must be called before any methods that use the system
90
+ * const system = await useBase('my-app')
91
+ * .setBase(BaseSubsystem)
92
+ * .use(useDatabase)
93
+ * .build();
94
+ * ```
95
+ */
96
+ setBase(BaseClass) {
97
+ if (this.#system) {
98
+ throw new Error('useBase.setBase: cannot change base class after system is created');
99
+ }
100
+ if (typeof BaseClass !== 'function') {
101
+ throw new Error('useBase.setBase: BaseClass must be a constructor function');
102
+ }
103
+ // Validate it's a subclass of BaseSubsystem
104
+ if (BaseClass !== BaseSubsystem && !(BaseClass.prototype instanceof BaseSubsystem)) {
105
+ throw new Error('useBase.setBase: BaseClass must be BaseSubsystem or a subclass of BaseSubsystem');
106
+ }
107
+ this.#BaseClass = BaseClass;
108
+ return this;
56
109
  }
57
110
 
58
111
  /**
@@ -70,7 +123,7 @@ class UseBaseBuilder {
70
123
  if (typeof hook !== 'function') {
71
124
  throw new Error('useBase.use: hook must be a function');
72
125
  }
73
- this.#system.use(hook);
126
+ this.#getSystem().use(hook);
74
127
  return this;
75
128
  }
76
129
 
@@ -93,6 +146,75 @@ class UseBaseBuilder {
93
146
  return this;
94
147
  }
95
148
 
149
+ /**
150
+ * Register multiple hooks at once
151
+ *
152
+ * @param {Array<Function>} hooks - Array of hook functions to register
153
+ * @returns {UseBaseBuilder} This builder for chaining
154
+ *
155
+ * @example
156
+ * ```javascript
157
+ * builder.useMultiple([useDatabase, useCache, useAuth]);
158
+ * ```
159
+ *
160
+ * @example
161
+ * ```javascript
162
+ * // Can be combined with other methods
163
+ * builder
164
+ * .use(useLogger)
165
+ * .useMultiple([useDatabase, useCache])
166
+ * .use(useAuth);
167
+ * ```
168
+ */
169
+ useMultiple(hooks) {
170
+ if (!Array.isArray(hooks)) {
171
+ throw new Error('useBase.useMultiple: hooks must be an array');
172
+ }
173
+
174
+ const system = this.#getSystem();
175
+ for (const hook of hooks) {
176
+ if (typeof hook !== 'function') {
177
+ throw new Error('useBase.useMultiple: all hooks must be functions');
178
+ }
179
+ system.use(hook);
180
+ }
181
+
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Conditionally register multiple hooks
187
+ *
188
+ * @param {boolean} condition - Whether to register the hooks
189
+ * @param {Array<Function>} hooks - Array of hook functions to register if condition is true
190
+ * @returns {UseBaseBuilder} This builder for chaining
191
+ *
192
+ * @example
193
+ * ```javascript
194
+ * builder.useIfMultiple(process.env.NODE_ENV === 'development', [
195
+ * useDebugTools,
196
+ * useDevLogger
197
+ * ]);
198
+ * ```
199
+ *
200
+ * @example
201
+ * ```javascript
202
+ * const optionalHooks = [];
203
+ * if (enableCache) optionalHooks.push(useCache);
204
+ * if (enableAuth) optionalHooks.push(useAuth);
205
+ *
206
+ * builder
207
+ * .use(useDatabase)
208
+ * .useIfMultiple(optionalHooks.length > 0, optionalHooks);
209
+ * ```
210
+ */
211
+ useIfMultiple(condition, hooks) {
212
+ if (condition) {
213
+ return this.useMultiple(hooks);
214
+ }
215
+ return this;
216
+ }
217
+
96
218
  /**
97
219
  * Add or update configuration for a specific facet kind
98
220
  *
@@ -122,13 +244,18 @@ class UseBaseBuilder {
122
244
  throw new Error('useBase.config: kind must be a non-empty string');
123
245
  }
124
246
 
125
- // Initialize config object if needed
126
- if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
127
- this.#system.ctx.config = {};
247
+ // Get existing config for this kind (from pending or system if created)
248
+ let existingConfig;
249
+ if (this.#system) {
250
+ if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
251
+ this.#system.ctx.config = {};
252
+ }
253
+ existingConfig = this.#system.ctx.config[kind];
254
+ } else {
255
+ // System not created yet, check pending config
256
+ existingConfig = this.#pendingConfig[kind];
128
257
  }
129
258
 
130
- // Get existing config for this kind
131
- const existingConfig = this.#system.ctx.config[kind];
132
259
  const pendingConfig = this.#pendingConfig[kind];
133
260
 
134
261
  // Determine the base config (existing or pending)
@@ -153,6 +280,49 @@ class UseBaseBuilder {
153
280
  return this;
154
281
  }
155
282
 
283
+ /**
284
+ * Add or update configurations for multiple facet kinds at once
285
+ *
286
+ * Configurations are merged when possible (deep merge for objects).
287
+ *
288
+ * @param {Object} configs - Object where keys are facet kinds and values are configurations
289
+ * @returns {UseBaseBuilder} This builder for chaining
290
+ *
291
+ * @example
292
+ * ```javascript
293
+ * builder.configMultiple({
294
+ * database: { host: 'localhost', port: 5432 },
295
+ * cache: { ttl: 3600 },
296
+ * auth: { secret: 'abc123' }
297
+ * });
298
+ * ```
299
+ *
300
+ * @example
301
+ * ```javascript
302
+ * // Merge configurations
303
+ * builder
304
+ * .configMultiple({
305
+ * database: { host: 'localhost' },
306
+ * cache: { ttl: 3600 }
307
+ * })
308
+ * .configMultiple({
309
+ * database: { port: 5432 }, // Merges with existing
310
+ * auth: { secret: 'abc123' }
311
+ * });
312
+ * ```
313
+ */
314
+ configMultiple(configs) {
315
+ if (!configs || typeof configs !== 'object' || Array.isArray(configs)) {
316
+ throw new Error('useBase.configMultiple: configs must be an object');
317
+ }
318
+
319
+ for (const [kind, config] of Object.entries(configs)) {
320
+ this.config(kind, config);
321
+ }
322
+
323
+ return this;
324
+ }
325
+
156
326
  /**
157
327
  * Add an initialization callback
158
328
  *
@@ -170,7 +340,7 @@ class UseBaseBuilder {
170
340
  if (typeof callback !== 'function') {
171
341
  throw new Error('useBase.onInit: callback must be a function');
172
342
  }
173
- this.#system.onInit(callback);
343
+ this.#getSystem().onInit(callback);
174
344
  return this;
175
345
  }
176
346
 
@@ -191,7 +361,7 @@ class UseBaseBuilder {
191
361
  if (typeof callback !== 'function') {
192
362
  throw new Error('useBase.onDispose: callback must be a function');
193
363
  }
194
- this.#system.onDispose(callback);
364
+ this.#getSystem().onDispose(callback);
195
365
  return this;
196
366
  }
197
367
 
@@ -212,16 +382,18 @@ class UseBaseBuilder {
212
382
  * ```
213
383
  */
214
384
  async build(ctx = {}) {
385
+ const system = this.#getSystem();
386
+
215
387
  // Apply pending configurations
216
388
  if (Object.keys(this.#pendingConfig).length > 0) {
217
389
  // Merge pending config into system config
218
- if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
219
- this.#system.ctx.config = {};
390
+ if (!system.ctx.config || typeof system.ctx.config !== 'object') {
391
+ system.ctx.config = {};
220
392
  }
221
393
 
222
394
  // Deep merge pending configs
223
395
  for (const [kind, config] of Object.entries(this.#pendingConfig)) {
224
- const existing = this.#system.ctx.config[kind];
396
+ const existing = system.ctx.config[kind];
225
397
  if (
226
398
  existing &&
227
399
  typeof existing === 'object' &&
@@ -230,9 +402,9 @@ class UseBaseBuilder {
230
402
  typeof config === 'object' &&
231
403
  !Array.isArray(config)
232
404
  ) {
233
- this.#system.ctx.config[kind] = deepMerge(existing, config);
405
+ system.ctx.config[kind] = deepMerge(existing, config);
234
406
  } else {
235
- this.#system.ctx.config[kind] = config;
407
+ system.ctx.config[kind] = config;
236
408
  }
237
409
  }
238
410
 
@@ -243,20 +415,23 @@ class UseBaseBuilder {
243
415
  // Merge any additional context
244
416
  if (ctx && typeof ctx === 'object' && !Array.isArray(ctx)) {
245
417
  if (ctx.config && typeof ctx.config === 'object' && !Array.isArray(ctx.config)) {
246
- if (!this.#system.ctx.config) {
247
- this.#system.ctx.config = {};
418
+ if (!system.ctx.config) {
419
+ system.ctx.config = {};
248
420
  }
249
- this.#system.ctx.config = deepMerge(this.#system.ctx.config, ctx.config);
421
+ system.ctx.config = deepMerge(system.ctx.config, ctx.config);
250
422
  }
251
423
  // Merge other ctx properties (shallow)
252
- Object.assign(this.#system.ctx, ctx);
424
+ Object.assign(system.ctx, ctx);
253
425
  }
254
426
 
255
427
  // Build the system
256
- await this.#system.build(ctx);
428
+ await system.build(ctx);
257
429
 
258
430
  // Return the system instance
259
- return this.#system;
431
+ return system;
260
432
  }
261
433
  }
262
434
 
435
+
436
+
437
+
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Mycelia Plugin System - Vue Builder Helpers
3
+ *
4
+ * Vue utilities for creating system builders.
5
+ */
6
+
7
+ /**
8
+ * createVueSystemBuilder - Create a reusable system builder function
9
+ *
10
+ * @param {string} name - System name
11
+ * @param {Function} configure - Configuration function: (builder) => builder
12
+ * @returns {Function} Build function: () => Promise<System>
13
+ *
14
+ * @example
15
+ * ```js
16
+ * import { useBase } from 'mycelia-kernel-plugin';
17
+ * import { createVueSystemBuilder } from 'mycelia-kernel-plugin/vue';
18
+ *
19
+ * const buildTodoSystem = createVueSystemBuilder('todo-app', (b) =>
20
+ * b
21
+ * .config('database', { host: 'localhost' })
22
+ * .use(useDatabase)
23
+ * .use(useListeners)
24
+ * );
25
+ *
26
+ * // Then use in plugin
27
+ * app.use(MyceliaPlugin, { build: buildTodoSystem });
28
+ * ```
29
+ */
30
+ export function createVueSystemBuilder(name, configure) {
31
+ return async function build() {
32
+ // Import useBase - users should have it available
33
+ // This avoids bundling issues by letting users import useBase themselves
34
+ const { useBase } = await import('../utils/use-base.js');
35
+ let builder = useBase(name);
36
+ builder = configure(builder);
37
+ return builder.build();
38
+ };
39
+ }
40
+
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Mycelia Plugin System - Vue Composable Generator
3
+ *
4
+ * Utility for generating custom composables for specific facets.
5
+ */
6
+
7
+ import { useFacet } from './index.js';
8
+
9
+ /**
10
+ * createFacetComposable - Generate a custom composable for a specific facet kind
11
+ *
12
+ * @param {string} kind - Facet kind identifier
13
+ * @returns {Function} Custom composable: () => import('vue').Ref<Object|null>
14
+ *
15
+ * @example
16
+ * ```js
17
+ * // In composables/todo.js
18
+ * import { createFacetComposable } from 'mycelia-kernel-plugin/vue';
19
+ *
20
+ * export const useTodos = createFacetComposable('todos');
21
+ * export const useAuth = createFacetComposable('auth');
22
+ *
23
+ * // In component
24
+ * export default {
25
+ * setup() {
26
+ * const todos = useTodos();
27
+ * // Use todos.value...
28
+ * }
29
+ * }
30
+ * ```
31
+ */
32
+ export function createFacetComposable(kind) {
33
+ return function useNamedFacet() {
34
+ return useFacet(kind);
35
+ };
36
+ }
37
+
@@ -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
+