mycelia-kernel-plugin 1.1.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
@@ -285,6 +285,7 @@ Comprehensive documentation is available in the [`docs/`](./docs/) directory:
285
285
  - **[Getting Started Guide](./docs/getting-started/README.md)** - Quick start with examples
286
286
  - **[Hooks and Facets Overview](./docs/core-concepts/HOOKS-AND-FACETS-OVERVIEW.md)** - Core concepts
287
287
  - **[Built-in Hooks](./docs/hooks/README.md)** - Documentation for `useListeners`, `useQueue`, and `useSpeak`
288
+ - **[React Bindings](./docs/react/README.md)** - React integration utilities
288
289
  - **[Standalone Plugin System](./docs/standalone/STANDALONE-PLUGIN-SYSTEM.md)** - Complete usage guide
289
290
  - **[Documentation Index](./docs/README.md)** - Full documentation index
290
291
 
@@ -297,6 +298,10 @@ See the `examples/` directory for:
297
298
  - Contract validation
298
299
  - Hot reloading
299
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`)
300
305
 
301
306
  ## CLI Tool
302
307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycelia-kernel-plugin",
3
- "version": "1.1.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",
@@ -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
+