mycelia-kernel-plugin 1.0.0 → 1.2.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 +99 -1
- package/package.json +6 -2
- package/src/index.js +1 -0
- package/src/react/README.md +174 -0
- package/src/react/index.js +401 -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,8 @@ 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`
|
|
288
|
+
- **[React Bindings](./docs/react/README.md)** - React integration utilities
|
|
196
289
|
- **[Standalone Plugin System](./docs/standalone/STANDALONE-PLUGIN-SYSTEM.md)** - Complete usage guide
|
|
197
290
|
- **[Documentation Index](./docs/README.md)** - Full documentation index
|
|
198
291
|
|
|
@@ -204,6 +297,11 @@ See the `examples/` directory for:
|
|
|
204
297
|
- Lifecycle management
|
|
205
298
|
- Contract validation
|
|
206
299
|
- Hot reloading
|
|
300
|
+
- useBase fluent API
|
|
301
|
+
- **React Todo App** – A real-world example showing:
|
|
302
|
+
- Domain logic as a Mycelia facet (`useTodos` hook)
|
|
303
|
+
- Event-driven state synchronization (`todos:changed` events)
|
|
304
|
+
- React bindings (`MyceliaProvider`, `useFacet`, `useListener`)
|
|
207
305
|
|
|
208
306
|
## CLI Tool
|
|
209
307
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mycelia-kernel-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A sophisticated, dependency-aware plugin system with transaction safety and lifecycle management",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"./builder": "./src/builder/index.js",
|
|
35
35
|
"./system": "./src/system/index.js",
|
|
36
36
|
"./contract": "./src/contract/index.js",
|
|
37
|
-
"./contract/contracts": "./src/contract/contracts/index.js"
|
|
37
|
+
"./contract/contracts": "./src/contract/contracts/index.js",
|
|
38
|
+
"./react": "./src/react/index.js"
|
|
38
39
|
},
|
|
39
40
|
"files": [
|
|
40
41
|
"src/",
|
|
@@ -50,6 +51,9 @@
|
|
|
50
51
|
"test:watch": "vitest watch",
|
|
51
52
|
"test:coverage": "vitest run --coverage"
|
|
52
53
|
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"react": ">=16.8.0"
|
|
56
|
+
},
|
|
53
57
|
"devDependencies": {
|
|
54
58
|
"@eslint/js": "^9.36.0",
|
|
55
59
|
"eslint": "^9.36.0",
|
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,174 @@
|
|
|
1
|
+
# Mycelia Plugin System - React Bindings
|
|
2
|
+
|
|
3
|
+
React utilities that make the Mycelia Plugin System feel natural inside React applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install mycelia-kernel-plugin react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { MyceliaProvider, useFacet, useListener } from 'mycelia-kernel-plugin/react';
|
|
15
|
+
import { useBase, useListeners, useDatabase } from 'mycelia-kernel-plugin';
|
|
16
|
+
|
|
17
|
+
// Create system builder
|
|
18
|
+
const buildSystem = () =>
|
|
19
|
+
useBase('my-app')
|
|
20
|
+
.config('database', { host: 'localhost' })
|
|
21
|
+
.use(useDatabase)
|
|
22
|
+
.use(useListeners)
|
|
23
|
+
.build();
|
|
24
|
+
|
|
25
|
+
// Bootstrap your app
|
|
26
|
+
function App() {
|
|
27
|
+
return (
|
|
28
|
+
<MyceliaProvider build={buildSystem}>
|
|
29
|
+
<MyComponent />
|
|
30
|
+
</MyceliaProvider>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use in components
|
|
35
|
+
function MyComponent() {
|
|
36
|
+
const db = useFacet('database');
|
|
37
|
+
const system = useMycelia();
|
|
38
|
+
|
|
39
|
+
useListener('user:created', (msg) => {
|
|
40
|
+
console.log('User created:', msg.body);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Use db, system, etc.
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API Reference
|
|
48
|
+
|
|
49
|
+
### Core Bindings
|
|
50
|
+
|
|
51
|
+
#### `MyceliaProvider`
|
|
52
|
+
|
|
53
|
+
Provides Mycelia system to React tree.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<MyceliaProvider
|
|
57
|
+
build={() => useBase('app').use(useDatabase).build()}
|
|
58
|
+
fallback={<Loading />} // Optional
|
|
59
|
+
>
|
|
60
|
+
<App />
|
|
61
|
+
</MyceliaProvider>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### `useMycelia()`
|
|
65
|
+
|
|
66
|
+
Get the Mycelia system from context.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
const system = useMycelia();
|
|
70
|
+
const db = system.find('database');
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### `useFacet(kind)`
|
|
74
|
+
|
|
75
|
+
Get a facet by kind.
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
const db = useFacet('database');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Listener Helpers
|
|
82
|
+
|
|
83
|
+
#### `useListener(eventName, handler, deps?)`
|
|
84
|
+
|
|
85
|
+
Register an event listener with automatic cleanup.
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
useListener('todo:created', (msg) => {
|
|
89
|
+
console.log('Todo created:', msg.body);
|
|
90
|
+
}, []); // deps array
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### `useEventStream(eventName, options?)`
|
|
94
|
+
|
|
95
|
+
Subscribe to events and keep them in React state.
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
// Latest event
|
|
99
|
+
const latestEvent = useEventStream('todo:created');
|
|
100
|
+
|
|
101
|
+
// Accumulated events
|
|
102
|
+
const allEvents = useEventStream('todo:created', { accumulate: true });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Queue Helpers
|
|
106
|
+
|
|
107
|
+
#### `useQueueStatus()`
|
|
108
|
+
|
|
109
|
+
Get queue status with reactive updates.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
const status = useQueueStatus();
|
|
113
|
+
// { size, capacity, utilization, isFull }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### `useQueueDrain(options?)`
|
|
117
|
+
|
|
118
|
+
Automatically drain queue on mount.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
useQueueDrain({
|
|
122
|
+
interval: 100,
|
|
123
|
+
onMessage: (msg, options) => {
|
|
124
|
+
console.log('Processed:', msg);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Builder Helpers
|
|
130
|
+
|
|
131
|
+
#### `createReactSystemBuilder(name, configure)`
|
|
132
|
+
|
|
133
|
+
Create a reusable system builder function.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const buildTodoSystem = createReactSystemBuilder('todo-app', (b) =>
|
|
137
|
+
b
|
|
138
|
+
.config('database', { host: 'localhost' })
|
|
139
|
+
.use(useDatabase)
|
|
140
|
+
.use(useListeners)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
<MyceliaProvider build={buildTodoSystem}>
|
|
144
|
+
<App />
|
|
145
|
+
</MyceliaProvider>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Facet Hook Generator
|
|
149
|
+
|
|
150
|
+
#### `createFacetHook(kind)`
|
|
151
|
+
|
|
152
|
+
Generate a custom hook for a specific facet kind.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
// In bindings/todo-hooks.ts
|
|
156
|
+
export const useTodoStore = createFacetHook('todoStore');
|
|
157
|
+
export const useAuth = createFacetHook('auth');
|
|
158
|
+
|
|
159
|
+
// In component
|
|
160
|
+
function TodoList() {
|
|
161
|
+
const todoStore = useTodoStore();
|
|
162
|
+
// Use todoStore...
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
See the main [README.md](../../README.md) and [examples](../../examples/) directory for more examples.
|
|
169
|
+
|
|
170
|
+
## Requirements
|
|
171
|
+
|
|
172
|
+
- React >= 16.8.0 (for hooks support)
|
|
173
|
+
- Mycelia Plugin System (included)
|
|
174
|
+
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mycelia Plugin System - React Bindings
|
|
3
|
+
*
|
|
4
|
+
* React utilities that make the Mycelia Plugin System feel natural
|
|
5
|
+
* inside React applications.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { MyceliaProvider, useFacet, useListener } from 'mycelia-kernel-plugin/react';
|
|
10
|
+
*
|
|
11
|
+
* const buildSystem = () => useBase('app')
|
|
12
|
+
* .use(useDatabase)
|
|
13
|
+
* .build();
|
|
14
|
+
*
|
|
15
|
+
* function App() {
|
|
16
|
+
* return (
|
|
17
|
+
* <MyceliaProvider build={buildSystem}>
|
|
18
|
+
* <MyComponent />
|
|
19
|
+
* </MyceliaProvider>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* function MyComponent() {
|
|
24
|
+
* const db = useFacet('database');
|
|
25
|
+
* useListener('user:created', (msg) => console.log(msg));
|
|
26
|
+
* // ...
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import React, {
|
|
32
|
+
createContext,
|
|
33
|
+
useContext,
|
|
34
|
+
useEffect,
|
|
35
|
+
useRef,
|
|
36
|
+
useState,
|
|
37
|
+
useCallback
|
|
38
|
+
} from 'react';
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Core Bindings: Provider + Basic Hooks
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
const MyceliaContext = createContext(null);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* MyceliaProvider - Provides Mycelia system to React tree
|
|
48
|
+
*
|
|
49
|
+
* @param {Object} props
|
|
50
|
+
* @param {Function} props.build - Async function that returns a built system
|
|
51
|
+
* @param {React.ReactNode} props.children - Child components
|
|
52
|
+
* @param {React.ReactNode} [props.fallback] - Optional loading/fallback component
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* <MyceliaProvider build={buildSystem}>
|
|
57
|
+
* <App />
|
|
58
|
+
* </MyceliaProvider>
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export function MyceliaProvider({ build, children, fallback = null }) {
|
|
62
|
+
const [system, setSystem] = useState(null);
|
|
63
|
+
const [error, setError] = useState(null);
|
|
64
|
+
const buildRef = useRef(build);
|
|
65
|
+
|
|
66
|
+
// Update build ref when it changes
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
buildRef.current = build;
|
|
69
|
+
}, [build]);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
let isMounted = true;
|
|
73
|
+
let currentSystem = null;
|
|
74
|
+
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
currentSystem = await buildRef.current();
|
|
78
|
+
if (isMounted) {
|
|
79
|
+
setSystem(currentSystem);
|
|
80
|
+
setError(null);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (isMounted) {
|
|
84
|
+
setError(err);
|
|
85
|
+
setSystem(null);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
isMounted = false;
|
|
92
|
+
if (currentSystem && typeof currentSystem.dispose === 'function') {
|
|
93
|
+
currentSystem.dispose().catch(() => {
|
|
94
|
+
// Ignore disposal errors
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}, []); // Only run once on mount
|
|
99
|
+
|
|
100
|
+
if (error) {
|
|
101
|
+
throw error; // Let error boundaries handle it
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!system) {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<MyceliaContext.Provider value={system}>
|
|
110
|
+
{children}
|
|
111
|
+
</MyceliaContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* useMycelia - Get the Mycelia system from context
|
|
117
|
+
*
|
|
118
|
+
* @returns {Object} The Mycelia system instance
|
|
119
|
+
* @throws {Error} If used outside MyceliaProvider
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* function MyComponent() {
|
|
124
|
+
* const system = useMycelia();
|
|
125
|
+
* // Use system.find(), system.listeners, etc.
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export function useMycelia() {
|
|
130
|
+
const system = useContext(MyceliaContext);
|
|
131
|
+
if (!system) {
|
|
132
|
+
throw new Error('useMycelia must be used within a MyceliaProvider');
|
|
133
|
+
}
|
|
134
|
+
return system;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* useFacet - Get a facet by kind from the system
|
|
139
|
+
*
|
|
140
|
+
* @param {string} kind - Facet kind identifier
|
|
141
|
+
* @returns {Object|null} The facet instance, or null if not found
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```tsx
|
|
145
|
+
* function UserList() {
|
|
146
|
+
* const db = useFacet('database');
|
|
147
|
+
* // Use db.query(), etc.
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function useFacet(kind) {
|
|
152
|
+
const system = useMycelia();
|
|
153
|
+
const [facet, setFacet] = useState(() => system.find?.(kind) ?? null);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
setFacet(system.find?.(kind) ?? null);
|
|
157
|
+
}, [system, kind]);
|
|
158
|
+
|
|
159
|
+
return facet;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================================
|
|
163
|
+
// Listener Helpers
|
|
164
|
+
// ============================================================================
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* useListener - Register an event listener with automatic cleanup
|
|
168
|
+
*
|
|
169
|
+
* @param {string} eventName - Event name/path to listen for
|
|
170
|
+
* @param {Function} handler - Handler function: (message) => void
|
|
171
|
+
* @param {Array} [deps=[]] - React dependency array for handler
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```tsx
|
|
175
|
+
* function AuditLog() {
|
|
176
|
+
* useListener('user:created', (msg) => {
|
|
177
|
+
* console.log('User created:', msg.body);
|
|
178
|
+
* });
|
|
179
|
+
* // ...
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function useListener(eventName, handler, deps = []) {
|
|
184
|
+
const system = useMycelia();
|
|
185
|
+
const listeners = system.listeners; // useListeners facet
|
|
186
|
+
const handlerRef = useRef(handler);
|
|
187
|
+
|
|
188
|
+
// Update handler ref when it changes
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
handlerRef.current = handler;
|
|
191
|
+
}, [handler, ...deps]);
|
|
192
|
+
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (!listeners || !listeners.hasListeners?.()) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const wrappedHandler = (msg) => {
|
|
199
|
+
handlerRef.current(msg);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
listeners.on(eventName, wrappedHandler);
|
|
203
|
+
|
|
204
|
+
return () => {
|
|
205
|
+
listeners.off?.(eventName, wrappedHandler);
|
|
206
|
+
};
|
|
207
|
+
}, [listeners, eventName]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* useEventStream - Subscribe to events and keep them in React state
|
|
212
|
+
*
|
|
213
|
+
* @param {string} eventName - Event name/path to listen for
|
|
214
|
+
* @param {Object} [options={}] - Options
|
|
215
|
+
* @param {boolean} [options.accumulate=false] - If true, accumulate events in array
|
|
216
|
+
* @returns {any|any[]|null} Latest event value, array of events, or null
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```tsx
|
|
220
|
+
* function EventList() {
|
|
221
|
+
* const events = useEventStream('todo:created', { accumulate: true });
|
|
222
|
+
* return <ul>{events?.map(e => <li key={e.id}>{e.text}</li>)}</ul>;
|
|
223
|
+
* }
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export function useEventStream(eventName, options = {}) {
|
|
227
|
+
const { accumulate = false } = options;
|
|
228
|
+
const [value, setValue] = useState(accumulate ? [] : null);
|
|
229
|
+
|
|
230
|
+
useListener(eventName, (msg) => {
|
|
231
|
+
if (accumulate) {
|
|
232
|
+
setValue((prev) => [...prev, msg.body]);
|
|
233
|
+
} else {
|
|
234
|
+
setValue(msg.body);
|
|
235
|
+
}
|
|
236
|
+
}, [accumulate]);
|
|
237
|
+
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Queue Helpers
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* useQueueStatus - Get queue status with reactive updates
|
|
247
|
+
*
|
|
248
|
+
* @returns {Object} Queue status object
|
|
249
|
+
* @returns {number} size - Current queue size
|
|
250
|
+
* @returns {number} capacity - Maximum queue capacity
|
|
251
|
+
* @returns {number} utilization - Utilization ratio (0-1)
|
|
252
|
+
* @returns {boolean} isFull - Whether queue is full
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* ```tsx
|
|
256
|
+
* function QueueStatus() {
|
|
257
|
+
* const status = useQueueStatus();
|
|
258
|
+
* return <div>Queue: {status.size}/{status.capacity}</div>;
|
|
259
|
+
* }
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export function useQueueStatus() {
|
|
263
|
+
const queue = useFacet('queue');
|
|
264
|
+
const [status, setStatus] = useState(() => {
|
|
265
|
+
if (queue?.getQueueStatus) {
|
|
266
|
+
return queue.getQueueStatus();
|
|
267
|
+
}
|
|
268
|
+
return { size: 0, maxSize: 0, utilization: 0, isFull: false };
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Poll for status updates (could be improved with event-based updates)
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!queue || !queue.getQueueStatus) return;
|
|
274
|
+
|
|
275
|
+
const interval = setInterval(() => {
|
|
276
|
+
const newStatus = queue.getQueueStatus();
|
|
277
|
+
setStatus(newStatus);
|
|
278
|
+
}, 100); // Poll every 100ms
|
|
279
|
+
|
|
280
|
+
return () => clearInterval(interval);
|
|
281
|
+
}, [queue]);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
size: status.size || 0,
|
|
285
|
+
capacity: status.maxSize || 0,
|
|
286
|
+
utilization: status.utilization || 0,
|
|
287
|
+
isFull: status.isFull || false
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* useQueueDrain - Automatically drain queue on mount
|
|
293
|
+
*
|
|
294
|
+
* @param {Object} [options={}] - Options
|
|
295
|
+
* @param {number} [options.interval=100] - Polling interval in ms
|
|
296
|
+
* @param {Function} [options.onMessage] - Callback for each message: (msg, options) => void
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```tsx
|
|
300
|
+
* function QueueProcessor() {
|
|
301
|
+
* useQueueDrain({
|
|
302
|
+
* interval: 50,
|
|
303
|
+
* onMessage: (msg) => console.log('Processed:', msg)
|
|
304
|
+
* });
|
|
305
|
+
* return null;
|
|
306
|
+
* }
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
export function useQueueDrain(options = {}) {
|
|
310
|
+
const { interval = 100, onMessage } = options;
|
|
311
|
+
const queue = useFacet('queue');
|
|
312
|
+
const onMessageRef = useRef(onMessage);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
onMessageRef.current = onMessage;
|
|
316
|
+
}, [onMessage]);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (!queue || !queue.hasMessagesToProcess) return;
|
|
320
|
+
|
|
321
|
+
const processInterval = setInterval(() => {
|
|
322
|
+
if (queue.hasMessagesToProcess()) {
|
|
323
|
+
const next = queue.selectNextMessage();
|
|
324
|
+
if (next && onMessageRef.current) {
|
|
325
|
+
onMessageRef.current(next.msg, next.options);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}, interval);
|
|
329
|
+
|
|
330
|
+
return () => clearInterval(processInterval);
|
|
331
|
+
}, [queue, interval]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// Builder Helpers
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* createReactSystemBuilder - Create a reusable system builder function
|
|
340
|
+
*
|
|
341
|
+
* @param {string} name - System name
|
|
342
|
+
* @param {Function} configure - Configuration function: (builder) => builder
|
|
343
|
+
* @returns {Function} Build function: () => Promise<System>
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* ```ts
|
|
347
|
+
* import { useBase } from 'mycelia-kernel-plugin';
|
|
348
|
+
*
|
|
349
|
+
* const buildTodoSystem = createReactSystemBuilder('todo-app', (b) =>
|
|
350
|
+
* b
|
|
351
|
+
* .config('database', { host: 'localhost' })
|
|
352
|
+
* .use(useDatabase)
|
|
353
|
+
* .use(useListeners)
|
|
354
|
+
* );
|
|
355
|
+
*
|
|
356
|
+
* // Then use in Provider
|
|
357
|
+
* <MyceliaProvider build={buildTodoSystem}>
|
|
358
|
+
* <App />
|
|
359
|
+
* </MyceliaProvider>
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
export function createReactSystemBuilder(name, configure) {
|
|
363
|
+
return async function build() {
|
|
364
|
+
// Import useBase - users should have it available
|
|
365
|
+
// This avoids bundling issues by letting users import useBase themselves
|
|
366
|
+
const { useBase } = await import('../utils/use-base.js');
|
|
367
|
+
let builder = useBase(name);
|
|
368
|
+
builder = configure(builder);
|
|
369
|
+
return builder.build();
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Facet Hook Generator
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* createFacetHook - Generate a custom hook for a specific facet kind
|
|
379
|
+
*
|
|
380
|
+
* @param {string} kind - Facet kind identifier
|
|
381
|
+
* @returns {Function} Custom hook: () => Facet | null
|
|
382
|
+
*
|
|
383
|
+
* @example
|
|
384
|
+
* ```ts
|
|
385
|
+
* // In bindings/todo-hooks.ts
|
|
386
|
+
* export const useTodoStore = createFacetHook('todoStore');
|
|
387
|
+
* export const useAuth = createFacetHook('auth');
|
|
388
|
+
*
|
|
389
|
+
* // In component
|
|
390
|
+
* function TodoList() {
|
|
391
|
+
* const todoStore = useTodoStore();
|
|
392
|
+
* // Use todoStore...
|
|
393
|
+
* }
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
export function createFacetHook(kind) {
|
|
397
|
+
return function useNamedFacet() {
|
|
398
|
+
return useFacet(kind);
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
@@ -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
|
+
|