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 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 { StandalonePluginSystem, createHook, Facet } from 'mycelia-kernel-plugin';
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.0.0",
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
+