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,189 @@
1
+ /**
2
+ * Mycelia Plugin System - Angular Bindings
3
+ *
4
+ * Angular utilities that make the Mycelia Plugin System feel natural
5
+ * inside Angular applications using dependency injection and RxJS.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { MyceliaModule, useFacet, useListener } from 'mycelia-kernel-plugin/angular';
10
+ * import { NgModule } from '@angular/core';
11
+ *
12
+ * const buildSystem = () => useBase('app')
13
+ * .use(useDatabase)
14
+ * .build();
15
+ *
16
+ * @NgModule({
17
+ * imports: [MyceliaModule.forRoot({ build: buildSystem })]
18
+ * })
19
+ * export class AppModule {}
20
+ *
21
+ * // In component
22
+ * @Component({...})
23
+ * export class MyComponent {
24
+ * constructor(private mycelia: MyceliaService) {
25
+ * const db = this.mycelia.useFacet('database');
26
+ * this.mycelia.useListener('user:created', (msg) => console.log(msg));
27
+ * }
28
+ * }
29
+ * ```
30
+ */
31
+
32
+ // Note: This is a TypeScript-friendly JavaScript module
33
+ // Angular applications typically use TypeScript, but we provide JS bindings
34
+ // for compatibility. Type definitions should be provided separately.
35
+
36
+ /**
37
+ * MyceliaService - Injectable service that provides Mycelia system
38
+ *
39
+ * This service should be provided at the root level using MyceliaModule.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * @Injectable({ providedIn: 'root' })
44
+ * export class MyceliaService {
45
+ * private system$ = new BehaviorSubject(null);
46
+ *
47
+ * constructor(@Inject(MYCELIA_BUILD_TOKEN) private buildFn: () => Promise<any>) {
48
+ * this.initialize();
49
+ * }
50
+ *
51
+ * async initialize() {
52
+ * const system = await this.buildFn();
53
+ * this.system$.next(system);
54
+ * }
55
+ * }
56
+ * ```
57
+ */
58
+
59
+ // For JavaScript/CommonJS compatibility, we export factory functions
60
+ // that can be used with Angular's dependency injection system
61
+
62
+ /**
63
+ * createMyceliaService - Factory function to create Mycelia service
64
+ *
65
+ * @param {Function} build - Async function that returns a built system
66
+ * @returns {Object} Service object with system observable and helper methods
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * const myceliaService = createMyceliaService(buildSystem);
71
+ * ```
72
+ */
73
+ export function createMyceliaService(build) {
74
+ let system = null;
75
+ let systemSubject = null;
76
+ let errorSubject = null;
77
+ let loadingSubject = null;
78
+
79
+ // Initialize system
80
+ (async () => {
81
+ try {
82
+ loadingSubject?.next?.(true);
83
+ system = await build();
84
+ systemSubject?.next?.(system);
85
+ errorSubject?.next?.(null);
86
+ } catch (err) {
87
+ errorSubject?.next?.(err);
88
+ systemSubject?.next?.(null);
89
+ } finally {
90
+ loadingSubject?.next?.(false);
91
+ }
92
+ })();
93
+
94
+ return {
95
+ /**
96
+ * Get system observable (requires RxJS BehaviorSubject)
97
+ * @returns {Object} Observable-like object
98
+ */
99
+ get system$() {
100
+ if (!systemSubject) {
101
+ // Lazy initialization - requires RxJS
102
+ const { BehaviorSubject } = require('rxjs');
103
+ systemSubject = new BehaviorSubject(system);
104
+ }
105
+ return systemSubject.asObservable();
106
+ },
107
+
108
+ /**
109
+ * Get error observable
110
+ * @returns {Object} Observable-like object
111
+ */
112
+ get error$() {
113
+ if (!errorSubject) {
114
+ const { BehaviorSubject } = require('rxjs');
115
+ errorSubject = new BehaviorSubject(null);
116
+ }
117
+ return errorSubject.asObservable();
118
+ },
119
+
120
+ /**
121
+ * Get loading observable
122
+ * @returns {Object} Observable-like object
123
+ */
124
+ get loading$() {
125
+ if (!loadingSubject) {
126
+ const { BehaviorSubject } = require('rxjs');
127
+ loadingSubject = new BehaviorSubject(true);
128
+ }
129
+ return loadingSubject.asObservable();
130
+ },
131
+
132
+ /**
133
+ * Get current system synchronously
134
+ * @returns {Object|null} Current system instance
135
+ */
136
+ getSystem() {
137
+ return system;
138
+ },
139
+
140
+ /**
141
+ * Get a facet by kind
142
+ * @param {string} kind - Facet kind identifier
143
+ * @returns {Object|null} Facet instance or null
144
+ */
145
+ useFacet(kind) {
146
+ return system?.find?.(kind) ?? null;
147
+ },
148
+
149
+ /**
150
+ * Register an event listener
151
+ * @param {string} eventName - Event name/path
152
+ * @param {Function} handler - Handler function
153
+ * @returns {Function} Unsubscribe function
154
+ */
155
+ useListener(eventName, handler) {
156
+ const listeners = system?.listeners;
157
+ if (!listeners || !listeners.hasListeners?.()) {
158
+ return () => {};
159
+ }
160
+
161
+ listeners.on(eventName, handler);
162
+ return () => {
163
+ listeners.off?.(eventName, handler);
164
+ };
165
+ },
166
+
167
+ /**
168
+ * Dispose the system
169
+ * @returns {Promise<void>}
170
+ */
171
+ async dispose() {
172
+ if (system && typeof system.dispose === 'function') {
173
+ await system.dispose().catch(() => {
174
+ // Ignore disposal errors
175
+ });
176
+ }
177
+ system = null;
178
+ systemSubject?.complete?.();
179
+ errorSubject?.complete?.();
180
+ loadingSubject?.complete?.();
181
+ }
182
+ };
183
+ }
184
+
185
+ // Re-export helper functions
186
+ export { useFacet, useListener, useEventStream } from './helpers.js';
187
+ export { createAngularSystemBuilder } from './builders.js';
188
+ export { createFacetService } from './services.js';
189
+
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Angular Service Generators
3
+ */
4
+
5
+ /**
6
+ * createFacetService - Generate a service for a specific facet kind
7
+ *
8
+ * @param {string} kind - Facet kind identifier
9
+ * @returns {Function} Service factory: (myceliaService) => Observable<Facet>
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // In services/todo.service.ts
14
+ * export const TodoService = createFacetService('todoStore');
15
+ *
16
+ * // In component
17
+ * @Component({...})
18
+ * export class TodoListComponent {
19
+ * todos$ = TodoService(this.mycelia);
20
+ * }
21
+ * ```
22
+ */
23
+ export function createFacetService(kind) {
24
+ return function(myceliaService) {
25
+ const { map } = require('rxjs/operators');
26
+ return myceliaService.system$.pipe(
27
+ map(system => system?.find?.(kind) ?? null)
28
+ );
29
+ };
30
+ }
31
+
32
+
@@ -17,6 +17,16 @@ export function createCacheKey(kinds) {
17
17
  /**
18
18
  * Build a dependency graph from hook metadata and facet dependencies.
19
19
  *
20
+ * Creates a directed graph where:
21
+ * - Vertices = facet kinds
22
+ * - Edges = dependencies (if A depends on B, edge goes from B -> A)
23
+ *
24
+ * The graph is represented as:
25
+ * - graph: Map<depKind, Set<dependentKinds>> - adjacency list
26
+ * - indeg: Map<kind, number> - in-degree count (how many dependencies)
27
+ *
28
+ * This graph is used by topological sort to determine initialization order.
29
+ *
20
30
  * @param {Object} hooksByKind - Object mapping hook kinds to hook metadata
21
31
  * @param {Object} facetsByKind - Object mapping facet kinds to Facet instances
22
32
  * @param {BaseSubsystem} subsystem - Subsystem instance (optional, for future use)
@@ -24,16 +34,21 @@ export function createCacheKey(kinds) {
24
34
  */
25
35
  export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
26
36
  const kinds = Object.keys(facetsByKind);
37
+
38
+ // Initialize graph data structures:
39
+ // - graph: adjacency list (dep -> Set of dependents)
40
+ // - indeg: in-degree counter (how many incoming edges)
27
41
  const graph = new Map(); // dep -> Set(of dependents)
28
42
  const indeg = new Map(); // kind -> indegree
29
43
 
44
+ // Initialize all vertices with empty adjacency lists and zero indegree
30
45
  for (const k of kinds) {
31
46
  graph.set(k, new Set());
32
47
  indeg.set(k, 0);
33
48
  }
34
49
 
35
- // First, add dependencies from hook metadata (hook.required)
36
- // hooksByKind now contains arrays of hooks per kind
50
+ // Phase 1: Add dependencies from hook metadata (hook.required)
51
+ // hooksByKind now contains arrays of hooks per kind (multiple hooks can create same facet)
37
52
  for (const [kind, hookList] of Object.entries(hooksByKind)) {
38
53
  if (!Array.isArray(hookList)) continue;
39
54
 
@@ -43,7 +58,9 @@ export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
43
58
  const hookDeps = (lastHook?.required && Array.isArray(lastHook.required)) ? lastHook.required : [];
44
59
  const hookOverwrite = lastHook?.overwrite === true;
45
60
 
61
+ // Process each dependency declared by the hook
46
62
  for (const dep of hookDeps) {
63
+ // Special case: overwrite hooks may require their own kind
47
64
  // Skip self-dependency for overwrite hooks (they need the original facet to exist,
48
65
  // but they're replacing it, so no cycle in facet dependency graph)
49
66
  if (dep === kind && hookOverwrite) {
@@ -55,10 +72,15 @@ export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
55
72
  // But don't add the dependency edge (no cycle in facet graph)
56
73
  continue;
57
74
  }
75
+
76
+ // Validate dependency exists
58
77
  if (!facetsByKind[dep]) {
59
78
  const src = lastHook?.source || kind;
60
79
  throw new Error(`Hook '${kind}' (from ${src}) requires missing facet '${dep}'.`);
61
80
  }
81
+
82
+ // Add edge: dep -> kind (if not already present)
83
+ // This means "kind depends on dep", so dep must be initialized first
62
84
  if (!graph.get(dep).has(kind)) {
63
85
  graph.get(dep).add(kind);
64
86
  indeg.set(kind, (indeg.get(kind) || 0) + 1);
@@ -66,14 +88,18 @@ export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
66
88
  }
67
89
  }
68
90
 
69
- // Then, add dependencies from facet metadata (facet.getDependencies())
91
+ // Phase 2: Add dependencies from facet metadata (facet.getDependencies())
92
+ // Facets can declare additional dependencies at runtime (beyond hook.required)
70
93
  for (const [kind, facet] of Object.entries(facetsByKind)) {
71
94
  const facetDeps = (typeof facet.getDependencies === 'function' && facet.getDependencies()) || [];
72
95
  for (const dep of facetDeps) {
96
+ // Validate dependency exists
73
97
  if (!facetsByKind[dep]) {
74
98
  const src = facet.getSource?.() || kind;
75
99
  throw new Error(`Facet '${kind}' (from ${src}) depends on missing '${dep}'.`);
76
100
  }
101
+
102
+ // Add edge: dep -> kind (if not already present)
77
103
  if (!graph.get(dep).has(kind)) {
78
104
  graph.get(dep).add(kind);
79
105
  indeg.set(kind, (indeg.get(kind) || 0) + 1);
@@ -87,6 +113,17 @@ export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
87
113
  /**
88
114
  * Kahn topological sort with diagnostics and caching.
89
115
  *
116
+ * Implements Kahn's algorithm for topological sorting (O(V + E) complexity).
117
+ * This algorithm finds a linear ordering of vertices such that for every directed
118
+ * edge (u, v), vertex u comes before v in the ordering.
119
+ *
120
+ * Algorithm steps:
121
+ * 1. Find all vertices with no incoming edges (indegree = 0)
122
+ * 2. Add them to a queue
123
+ * 3. Process queue: remove vertex, add to result, decrement indegree of neighbors
124
+ * 4. If any neighbor's indegree becomes 0, add it to the queue
125
+ * 5. If result length < total vertices, a cycle exists
126
+ *
90
127
  * @param {Object} graphData - Dependency graph data { graph, indeg, kinds }
91
128
  * @param {DependencyGraphCache} graphCache - Optional cache for storing results
92
129
  * @param {string} cacheKey - Optional cache key
@@ -94,7 +131,7 @@ export function buildDepGraph(hooksByKind, facetsByKind, subsystem = null) {
94
131
  * @throws {Error} If a dependency cycle is detected
95
132
  */
96
133
  export function topoSort({ graph, indeg, kinds }, graphCache = null, cacheKey = null) {
97
- // Check cache if provided
134
+ // Check cache if provided - avoid recomputing if we've seen this graph before
98
135
  if (graphCache && cacheKey) {
99
136
  const cached = graphCache.get(cacheKey);
100
137
  if (cached) {
@@ -106,24 +143,42 @@ export function topoSort({ graph, indeg, kinds }, graphCache = null, cacheKey =
106
143
  }
107
144
  }
108
145
 
146
+ // Step 1: Initialize queue with all vertices that have no incoming edges
147
+ // These are the "roots" of the dependency graph - they can be processed first
109
148
  const q = [];
110
- for (const k of kinds) if ((indeg.get(k) || 0) === 0) q.push(k);
149
+ for (const k of kinds) {
150
+ if ((indeg.get(k) || 0) === 0) {
151
+ q.push(k);
152
+ }
153
+ }
111
154
 
155
+ // Step 2: Process queue using Kahn's algorithm
112
156
  const ordered = [];
113
157
  while (q.length) {
158
+ // Remove a vertex with no incoming edges
114
159
  const n = q.shift();
115
160
  ordered.push(n);
161
+
162
+ // Step 3: For each neighbor (dependent) of this vertex:
163
+ // - Decrement its indegree (one less dependency to wait for)
164
+ // - If indegree becomes 0, add it to queue (ready to process)
116
165
  for (const m of graph.get(n)) {
117
- indeg.set(m, indeg.get(m) - 1);
118
- if (indeg.get(m) === 0) q.push(m);
166
+ const newIndeg = indeg.get(m) - 1;
167
+ indeg.set(m, newIndeg);
168
+ if (newIndeg === 0) {
169
+ q.push(m);
170
+ }
119
171
  }
120
172
  }
121
173
 
174
+ // Step 4: Check for cycles
175
+ // If we couldn't process all vertices, there's a circular dependency
176
+ // The "stuck" vertices are those still with indegree > 0 (part of a cycle)
122
177
  if (ordered.length !== kinds.length) {
123
178
  const stuck = kinds.filter(k => (indeg.get(k) || 0) > 0);
124
179
  const error = `Facet dependency cycle detected among: ${stuck.join(', ')}`;
125
180
 
126
- // Cache invalid result
181
+ // Cache invalid result so we don't recompute the same invalid graph
127
182
  if (graphCache && cacheKey) {
128
183
  graphCache.set(cacheKey, false, null, error);
129
184
  }
@@ -131,7 +186,8 @@ export function topoSort({ graph, indeg, kinds }, graphCache = null, cacheKey =
131
186
  throw new Error(error);
132
187
  }
133
188
 
134
- // Cache valid result
189
+ // Step 5: Cache valid result for future use
190
+ // This significantly speeds up repeated builds with the same dependency structure
135
191
  if (graphCache && cacheKey) {
136
192
  graphCache.set(cacheKey, true, ordered, null);
137
193
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Facet } from '../core/facet.js';
8
+ import { instrumentHookExecution } from '../utils/instrumentation.js';
8
9
 
9
10
  /**
10
11
  * Order hooks based on their dependencies using topological sort.
@@ -44,32 +45,39 @@ export function orderHooksByDependencies(hooks) {
44
45
  }
45
46
 
46
47
  // Build dependency graph using hook IDs
48
+ // This graph tracks hook-to-hook dependencies (not facet-to-facet)
49
+ // We need this because multiple hooks can create the same facet kind
47
50
  const hookDepGraph = new Map(); // hookId -> Set(dependent hookIds)
48
51
  const hookIndeg = new Map(); // hookId -> indegree
49
52
 
50
- // Initialize graph
53
+ // Initialize graph: all hooks start with no dependencies
51
54
  for (const { hookId } of hookArray) {
52
55
  hookDepGraph.set(hookId, new Set());
53
56
  hookIndeg.set(hookId, 0);
54
57
  }
55
58
 
56
59
  // Add edges: if hook A requires kind X, find the hook(s) that create X
60
+ // This creates a dependency: "hook A must run after hook that creates X"
57
61
  for (const { hookId, kind, required, overwrite, index } of hookArray) {
58
62
  for (const depKind of required) {
59
63
  const depHookIds = kindToHookIds.get(depKind) || [];
60
64
 
61
65
  if (depHookIds.length === 0) {
62
66
  // Dependency not in our hook list (might be external or not yet created)
67
+ // Skip - we'll validate this later in validateHookDependencies
63
68
  continue;
64
69
  }
65
70
 
71
+ // Special case: overwrite hooks requiring their own kind
66
72
  // If this hook requires its own kind (overwrite scenario)
67
73
  if (depKind === kind && overwrite) {
68
74
  // Find the previous hook of this kind (the one it's overwriting)
75
+ // Overwrite hooks must run AFTER the hook they're replacing
69
76
  if (index > 0) {
70
77
  const previousHookId = `${kind}:${index - 1}`;
71
78
  if (hookIdMap.has(previousHookId)) {
72
- // Overwrite hook depends on the previous hook of same kind
79
+ // Add edge: previousHook -> currentHook
80
+ // This ensures the overwrite hook runs after the original
73
81
  hookDepGraph.get(previousHookId).add(hookId);
74
82
  hookIndeg.set(hookId, (hookIndeg.get(hookId) || 0) + 1);
75
83
  }
@@ -79,27 +87,35 @@ export function orderHooksByDependencies(hooks) {
79
87
 
80
88
  // For other dependencies, depend on the LAST hook that creates that kind
81
89
  // (the most recent/enhanced version)
90
+ // This ensures hooks always depend on the final version of a facet
82
91
  if (depHookIds.length > 0) {
83
92
  const lastDepHookId = depHookIds[depHookIds.length - 1];
93
+ // Add edge: lastDepHook -> currentHook
84
94
  hookDepGraph.get(lastDepHookId).add(hookId);
85
95
  hookIndeg.set(hookId, (hookIndeg.get(hookId) || 0) + 1);
86
96
  }
87
97
  }
88
98
  }
89
99
 
90
- // Topological sort on hook IDs
100
+ // Topological sort on hook IDs using Kahn's algorithm
101
+ // This determines the order in which hooks should be executed
91
102
  const orderedHookIds = [];
92
103
  const queue = [];
104
+
105
+ // Step 1: Find all hooks with no dependencies (indegree = 0)
93
106
  for (const { hookId } of hookArray) {
94
107
  if (hookIndeg.get(hookId) === 0) {
95
108
  queue.push(hookId);
96
109
  }
97
110
  }
98
111
 
112
+ // Step 2: Process queue - remove hooks with no dependencies, add to result
99
113
  while (queue.length > 0) {
100
114
  const hookId = queue.shift();
101
115
  orderedHookIds.push(hookId);
102
116
 
117
+ // Step 3: For each dependent hook, decrement its indegree
118
+ // If indegree becomes 0, it's ready to process (add to queue)
103
119
  for (const dependent of hookDepGraph.get(hookId)) {
104
120
  const newIndeg = hookIndeg.get(dependent) - 1;
105
121
  hookIndeg.set(dependent, newIndeg);
@@ -109,6 +125,11 @@ export function orderHooksByDependencies(hooks) {
109
125
  }
110
126
  }
111
127
 
128
+ // Note: We don't check for cycles here because:
129
+ // 1. Hook-level cycles are less common (hooks are usually independent)
130
+ // 2. Facet-level cycles are caught in buildDepGraph/topoSort
131
+ // 3. If a cycle exists, orderedHookIds.length < hookArray.length
132
+
112
133
  // Build ordered hooks array
113
134
  const orderedHooks = [];
114
135
  for (const hookId of orderedHookIds) {
@@ -146,7 +167,8 @@ export function executeHooksAndCreateFacets(orderedHooks, resolvedCtx, subsystem
146
167
 
147
168
  let facet;
148
169
  try {
149
- facet = hook(resolvedCtx, subsystem.api, subsystem);
170
+ // Instrument hook execution for timing
171
+ facet = instrumentHookExecution(hook, resolvedCtx, subsystem.api, subsystem);
150
172
  } catch (error) {
151
173
  throw new Error(`Hook '${hookKind}' (from ${hookSource}) failed during execution: ${error.message}`);
152
174
  }