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.
- package/README.md +54 -7
- package/bin/cli.js +261 -0
- package/package.json +15 -5
- package/src/angular/builders.js +37 -0
- package/src/angular/helpers.js +102 -0
- package/src/angular/index.js +189 -0
- package/src/angular/services.js +32 -0
- package/src/builder/dependency-graph.js +65 -9
- package/src/builder/hook-processor.js +26 -4
- package/src/builder/utils.js +78 -22
- package/src/contract/contracts/listeners.contract.js +2 -0
- package/src/core/facet.js +16 -3
- package/src/hooks/listeners/use-simple-listeners.js +172 -0
- package/src/index.js +19 -0
- package/src/manager/facet-manager.js +10 -2
- package/src/qwik/builders.js +39 -0
- package/src/qwik/index.js +178 -0
- package/src/qwik/listeners.js +96 -0
- package/src/qwik/queues.js +87 -0
- package/src/qwik/signals.js +32 -0
- package/src/react/README.md +3 -0
- package/src/solid/README.md +69 -0
- package/src/solid/index.js +387 -0
- package/src/utils/instrumentation.js +204 -0
- package/src/utils/use-base.js +205 -30
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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)
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
}
|