@telorun/kernel 0.2.4 → 0.2.5
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/dist/boot-context-registry.d.ts.map +1 -1
- package/dist/boot-context-registry.js +6 -6
- package/dist/boot-context-registry.js.map +1 -1
- package/dist/capabilities/capabilities/component.yaml +2 -1
- package/dist/capabilities/capabilities/executable.yaml +2 -1
- package/dist/capabilities/capabilities/handler.yaml +2 -1
- package/dist/capabilities/capabilities/listener.yaml +2 -1
- package/dist/capabilities/capabilities/provider.yaml +2 -1
- package/dist/capabilities/capabilities/template.yaml +2 -1
- package/dist/capabilities/capabilities/type.yaml +2 -1
- package/dist/capabilities/component.yaml +1 -1
- package/dist/capabilities/executable.yaml +1 -1
- package/dist/capabilities/handler.yaml +1 -1
- package/dist/capabilities/listener.yaml +1 -1
- package/dist/capabilities/provider.yaml +1 -1
- package/dist/capabilities/template.yaml +1 -1
- package/dist/capabilities/type.yaml +1 -1
- package/dist/controller-loader.d.ts +1 -1
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +4 -2
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-registry.d.ts +1 -2
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +38 -0
- package/dist/controllers/module/import-controller.d.ts.map +1 -0
- package/dist/controllers/module/import-controller.js +119 -0
- package/dist/controllers/module/import-controller.js.map +1 -0
- package/dist/controllers/module/module-controller.d.ts +57 -11
- package/dist/controllers/module/module-controller.d.ts.map +1 -1
- package/dist/controllers/module/module-controller.js +46 -82
- package/dist/controllers/module/module-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +12 -4
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +91 -0
- package/dist/evaluation-context.d.ts.map +1 -0
- package/dist/evaluation-context.js +220 -0
- package/dist/evaluation-context.js.map +1 -0
- package/dist/execution-context.d.ts +13 -0
- package/dist/execution-context.d.ts.map +1 -0
- package/dist/execution-context.js +14 -0
- package/dist/execution-context.js.map +1 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +23 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +212 -333
- package/dist/kernel.js.map +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +29 -9
- package/dist/loader.js.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.js +21 -12
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -25
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +3 -22
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context-registry.d.ts +48 -0
- package/dist/module-context-registry.d.ts.map +1 -0
- package/dist/module-context-registry.js +91 -0
- package/dist/module-context-registry.js.map +1 -0
- package/dist/module-context.d.ts +31 -0
- package/dist/module-context.d.ts.map +1 -0
- package/dist/module-context.js +67 -0
- package/dist/module-context.js.map +1 -0
- package/dist/registry.d.ts +1 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +1 -1
- package/dist/resource-context.d.ts +25 -5
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +74 -28
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts.map +1 -1
- package/dist/schema-valiator.js +3 -1
- package/dist/schema-valiator.js.map +1 -1
- package/dist/snapshot-serializer.d.ts +1 -2
- package/dist/snapshot-serializer.d.ts.map +1 -1
- package/dist/snapshot-serializer.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -6
- package/src/boot-context-registry.ts +169 -0
- package/src/capabilities/component.yaml +4 -0
- package/src/capabilities/executable.yaml +8 -0
- package/src/capabilities/handler.yaml +4 -0
- package/src/capabilities/listener.yaml +4 -0
- package/src/capabilities/provider.yaml +4 -0
- package/src/capabilities/template.yaml +4 -0
- package/src/capabilities/type.yaml +4 -0
- package/src/controller-loader.ts +298 -0
- package/src/controller-registry.ts +206 -0
- package/src/controllers/capability/capability-controller.ts +41 -0
- package/src/controllers/module/import-controller.ts +143 -0
- package/src/controllers/module/module-controller.ts +67 -0
- package/src/controllers/module/module.json +48 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +87 -0
- package/src/controllers/resource-definition/resource-definition.json +18 -0
- package/src/event-stream.ts +121 -0
- package/src/events.ts +99 -0
- package/src/index.ts +7 -0
- package/src/kernel.ts +558 -0
- package/src/loader.ts +245 -0
- package/src/manifest-adapters/http-adapter.ts +35 -0
- package/src/manifest-adapters/local-file-adapter.ts +69 -0
- package/src/manifest-adapters/manifest-adapter.ts +33 -0
- package/src/manifest-adapters/registry-adapter.ts +56 -0
- package/src/manifest-schemas.ts +49 -0
- package/src/registry.ts +137 -0
- package/src/resource-context.ts +266 -0
- package/src/resource-uri.ts +200 -0
- package/src/schema-valiator.ts +57 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -109
- package/dist/cli.js.map +0 -1
- package/dist/expressions.d.ts +0 -20
- package/dist/expressions.d.ts.map +0 -1
- package/dist/expressions.js +0 -253
- package/dist/expressions.js.map +0 -1
- package/dist/template-definition.d.ts +0 -38
- package/dist/template-definition.d.ts.map +0 -1
- package/dist/template-definition.js +0 -26
- package/dist/template-definition.js.map +0 -1
- package/dist/template-expander.d.ts +0 -19
- package/dist/template-expander.d.ts.map +0 -1
- package/dist/template-expander.js +0 -425
- package/dist/template-expander.js.map +0 -1
- /package/{dist/src → src}/controllers/module/module.yaml +0 -0
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ControllerContext,
|
|
3
|
+
EvaluationContext,
|
|
4
|
+
Kernel as IKernel,
|
|
5
|
+
ModuleContext,
|
|
6
|
+
ResourceContext,
|
|
7
|
+
ResourceDefinition,
|
|
8
|
+
ResourceInstance,
|
|
9
|
+
ResourceManifest,
|
|
10
|
+
RuntimeError,
|
|
11
|
+
RuntimeEvent,
|
|
12
|
+
} from "@telorun/sdk";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { ControllerRegistry } from "./controller-registry.js";
|
|
15
|
+
import { EventStream } from "./event-stream.js";
|
|
16
|
+
import { EventBus } from "./events.js";
|
|
17
|
+
import { Loader } from "./loader.js";
|
|
18
|
+
import { ResourceContextImpl } from "./resource-context.js";
|
|
19
|
+
import { SchemaValidator } from "./schema-valiator.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Kernel: Central orchestrator managing lifecycle and message bus
|
|
23
|
+
* Handles resource loading, initialization, and execution through controllers
|
|
24
|
+
*/
|
|
25
|
+
export class Kernel implements IKernel {
|
|
26
|
+
private loader: Loader = new Loader();
|
|
27
|
+
// private manifests: ManifestRegistry = new ManifestRegistry();
|
|
28
|
+
private controllers: ControllerRegistry = new ControllerRegistry();
|
|
29
|
+
private eventBus: EventBus = new EventBus();
|
|
30
|
+
private eventStream: EventStream = new EventStream();
|
|
31
|
+
// private snapshotSerializer: SnapshotSerializer = new SnapshotSerializer();
|
|
32
|
+
// private runtimeManifests: ResourceManifest[] | null = null;
|
|
33
|
+
// private resourceInstances: Map<
|
|
34
|
+
// string,
|
|
35
|
+
// { resource: ResourceManifest; instance: ResourceInstance }
|
|
36
|
+
// > = new Map();
|
|
37
|
+
private resourceEventBuses: Map<string, EventBus> = new Map();
|
|
38
|
+
private holdCount = 0;
|
|
39
|
+
private idleResolvers: Array<() => void> = [];
|
|
40
|
+
private _exitCode = 0;
|
|
41
|
+
// private bootContextRegistry = new BootContextRegistry();
|
|
42
|
+
private readonly sharedSchemaValidator = new SchemaValidator();
|
|
43
|
+
private rootContext!: ModuleContext;
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
this.setupEventStreaming();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// async teardownResource(module: string, kind: string, name: string): Promise<void> {
|
|
50
|
+
// const key = this.getResourceKey(module, kind, name);
|
|
51
|
+
// const entry = this.resourceInstances.get(key);
|
|
52
|
+
// if (!entry) return;
|
|
53
|
+
// const { resource, instance } = entry;
|
|
54
|
+
// if (instance.teardown) {
|
|
55
|
+
// await instance.teardown();
|
|
56
|
+
// }
|
|
57
|
+
// await this.eventBus.emit(`${resource.kind}.${resource.metadata.name}.Teardown`, {
|
|
58
|
+
// resource: { kind: resource.kind, name: resource.metadata.name },
|
|
59
|
+
// });
|
|
60
|
+
// // this.resourceInstances.delete(key);
|
|
61
|
+
// this.resourceEventBuses.delete(key);
|
|
62
|
+
// }
|
|
63
|
+
|
|
64
|
+
async registerController(
|
|
65
|
+
moduleName: string,
|
|
66
|
+
kindName: string,
|
|
67
|
+
controllerInstance: any,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
this.controllers.registerController(`${moduleName}.${kindName}`, controllerInstance);
|
|
70
|
+
await controllerInstance.register?.(this.createControllerContext(`${moduleName}.${kindName}`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Register a resource definition with the controller registry
|
|
75
|
+
*/
|
|
76
|
+
registerResourceDefinition(
|
|
77
|
+
definition: ResourceDefinition,
|
|
78
|
+
// basePath?: string,
|
|
79
|
+
// namespace?: string | null,
|
|
80
|
+
): void {
|
|
81
|
+
this.controllers.registerDefinition(definition);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
registerCapability(name: string, schema?: Record<string, any>): void {
|
|
85
|
+
this.controllers.registerCapability(name, schema);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getModuleContext(_moduleName: string): ModuleContext {
|
|
89
|
+
return this.rootContext;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
resolveModuleAlias(_declaringModule: string, alias: string): string | undefined {
|
|
93
|
+
return this.rootContext.importAliases.get(alias);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
registerModuleImport(
|
|
97
|
+
_declaringModule: string,
|
|
98
|
+
alias: string,
|
|
99
|
+
targetModule: string,
|
|
100
|
+
kinds: string[],
|
|
101
|
+
): void {
|
|
102
|
+
this.rootContext.registerImport(alias, targetModule, kinds);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
isCapabilityRegistered(name: string): boolean {
|
|
106
|
+
return this.controllers.isCapabilityRegistered(name);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getCapabilitySchema(name: string): Record<string, any> | null | undefined {
|
|
110
|
+
return this.controllers.getCapabilitySchema(name);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load built-in Runtime definitions (e.g., Kernel.Module)
|
|
115
|
+
* Also declares all known module namespaces upfront so that resources can be
|
|
116
|
+
* registered to them. User-defined modules are declared explicitly by Kernel.Module
|
|
117
|
+
* resources during the initialization phase.
|
|
118
|
+
*/
|
|
119
|
+
private async loadBuiltinDefinitions(): Promise<void> {
|
|
120
|
+
// Declare built-in module namespaces upfront so getContext() can distinguish
|
|
121
|
+
// "not yet populated" from a completely unknown module name.
|
|
122
|
+
this.rootContext.registerImport("Kernel", "Kernel", []); // built-ins, unrestricted
|
|
123
|
+
// this.moduleContextRegistry.declareModule("default"); // user resources with no module field
|
|
124
|
+
|
|
125
|
+
this.controllers.registerDefinition({
|
|
126
|
+
kind: "Kernel.Definition",
|
|
127
|
+
metadata: { name: "Definition", module: "Kernel" },
|
|
128
|
+
capabilities: ["template"],
|
|
129
|
+
schema: { type: "object" },
|
|
130
|
+
});
|
|
131
|
+
this.controllers.registerController(
|
|
132
|
+
"Kernel.Definition",
|
|
133
|
+
await import("./controllers/resource-definition/resource-definition-controller.js"),
|
|
134
|
+
);
|
|
135
|
+
this.controllers.registerDefinition({
|
|
136
|
+
kind: "Kernel.Definition",
|
|
137
|
+
metadata: { name: "Module", module: "Kernel" },
|
|
138
|
+
capabilities: ["template"],
|
|
139
|
+
schema: { type: "object" },
|
|
140
|
+
});
|
|
141
|
+
this.controllers.registerController(
|
|
142
|
+
"Kernel.Module",
|
|
143
|
+
await import("./controllers/module/module-controller.js"),
|
|
144
|
+
);
|
|
145
|
+
this.controllers.registerDefinition({
|
|
146
|
+
kind: "Kernel.Definition",
|
|
147
|
+
metadata: { name: "Import", module: "Kernel" },
|
|
148
|
+
capabilities: ["template"],
|
|
149
|
+
schema: { type: "object" },
|
|
150
|
+
});
|
|
151
|
+
this.controllers.registerController(
|
|
152
|
+
"Kernel.Import",
|
|
153
|
+
await import("./controllers/module/import-controller.js"),
|
|
154
|
+
);
|
|
155
|
+
this.controllers.registerDefinition({
|
|
156
|
+
kind: "Kernel.Definition",
|
|
157
|
+
metadata: { name: "Capability", module: "Kernel" },
|
|
158
|
+
capabilities: ["template"],
|
|
159
|
+
schema: { type: "object" },
|
|
160
|
+
});
|
|
161
|
+
this.controllers.registerController(
|
|
162
|
+
"Kernel.Capability",
|
|
163
|
+
await import("./controllers/capability/capability-controller.js"),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Load from runtime configuration file
|
|
169
|
+
*/
|
|
170
|
+
async loadFromConfig(runtimeYamlPath: string): Promise<void> {
|
|
171
|
+
this.rootContext = new ModuleContext(
|
|
172
|
+
this.loader.resolvePath(`file://${process.cwd()}/`, runtimeYamlPath),
|
|
173
|
+
{},
|
|
174
|
+
{},
|
|
175
|
+
{},
|
|
176
|
+
[],
|
|
177
|
+
this._createInstance.bind(this),
|
|
178
|
+
(event, payload) => this.eventBus.emit(event, payload),
|
|
179
|
+
);
|
|
180
|
+
// Initialize built-in Runtime definitions first
|
|
181
|
+
await this.loadBuiltinDefinitions();
|
|
182
|
+
|
|
183
|
+
// Load built-in capability manifests before user configuration so that
|
|
184
|
+
// capability resources are available in the first initialization pass
|
|
185
|
+
const { fileURLToPath } = await import("url");
|
|
186
|
+
const capabilitiesDir = fileURLToPath(new URL("./capabilities/", import.meta.url));
|
|
187
|
+
const capabilityManifests = await this.loader.loadDirectory(capabilitiesDir);
|
|
188
|
+
|
|
189
|
+
// Load runtime configuration — root module gets access to host env
|
|
190
|
+
const userManifests = await this.loader.loadManifest(
|
|
191
|
+
runtimeYamlPath,
|
|
192
|
+
`file://${process.cwd()}/`,
|
|
193
|
+
{ env: process.env },
|
|
194
|
+
);
|
|
195
|
+
const allManifests = [...capabilityManifests, ...userManifests];
|
|
196
|
+
|
|
197
|
+
for (const manifest of allManifests) {
|
|
198
|
+
if (manifest.kind === "Kernel.Module") {
|
|
199
|
+
this.rootContext.setSecrets(manifest.secrets ?? {});
|
|
200
|
+
this.rootContext.setVariables(manifest.variables ?? {});
|
|
201
|
+
this.rootContext.setTargets(manifest.targets ?? []);
|
|
202
|
+
}
|
|
203
|
+
this.rootContext.registerManifest(manifest);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Phase 1: Load - Ingest files from directory and load runtime config
|
|
209
|
+
* @deprecated Use loadFromConfig instead
|
|
210
|
+
*/
|
|
211
|
+
async loadDirectory(dirPath: string): Promise<void> {
|
|
212
|
+
const configYamlPath = path.join(dirPath, "module.yaml");
|
|
213
|
+
|
|
214
|
+
await this.loadFromConfig(configYamlPath);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Phase 2: Start - Initialize resources
|
|
219
|
+
*/
|
|
220
|
+
async start(): Promise<void> {
|
|
221
|
+
// Call controller register hooks first (before any initialization)
|
|
222
|
+
for (const kind of this.controllers.getKinds()) {
|
|
223
|
+
const controller = this.controllers.getController(kind);
|
|
224
|
+
if (controller?.register) {
|
|
225
|
+
await controller.register(this.createControllerContext(`controller:${kind}`));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Initialize resources
|
|
230
|
+
try {
|
|
231
|
+
await this.rootContext.initializeResources();
|
|
232
|
+
await this.eventBus.emit("Kernel.Initialized", {});
|
|
233
|
+
await this.eventBus.emit("Kernel.Starting", {});
|
|
234
|
+
await this.rootContext.runTargets();
|
|
235
|
+
await this.eventBus.emit("Kernel.Started", {});
|
|
236
|
+
await this.waitForIdle();
|
|
237
|
+
} finally {
|
|
238
|
+
await this.eventBus.emit("Kernel.Stopping", {});
|
|
239
|
+
await this.rootContext.teardownResources();
|
|
240
|
+
await this.eventBus.emit("Kernel.Stopped", { exitCode: this._exitCode });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// async runInstances(): Promise<void> {
|
|
245
|
+
// for (const { instance } of this.resourceInstances.values()) {
|
|
246
|
+
// if (instance.run) {
|
|
247
|
+
// await instance.run();
|
|
248
|
+
// }
|
|
249
|
+
// }
|
|
250
|
+
// }
|
|
251
|
+
|
|
252
|
+
async emitRuntimeEvent(event: string, payload?: any): Promise<void> {
|
|
253
|
+
await this.eventBus.emit(event, payload);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
get exitCode(): number {
|
|
257
|
+
return this._exitCode;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
requestExit(code: number): void {
|
|
261
|
+
this._exitCode = Math.max(this._exitCode, code);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
acquireHold(reason?: string): () => void {
|
|
265
|
+
this.holdCount += 1;
|
|
266
|
+
if (this.holdCount === 1) {
|
|
267
|
+
void this.eventBus.emit("Kernel.Blocked", {
|
|
268
|
+
reason,
|
|
269
|
+
count: this.holdCount,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
let released = false;
|
|
273
|
+
return () => {
|
|
274
|
+
if (released) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
released = true;
|
|
278
|
+
this.holdCount = Math.max(0, this.holdCount - 1);
|
|
279
|
+
if (this.holdCount === 0) {
|
|
280
|
+
const resolvers = this.idleResolvers.splice(0);
|
|
281
|
+
for (const resolve of resolvers) {
|
|
282
|
+
resolve();
|
|
283
|
+
}
|
|
284
|
+
void this.eventBus.emit("Kernel.Unblocked", { count: this.holdCount });
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
waitForIdle(): Promise<void> {
|
|
290
|
+
if (this.holdCount === 0) {
|
|
291
|
+
return Promise.resolve();
|
|
292
|
+
}
|
|
293
|
+
return new Promise((resolve) => {
|
|
294
|
+
this.idleResolvers.push(resolve);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Force-resolve waitForIdle() regardless of active holds.
|
|
300
|
+
* Used for graceful shutdown when external signals (e.g. SIGINT) should
|
|
301
|
+
* bypass resource holds and proceed directly to teardown.
|
|
302
|
+
*/
|
|
303
|
+
shutdown(): void {
|
|
304
|
+
const resolvers = this.idleResolvers.splice(0);
|
|
305
|
+
for (const resolve of resolvers) resolve();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
hasEventHandlers(event: string): boolean {
|
|
309
|
+
return this.eventBus.hasHandlers(event);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
on(event: string, handler: (event: RuntimeEvent) => void | Promise<void>): void {
|
|
313
|
+
this.eventBus.on(event, handler);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private createControllerContext(kind: string): ControllerContext {
|
|
317
|
+
return {
|
|
318
|
+
on: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
319
|
+
this.eventBus.on(event, handler),
|
|
320
|
+
once: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
321
|
+
this.eventBus.once(event, handler),
|
|
322
|
+
off: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
323
|
+
this.eventBus.off(event, handler),
|
|
324
|
+
emit: (event: string, payload?: any) => {
|
|
325
|
+
const namespaced = event.includes(".") ? event : `${kind}.${event}`;
|
|
326
|
+
void this.eventBus.emit(namespaced, payload);
|
|
327
|
+
},
|
|
328
|
+
acquireHold: (reason?: string) => this.acquireHold(reason),
|
|
329
|
+
expandValue: (value: any, context: Record<string, any>) =>
|
|
330
|
+
this.rootContext.merge(context).expand(value),
|
|
331
|
+
requestExit: (code: number) => this.requestExit(code),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private createResourceContext(
|
|
336
|
+
moduleContext: ModuleContext,
|
|
337
|
+
resource: ResourceManifest,
|
|
338
|
+
): ResourceContext {
|
|
339
|
+
return new ResourceContextImpl(
|
|
340
|
+
this,
|
|
341
|
+
moduleContext,
|
|
342
|
+
resource.metadata,
|
|
343
|
+
this.sharedSchemaValidator,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create a resource instance: resolves the controller, validates the schema,
|
|
349
|
+
* calls create/init, registers context providers, stores the snapshot, and
|
|
350
|
+
* mirrors the instance into the Kernel-level and module-level registries.
|
|
351
|
+
* Returns null when the controller is not yet registered (retry signal).
|
|
352
|
+
*/
|
|
353
|
+
private async _createInstance(
|
|
354
|
+
evalContext: ModuleContext,
|
|
355
|
+
resource: ResourceManifest,
|
|
356
|
+
): Promise<ResourceInstance | null> {
|
|
357
|
+
const kind = resource.kind;
|
|
358
|
+
|
|
359
|
+
// Resolve the alias-prefixed kind to its real fully-qualified kind.
|
|
360
|
+
// resolveKind() throws with a clear message if the alias or kind is not found.
|
|
361
|
+
const resolvedKind = this.rootContext.resolveKind(kind);
|
|
362
|
+
|
|
363
|
+
const controller = this.controllers.getControllerOrUndefined(resolvedKind);
|
|
364
|
+
if (!controller) {
|
|
365
|
+
const kindInfo =
|
|
366
|
+
resolvedKind !== kind ? `'${kind}' (resolved to '${resolvedKind}')` : `'${kind}'`;
|
|
367
|
+
throw new Error(
|
|
368
|
+
`No controller registered for kind ${kindInfo}, known controllers are: ${this.controllers.getKinds().join(", ")}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!controller.create) {
|
|
373
|
+
throw new RuntimeError(
|
|
374
|
+
"ERR_CONTROLLER_INVALID",
|
|
375
|
+
`Controller for ${kind} does not implement create method`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!controller.schema?.type) {
|
|
379
|
+
throw new Error(`No schema defined for ${kind} controller`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
this.sharedSchemaValidator.compile(controller.schema).validate(resource);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
throw new RuntimeError(
|
|
386
|
+
"ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
|
|
387
|
+
`Resource does not match schema for kind ${kind}: ${error instanceof Error ? error.message : String(error)}`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const ctx = this.createResourceContext(evalContext, resource);
|
|
392
|
+
const instance = await controller.create(resource, ctx);
|
|
393
|
+
if (!instance) return null;
|
|
394
|
+
|
|
395
|
+
if (instance.init) await instance.init(ctx);
|
|
396
|
+
|
|
397
|
+
// if (isContextProvider(instance)) {
|
|
398
|
+
// this.bootContextRegistry.register(
|
|
399
|
+
// resource.kind,
|
|
400
|
+
// resource.metadata.name,
|
|
401
|
+
// resource.metadata.module,
|
|
402
|
+
// resource.grants as string[] | undefined,
|
|
403
|
+
// instance.provideContext(),
|
|
404
|
+
// );
|
|
405
|
+
// }
|
|
406
|
+
|
|
407
|
+
if (instance.snapshot) {
|
|
408
|
+
const snap = await Promise.resolve(instance.snapshot()).catch(() => ({}));
|
|
409
|
+
if (evalContext instanceof ModuleContext) {
|
|
410
|
+
evalContext.setResource(resource.metadata.name, (snap as Record<string, unknown>) ?? {});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return instance;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Tear down all resource instances owned by a dynamically-spawned context,
|
|
419
|
+
* cascading depth-first through its children. Removes entries from both
|
|
420
|
+
* ctx.resourceInstances and the Kernel-level resourceInstances map, and
|
|
421
|
+
* emits Teardown events (matching the behaviour of teardownResource()).
|
|
422
|
+
*/
|
|
423
|
+
async teardownContext(ctx: EvaluationContext): Promise<void> {
|
|
424
|
+
for (const child of [...ctx.children].reverse()) {
|
|
425
|
+
await this.teardownContext(child);
|
|
426
|
+
}
|
|
427
|
+
const entries = [...ctx.resourceInstances.entries()].reverse();
|
|
428
|
+
for (const [key, { resource, instance }] of entries) {
|
|
429
|
+
if (instance.teardown) await instance.teardown();
|
|
430
|
+
await this.eventBus.emit(`${resource.kind}.${resource.metadata.name}.Teardown`, {
|
|
431
|
+
resource: { kind: resource.kind, name: resource.metadata.name },
|
|
432
|
+
});
|
|
433
|
+
// this.resourceInstances.delete(key);
|
|
434
|
+
ctx.resourceInstances.delete(key);
|
|
435
|
+
this.resourceEventBuses.delete(key);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// getResourcesByKind(kind: string): RuntimeResource[] {
|
|
440
|
+
// const resources: RuntimeResource[] = [];
|
|
441
|
+
// for (const entry of this.resourceInstances.values()) {
|
|
442
|
+
// if (entry.resource.kind === kind) {
|
|
443
|
+
// resources.push(entry.instance as any);
|
|
444
|
+
// }
|
|
445
|
+
// }
|
|
446
|
+
// return resources;
|
|
447
|
+
// }
|
|
448
|
+
|
|
449
|
+
// getResourceByName(declaringModule: string, alias: string, name: string): RuntimeResource | null {
|
|
450
|
+
// const realModule = this.moduleContextRegistry.resolveAlias(declaringModule, alias) ?? alias;
|
|
451
|
+
// for (const { resource, instance } of this.resourceInstances.values()) {
|
|
452
|
+
// if (resource.metadata.module === realModule && resource.metadata.name === name) {
|
|
453
|
+
// return instance as RuntimeResource;
|
|
454
|
+
// }
|
|
455
|
+
// }
|
|
456
|
+
// return null;
|
|
457
|
+
// }
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Returns the unique set of local file paths from which resources were loaded.
|
|
461
|
+
* HTTP/HTTPS sources are excluded — they cannot be watched on disk.
|
|
462
|
+
*/
|
|
463
|
+
// getSourceFiles(): string[] {
|
|
464
|
+
// const seen = new Set<string>();
|
|
465
|
+
// for (const { resource } of this.resourceInstances.values()) {
|
|
466
|
+
// const src = resource.metadata.source;
|
|
467
|
+
// if (src && !src.startsWith("http://") && !src.startsWith("https://")) {
|
|
468
|
+
// seen.add(src);
|
|
469
|
+
// }
|
|
470
|
+
// }
|
|
471
|
+
// return Array.from(seen);
|
|
472
|
+
// }
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Reload all resources that were loaded from the given source file.
|
|
476
|
+
* Safe order: parse first → if parse succeeds, tear down old → init new → run new only.
|
|
477
|
+
*/
|
|
478
|
+
// async reloadSource(sourcePath: string): Promise<void> {
|
|
479
|
+
// // Parse first — bail before touching running resources if the file is invalid
|
|
480
|
+
// const newManifests = await this.loader.loadManifest(sourcePath, `file://${process.cwd()}/`, {
|
|
481
|
+
// env: process.env,
|
|
482
|
+
// });
|
|
483
|
+
|
|
484
|
+
// // Collect keys of resources loaded from this source (in insertion order)
|
|
485
|
+
// const keysFromSource: string[] = [];
|
|
486
|
+
// for (const [key, { resource }] of this.resourceInstances.entries()) {
|
|
487
|
+
// if (resource.metadata.source === sourcePath) {
|
|
488
|
+
// keysFromSource.push(key);
|
|
489
|
+
// }
|
|
490
|
+
// }
|
|
491
|
+
// // Tear down in reverse order (children first via cascade)
|
|
492
|
+
// for (const key of [...keysFromSource].reverse()) {
|
|
493
|
+
// const entry = this.resourceInstances.get(key);
|
|
494
|
+
// if (!entry) continue; // already removed by a cascade
|
|
495
|
+
// await this.teardownResource(
|
|
496
|
+
// entry.resource.metadata.module,
|
|
497
|
+
// entry.resource.kind,
|
|
498
|
+
// entry.resource.metadata.name,
|
|
499
|
+
// );
|
|
500
|
+
// }
|
|
501
|
+
|
|
502
|
+
// for (const manifest of newManifests) {
|
|
503
|
+
// this.rootContext.registerManifest(manifest);
|
|
504
|
+
// }
|
|
505
|
+
// const keysBefore = new Set(this.resourceInstances.keys());
|
|
506
|
+
// await this.initializeResources();
|
|
507
|
+
|
|
508
|
+
// // Run only newly created instances (not all instances — avoids double-run)
|
|
509
|
+
// const newKeys = Array.from(this.resourceInstances.keys()).filter((k) => !keysBefore.has(k));
|
|
510
|
+
// for (const key of newKeys) {
|
|
511
|
+
// const entry = this.resourceInstances.get(key);
|
|
512
|
+
// if (entry?.instance.run) {
|
|
513
|
+
// await entry.instance.run();
|
|
514
|
+
// }
|
|
515
|
+
// }
|
|
516
|
+
// }
|
|
517
|
+
|
|
518
|
+
private getResourceKey(module: string, kind: string, name: string): string {
|
|
519
|
+
if (!kind.includes(".")) {
|
|
520
|
+
throw new Error(`Resource kind must include module prefix: ${kind}`);
|
|
521
|
+
}
|
|
522
|
+
return `${module}.${kind}.${name}`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Enable event streaming to a file (JSONL format)
|
|
527
|
+
*/
|
|
528
|
+
async enableEventStream(filePath: string): Promise<void> {
|
|
529
|
+
await this.eventStream.enable(filePath);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Disable event streaming
|
|
534
|
+
*/
|
|
535
|
+
disableEventStream(): void {
|
|
536
|
+
this.eventStream.disable();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get the event stream for testing and inspection
|
|
541
|
+
*/
|
|
542
|
+
getEventStream(): EventStream {
|
|
543
|
+
return this.eventStream;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Setup event streaming hook to capture all events
|
|
548
|
+
*/
|
|
549
|
+
private setupEventStreaming(): void {
|
|
550
|
+
const originalEmit = this.eventBus.emit.bind(this.eventBus);
|
|
551
|
+
this.eventBus.emit = async (event: string, payload?: any) => {
|
|
552
|
+
if (this.eventStream.isEnabledStream()) {
|
|
553
|
+
await this.eventStream.log(event, payload);
|
|
554
|
+
}
|
|
555
|
+
return originalEmit(event, payload);
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|