mycelia-kernel-plugin 1.3.0 → 1.4.1

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.
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Mycelia Plugin System - Solid.js Bindings
3
+ *
4
+ * Solid.js utilities that make the Mycelia Plugin System feel natural
5
+ * inside Solid.js applications using signals and context.
6
+ *
7
+ * @example
8
+ * ```jsx
9
+ * import { MyceliaProvider, useFacet, useListener } from 'mycelia-kernel-plugin/solid';
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 { createContext, useContext, createSignal, createEffect, onCleanup, onMount } from 'solid-js';
32
+
33
+ // ============================================================================
34
+ // Core Bindings: Provider + Basic Hooks
35
+ // ============================================================================
36
+
37
+ const MyceliaContext = createContext();
38
+
39
+ /**
40
+ * MyceliaProvider - Provides Mycelia system to Solid.js component tree
41
+ *
42
+ * @param {Object} props
43
+ * @param {Function} props.build - Async function that returns a built system
44
+ * @param {JSX.Element} props.children - Child components
45
+ * @param {JSX.Element} [props.fallback] - Optional loading/fallback component
46
+ *
47
+ * @example
48
+ * ```jsx
49
+ * <MyceliaProvider build={buildSystem}>
50
+ * <App />
51
+ * </MyceliaProvider>
52
+ * ```
53
+ */
54
+ export function MyceliaProvider(props) {
55
+ const [system, setSystem] = createSignal(null);
56
+ const [error, setError] = createSignal(null);
57
+ const [loading, setLoading] = createSignal(true);
58
+ let currentSystem = null;
59
+
60
+ onMount(async () => {
61
+ try {
62
+ currentSystem = await props.build();
63
+ setSystem(currentSystem);
64
+ setError(null);
65
+ setLoading(false);
66
+ } catch (err) {
67
+ setError(err);
68
+ setSystem(null);
69
+ setLoading(false);
70
+ }
71
+ });
72
+
73
+ onCleanup(() => {
74
+ if (currentSystem && typeof currentSystem.dispose === 'function') {
75
+ currentSystem.dispose().catch(() => {
76
+ // Ignore disposal errors
77
+ });
78
+ }
79
+ });
80
+
81
+ if (error()) {
82
+ throw error(); // Let error boundaries handle it
83
+ }
84
+
85
+ if (loading()) {
86
+ return props.fallback || null;
87
+ }
88
+
89
+ return (
90
+ <MyceliaContext.Provider value={system}>
91
+ {props.children}
92
+ </MyceliaContext.Provider>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * useMycelia - Get the Mycelia system from context
98
+ *
99
+ * @returns {Function} Signal accessor that returns the Mycelia system instance
100
+ * @throws {Error} If used outside MyceliaProvider
101
+ *
102
+ * @example
103
+ * ```jsx
104
+ * function MyComponent() {
105
+ * const system = useMycelia();
106
+ * // Use system().find(), system().listeners, etc.
107
+ * }
108
+ * ```
109
+ */
110
+ export function useMycelia() {
111
+ const context = useContext(MyceliaContext);
112
+ if (!context) {
113
+ throw new Error('useMycelia must be used within a MyceliaProvider');
114
+ }
115
+ return context;
116
+ }
117
+
118
+ /**
119
+ * useFacet - Get a facet by kind from the system with reactivity
120
+ *
121
+ * @param {string} kind - Facet kind identifier
122
+ * @returns {Function} Signal accessor that returns the facet instance, or null if not found
123
+ *
124
+ * @example
125
+ * ```jsx
126
+ * function UserList() {
127
+ * const db = useFacet('database');
128
+ * // Use db().query(), etc.
129
+ * }
130
+ * ```
131
+ */
132
+ export function useFacet(kind) {
133
+ const system = useMycelia();
134
+ const [facet, setFacet] = createSignal(() => {
135
+ const sys = system();
136
+ return sys?.find?.(kind) ?? null;
137
+ });
138
+
139
+ createEffect(() => {
140
+ const sys = system();
141
+ setFacet(sys?.find?.(kind) ?? null);
142
+ });
143
+
144
+ return facet;
145
+ }
146
+
147
+ // ============================================================================
148
+ // Listener Helpers
149
+ // ============================================================================
150
+
151
+ /**
152
+ * useListener - Register an event listener with automatic cleanup
153
+ *
154
+ * @param {string} eventName - Event name/path to listen for
155
+ * @param {Function} handler - Handler function: (message) => void
156
+ *
157
+ * @example
158
+ * ```jsx
159
+ * function AuditLog() {
160
+ * useListener('user:created', (msg) => {
161
+ * console.log('User created:', msg.body);
162
+ * });
163
+ * // ...
164
+ * }
165
+ * ```
166
+ */
167
+ export function useListener(eventName, handler) {
168
+ const system = useMycelia();
169
+ let listeners = null;
170
+ let wrappedHandler = null;
171
+
172
+ createEffect(() => {
173
+ const sys = system();
174
+ if (!sys) return;
175
+
176
+ listeners = sys.listeners; // useListeners facet
177
+ if (!listeners || !listeners.hasListeners?.()) {
178
+ return;
179
+ }
180
+
181
+ wrappedHandler = (msg) => {
182
+ handler(msg);
183
+ };
184
+
185
+ listeners.on(eventName, wrappedHandler);
186
+ });
187
+
188
+ onCleanup(() => {
189
+ if (listeners && wrappedHandler) {
190
+ listeners.off?.(eventName, wrappedHandler);
191
+ }
192
+ });
193
+ }
194
+
195
+ /**
196
+ * useEventStream - Subscribe to events and keep them in reactive state
197
+ *
198
+ * @param {string} eventName - Event name/path to listen for
199
+ * @param {Object} [options={}] - Options
200
+ * @param {boolean} [options.accumulate=false] - If true, accumulate events in array
201
+ * @returns {Function} Signal accessor that returns latest event value, array of events, or null
202
+ *
203
+ * @example
204
+ * ```jsx
205
+ * function EventList() {
206
+ * const events = useEventStream('todo:created', { accumulate: true });
207
+ * return <ul>{events()?.map(e => <li key={e.id}>{e.text}</li>)}</ul>;
208
+ * }
209
+ * ```
210
+ */
211
+ export function useEventStream(eventName, options = {}) {
212
+ const { accumulate = false } = options;
213
+ const [value, setValue] = createSignal(accumulate ? [] : null);
214
+
215
+ useListener(eventName, (msg) => {
216
+ if (accumulate) {
217
+ setValue((prev) => [...prev, msg.body]);
218
+ } else {
219
+ setValue(msg.body);
220
+ }
221
+ });
222
+
223
+ return value;
224
+ }
225
+
226
+ // ============================================================================
227
+ // Queue Helpers
228
+ // ============================================================================
229
+
230
+ /**
231
+ * useQueueStatus - Get queue status with reactive updates
232
+ *
233
+ * @returns {Function} Signal accessor that returns queue status object
234
+ * @returns {number} size - Current queue size
235
+ * @returns {number} capacity - Maximum queue capacity
236
+ * @returns {number} utilization - Utilization ratio (0-1)
237
+ * @returns {boolean} isFull - Whether queue is full
238
+ *
239
+ * @example
240
+ * ```jsx
241
+ * function QueueStatus() {
242
+ * const status = useQueueStatus();
243
+ * return <div>Queue: {status().size}/{status().capacity}</div>;
244
+ * }
245
+ * ```
246
+ */
247
+ export function useQueueStatus() {
248
+ const queue = useFacet('queue');
249
+ const [status, setStatus] = createSignal(() => {
250
+ const q = queue();
251
+ if (q?.getQueueStatus) {
252
+ return q.getQueueStatus();
253
+ }
254
+ return { size: 0, maxSize: 0, utilization: 0, isFull: false };
255
+ });
256
+
257
+ // Poll for status updates (could be improved with event-based updates)
258
+ createEffect(() => {
259
+ const q = queue();
260
+ if (!q || !q.getQueueStatus) return;
261
+
262
+ const interval = setInterval(() => {
263
+ const newStatus = q.getQueueStatus();
264
+ setStatus(newStatus);
265
+ }, 100); // Poll every 100ms
266
+
267
+ onCleanup(() => clearInterval(interval));
268
+ });
269
+
270
+ return () => {
271
+ const s = status();
272
+ return {
273
+ size: s.size || 0,
274
+ capacity: s.maxSize || 0,
275
+ utilization: s.utilization || 0,
276
+ isFull: s.isFull || false
277
+ };
278
+ };
279
+ }
280
+
281
+ /**
282
+ * useQueueDrain - Automatically drain queue on mount
283
+ *
284
+ * @param {Object} [options={}] - Options
285
+ * @param {number} [options.interval=100] - Polling interval in ms
286
+ * @param {Function} [options.onMessage] - Callback for each message: (msg, options) => void
287
+ *
288
+ * @example
289
+ * ```jsx
290
+ * function QueueProcessor() {
291
+ * useQueueDrain({
292
+ * interval: 50,
293
+ * onMessage: (msg) => console.log('Processed:', msg)
294
+ * });
295
+ * return null;
296
+ * }
297
+ * ```
298
+ */
299
+ export function useQueueDrain(options = {}) {
300
+ const { interval = 100, onMessage } = options;
301
+ const queue = useFacet('queue');
302
+
303
+ createEffect(() => {
304
+ const q = queue();
305
+ if (!q || !q.hasMessagesToProcess) return;
306
+
307
+ const processInterval = setInterval(() => {
308
+ if (q.hasMessagesToProcess()) {
309
+ const next = q.selectNextMessage();
310
+ if (next && onMessage) {
311
+ onMessage(next.msg, next.options);
312
+ }
313
+ }
314
+ }, interval);
315
+
316
+ onCleanup(() => clearInterval(processInterval));
317
+ });
318
+ }
319
+
320
+ // ============================================================================
321
+ // Builder Helpers
322
+ // ============================================================================
323
+
324
+ /**
325
+ * createSolidSystemBuilder - Create a reusable system builder function
326
+ *
327
+ * @param {string} name - System name
328
+ * @param {Function} configure - Configuration function: (builder) => builder
329
+ * @returns {Function} Build function: () => Promise<System>
330
+ *
331
+ * @example
332
+ * ```js
333
+ * import { useBase } from 'mycelia-kernel-plugin';
334
+ *
335
+ * const buildTodoSystem = createSolidSystemBuilder('todo-app', (b) =>
336
+ * b
337
+ * .config('database', { host: 'localhost' })
338
+ * .use(useDatabase)
339
+ * .use(useListeners)
340
+ * );
341
+ *
342
+ * // Then use in Provider
343
+ * <MyceliaProvider build={buildTodoSystem}>
344
+ * <App />
345
+ * </MyceliaProvider>
346
+ * ```
347
+ */
348
+ export function createSolidSystemBuilder(name, configure) {
349
+ return async function build() {
350
+ // Import useBase - users should have it available
351
+ // This avoids bundling issues by letting users import useBase themselves
352
+ const { useBase } = await import('../utils/use-base.js');
353
+ let builder = useBase(name);
354
+ builder = configure(builder);
355
+ return builder.build();
356
+ };
357
+ }
358
+
359
+ // ============================================================================
360
+ // Facet Hook Generator
361
+ // ============================================================================
362
+
363
+ /**
364
+ * createFacetSignal - Generate a custom signal hook for a specific facet kind
365
+ *
366
+ * @param {string} kind - Facet kind identifier
367
+ * @returns {Function} Custom hook: () => Signal<Facet | null>
368
+ *
369
+ * @example
370
+ * ```js
371
+ * // In bindings/todo-hooks.ts
372
+ * export const useTodoStore = createFacetSignal('todoStore');
373
+ * export const useAuth = createFacetSignal('auth');
374
+ *
375
+ * // In component
376
+ * function TodoList() {
377
+ * const todoStore = useTodoStore();
378
+ * // Use todoStore()...
379
+ * }
380
+ * ```
381
+ */
382
+ export function createFacetSignal(kind) {
383
+ return function useNamedFacet() {
384
+ return useFacet(kind);
385
+ };
386
+ }
387
+
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Instrumentation Utilities
3
+ *
4
+ * Provides timing instrumentation for debugging build, initialization, and disposal phases.
5
+ * Helps identify slow hooks and facets when debugging performance issues.
6
+ */
7
+
8
+ import { createSubsystemLogger } from './logger.js';
9
+
10
+ /**
11
+ * Default thresholds for timing warnings (in milliseconds)
12
+ */
13
+ const DEFAULT_THRESHOLDS = {
14
+ hookExecution: 50, // Warn if hook execution takes > 50ms
15
+ facetInit: 100, // Warn if facet init takes > 100ms
16
+ facetDispose: 50, // Warn if facet dispose takes > 50ms
17
+ };
18
+
19
+ /**
20
+ * Check if instrumentation is enabled
21
+ *
22
+ * @param {BaseSubsystem} subsystem - Subsystem instance
23
+ * @returns {boolean} True if instrumentation is enabled
24
+ */
25
+ export function isInstrumentationEnabled(subsystem) {
26
+ // Enable if debug is on, or if instrumentation is explicitly enabled
27
+ return subsystem?.debug === true || subsystem?.ctx?.instrumentation === true;
28
+ }
29
+
30
+ /**
31
+ * Get timing thresholds from config or use defaults
32
+ *
33
+ * @param {BaseSubsystem} subsystem - Subsystem instance
34
+ * @returns {Object} Thresholds object
35
+ */
36
+ function getThresholds(subsystem) {
37
+ const config = subsystem?.ctx?.config?.instrumentation || {};
38
+ return {
39
+ hookExecution: config.hookExecutionThreshold ?? DEFAULT_THRESHOLDS.hookExecution,
40
+ facetInit: config.facetInitThreshold ?? DEFAULT_THRESHOLDS.facetInit,
41
+ facetDispose: config.facetDisposeThreshold ?? DEFAULT_THRESHOLDS.facetDispose,
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Time a hook execution and log if it exceeds threshold
47
+ *
48
+ * @param {Function} hook - Hook function to execute
49
+ * @param {Object} resolvedCtx - Resolved context
50
+ * @param {Object} api - Subsystem API
51
+ * @param {BaseSubsystem} subsystem - Subsystem instance
52
+ * @returns {*} Result of hook execution
53
+ */
54
+ export function instrumentHookExecution(hook, resolvedCtx, api, subsystem) {
55
+ if (!isInstrumentationEnabled(subsystem)) {
56
+ // No instrumentation - just execute
57
+ return hook(resolvedCtx, api, subsystem);
58
+ }
59
+
60
+ const logger = createSubsystemLogger(subsystem);
61
+ const hookKind = hook.kind || '<unknown>';
62
+ const hookSource = hook.source || '<unknown>';
63
+ const thresholds = getThresholds(subsystem);
64
+
65
+ const start = performance.now();
66
+ let result;
67
+ try {
68
+ result = hook(resolvedCtx, api, subsystem);
69
+ } finally {
70
+ const duration = performance.now() - start;
71
+
72
+ if (duration > thresholds.hookExecution) {
73
+ logger.warn(
74
+ `⚠️ Slow hook execution: '${hookKind}' took ${duration.toFixed(2)}ms ` +
75
+ `(threshold: ${thresholds.hookExecution}ms) [${hookSource}]`
76
+ );
77
+ } else {
78
+ logger.log(`✓ Hook '${hookKind}' executed in ${duration.toFixed(2)}ms [${hookSource}]`);
79
+ }
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ /**
86
+ * Time a facet initialization callback and log if it exceeds threshold
87
+ *
88
+ * @param {Facet} facet - Facet instance
89
+ * @param {Object} ctx - Context object
90
+ * @param {Object} api - Subsystem API
91
+ * @param {BaseSubsystem} subsystem - Subsystem instance
92
+ * @param {Function} initCallback - The init callback to execute
93
+ * @returns {Promise<void>}
94
+ */
95
+ export async function instrumentFacetInit(facet, ctx, api, subsystem, initCallback) {
96
+ if (!initCallback) {
97
+ return;
98
+ }
99
+
100
+ if (!isInstrumentationEnabled(subsystem)) {
101
+ // No instrumentation - call callback directly
102
+ return initCallback({ ctx, api, subsystem, facet });
103
+ }
104
+
105
+ const logger = createSubsystemLogger(subsystem);
106
+ const facetKind = facet.getKind?.() || '<unknown>';
107
+ const facetSource = facet.getSource?.() || '<unknown>';
108
+ const thresholds = getThresholds(subsystem);
109
+
110
+ const start = performance.now();
111
+ try {
112
+ await initCallback({ ctx, api, subsystem, facet });
113
+ } finally {
114
+ const duration = performance.now() - start;
115
+
116
+ if (duration > thresholds.facetInit) {
117
+ logger.warn(
118
+ `⚠️ Slow facet initialization: '${facetKind}' took ${duration.toFixed(2)}ms ` +
119
+ `(threshold: ${thresholds.facetInit}ms) [${facetSource}]`
120
+ );
121
+ } else {
122
+ logger.log(`✓ Facet '${facetKind}' initialized in ${duration.toFixed(2)}ms [${facetSource}]`);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Time a facet disposal callback and warn if it exceeds threshold
129
+ *
130
+ * @param {Facet} facet - Facet instance
131
+ * @param {BaseSubsystem} subsystem - Subsystem instance
132
+ * @param {Function} disposeCallback - The dispose callback to execute
133
+ * @returns {Promise<void>}
134
+ */
135
+ export async function instrumentDisposeCallback(facet, subsystem, disposeCallback) {
136
+ if (!disposeCallback) {
137
+ return;
138
+ }
139
+
140
+ if (!isInstrumentationEnabled(subsystem)) {
141
+ // No instrumentation - call callback directly
142
+ return disposeCallback(facet);
143
+ }
144
+
145
+ const logger = createSubsystemLogger(subsystem);
146
+ const facetKind = facet.getKind?.() || '<unknown>';
147
+ const facetSource = facet.getSource?.() || '<unknown>';
148
+ const thresholds = getThresholds(subsystem);
149
+
150
+ const start = performance.now();
151
+ let errorOccurred = false;
152
+ try {
153
+ await disposeCallback(facet);
154
+ const duration = performance.now() - start;
155
+
156
+ if (duration > thresholds.facetDispose) {
157
+ logger.warn(
158
+ `⚠️ Slow facet disposal: '${facetKind}' took ${duration.toFixed(2)}ms ` +
159
+ `(threshold: ${thresholds.facetDispose}ms) [${facetSource}]`
160
+ );
161
+ } else {
162
+ logger.log(`✓ Facet '${facetKind}' disposed in ${duration.toFixed(2)}ms [${facetSource}]`);
163
+ }
164
+ } catch (error) {
165
+ errorOccurred = true;
166
+ const duration = performance.now() - start;
167
+ logger.warn(
168
+ `⚠️ Facet disposal error: '${facetKind}' failed after ${duration.toFixed(2)}ms ` +
169
+ `[${facetSource}]: ${error.message}`
170
+ );
171
+ // Re-throw so disposeAll can catch and handle it
172
+ throw error;
173
+ } finally {
174
+ // Ensure timing is logged even if error occurred
175
+ if (!errorOccurred) {
176
+ // Already logged in try block
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Time the entire build phase and log summary
183
+ *
184
+ * @param {BaseSubsystem} subsystem - Subsystem instance
185
+ * @param {Function} buildFn - Build function to execute
186
+ * @returns {Promise<void>}
187
+ */
188
+ export async function instrumentBuildPhase(subsystem, buildFn) {
189
+ if (!isInstrumentationEnabled(subsystem)) {
190
+ // No instrumentation - just build
191
+ return buildFn();
192
+ }
193
+
194
+ const logger = createSubsystemLogger(subsystem);
195
+ const start = performance.now();
196
+
197
+ try {
198
+ await buildFn();
199
+ } finally {
200
+ const duration = performance.now() - start;
201
+ logger.log(`📦 Build phase completed in ${duration.toFixed(2)}ms`);
202
+ }
203
+ }
204
+