mycelia-kernel-plugin 1.4.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/README.md CHANGED
@@ -320,6 +320,7 @@ Comprehensive documentation is available in the [`docs/`](./docs/) directory:
320
320
  - **[Svelte Bindings](./docs/svelte/README.md)** - Svelte integration utilities (`setMyceliaSystem`, `useFacet`, `useListener`) ⭐
321
321
  - **[Angular Bindings](./docs/angular/README.md)** - Angular integration utilities (`MyceliaService`, `useFacet`, `useListener`) ⭐
322
322
  - **[Qwik Bindings](./docs/qwik/README.md)** - Qwik integration utilities (`MyceliaProvider`, `useFacet`, `useListener`) ⭐
323
+ - **[Solid.js Bindings](./src/solid/README.md)** - Solid.js integration utilities (`MyceliaProvider`, `useFacet`, `useListener`) ⭐
323
324
  - **[Standalone Plugin System](./docs/standalone/STANDALONE-PLUGIN-SYSTEM.md)** - Complete usage guide
324
325
  - **[Documentation Index](./docs/README.md)** - Full documentation index
325
326
 
@@ -347,14 +348,13 @@ See the `examples/` directory for:
347
348
  - Composition API integration with reactive state management
348
349
 
349
350
  - **[Svelte Todo App](./examples/svelte-todo/README.md)** ⭐ – A complete Svelte example demonstrating:
350
- - **[Solid.js Todo App](./examples/solid-todo/README.md)** A complete Solid.js example demonstrating:
351
- - **Framework-agnostic plugins** - Uses the same shared plugin code as React, Vue, and Svelte examples
351
+ - **Framework-agnostic plugins** - Uses the same shared plugin code as React, Vue, and other examples
352
352
  - Event-driven state synchronization (`todos:changed` events)
353
- - Solid.js bindings (`MyceliaProvider`, `useFacet`, `useListener`)
354
- - Signal-based reactivity with automatic updates
353
+ - Svelte bindings (`setMyceliaSystem`, `useFacet`, `useListener`)
354
+ - Svelte stores for reactive state management
355
355
 
356
356
  - **[Angular Todo App](./examples/angular-todo/README.md)** ⭐ – A complete Angular example demonstrating:
357
- - **Framework-agnostic plugins** - Uses the same shared plugin code as React, Vue, and Svelte examples
357
+ - **Framework-agnostic plugins** - Uses the same shared plugin code as React, Vue, Svelte, and other examples
358
358
  - Event-driven state synchronization (`todos:changed` events)
359
359
  - Angular bindings (`MyceliaService`, `useFacet`, `useListener`)
360
360
  - RxJS observables for reactive state management
@@ -365,6 +365,12 @@ See the `examples/` directory for:
365
365
  - Qwik bindings (`MyceliaProvider`, `useFacet`, `useListener`)
366
366
  - Qwik signals for reactive state management
367
367
 
368
+ - **[Solid.js Todo App](./examples/solid-todo/README.md)** ⭐ – A complete Solid.js example demonstrating:
369
+ - **Framework-agnostic plugins** - Uses the same shared plugin code as React, Vue, Svelte, Angular, and Qwik examples
370
+ - Event-driven state synchronization (`todos:changed` events)
371
+ - Solid.js bindings (`MyceliaProvider`, `useFacet`, `useListener`)
372
+ - Signal-based reactivity with automatic updates
373
+
368
374
  All six examples use the **exact same Mycelia plugin code** from `examples/todo-shared/`, proving that plugins are truly framework-independent. Write your domain logic once, use it everywhere!
369
375
 
370
376
  ## CLI Tool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycelia-kernel-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A sophisticated, framework-agnostic plugin system with transaction safety, lifecycle management, and official bindings for React, Vue 3, Svelte, Angular, Qwik, and Solid.js",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -57,13 +57,13 @@
57
57
  "test:coverage": "vitest run --coverage"
58
58
  },
59
59
  "peerDependencies": {
60
- "react": ">=16.8.0",
61
- "vue": ">=3.0.0",
62
- "svelte": ">=3.0.0",
63
60
  "@angular/core": ">=15.0.0",
64
61
  "@builder.io/qwik": ">=1.0.0",
62
+ "react": ">=16.8.0",
63
+ "rxjs": ">=7.0.0",
65
64
  "solid-js": ">=1.0.0",
66
- "rxjs": ">=7.0.0"
65
+ "svelte": ">=3.0.0",
66
+ "vue": ">=3.0.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@eslint/js": "^9.36.0",
@@ -73,5 +73,8 @@
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=18.0.0"
76
+ },
77
+ "dependencies": {
78
+ "mitt": "^3.0.1"
76
79
  }
77
80
  }
@@ -35,3 +35,5 @@ export function createAngularSystemBuilder(name, configure) {
35
35
  }
36
36
 
37
37
 
38
+
39
+
@@ -100,3 +100,5 @@ export function useEventStream(myceliaService, eventName, options = {}) {
100
100
  }
101
101
 
102
102
 
103
+
104
+
@@ -30,3 +30,5 @@ export function createFacetService(kind) {
30
30
  }
31
31
 
32
32
 
33
+
34
+
@@ -19,6 +19,7 @@ import { createFacetContract } from '../facet-contract.js';
19
19
  * Required methods:
20
20
  * - on: Register a listener for a specific message path
21
21
  * - off: Unregister a listener for a specific message path
22
+ * - emit: Emit an event to listeners for a specific path
22
23
  * - hasListeners: Check if listeners are enabled
23
24
  * - enableListeners: Enable listeners and initialize ListenerManager
24
25
  * - disableListeners: Disable listeners (but keep manager instance)
@@ -37,6 +38,7 @@ export const listenersContract = createFacetContract({
37
38
  requiredMethods: [
38
39
  'on',
39
40
  'off',
41
+ 'emit',
40
42
  'hasListeners',
41
43
  'enableListeners',
42
44
  'disableListeners'
@@ -0,0 +1,172 @@
1
+ /**
2
+ * useSimpleListeners Hook
3
+ *
4
+ * Provides simple listener management functionality to subsystems using mitt.
5
+ * A lightweight alternative to useListeners that wraps the mitt event emitter.
6
+ *
7
+ * @param {Object} ctx - Context object containing config.listeners for listener configuration
8
+ * @param {Object} api - Subsystem API being built
9
+ * @param {BaseSubsystem} subsystem - Subsystem instance
10
+ * @returns {Facet} Facet object with listener methods
11
+ */
12
+ import mitt from 'mitt';
13
+ import { Facet } from '../../core/facet.js';
14
+ import { createHook } from '../../core/create-hook.js';
15
+ import { getDebugFlag } from '../../utils/debug-flag.js';
16
+ import { createLogger } from '../../utils/logger.js';
17
+
18
+ export const useSimpleListeners = createHook({
19
+ kind: 'listeners',
20
+ version: '1.0.0',
21
+ overwrite: false,
22
+ required: [],
23
+ attach: true,
24
+ source: import.meta.url,
25
+ contract: 'listeners',
26
+ // eslint-disable-next-line no-unused-vars
27
+ fn: (ctx, api, _subsystem) => {
28
+ const { name } = api;
29
+ const config = ctx.config?.listeners || {};
30
+ const debug = getDebugFlag(config, ctx);
31
+
32
+ // Listeners are optional - can be enabled/disabled
33
+ let emitter = null;
34
+ let listenersEnabled = false;
35
+
36
+ return new Facet('listeners', { attach: true, source: import.meta.url, contract: 'listeners' })
37
+ .add({
38
+
39
+ /**
40
+ * Check if listeners are enabled
41
+ * @returns {boolean} True if listeners are enabled
42
+ */
43
+ hasListeners() {
44
+ return listenersEnabled && emitter !== null;
45
+ },
46
+
47
+ /**
48
+ * Enable listeners
49
+ * @param {Object} [listenerOptions={}] - Options (currently unused, for API compatibility)
50
+ */
51
+ enableListeners(listenerOptions = {}) {
52
+ if (emitter === null) {
53
+ emitter = mitt();
54
+ }
55
+ listenersEnabled = true;
56
+ },
57
+
58
+ /**
59
+ * Disable listeners
60
+ */
61
+ disableListeners() {
62
+ listenersEnabled = false;
63
+ },
64
+
65
+ /**
66
+ * Register a listener for a specific path
67
+ * @param {string} path - Message path to listen for
68
+ * @param {Function} handler - Handler function
69
+ * @param {Object} [options={}] - Registration options (for API compatibility)
70
+ * @returns {boolean} Success status
71
+ */
72
+ on(path, handler, options = {}) {
73
+ // Check if listeners are enabled
74
+ if (!listenersEnabled || emitter === null) {
75
+ const runtimeDebug = options.debug !== undefined ? options.debug : debug;
76
+ if (runtimeDebug) {
77
+ const runtimeLogger = createLogger(runtimeDebug, `useSimpleListeners ${name}`);
78
+ runtimeLogger.warn('Cannot register listener - listeners not enabled');
79
+ }
80
+ return false;
81
+ }
82
+
83
+ // Validate handler is a function
84
+ if (typeof handler !== 'function') {
85
+ if (debug) {
86
+ const runtimeLogger = createLogger(debug, `useSimpleListeners ${name}`);
87
+ runtimeLogger.warn('Handler must be a function');
88
+ }
89
+ return false;
90
+ }
91
+
92
+ // Register with mitt
93
+ emitter.on(path, handler);
94
+ return true;
95
+ },
96
+
97
+ /**
98
+ * Unregister a listener for a specific path
99
+ * @param {string} path - Message path
100
+ * @param {Function} handler - Handler function to remove
101
+ * @param {Object} [options={}] - Unregistration options (for API compatibility)
102
+ * @returns {boolean} Success status
103
+ */
104
+ off(path, handler, options = {}) {
105
+ // Check if listeners are enabled
106
+ if (!listenersEnabled || emitter === null) {
107
+ const runtimeDebug = options.debug !== undefined ? options.debug : debug;
108
+ if (runtimeDebug) {
109
+ const runtimeLogger = createLogger(runtimeDebug, `useSimpleListeners ${name}`);
110
+ runtimeLogger.warn('Cannot unregister listener - listeners not enabled');
111
+ }
112
+ return false;
113
+ }
114
+
115
+ // Validate handler is a function
116
+ if (typeof handler !== 'function') {
117
+ if (debug) {
118
+ const runtimeLogger = createLogger(debug, `useSimpleListeners ${name}`);
119
+ runtimeLogger.warn('Handler must be a function');
120
+ }
121
+ return false;
122
+ }
123
+
124
+ // Unregister from mitt
125
+ emitter.off(path, handler);
126
+ return true;
127
+ },
128
+
129
+ /**
130
+ * Emit an event to listeners for a specific path
131
+ * @param {string} path - Message path to emit to
132
+ * @param {any} message - Message/data to send to listeners
133
+ * @returns {number} Number of listeners notified, or 0 if listeners not enabled
134
+ *
135
+ * @example
136
+ * // Emit event to listeners
137
+ * const notified = subsystem.listeners.emit('layers/create', message);
138
+ */
139
+ emit(path, message) {
140
+ // Check if listeners are enabled
141
+ if (!listenersEnabled || emitter === null) {
142
+ if (debug) {
143
+ const runtimeLogger = createLogger(debug, `useSimpleListeners ${name}`);
144
+ runtimeLogger.warn('Cannot emit event - listeners not enabled');
145
+ }
146
+ return 0;
147
+ }
148
+
149
+ // Get listeners count before emitting (mitt doesn't return count)
150
+ const listeners = emitter.all.get(path);
151
+ const count = listeners ? listeners.length : 0;
152
+
153
+ // Emit to mitt
154
+ emitter.emit(path, message);
155
+
156
+ return count;
157
+ },
158
+
159
+ /**
160
+ * Expose emitter property for direct access
161
+ * Returns null if listeners are not enabled
162
+ */
163
+ get listeners() {
164
+ return emitter;
165
+ },
166
+
167
+ // Expose emitter for internal use (compatible with listeners contract)
168
+ _listenerManager: () => emitter
169
+ });
170
+ }
171
+ });
172
+
package/src/index.js CHANGED
@@ -31,6 +31,7 @@ export * from './contract/contracts/index.js';
31
31
 
32
32
  // Hook exports
33
33
  export { useListeners } from './hooks/listeners/use-listeners.js';
34
+ export { useSimpleListeners } from './hooks/listeners/use-simple-listeners.js';
34
35
  export { useQueue } from './hooks/queue/use-queue.js';
35
36
  export { useSpeak } from './hooks/speak/use-speak.js';
36
37
 
@@ -510,12 +510,20 @@ export class FacetManager {
510
510
  }
511
511
 
512
512
  clear() {
513
- // Dispose all facets before clearing
513
+ // Dispose all facets before clearing (synchronous best-effort)
514
+ // Note: dispose() is async, but clear() is synchronous
515
+ // For async disposal, use disposeAll() instead
514
516
  for (const [, facets] of this.#facets.entries()) {
515
517
  if (Array.isArray(facets)) {
516
518
  for (const facet of facets) {
517
519
  try {
518
- facet?.dispose?.(this.#subsystem);
520
+ const disposeResult = facet?.dispose?.(this.#subsystem);
521
+ // If dispose returns a promise, catch any errors to prevent unhandled rejections
522
+ if (disposeResult && typeof disposeResult.catch === 'function') {
523
+ disposeResult.catch(() => {
524
+ // Best-effort disposal - errors are expected and handled
525
+ });
526
+ }
519
527
  } catch {
520
528
  // Best-effort disposal
521
529
  }
@@ -523,7 +531,13 @@ export class FacetManager {
523
531
  } else {
524
532
  // Legacy: single facet
525
533
  try {
526
- facets?.dispose?.(this.#subsystem);
534
+ const disposeResult = facets?.dispose?.(this.#subsystem);
535
+ // If dispose returns a promise, catch any errors to prevent unhandled rejections
536
+ if (disposeResult && typeof disposeResult.catch === 'function') {
537
+ disposeResult.catch(() => {
538
+ // Best-effort disposal - errors are expected and handled
539
+ });
540
+ }
527
541
  } catch {
528
542
  // Best-effort disposal
529
543
  }
@@ -37,3 +37,5 @@ export function createQwikSystemBuilder(name, configure) {
37
37
  }
38
38
 
39
39
 
40
+
41
+
@@ -85,3 +85,5 @@ export function useQueueDrain(options = {}) {
85
85
  }
86
86
 
87
87
 
88
+
89
+
@@ -30,3 +30,5 @@ export function createFacetSignal(kind) {
30
30
  }
31
31
 
32
32
 
33
+
34
+
@@ -175,3 +175,5 @@ See the main [README.md](../../README.md) and [examples](../../examples/) direct
175
175
 
176
176
 
177
177
 
178
+
179
+
@@ -67,3 +67,5 @@ function MyComponent() {
67
67
  - **Automatic Cleanup** - Listeners and effects cleaned up automatically
68
68
  - **Solid Signals** - Reactive state with Solid.js signals
69
69
 
70
+
71
+
@@ -247,8 +247,8 @@ class UseBaseBuilder {
247
247
  // Get existing config for this kind (from pending or system if created)
248
248
  let existingConfig;
249
249
  if (this.#system) {
250
- if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
251
- this.#system.ctx.config = {};
250
+ if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
251
+ this.#system.ctx.config = {};
252
252
  }
253
253
  existingConfig = this.#system.ctx.config[kind];
254
254
  } else {