mycelia-kernel-plugin 1.2.0 → 1.4.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.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Mycelia Plugin System - Svelte Bindings
3
+ *
4
+ * Svelte utilities that make the Mycelia Plugin System feel natural
5
+ * inside Svelte applications using stores and context.
6
+ *
7
+ * @example
8
+ * ```svelte
9
+ * <script>
10
+ * import { setMyceliaSystem, useFacet, useListener } from 'mycelia-kernel-plugin/svelte';
11
+ * import { buildSystem } from './system.js';
12
+ *
13
+ * let system;
14
+ * buildSystem().then(s => {
15
+ * system = s;
16
+ * setMyceliaSystem(system);
17
+ * });
18
+ * </script>
19
+ * ```
20
+ */
21
+
22
+ import { writable, derived, get } from 'svelte/store';
23
+ import { setContext, getContext } from 'svelte';
24
+
25
+ // ============================================================================
26
+ // Core Bindings: Context + Basic Stores
27
+ // ============================================================================
28
+
29
+ const MYCELIA_KEY = Symbol('mycelia');
30
+
31
+ /**
32
+ * setMyceliaSystem - Provide Mycelia system to Svelte component tree
33
+ *
34
+ * @param {Object} system - The Mycelia system instance
35
+ * @returns {Object} Context object with system, loading, and error stores
36
+ *
37
+ * @example
38
+ * ```svelte
39
+ * <script>
40
+ * import { onMount } from 'svelte';
41
+ * import { setMyceliaSystem } from 'mycelia-kernel-plugin/svelte';
42
+ * import { buildSystem } from './system.js';
43
+ *
44
+ * let system;
45
+ *
46
+ * onMount(async () => {
47
+ * system = await buildSystem();
48
+ * setMyceliaSystem(system);
49
+ * });
50
+ * </script>
51
+ * ```
52
+ */
53
+ export function setMyceliaSystem(system) {
54
+ const systemStore = writable(system);
55
+ const loadingStore = writable(false);
56
+ const errorStore = writable(null);
57
+
58
+ const context = {
59
+ system: systemStore,
60
+ loading: loadingStore,
61
+ error: errorStore
62
+ };
63
+
64
+ setContext(MYCELIA_KEY, context);
65
+
66
+ return context;
67
+ }
68
+
69
+ /**
70
+ * getMyceliaSystem - Get Mycelia system store from context
71
+ *
72
+ * @returns {import('svelte/store').Writable} Writable store containing the system instance
73
+ * @throws {Error} If used outside setMyceliaSystem context
74
+ *
75
+ * @example
76
+ * ```svelte
77
+ * <script>
78
+ * import { getMyceliaSystem } from 'mycelia-kernel-plugin/svelte';
79
+ *
80
+ * const systemStore = getMyceliaSystem();
81
+ * $: system = $systemStore;
82
+ * </script>
83
+ * ```
84
+ */
85
+ export function getMyceliaSystem() {
86
+ const context = getContext(MYCELIA_KEY);
87
+ if (!context) {
88
+ throw new Error('getMyceliaSystem must be used within setMyceliaSystem context');
89
+ }
90
+ return context.system;
91
+ }
92
+
93
+ /**
94
+ * getMyceliaContext - Get the full Mycelia context (system, loading, error stores)
95
+ *
96
+ * @returns {Object} Context object with system, loading, and error stores
97
+ * @throws {Error} If used outside setMyceliaSystem context
98
+ *
99
+ * @example
100
+ * ```svelte
101
+ * <script>
102
+ * import { getMyceliaContext } from 'mycelia-kernel-plugin/svelte';
103
+ *
104
+ * const { system, loading, error } = getMyceliaContext();
105
+ * </script>
106
+ *
107
+ * {#if $loading}
108
+ * <div>Loading...</div>
109
+ * {:else if $error}
110
+ * <div>Error: {$error.message}</div>
111
+ * {:else}
112
+ * <div>System ready: {$system.name}</div>
113
+ * {/if}
114
+ * ```
115
+ */
116
+ export function getMyceliaContext() {
117
+ const context = getContext(MYCELIA_KEY);
118
+ if (!context) {
119
+ throw new Error('getMyceliaContext must be used within setMyceliaSystem context');
120
+ }
121
+ return context;
122
+ }
123
+
124
+ /**
125
+ * useMycelia - Get Mycelia system store (reactive)
126
+ *
127
+ * @returns {import('svelte/store').Writable} Writable store containing the system instance
128
+ *
129
+ * @example
130
+ * ```svelte
131
+ * <script>
132
+ * import { useMycelia } from 'mycelia-kernel-plugin/svelte';
133
+ *
134
+ * const systemStore = useMycelia();
135
+ * $: system = $systemStore;
136
+ * $: db = system?.find('database');
137
+ * </script>
138
+ * ```
139
+ */
140
+ export function useMycelia() {
141
+ return getMyceliaSystem();
142
+ }
143
+
144
+ /**
145
+ * useFacet - Get a facet by kind from the system (reactive store)
146
+ *
147
+ * @param {string} kind - Facet kind identifier
148
+ * @returns {import('svelte/store').Readable} Readable store containing the facet instance, or null if not found
149
+ *
150
+ * @example
151
+ * ```svelte
152
+ * <script>
153
+ * import { useFacet } from 'mycelia-kernel-plugin/svelte';
154
+ *
155
+ * const dbStore = useFacet('database');
156
+ * $: db = $dbStore;
157
+ * </script>
158
+ *
159
+ * {#if db}
160
+ * <div>Database ready</div>
161
+ * {/if}
162
+ * ```
163
+ */
164
+ export function useFacet(kind) {
165
+ const systemStore = getMyceliaSystem();
166
+
167
+ return derived(systemStore, ($system) => {
168
+ return $system?.find?.(kind) ?? null;
169
+ });
170
+ }
171
+
172
+ // Re-export listener helpers
173
+ export { useListener, useEventStream } from './listeners.js';
174
+
175
+ // Re-export queue helpers
176
+ export { useQueueStatus, useQueueDrain } from './queues.js';
177
+
178
+ // Re-export builder helpers
179
+ export { createSvelteSystemBuilder } from './builders.js';
180
+
181
+ // Re-export store generator
182
+ export { createFacetStore } from './stores.js';
183
+
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Mycelia Plugin System - Svelte Listener Helpers
3
+ *
4
+ * Svelte utilities for event listener management.
5
+ */
6
+
7
+ import { writable, get } from 'svelte/store';
8
+ import { onDestroy } from 'svelte';
9
+ import { getMyceliaSystem } from './index.js';
10
+
11
+ /**
12
+ * useListener - Register an event listener with automatic cleanup
13
+ *
14
+ * @param {string} eventName - Event name/path to listen for
15
+ * @param {Function} handler - Handler function: (message) => void
16
+ *
17
+ * @example
18
+ * ```svelte
19
+ * <script>
20
+ * import { useListener } from 'mycelia-kernel-plugin/svelte';
21
+ *
22
+ * useListener('user:created', (msg) => {
23
+ * console.log('User created:', msg.body);
24
+ * });
25
+ * </script>
26
+ * ```
27
+ */
28
+ export function useListener(eventName, handler) {
29
+ const systemStore = getMyceliaSystem();
30
+ let listeners = null;
31
+ let unsubscribe = null;
32
+
33
+ const setup = () => {
34
+ const system = get(systemStore);
35
+ if (!system) return;
36
+
37
+ listeners = system?.listeners;
38
+ if (!listeners || !listeners.hasListeners?.()) return;
39
+
40
+ listeners.on(eventName, handler);
41
+ };
42
+
43
+ // Subscribe to system store to set up listener when system is available
44
+ unsubscribe = systemStore.subscribe(() => {
45
+ setup();
46
+ });
47
+
48
+ // Initial setup
49
+ setup();
50
+
51
+ onDestroy(() => {
52
+ if (listeners) {
53
+ listeners.off?.(eventName, handler);
54
+ }
55
+ if (unsubscribe) {
56
+ unsubscribe();
57
+ }
58
+ });
59
+ }
60
+
61
+ /**
62
+ * useEventStream - Subscribe to events and keep them in a reactive store
63
+ *
64
+ * @param {string} eventName - Event name/path to listen for
65
+ * @param {Object} [options={}] - Options
66
+ * @param {boolean} [options.accumulate=false] - If true, accumulate events in array
67
+ * @returns {import('svelte/store').Writable} Writable store containing latest event value, array of events, or null
68
+ *
69
+ * @example
70
+ * ```svelte
71
+ * <script>
72
+ * import { useEventStream } from 'mycelia-kernel-plugin/svelte';
73
+ *
74
+ * const events = useEventStream('todo:created', { accumulate: true });
75
+ * </script>
76
+ *
77
+ * {#each $events as event}
78
+ * <div>{event.text}</div>
79
+ * {/each}
80
+ * ```
81
+ */
82
+ export function useEventStream(eventName, options = {}) {
83
+ const { accumulate = false } = options;
84
+ const store = writable(accumulate ? [] : null);
85
+
86
+ useListener(eventName, (msg) => {
87
+ if (accumulate) {
88
+ store.update(events => [...events, msg.body]);
89
+ } else {
90
+ store.set(msg.body);
91
+ }
92
+ });
93
+
94
+ return store;
95
+ }
96
+
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Mycelia Plugin System - Svelte Queue Helpers
3
+ *
4
+ * Svelte utilities for queue management.
5
+ */
6
+
7
+ import { writable, get } from 'svelte/store';
8
+ import { onMount, onDestroy } from 'svelte';
9
+ import { useFacet } from './index.js';
10
+
11
+ /**
12
+ * useQueueStatus - Get queue status with reactive updates
13
+ *
14
+ * @returns {import('svelte/store').Writable} Writable store containing queue status object
15
+ * @returns {number} size - Current queue size
16
+ * @returns {number} capacity - Maximum queue capacity
17
+ * @returns {number} utilization - Utilization ratio (0-1)
18
+ * @returns {boolean} isFull - Whether queue is full
19
+ *
20
+ * @example
21
+ * ```svelte
22
+ * <script>
23
+ * import { useQueueStatus } from 'mycelia-kernel-plugin/svelte';
24
+ *
25
+ * const status = useQueueStatus();
26
+ * </script>
27
+ *
28
+ * <div>Queue: {$status.size}/{$status.capacity}</div>
29
+ * ```
30
+ */
31
+ export function useQueueStatus() {
32
+ const queueStore = useFacet('queue');
33
+ const statusStore = writable({
34
+ size: 0,
35
+ capacity: 0,
36
+ utilization: 0,
37
+ isFull: false
38
+ });
39
+
40
+ let intervalId = null;
41
+
42
+ const updateStatus = () => {
43
+ const queue = get(queueStore);
44
+ if (!queue?.getQueueStatus) return;
45
+
46
+ const newStatus = queue.getQueueStatus();
47
+ statusStore.set({
48
+ size: newStatus.size || 0,
49
+ capacity: newStatus.maxSize || 0,
50
+ utilization: newStatus.utilization || 0,
51
+ isFull: newStatus.isFull || false
52
+ });
53
+ };
54
+
55
+ onMount(() => {
56
+ updateStatus();
57
+ intervalId = setInterval(updateStatus, 100); // Poll every 100ms
58
+ });
59
+
60
+ onDestroy(() => {
61
+ if (intervalId) {
62
+ clearInterval(intervalId);
63
+ }
64
+ });
65
+
66
+ return statusStore;
67
+ }
68
+
69
+ /**
70
+ * useQueueDrain - Automatically drain queue on mount
71
+ *
72
+ * @param {Object} [options={}] - Options
73
+ * @param {number} [options.interval=100] - Polling interval in ms
74
+ * @param {Function} [options.onMessage] - Callback for each message: (msg, options) => void
75
+ *
76
+ * @example
77
+ * ```svelte
78
+ * <script>
79
+ * import { useQueueDrain } from 'mycelia-kernel-plugin/svelte';
80
+ *
81
+ * useQueueDrain({
82
+ * interval: 50,
83
+ * onMessage: (msg) => console.log('Processed:', msg)
84
+ * });
85
+ * </script>
86
+ * ```
87
+ */
88
+ export function useQueueDrain(options = {}) {
89
+ const { interval = 100, onMessage } = options;
90
+ const queueStore = useFacet('queue');
91
+ let processInterval = null;
92
+
93
+ onMount(() => {
94
+ const queue = get(queueStore);
95
+ if (!queue || !queue.hasMessagesToProcess) return;
96
+
97
+ processInterval = setInterval(() => {
98
+ const currentQueue = get(queueStore);
99
+ if (currentQueue?.hasMessagesToProcess()) {
100
+ const next = currentQueue.selectNextMessage();
101
+ if (next && onMessage) {
102
+ onMessage(next.msg, next.options);
103
+ }
104
+ }
105
+ }, interval);
106
+ });
107
+
108
+ onDestroy(() => {
109
+ if (processInterval) {
110
+ clearInterval(processInterval);
111
+ }
112
+ });
113
+ }
114
+
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Mycelia Plugin System - Svelte Store Generator
3
+ *
4
+ * Utility for generating custom stores for specific facets.
5
+ */
6
+
7
+ import { useFacet } from './index.js';
8
+
9
+ /**
10
+ * createFacetStore - Generate a custom store for a specific facet kind
11
+ *
12
+ * @param {string} kind - Facet kind identifier
13
+ * @returns {Function} Custom store function: () => import('svelte/store').Readable
14
+ *
15
+ * @example
16
+ * ```js
17
+ * // stores/todos.js
18
+ * import { createFacetStore } from 'mycelia-kernel-plugin/svelte';
19
+ *
20
+ * export const useTodos = createFacetStore('todos');
21
+ *
22
+ * // In component
23
+ * <script>
24
+ * import { useTodos } from './stores/todos.js';
25
+ *
26
+ * const todosStore = useTodos();
27
+ * $: todos = $todosStore;
28
+ * </script>
29
+ * ```
30
+ */
31
+ export function createFacetStore(kind) {
32
+ return function useNamedFacet() {
33
+ return useFacet(kind);
34
+ };
35
+ }
36
+
@@ -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
+