mycelia-kernel-plugin 1.0.0 → 1.1.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 +94 -1
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/utils/use-base.js +262 -0
package/README.md
CHANGED
|
@@ -13,11 +13,16 @@ Mycelia Plugin System is a standalone plugin architecture extracted from [Myceli
|
|
|
13
13
|
- **Hot reloading** - Reload and extend plugins without full teardown
|
|
14
14
|
- **Facet contracts** - Runtime validation of plugin interfaces
|
|
15
15
|
- **Standalone mode** - Works without message system or other dependencies
|
|
16
|
+
- **Built-in hooks** - Ships with `useListeners` for event-driven architectures (see [Simple Event System Example](#simple-event-system-example)), plus `useQueue` and `useSpeak`
|
|
17
|
+
|
|
18
|
+
**Facets** are the concrete runtime capabilities produced by hooks and attached to the system.
|
|
16
19
|
|
|
17
20
|
## Quick Start
|
|
18
21
|
|
|
22
|
+
### Using useBase (Recommended)
|
|
23
|
+
|
|
19
24
|
```javascript
|
|
20
|
-
import {
|
|
25
|
+
import { useBase, createHook, Facet } from 'mycelia-kernel-plugin';
|
|
21
26
|
|
|
22
27
|
// Create a hook
|
|
23
28
|
const useDatabase = createHook({
|
|
@@ -51,6 +56,25 @@ const useDatabase = createHook({
|
|
|
51
56
|
}
|
|
52
57
|
});
|
|
53
58
|
|
|
59
|
+
// Create and use the system with fluent API
|
|
60
|
+
const system = await useBase('my-app')
|
|
61
|
+
.config('database', { host: 'localhost' })
|
|
62
|
+
.use(useDatabase)
|
|
63
|
+
.build();
|
|
64
|
+
|
|
65
|
+
// Use the plugin
|
|
66
|
+
const db = system.find('database');
|
|
67
|
+
await db.query('SELECT * FROM users');
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Using StandalonePluginSystem Directly
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
import { StandalonePluginSystem, createHook, Facet } from 'mycelia-kernel-plugin';
|
|
74
|
+
|
|
75
|
+
// Create a hook (same as above)
|
|
76
|
+
const useDatabase = createHook({ /* ... */ });
|
|
77
|
+
|
|
54
78
|
// Create and use the system
|
|
55
79
|
const system = new StandalonePluginSystem('my-app', {
|
|
56
80
|
config: {
|
|
@@ -67,12 +91,71 @@ const db = system.find('database');
|
|
|
67
91
|
await db.query('SELECT * FROM users');
|
|
68
92
|
```
|
|
69
93
|
|
|
94
|
+
### Simple Event System Example
|
|
95
|
+
|
|
96
|
+
Create an event-driven system with `useBase` and `useListeners`:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
import { useBase, useListeners } from 'mycelia-kernel-plugin';
|
|
100
|
+
|
|
101
|
+
// Create an event system
|
|
102
|
+
const eventSystem = await useBase('event-system')
|
|
103
|
+
.config('listeners', { registrationPolicy: 'multiple' })
|
|
104
|
+
.use(useListeners)
|
|
105
|
+
.build();
|
|
106
|
+
|
|
107
|
+
// Enable listeners
|
|
108
|
+
eventSystem.listeners.enableListeners();
|
|
109
|
+
|
|
110
|
+
// Register event handlers
|
|
111
|
+
eventSystem.listeners.on('user:created', (message) => {
|
|
112
|
+
console.log('User created:', message.body);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
eventSystem.listeners.on('user:updated', (message) => {
|
|
116
|
+
console.log('User updated:', message.body);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Emit events
|
|
120
|
+
eventSystem.listeners.emit('user:created', {
|
|
121
|
+
type: 'user:created',
|
|
122
|
+
body: { id: 1, name: 'John Doe' }
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
eventSystem.listeners.emit('user:updated', {
|
|
126
|
+
type: 'user:updated',
|
|
127
|
+
body: { id: 1, name: 'Jane Doe' }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Multiple handlers for the same event
|
|
131
|
+
eventSystem.listeners.on('order:placed', (message) => {
|
|
132
|
+
console.log('Order notification:', message.body);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
eventSystem.listeners.on('order:placed', (message) => {
|
|
136
|
+
console.log('Order logging:', message.body);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Both handlers will be called
|
|
140
|
+
eventSystem.listeners.emit('order:placed', {
|
|
141
|
+
type: 'order:placed',
|
|
142
|
+
body: { orderId: 123, total: 99.99 }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Cleanup
|
|
146
|
+
await eventSystem.dispose();
|
|
147
|
+
```
|
|
148
|
+
|
|
70
149
|
## Installation
|
|
71
150
|
|
|
72
151
|
```bash
|
|
73
152
|
npm install mycelia-kernel-plugin
|
|
74
153
|
```
|
|
75
154
|
|
|
155
|
+
## What This System Is Not
|
|
156
|
+
|
|
157
|
+
This system intentionally does not provide dependency injection containers, service locators, or global mutable state. It focuses on explicit lifecycle management and composable plugin architecture rather than implicit dependency resolution or shared global state.
|
|
158
|
+
|
|
76
159
|
## Features
|
|
77
160
|
|
|
78
161
|
### Hook System
|
|
@@ -141,6 +224,8 @@ The `reload()` method:
|
|
|
141
224
|
- Allows adding more hooks and rebuilding
|
|
142
225
|
- Perfect for development and hot-reload scenarios
|
|
143
226
|
|
|
227
|
+
**Note:** Persistent external state (e.g., database contents, file system state) is not automatically reverted. The `reload()` method only manages the plugin system's internal state.
|
|
228
|
+
|
|
144
229
|
### Facet Contracts
|
|
145
230
|
Validate plugin interfaces at build time:
|
|
146
231
|
|
|
@@ -156,6 +241,11 @@ const databaseContract = createFacetContract({
|
|
|
156
241
|
// Contract is automatically enforced during build
|
|
157
242
|
```
|
|
158
243
|
|
|
244
|
+
If a facet doesn't satisfy its contract, build fails with a clear error:
|
|
245
|
+
```
|
|
246
|
+
Error: FacetContract 'database': facet is missing required methods: close
|
|
247
|
+
```
|
|
248
|
+
|
|
159
249
|
## Architecture
|
|
160
250
|
|
|
161
251
|
```
|
|
@@ -181,6 +271,7 @@ StandalonePluginSystem
|
|
|
181
271
|
|
|
182
272
|
- **`createHook()`** - Create a plugin hook
|
|
183
273
|
- **`createFacetContract()`** - Create a facet contract
|
|
274
|
+
- **`useBase()`** - Fluent API builder for StandalonePluginSystem
|
|
184
275
|
|
|
185
276
|
### Utilities
|
|
186
277
|
|
|
@@ -193,6 +284,7 @@ Comprehensive documentation is available in the [`docs/`](./docs/) directory:
|
|
|
193
284
|
|
|
194
285
|
- **[Getting Started Guide](./docs/getting-started/README.md)** - Quick start with examples
|
|
195
286
|
- **[Hooks and Facets Overview](./docs/core-concepts/HOOKS-AND-FACETS-OVERVIEW.md)** - Core concepts
|
|
287
|
+
- **[Built-in Hooks](./docs/hooks/README.md)** - Documentation for `useListeners`, `useQueue`, and `useSpeak`
|
|
196
288
|
- **[Standalone Plugin System](./docs/standalone/STANDALONE-PLUGIN-SYSTEM.md)** - Complete usage guide
|
|
197
289
|
- **[Documentation Index](./docs/README.md)** - Full documentation index
|
|
198
290
|
|
|
@@ -204,6 +296,7 @@ See the `examples/` directory for:
|
|
|
204
296
|
- Lifecycle management
|
|
205
297
|
- Contract validation
|
|
206
298
|
- Hot reloading
|
|
299
|
+
- useBase fluent API
|
|
207
300
|
|
|
208
301
|
## CLI Tool
|
|
209
302
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export { useSpeak } from './hooks/speak/use-speak.js';
|
|
|
38
38
|
export { createLogger, createSubsystemLogger } from './utils/logger.js';
|
|
39
39
|
export { getDebugFlag } from './utils/debug-flag.js';
|
|
40
40
|
export { findFacet } from './utils/find-facet.js';
|
|
41
|
+
export { useBase } from './utils/use-base.js';
|
|
41
42
|
export {
|
|
42
43
|
parseVersion,
|
|
43
44
|
isValidSemver,
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { StandalonePluginSystem } from '../system/standalone-plugin-system.js';
|
|
2
|
+
import { deepMerge } from '../builder/context-resolver.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* useBase - Fluent API Builder for StandalonePluginSystem
|
|
6
|
+
*
|
|
7
|
+
* Provides a convenient, chainable API for creating and configuring
|
|
8
|
+
* StandalonePluginSystem instances.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} name - Unique name for the plugin system
|
|
11
|
+
* @param {Object} [options={}] - Initial configuration options
|
|
12
|
+
* @param {Object} [options.config={}] - Initial configuration object keyed by facet kind
|
|
13
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
14
|
+
* @param {Array} [options.defaultHooks=[]] - Optional default hooks to install
|
|
15
|
+
* @returns {UseBaseBuilder} Builder instance with fluent API
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```javascript
|
|
19
|
+
* import { useBase } from 'mycelia-kernel-plugin';
|
|
20
|
+
* import { useDatabase, useCache } from './hooks';
|
|
21
|
+
*
|
|
22
|
+
* const system = await useBase('my-app')
|
|
23
|
+
* .config('database', { host: 'localhost' })
|
|
24
|
+
* .config('cache', { ttl: 3600 })
|
|
25
|
+
* .use(useDatabase)
|
|
26
|
+
* .use(useCache)
|
|
27
|
+
* .onInit(async (api, ctx) => {
|
|
28
|
+
* console.log('System initialized');
|
|
29
|
+
* })
|
|
30
|
+
* .build();
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function useBase(name, options = {}) {
|
|
34
|
+
if (!name || typeof name !== 'string') {
|
|
35
|
+
throw new Error('useBase: name must be a non-empty string');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create the system instance
|
|
39
|
+
const system = new StandalonePluginSystem(name, options);
|
|
40
|
+
|
|
41
|
+
// Create builder with fluent API
|
|
42
|
+
return new UseBaseBuilder(system);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* UseBaseBuilder - Fluent API builder for StandalonePluginSystem
|
|
47
|
+
*
|
|
48
|
+
* Provides chainable methods for configuring and building the system.
|
|
49
|
+
*/
|
|
50
|
+
class UseBaseBuilder {
|
|
51
|
+
#system;
|
|
52
|
+
#pendingConfig = {};
|
|
53
|
+
|
|
54
|
+
constructor(system) {
|
|
55
|
+
this.#system = system;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Register a hook
|
|
60
|
+
*
|
|
61
|
+
* @param {Function} hook - Hook function to register
|
|
62
|
+
* @returns {UseBaseBuilder} This builder for chaining
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```javascript
|
|
66
|
+
* builder.use(useDatabase).use(useCache);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
use(hook) {
|
|
70
|
+
if (typeof hook !== 'function') {
|
|
71
|
+
throw new Error('useBase.use: hook must be a function');
|
|
72
|
+
}
|
|
73
|
+
this.#system.use(hook);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Conditionally register a hook
|
|
79
|
+
*
|
|
80
|
+
* @param {boolean} condition - Whether to register the hook
|
|
81
|
+
* @param {Function} hook - Hook function to register if condition is true
|
|
82
|
+
* @returns {UseBaseBuilder} This builder for chaining
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```javascript
|
|
86
|
+
* builder.useIf(process.env.ENABLE_CACHE === 'true', useCache);
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
useIf(condition, hook) {
|
|
90
|
+
if (condition) {
|
|
91
|
+
return this.use(hook);
|
|
92
|
+
}
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Add or update configuration for a specific facet kind
|
|
98
|
+
*
|
|
99
|
+
* Configurations are merged when possible (deep merge for objects).
|
|
100
|
+
*
|
|
101
|
+
* @param {string} kind - Facet kind identifier (e.g., 'database', 'cache')
|
|
102
|
+
* @param {*} config - Configuration value for this facet kind
|
|
103
|
+
* @returns {UseBaseBuilder} This builder for chaining
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```javascript
|
|
107
|
+
* builder
|
|
108
|
+
* .config('database', { host: 'localhost', port: 5432 })
|
|
109
|
+
* .config('cache', { ttl: 3600 });
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```javascript
|
|
114
|
+
* // Merge configurations
|
|
115
|
+
* builder
|
|
116
|
+
* .config('database', { host: 'localhost' })
|
|
117
|
+
* .config('database', { port: 5432 }); // Merges with existing
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
config(kind, config) {
|
|
121
|
+
if (!kind || typeof kind !== 'string') {
|
|
122
|
+
throw new Error('useBase.config: kind must be a non-empty string');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Initialize config object if needed
|
|
126
|
+
if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
|
|
127
|
+
this.#system.ctx.config = {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get existing config for this kind
|
|
131
|
+
const existingConfig = this.#system.ctx.config[kind];
|
|
132
|
+
const pendingConfig = this.#pendingConfig[kind];
|
|
133
|
+
|
|
134
|
+
// Determine the base config (existing or pending)
|
|
135
|
+
const baseConfig = pendingConfig !== undefined ? pendingConfig : existingConfig;
|
|
136
|
+
|
|
137
|
+
// Merge if both are objects
|
|
138
|
+
if (
|
|
139
|
+
baseConfig &&
|
|
140
|
+
typeof baseConfig === 'object' &&
|
|
141
|
+
!Array.isArray(baseConfig) &&
|
|
142
|
+
config &&
|
|
143
|
+
typeof config === 'object' &&
|
|
144
|
+
!Array.isArray(config)
|
|
145
|
+
) {
|
|
146
|
+
// Deep merge
|
|
147
|
+
this.#pendingConfig[kind] = deepMerge(baseConfig, config);
|
|
148
|
+
} else {
|
|
149
|
+
// Override
|
|
150
|
+
this.#pendingConfig[kind] = config;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add an initialization callback
|
|
158
|
+
*
|
|
159
|
+
* @param {Function} callback - Callback function: (api, ctx) => Promise<void> | void
|
|
160
|
+
* @returns {UseBaseBuilder} This builder for chaining
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```javascript
|
|
164
|
+
* builder.onInit(async (api, ctx) => {
|
|
165
|
+
* console.log('System initialized:', api.name);
|
|
166
|
+
* });
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
onInit(callback) {
|
|
170
|
+
if (typeof callback !== 'function') {
|
|
171
|
+
throw new Error('useBase.onInit: callback must be a function');
|
|
172
|
+
}
|
|
173
|
+
this.#system.onInit(callback);
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Add a disposal callback
|
|
179
|
+
*
|
|
180
|
+
* @param {Function} callback - Callback function: () => Promise<void> | void
|
|
181
|
+
* @returns {UseBaseBuilder} This builder for chaining
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```javascript
|
|
185
|
+
* builder.onDispose(async () => {
|
|
186
|
+
* console.log('System disposed');
|
|
187
|
+
* });
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
onDispose(callback) {
|
|
191
|
+
if (typeof callback !== 'function') {
|
|
192
|
+
throw new Error('useBase.onDispose: callback must be a function');
|
|
193
|
+
}
|
|
194
|
+
this.#system.onDispose(callback);
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build the system
|
|
200
|
+
*
|
|
201
|
+
* This method is required and must be called to build the system.
|
|
202
|
+
* It applies any pending configurations and builds the system.
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} [ctx={}] - Additional context to pass to build
|
|
205
|
+
* @returns {Promise<StandalonePluginSystem>} The built system instance
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```javascript
|
|
209
|
+
* const system = await useBase('my-app')
|
|
210
|
+
* .use(useDatabase)
|
|
211
|
+
* .build();
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
async build(ctx = {}) {
|
|
215
|
+
// Apply pending configurations
|
|
216
|
+
if (Object.keys(this.#pendingConfig).length > 0) {
|
|
217
|
+
// Merge pending config into system config
|
|
218
|
+
if (!this.#system.ctx.config || typeof this.#system.ctx.config !== 'object') {
|
|
219
|
+
this.#system.ctx.config = {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Deep merge pending configs
|
|
223
|
+
for (const [kind, config] of Object.entries(this.#pendingConfig)) {
|
|
224
|
+
const existing = this.#system.ctx.config[kind];
|
|
225
|
+
if (
|
|
226
|
+
existing &&
|
|
227
|
+
typeof existing === 'object' &&
|
|
228
|
+
!Array.isArray(existing) &&
|
|
229
|
+
config &&
|
|
230
|
+
typeof config === 'object' &&
|
|
231
|
+
!Array.isArray(config)
|
|
232
|
+
) {
|
|
233
|
+
this.#system.ctx.config[kind] = deepMerge(existing, config);
|
|
234
|
+
} else {
|
|
235
|
+
this.#system.ctx.config[kind] = config;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Clear pending configs
|
|
240
|
+
this.#pendingConfig = {};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Merge any additional context
|
|
244
|
+
if (ctx && typeof ctx === 'object' && !Array.isArray(ctx)) {
|
|
245
|
+
if (ctx.config && typeof ctx.config === 'object' && !Array.isArray(ctx.config)) {
|
|
246
|
+
if (!this.#system.ctx.config) {
|
|
247
|
+
this.#system.ctx.config = {};
|
|
248
|
+
}
|
|
249
|
+
this.#system.ctx.config = deepMerge(this.#system.ctx.config, ctx.config);
|
|
250
|
+
}
|
|
251
|
+
// Merge other ctx properties (shallow)
|
|
252
|
+
Object.assign(this.#system.ctx, ctx);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Build the system
|
|
256
|
+
await this.#system.build(ctx);
|
|
257
|
+
|
|
258
|
+
// Return the system instance
|
|
259
|
+
return this.#system;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|