@telorun/kernel 0.9.1 → 0.10.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.
@@ -47,6 +47,7 @@ class ResourceDefinition implements ResourceInstance {
47
47
  // here at the call site.
48
48
  const loader = new ControllerLoader({
49
49
  emit: (e) => ctx.emit(e.name, e.payload),
50
+ entryUrl: ctx.getEntryUrl(),
50
51
  });
51
52
  const controllerInstance = await loader.load(
52
53
  this.resource.controllers,
package/src/kernel.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  AnalysisRegistry,
3
+ flattenForAnalyzer,
4
+ flattenLoadedModule,
3
5
  isModuleKind,
4
6
  Loader,
5
7
  StaticAnalyzer,
@@ -48,6 +50,24 @@ function findEnclosingPolicy(ctx: IEvaluationContext): ControllerPolicy | undefi
48
50
  return findEnclosingModule(ctx)?.getControllerPolicy();
49
51
  }
50
52
 
53
+ function throwInvalidState(operation: string, reason: string): never {
54
+ throw new RuntimeError(
55
+ "ERR_KERNEL_STATE_INVALID",
56
+ `Cannot ${operation}(): ${reason}`,
57
+ );
58
+ }
59
+
60
+ function parseRef(ref: string): { kind: string; name: string } {
61
+ const lastDot = ref.lastIndexOf(".");
62
+ if (lastDot <= 0 || lastDot === ref.length - 1) {
63
+ throw new RuntimeError(
64
+ "ERR_INVALID_VALUE",
65
+ `Invalid resource reference '${ref}': expected '<Kind>.<Name>' (e.g. 'Http.Server.Main') or pass { kind, name } directly.`,
66
+ );
67
+ }
68
+ return { kind: ref.slice(0, lastDot), name: ref.slice(lastDot + 1) };
69
+ }
70
+
51
71
  export interface KernelOptions {
52
72
  stdin?: NodeJS.ReadableStream;
53
73
  stdout?: NodeJS.WritableStream;
@@ -96,6 +116,13 @@ export class Kernel implements IKernel {
96
116
  private readonly sharedSchemaValidator = new SchemaValidator();
97
117
  private rootContext!: ModuleContext;
98
118
  private staticManifests: ResourceManifest[] = [];
119
+ private _entryUrl?: string;
120
+ // Lifecycle state — guards boot/runTargets/teardown/invoke transitions.
121
+ // teardown() is the only idempotent method; everything else throws on misuse.
122
+ private _bootCalled = false;
123
+ private _isBooted = false;
124
+ private _targetsRan = false;
125
+ private _isTornDown = false;
99
126
 
100
127
  readonly stdin: NodeJS.ReadableStream;
101
128
  readonly stdout: NodeJS.WritableStream;
@@ -140,12 +167,15 @@ export class Kernel implements IKernel {
140
167
  this.registry.registerDefinition(definition);
141
168
  }
142
169
 
143
- loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]> {
144
- return this.loader.loadModule(url, options);
170
+ async loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]> {
171
+ const lm = await this.loader.loadModule(url, options);
172
+ return flattenLoadedModule(lm);
145
173
  }
146
174
 
147
- loadManifests(url: string): Promise<ResourceManifest[]> {
148
- return this.loader.loadManifests(url);
175
+ async loadManifests(url: string): Promise<ResourceManifest[]> {
176
+ const graph = await this.loader.loadGraph(url);
177
+ if (graph.errors.length > 0) throw graph.errors[0].error;
178
+ return flattenForAnalyzer(graph);
149
179
  }
150
180
 
151
181
  /** Returns the live analysis registry backed by this kernel's known definitions and aliases.
@@ -194,6 +224,7 @@ export class Kernel implements IKernel {
194
224
  */
195
225
  async load(url: string): Promise<void> {
196
226
  const sourceUrl = await this.loader.resolveEntryPoint(url);
227
+ this._entryUrl = sourceUrl;
197
228
  this.rootContext = new ModuleContext(
198
229
  sourceUrl,
199
230
  {},
@@ -218,7 +249,11 @@ export class Kernel implements IKernel {
218
249
 
219
250
  // Static analysis pre-flight: validates schemas and invocation context compatibility.
220
251
  // All errors are fatal — kernel does not start if analysis fails.
221
- const staticManifests = await this.loader.loadManifests(sourceUrl);
252
+ const analysisGraph = await this.loader.loadGraph(sourceUrl);
253
+ if (analysisGraph.errors.length > 0) {
254
+ throw analysisGraph.errors[0].error;
255
+ }
256
+ const staticManifests = flattenForAnalyzer(analysisGraph);
222
257
  this.staticManifests = staticManifests;
223
258
 
224
259
  // Register module identities for x-telo-ref resolution (Phase 3 prerequisite).
@@ -256,8 +291,11 @@ export class Kernel implements IKernel {
256
291
  );
257
292
  }
258
293
 
259
- // Load runtime configuration — root module gets access to host env
260
- const allManifests = await this.loader.loadModule(sourceUrl, { compile: true });
294
+ // Load runtime configuration — root module gets access to host env.
295
+ // Imports are loaded separately via the import-controller; this load is
296
+ // entry-only with compile-time CEL.
297
+ const lm = await this.loader.loadModule(sourceUrl, { compile: true });
298
+ const allManifests = flattenLoadedModule(lm);
261
299
 
262
300
  // Phase 2: normalize inline resources — extract inline values from x-telo-ref slots
263
301
  // into first-class named manifests and replace them in-place with {kind, name} references.
@@ -278,9 +316,25 @@ export class Kernel implements IKernel {
278
316
  }
279
317
 
280
318
  /**
281
- * Phase 2: Start - Initialize resources
319
+ * Initialize every resource declared in the manifest. Does not run targets
320
+ * and does not wait — returns as soon as the kernel is ready to accept
321
+ * `invoke()` calls.
322
+ *
323
+ * Throws ERR_KERNEL_STATE_INVALID if `load()` was not called first, on
324
+ * second call, or after teardown.
282
325
  */
283
- async start(): Promise<void> {
326
+ async boot(): Promise<void> {
327
+ if (this._isTornDown) {
328
+ throwInvalidState("boot", "kernel has been torn down");
329
+ }
330
+ if (this._bootCalled) {
331
+ throwInvalidState("boot", "boot() already called");
332
+ }
333
+ if (this._entryUrl === undefined) {
334
+ throwInvalidState("boot", "load() has not been called");
335
+ }
336
+ this._bootCalled = true;
337
+
284
338
  // Call register hooks for controllers actually loaded at this point (built-ins).
285
339
  // User-module kinds load their controllers during Phase 3 (Telo.Definition.init),
286
340
  // and registerController() fires their register hook there.
@@ -321,25 +375,103 @@ export class Kernel implements IKernel {
321
375
  this.rootContext.setInitOrder(order);
322
376
  }
323
377
 
324
- // Initialize resources
378
+ await this.rootContext.initializeResources();
379
+ await this.eventBus.emit("Kernel.Initialized", {});
380
+
381
+ this._isBooted = true;
382
+ }
383
+
384
+ /**
385
+ * Run the manifest's `targets` (Telo.Service / Telo.Runnable instances).
386
+ * Emits Kernel.Starting before, Kernel.Started after.
387
+ *
388
+ * Throws ERR_KERNEL_STATE_INVALID if called before `boot()` completes, after
389
+ * teardown, or a second time.
390
+ */
391
+ async runTargets(): Promise<void> {
392
+ if (this._isTornDown) {
393
+ throwInvalidState("runTargets", "kernel has been torn down");
394
+ }
395
+ if (!this._isBooted) {
396
+ throwInvalidState("runTargets", "boot() has not completed");
397
+ }
398
+ if (this._targetsRan) {
399
+ throwInvalidState("runTargets", "runTargets() already called");
400
+ }
401
+ this._targetsRan = true;
402
+
403
+ await this.eventBus.emit("Kernel.Starting", {});
404
+ await this.rootContext.runTargets();
405
+ await this.eventBus.emit("Kernel.Started", {});
406
+ }
407
+
408
+ /**
409
+ * Tear down every initialized resource. Emits Kernel.Stopping before,
410
+ * Kernel.Stopped after. Idempotent — second call is a no-op (does not
411
+ * re-emit). Tolerates partial state — a boot() that threw mid-init still
412
+ * cleans up whichever resources had initialized.
413
+ */
414
+ async teardown(): Promise<void> {
415
+ if (this._isTornDown) return;
416
+ this._isTornDown = true;
417
+
418
+ await this.eventBus.emit("Kernel.Stopping", {});
419
+ if (this.rootContext) {
420
+ await this.rootContext.teardownResources();
421
+ }
422
+ await this.eventBus.emit("Kernel.Stopped", { exitCode: this._exitCode });
423
+ }
424
+
425
+ /**
426
+ * Convenience: boot → runTargets → waitForIdle → teardown. The try wraps
427
+ * boot() and runTargets() too — init-time failures still drive teardown and
428
+ * still emit Kernel.Stopping / Kernel.Stopped, matching pre-split semantics.
429
+ */
430
+ async start(): Promise<void> {
325
431
  try {
326
- await this.rootContext.initializeResources();
327
- await this.eventBus.emit("Kernel.Initialized", {});
328
- await this.eventBus.emit("Kernel.Starting", {});
329
- await this.rootContext.runTargets();
330
- await this.eventBus.emit("Kernel.Started", {});
432
+ await this.boot();
433
+ await this.runTargets();
331
434
  await this.waitForIdle();
332
435
  } finally {
333
- await this.eventBus.emit("Kernel.Stopping", {});
334
- await this.rootContext.teardownResources();
335
- await this.eventBus.emit("Kernel.Stopped", { exitCode: this._exitCode });
436
+ await this.teardown();
336
437
  }
337
438
  }
338
439
 
440
+ /**
441
+ * Invoke a Telo.Invocable resource by `<kind>.<name>` (dot-form) or
442
+ * `{ kind, name }`. Resolves through the root module context, so the same
443
+ * dispatch, error path, and event emission that controller-to-controller
444
+ * invokes use apply here too.
445
+ */
446
+ async invoke<TInputs = any, TOutput = any>(
447
+ ref: string | { kind: string; name: string },
448
+ inputs: TInputs,
449
+ ): Promise<TOutput> {
450
+ if (this._isTornDown) {
451
+ throwInvalidState("invoke", "kernel has been torn down");
452
+ }
453
+ if (!this._isBooted) {
454
+ throwInvalidState("invoke", "boot() has not completed");
455
+ }
456
+ const parsed = typeof ref === "string" ? parseRef(ref) : ref;
457
+ return (await this.rootContext.invoke(parsed.kind, parsed.name, inputs)) as TOutput;
458
+ }
459
+
339
460
  async emitRuntimeEvent(event: string, payload?: any): Promise<void> {
340
461
  await this.eventBus.emit(event, payload);
341
462
  }
342
463
 
464
+ /**
465
+ * URL of the entry manifest passed to `load()`, or `undefined` before
466
+ * `load()` has been called. Used by controllers and the controller-loader
467
+ * to anchor per-manifest install roots so every resource in the process
468
+ * shares a single `node_modules` tree (and therefore one realpath for
469
+ * `@telorun/sdk`).
470
+ */
471
+ getEntryUrl(): string | undefined {
472
+ return this._entryUrl;
473
+ }
474
+
343
475
  get exitCode(): number {
344
476
  return this._exitCode;
345
477
  }
@@ -383,11 +515,14 @@ export class Kernel implements IKernel {
383
515
  }
384
516
 
385
517
  /**
386
- * Force-resolve waitForIdle() regardless of active holds.
387
- * Used for graceful shutdown when external signals (e.g. SIGINT) should
388
- * bypass resource holds and proceed directly to teardown.
518
+ * Force-resolve any pending `waitForIdle()` even when holds are still active.
519
+ * Used by external signal handlers (SIGINT/SIGTERM) to unblock graceful exit
520
+ * so `start()`'s waitForIdle returns and its finally clause runs `teardown()`.
521
+ *
522
+ * Does not tear down on its own — call `teardown()` directly if you're not
523
+ * inside `start()`.
389
524
  */
390
- shutdown(): void {
525
+ forceIdle(): void {
391
526
  const resolvers = this.idleResolvers.splice(0);
392
527
  for (const resolve of resolvers) resolve();
393
528
  }
@@ -262,6 +262,10 @@ export class ResourceContextImpl implements ResourceContext {
262
262
  return this.moduleContext.getControllerPolicy();
263
263
  }
264
264
 
265
+ getEntryUrl(): string | undefined {
266
+ return this.kernel.getEntryUrl();
267
+ }
268
+
265
269
  on(event: string, handler: (payload?: any) => void | Promise<void>): void {
266
270
  this.kernel.on(event, handler);
267
271
  }