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 +11 -5
- package/package.json +8 -5
- package/src/angular/builders.js +2 -0
- package/src/angular/helpers.js +2 -0
- package/src/angular/services.js +2 -0
- package/src/contract/contracts/listeners.contract.js +2 -0
- package/src/hooks/listeners/use-simple-listeners.js +172 -0
- package/src/index.js +1 -0
- package/src/manager/facet-manager.js +17 -3
- package/src/qwik/builders.js +2 -0
- package/src/qwik/queues.js +2 -0
- package/src/qwik/signals.js +2 -0
- package/src/react/README.md +2 -0
- package/src/solid/README.md +2 -0
- package/src/utils/use-base.js +2 -2
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
|
-
- **
|
|
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
|
-
-
|
|
354
|
-
-
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|
package/src/angular/builders.js
CHANGED
package/src/angular/helpers.js
CHANGED
package/src/angular/services.js
CHANGED
|
@@ -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
|
}
|
package/src/qwik/builders.js
CHANGED
package/src/qwik/queues.js
CHANGED
package/src/qwik/signals.js
CHANGED
package/src/react/README.md
CHANGED
package/src/solid/README.md
CHANGED
package/src/utils/use-base.js
CHANGED
|
@@ -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
|
-
|
|
251
|
-
|
|
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 {
|