@telorun/kernel 0.14.0 → 0.16.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.
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +50 -8
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +7 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +24 -3
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +40 -18
- package/dist/kernel.js.map +1 -1
- package/dist/module-context.d.ts +27 -3
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +80 -2
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +2 -0
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +19 -1
- package/dist/resource-context.js.map +1 -1
- package/package.json +6 -5
- package/src/controllers/module/import-controller.ts +66 -17
- package/src/evaluation-context.ts +30 -5
- package/src/kernel.ts +55 -22
- package/src/module-context.ts +101 -4
- package/src/resource-context.ts +27 -2
package/src/kernel.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
ResourceManifest,
|
|
20
20
|
RuntimeError,
|
|
21
21
|
RuntimeEvent,
|
|
22
|
+
type BootTarget,
|
|
22
23
|
type EvaluationContext as IEvaluationContext,
|
|
23
24
|
type ModuleContext as IModuleContext,
|
|
24
25
|
type LoadOptions,
|
|
@@ -380,7 +381,14 @@ export class Kernel implements IKernel {
|
|
|
380
381
|
// into first-class named manifests and replace them in-place with {kind, name} references.
|
|
381
382
|
// Update staticManifests so Phase 3 (validateReferences) and Phase 4 (DAG) see
|
|
382
383
|
// the same normalized structure.
|
|
383
|
-
|
|
384
|
+
// Pass the analyzer-flattened set (entry + forwarded library exports) as cross-module
|
|
385
|
+
// resolution targets so `!ref Alias.name` in the entry-only runtime manifests resolves
|
|
386
|
+
// to imported libraries' exported instances.
|
|
387
|
+
const normalizedManifests = this.analyzer.normalize(
|
|
388
|
+
allManifests,
|
|
389
|
+
this.registry,
|
|
390
|
+
staticManifests,
|
|
391
|
+
);
|
|
384
392
|
this.staticManifests = normalizedManifests;
|
|
385
393
|
|
|
386
394
|
let rootApplicationManifest: ResourceManifest | undefined;
|
|
@@ -392,25 +400,32 @@ export class Kernel implements IKernel {
|
|
|
392
400
|
// `process.env` after the manifest loop so imports can read
|
|
393
401
|
// `${{ variables.X }}` during their own init.
|
|
394
402
|
//
|
|
395
|
-
// Targets
|
|
396
|
-
//
|
|
397
|
-
// `!ref <name>` to
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
403
|
+
// Targets are preserved as their full shape so the boot runner can
|
|
404
|
+
// evaluate `when` guards and inline invoke steps. The analyzer's
|
|
405
|
+
// `resolveRefSentinels` pass already substituted any `!ref <name>` to
|
|
406
|
+
// `{kind, name}` (including inside an inline target's `invoke`).
|
|
407
|
+
// Recognized shapes: bare string ref, resolved `{kind, name}`, gated
|
|
408
|
+
// `{ ref, when? }`, and inline `{ name?, invoke, inputs?, when?, retry? }`.
|
|
409
|
+
// Anything else (e.g. an unresolved sentinel, or a malformed manifest)
|
|
410
|
+
// is a hard error — silently dropping the entry would leave the user
|
|
411
|
+
// staring at a "no targets ran" outcome with no signal.
|
|
402
412
|
const rawTargets = (manifest.targets ?? []) as unknown[];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
413
|
+
rawTargets.forEach((t, index) => {
|
|
414
|
+
const ok =
|
|
415
|
+
typeof t === "string" ||
|
|
416
|
+
(!!t &&
|
|
417
|
+
typeof t === "object" &&
|
|
418
|
+
(typeof (t as { name?: unknown }).name === "string" ||
|
|
419
|
+
(t as { ref?: unknown }).ref != null ||
|
|
420
|
+
(t as { invoke?: unknown }).invoke !== undefined));
|
|
421
|
+
if (!ok) {
|
|
422
|
+
throw new RuntimeError(
|
|
423
|
+
"ERR_INVALID_VALUE",
|
|
424
|
+
`Telo.Application '${(manifest.metadata as { name?: string } | undefined)?.name ?? "(unnamed)"}' targets[${index}] is not a recognized target shape. Got: ${JSON.stringify(t)}`,
|
|
425
|
+
);
|
|
407
426
|
}
|
|
408
|
-
throw new RuntimeError(
|
|
409
|
-
"ERR_INVALID_VALUE",
|
|
410
|
-
`Telo.Application '${(manifest.metadata as { name?: string } | undefined)?.name ?? "(unnamed)"}' targets[${index}] could not be normalized to a resource name. Got: ${JSON.stringify(t)}`,
|
|
411
|
-
);
|
|
412
427
|
});
|
|
413
|
-
this.rootContext.setTargets(
|
|
428
|
+
this.rootContext.setTargets(rawTargets as BootTarget[]);
|
|
414
429
|
if (manifest.kind === "Telo.Application") {
|
|
415
430
|
rootApplicationManifest = manifest;
|
|
416
431
|
}
|
|
@@ -847,7 +862,7 @@ export class Kernel implements IKernel {
|
|
|
847
862
|
*/
|
|
848
863
|
private _injectDependencies(
|
|
849
864
|
resource: ResourceManifest,
|
|
850
|
-
getInstance: (name: string) => ResourceInstance | undefined,
|
|
865
|
+
getInstance: (name: string, alias?: string) => ResourceInstance | undefined,
|
|
851
866
|
): void {
|
|
852
867
|
this.registry.iterateFieldEntries(
|
|
853
868
|
resource,
|
|
@@ -1031,10 +1046,28 @@ function collectEvalPathsNode(
|
|
|
1031
1046
|
function injectAtPath(
|
|
1032
1047
|
resource: ResourceManifest,
|
|
1033
1048
|
fieldPath: string,
|
|
1034
|
-
getInstance: (name: string) => ResourceInstance | undefined,
|
|
1049
|
+
getInstance: (name: string, alias?: string) => ResourceInstance | undefined,
|
|
1035
1050
|
): void {
|
|
1036
1051
|
const parts = fieldPath.split(".");
|
|
1037
1052
|
|
|
1053
|
+
// Resolve a {kind, name, alias?} reference to its live instance. A non-`Self` alias is a
|
|
1054
|
+
// cross-module reference into an import's published exports; if that import hasn't
|
|
1055
|
+
// finished init() yet the instance is absent, so we throw to defer this resource to a
|
|
1056
|
+
// later pass of the multi-pass init loop (which catches and retries) rather than leaving
|
|
1057
|
+
// the ref unresolved. Local refs (no alias) that miss are left for topo ordering / later
|
|
1058
|
+
// diagnostics, matching prior behaviour.
|
|
1059
|
+
function resolveInto(ref: Record<string, unknown>): ResourceInstance | undefined {
|
|
1060
|
+
const alias = typeof ref.alias === "string" ? ref.alias : undefined;
|
|
1061
|
+
const instance = getInstance(ref.name as string, alias);
|
|
1062
|
+
if (!instance && alias && alias !== "Self") {
|
|
1063
|
+
throw new RuntimeError(
|
|
1064
|
+
"ERR_CROSS_MODULE_REF_PENDING",
|
|
1065
|
+
`Cross-module reference '${alias}.${String(ref.name)}' is not available yet (import not initialized)`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
return instance;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1038
1071
|
function traverse(obj: unknown, partsLeft: string[]): void {
|
|
1039
1072
|
if (!obj || typeof obj !== "object" || partsLeft.length === 0) return;
|
|
1040
1073
|
const [head, ...rest] = partsLeft;
|
|
@@ -1049,7 +1082,7 @@ function injectAtPath(
|
|
|
1049
1082
|
if (rest.length === 0) {
|
|
1050
1083
|
const ref = elem as Record<string, unknown>;
|
|
1051
1084
|
if (typeof ref.kind === "string" && typeof ref.name === "string") {
|
|
1052
|
-
const instance =
|
|
1085
|
+
const instance = resolveInto(ref);
|
|
1053
1086
|
if (instance) container[mapKey] = instance;
|
|
1054
1087
|
}
|
|
1055
1088
|
} else {
|
|
@@ -1073,7 +1106,7 @@ function injectAtPath(
|
|
|
1073
1106
|
if (rest.length === 0) {
|
|
1074
1107
|
const ref = elem as Record<string, unknown>;
|
|
1075
1108
|
if (typeof ref.kind === "string" && typeof ref.name === "string") {
|
|
1076
|
-
const instance =
|
|
1109
|
+
const instance = resolveInto(ref);
|
|
1077
1110
|
if (instance) val[i] = instance;
|
|
1078
1111
|
}
|
|
1079
1112
|
} else {
|
|
@@ -1085,7 +1118,7 @@ function injectAtPath(
|
|
|
1085
1118
|
if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
1086
1119
|
const ref = val as Record<string, unknown>;
|
|
1087
1120
|
if (typeof ref.kind === "string" && typeof ref.name === "string") {
|
|
1088
|
-
const instance =
|
|
1121
|
+
const instance = resolveInto(ref);
|
|
1089
1122
|
if (instance) container[key] = instance;
|
|
1090
1123
|
}
|
|
1091
1124
|
}
|
package/src/module-context.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { executeInvokeStep } from "@telorun/sdk";
|
|
1
2
|
import type {
|
|
3
|
+
BootTarget,
|
|
2
4
|
ControllerPolicy,
|
|
3
5
|
Invocable,
|
|
6
|
+
InvokeStep,
|
|
7
|
+
InvokeStepContext,
|
|
4
8
|
ModuleContext as IModuleContext,
|
|
9
|
+
ResourceInstance,
|
|
5
10
|
} from "@telorun/sdk";
|
|
6
11
|
import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
|
|
7
12
|
import { EvaluationContext } from "./evaluation-context.js";
|
|
@@ -68,6 +73,18 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
68
73
|
/** Maps import alias → allowed kind names. Absent entry = unrestricted (e.g. Kernel). */
|
|
69
74
|
private readonly importedKinds = new Map<string, Set<string>>();
|
|
70
75
|
|
|
76
|
+
/** Maps import alias → its child context's exported instances. Registered by the
|
|
77
|
+
* Telo.Import controller; read when resolving a cross-module `!ref Alias.name`
|
|
78
|
+
* reference (Phase 5 injection and boot targets). `names` is the import's
|
|
79
|
+
* `exports.resources` gate — only listed instances are reachable. */
|
|
80
|
+
private readonly importedScopes = new Map<
|
|
81
|
+
string,
|
|
82
|
+
{
|
|
83
|
+
names: Set<string>;
|
|
84
|
+
get: (name: string) => { kind: string; instance: ResourceInstance } | undefined;
|
|
85
|
+
}
|
|
86
|
+
>();
|
|
87
|
+
|
|
71
88
|
/**
|
|
72
89
|
* Resolved controller-selection policy for this module's `Telo.Definition`s.
|
|
73
90
|
* Stamped by the parent `Telo.Import` controller from the import's `runtime:`
|
|
@@ -82,7 +99,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
82
99
|
variables: Record<string, unknown> = {},
|
|
83
100
|
secrets: Record<string, unknown> = {},
|
|
84
101
|
resources: Record<string, unknown> = {},
|
|
85
|
-
private targets:
|
|
102
|
+
private targets: BootTarget[] = [],
|
|
86
103
|
createInstance: InstanceFactory = async () => null,
|
|
87
104
|
emit: EmitEvent,
|
|
88
105
|
private readonly _hostEnv?: Record<string, string | undefined>,
|
|
@@ -120,7 +137,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
120
137
|
this._rebuildContext();
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
setTargets(vars:
|
|
140
|
+
setTargets(vars: BootTarget[]): void {
|
|
124
141
|
this.targets = vars;
|
|
125
142
|
}
|
|
126
143
|
|
|
@@ -157,6 +174,37 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
|
|
177
|
+
/** Register an import alias's exported instances for cross-module reference resolution.
|
|
178
|
+
* `names` is the gate (the import's `exports.resources`); `get` reads the child context's
|
|
179
|
+
* live instance + its canonical kind lazily — they exist only after the import's `init()`
|
|
180
|
+
* runs. */
|
|
181
|
+
registerImportedScope(
|
|
182
|
+
alias: string,
|
|
183
|
+
names: string[],
|
|
184
|
+
get: (name: string) => { kind: string; instance: ResourceInstance } | undefined,
|
|
185
|
+
): void {
|
|
186
|
+
this.importedScopes.set(alias, { names: new Set(names), get });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Resolve `Alias.name` to a live exported instance, gated by `exports.resources`.
|
|
190
|
+
* Returns undefined when the alias is unknown, the name isn't exported, or the import
|
|
191
|
+
* hasn't initialized its instances yet (the injection path defers and retries). */
|
|
192
|
+
override resolveImportedInstance(alias: string, name: string): ResourceInstance | undefined {
|
|
193
|
+
const scope = this.importedScopes.get(alias);
|
|
194
|
+
if (!scope || !scope.names.has(name)) return undefined;
|
|
195
|
+
return scope.get(name)?.instance;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Like `resolveImportedInstance`, but returns the `{kind, name}` ref (canonical kind)
|
|
199
|
+
* for controllers that resolve step/handler invokes to refs rather than live instances
|
|
200
|
+
* (e.g. Run.Sequence via `resolveChildren`). The alias is reattached by the caller. */
|
|
201
|
+
resolveImportedRef(alias: string, name: string): { kind: string; name: string } | undefined {
|
|
202
|
+
const scope = this.importedScopes.get(alias);
|
|
203
|
+
if (!scope || !scope.names.has(name)) return undefined;
|
|
204
|
+
const entry = scope.get(name);
|
|
205
|
+
return entry ? { kind: entry.kind, name } : undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
160
208
|
hasImport(alias: string): boolean {
|
|
161
209
|
return this.importAliases.has(alias);
|
|
162
210
|
}
|
|
@@ -256,8 +304,57 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
256
304
|
}
|
|
257
305
|
|
|
258
306
|
async runTargets() {
|
|
259
|
-
|
|
260
|
-
|
|
307
|
+
const steps: Record<string, unknown> = {};
|
|
308
|
+
const stepCtx: InvokeStepContext = {
|
|
309
|
+
expandValue: (value, context) => this.expandWith(value, context),
|
|
310
|
+
invoke: (kind, name, inputs) => this.invoke(kind, name, inputs),
|
|
311
|
+
invokeResolved: (kind, name, instance, inputs) =>
|
|
312
|
+
this.invokeResolved(kind, name, instance, inputs),
|
|
313
|
+
resolveImportedInstance: (alias, name) => this.resolveImportedInstance(alias, name),
|
|
314
|
+
};
|
|
315
|
+
for (let i = 0; i < this.targets.length; i++) {
|
|
316
|
+
const target = this.targets[i]!;
|
|
317
|
+
if (typeof target === "string") {
|
|
318
|
+
await this.run(target);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if ("invoke" in target && target.invoke !== undefined) {
|
|
322
|
+
const step: InvokeStep = {
|
|
323
|
+
name: target.name ?? `Target${i}`,
|
|
324
|
+
when: target.when,
|
|
325
|
+
// A cross-module `!ref Alias.name` stays a `{kind, name, alias}` ref; the leaf's
|
|
326
|
+
// alias branch resolves it via `resolveImportedInstance` and dispatches through
|
|
327
|
+
// `invokeResolved` so invocation events and error wrapping still fire.
|
|
328
|
+
invoke: target.invoke,
|
|
329
|
+
inputs: target.inputs,
|
|
330
|
+
};
|
|
331
|
+
await executeInvokeStep(step, stepCtx, { steps });
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if ("ref" in target && target.ref != null) {
|
|
335
|
+
if (target.when === undefined || this.expandWith(target.when, { steps })) {
|
|
336
|
+
const ref = target.ref as unknown;
|
|
337
|
+
const r =
|
|
338
|
+
ref && typeof ref === "object" ? (ref as { name: string; alias?: string }) : undefined;
|
|
339
|
+
if (r && typeof r.alias === "string" && r.alias !== "Self") {
|
|
340
|
+
const inst = this.resolveImportedInstance(r.alias, r.name);
|
|
341
|
+
if (!inst || typeof inst.run !== "function") {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Boot target '${r.alias}.${r.name}' is not a runnable exported instance`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
await inst.run();
|
|
347
|
+
} else {
|
|
348
|
+
await this.run(typeof ref === "string" ? ref : r!.name);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
if ("name" in target && typeof target.name === "string") {
|
|
354
|
+
await this.run(target.name);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
throw new Error(`Unrecognized target shape at index ${i}: ${JSON.stringify(target)}`);
|
|
261
358
|
}
|
|
262
359
|
}
|
|
263
360
|
}
|
package/src/resource-context.ts
CHANGED
|
@@ -161,6 +161,10 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
161
161
|
return this.moduleContext.invokeResolved(kind, name, instance, inputs);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
resolveImportedInstance(alias: string, name: string): ResourceInstance | undefined {
|
|
165
|
+
return this.moduleContext.resolveImportedInstance(alias, name);
|
|
166
|
+
}
|
|
167
|
+
|
|
164
168
|
async run(name: string) {
|
|
165
169
|
await this.moduleContext.run(name);
|
|
166
170
|
}
|
|
@@ -195,7 +199,10 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
195
199
|
* @returns Normalized {kind, name} reference
|
|
196
200
|
* @throws RuntimeError if 'kind' is missing
|
|
197
201
|
*/
|
|
198
|
-
resolveChildren(
|
|
202
|
+
resolveChildren(
|
|
203
|
+
resource: any,
|
|
204
|
+
resourceName?: string,
|
|
205
|
+
): { kind: string; name: string; alias?: string } {
|
|
199
206
|
if (!resource || typeof resource !== "object") {
|
|
200
207
|
throw new RuntimeError(
|
|
201
208
|
"ERR_INVALID_VALUE",
|
|
@@ -217,7 +224,25 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
217
224
|
// this rescue — schemas exercising those hidden slots must use the
|
|
218
225
|
// legacy string or `{kind, name}` forms for now.
|
|
219
226
|
if (isRefSentinel(resource)) {
|
|
220
|
-
const
|
|
227
|
+
const source = resource.source;
|
|
228
|
+
const dot = source.indexOf(".");
|
|
229
|
+
const alias = dot > 0 ? source.slice(0, dot) : undefined;
|
|
230
|
+
// Cross-module exported instance (`!ref Alias.name`) — resolve the {kind, name} ref
|
|
231
|
+
// from the import's exported scope and reattach the alias so downstream scope
|
|
232
|
+
// resolution (executeInvokeStep → ScopeContext.getInstance) routes into the import's
|
|
233
|
+
// child context rather than scope-local resources.
|
|
234
|
+
if (alias && alias !== "Self") {
|
|
235
|
+
const name = source.slice(dot + 1);
|
|
236
|
+
const ref = this.moduleContext.resolveImportedRef(alias, name);
|
|
237
|
+
if (!ref) {
|
|
238
|
+
throw new RuntimeError(
|
|
239
|
+
"ERR_RESOURCE_NOT_FOUND",
|
|
240
|
+
`[${this.metadata.name}] !ref '${source}' is not an exported instance of import '${alias}'.`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return { kind: ref.kind, name: ref.name, alias };
|
|
244
|
+
}
|
|
245
|
+
const refName = alias === "Self" ? source.slice(dot + 1) : source;
|
|
221
246
|
const entry = this.moduleContext.resourceInstances.get(refName);
|
|
222
247
|
if (!entry) {
|
|
223
248
|
throw new RuntimeError(
|