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 +5 -0
- package/package.json +6 -2
- package/src/react/README.md +174 -0
- package/src/react/index.js +401 -0
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.
|
|
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
|
+
|