@telorun/kernel 0.82.1 → 0.83.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 +237 -12
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/module/shared-libraries.d.ts +83 -0
- package/dist/controllers/module/shared-libraries.d.ts.map +1 -0
- package/dist/controllers/module/shared-libraries.js +110 -0
- package/dist/controllers/module/shared-libraries.js.map +1 -0
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +104 -30
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +60 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +125 -2
- package/dist/evaluation-context.js.map +1 -1
- package/dist/module-context.d.ts +5 -0
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +5 -0
- package/dist/module-context.js.map +1 -1
- package/package.json +4 -4
- package/src/controllers/module/import-controller.ts +324 -13
- package/src/controllers/module/shared-libraries.ts +180 -0
- package/src/controllers/resource-definition/resource-template-controller.ts +129 -29
- package/src/evaluation-context.ts +136 -2
- package/src/module-context.ts +6 -0
|
@@ -5,28 +5,65 @@ import type {
|
|
|
5
5
|
ResourceContext,
|
|
6
6
|
ResourceInstance,
|
|
7
7
|
} from "@telorun/sdk";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
celEvalSites,
|
|
10
|
+
effectiveAuthorSchema,
|
|
11
|
+
evalPathCovers,
|
|
12
|
+
mergeCelEvalSites,
|
|
13
|
+
NO_CEL_EVAL_SITES,
|
|
14
|
+
pathMatchesScope,
|
|
15
|
+
type CelEvalSites,
|
|
16
|
+
} from "@telorun/analyzer";
|
|
17
|
+
import { isCompiledValue, type ResourceDefinition } from "@telorun/sdk";
|
|
9
18
|
import { isRefSentinel } from "@telorun/templating";
|
|
10
19
|
import { celSelfView } from "../../evaluation-context.js";
|
|
11
20
|
|
|
12
|
-
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* WHICH NODES OF A TEMPLATE BODY SURVIVE init() UNEXPANDED.
|
|
23
|
+
*
|
|
24
|
+
* A `resources:` entry is a DECLARATION of another kind, and that kind decides
|
|
25
|
+
* when each of its fields is evaluated: an `x-telo-eval: runtime` field, or any
|
|
26
|
+
* field under a CEL-bearing region (an `x-telo-context`, an error branch, a step
|
|
27
|
+
* body), is evaluated by the child's OWN controller against a scope only it can
|
|
28
|
+
* build. Those nodes must reach the child compiled.
|
|
29
|
+
*
|
|
30
|
+
* Read off the nested kind's schema through the containment matcher both halves
|
|
31
|
+
* already share (`celEvalSites` / `evalPathCovers`), never off a list of
|
|
32
|
+
* variable names. The name list — `request`, `result`, `steps`, `error` — was
|
|
33
|
+
* the reason a body could not read the call's own arguments (`inputs`) or an
|
|
34
|
+
* iteration's element (`item`): those two names were simply absent from it, so
|
|
35
|
+
* such a node was expanded at init() against a scope where they do not exist and
|
|
36
|
+
* failed with `Unknown variable: inputs`. A list has to be extended for every
|
|
37
|
+
* name any kind ever binds; the annotations already say where evaluation
|
|
38
|
+
* happens.
|
|
39
|
+
*/
|
|
40
|
+
/** True when a node at `path` inside a body is evaluated by the child's own
|
|
41
|
+
* controller rather than at the template's init(). `runtime` paths are
|
|
42
|
+
* property-only and match by containment; a region scope is a JSONPath
|
|
43
|
+
* (`$.routes[*].returns`) whose wildcards a plain prefix test cannot resolve. */
|
|
44
|
+
function isDeferredPath(sites: CelEvalSites, path: string): boolean {
|
|
45
|
+
return (
|
|
46
|
+
sites.runtime.some((p) => evalPathCovers(p, path)) ||
|
|
47
|
+
sites.regions.some((scope) => pathMatchesScope(path, scope))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
22
50
|
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
/**
|
|
52
|
+
* True when an expression reads anything other than `self`. A deferred node that
|
|
53
|
+
* names only `self` is still resolved at `init()`: `self` is fixed for the life
|
|
54
|
+
* of the instance, so expanding it there costs nothing and keeps a `self`-built
|
|
55
|
+
* literal (a SQL string, a route path) a literal.
|
|
56
|
+
*
|
|
57
|
+
* Read off the AST-derived roots the compile step stamps, never off the source
|
|
58
|
+
* text. A regex over the source both over- and under-matches — it fires on a
|
|
59
|
+
* word inside a string literal and misses a bare `${{ item }}` with no member
|
|
60
|
+
* access — and there is nothing to fall back FOR: every built-in engine carries
|
|
61
|
+
* `refs` through. An engine that surfaces none is treated as reading nothing but
|
|
62
|
+
* `self`, which resolves it at `init()` and fails loudly there rather than
|
|
63
|
+
* silently deferring a node the child cannot evaluate.
|
|
64
|
+
*/
|
|
65
|
+
function referencesBeyondSelf(value: CompiledValue): boolean {
|
|
66
|
+
return (value.refs ?? []).some((r) => r !== "self");
|
|
30
67
|
}
|
|
31
68
|
|
|
32
69
|
/** Matches a CEL source that is exactly a `self.<path>` member access (capturing
|
|
@@ -75,8 +112,18 @@ export function createTemplateController(definition: {
|
|
|
75
112
|
// `inputs:` sibling (same factoring as Run.Sequence steps), never in the
|
|
76
113
|
// target's resource body — the body is `self`-only so every child can be
|
|
77
114
|
// created once at init and reused across calls.
|
|
78
|
-
const targetName = (
|
|
115
|
+
const targetName = (
|
|
116
|
+
field: string | { kind?: string; name: string } | undefined,
|
|
117
|
+
): string | null => {
|
|
79
118
|
if (field == null) return null;
|
|
119
|
+
// `invoke: !ref body` names a sibling `resources:` entry — the same
|
|
120
|
+
// spelling every other reference uses. Phase 2.5 does not descend into a
|
|
121
|
+
// `Telo.Definition`, so the sentinel arrives raw; `Self.` is the
|
|
122
|
+
// explicit self-qualifier and names the same local entry.
|
|
123
|
+
if (isRefSentinel(field)) {
|
|
124
|
+
const source = field.source;
|
|
125
|
+
return source.startsWith("Self.") ? source.slice("Self.".length) : source;
|
|
126
|
+
}
|
|
80
127
|
const nameTemplate =
|
|
81
128
|
typeof field === "object" && !isCompiledValue(field) ? field.name : field;
|
|
82
129
|
return nameTemplate
|
|
@@ -146,16 +193,16 @@ export function createTemplateController(definition: {
|
|
|
146
193
|
}
|
|
147
194
|
|
|
148
195
|
// Expand a persistent child's body against `self`. Self-only CEL resolves
|
|
149
|
-
// to literals now;
|
|
150
|
-
// through compiled for the child's own controller. `!ref` sentinels are
|
|
196
|
+
// to literals now; a node the NESTED KIND evaluates later (see
|
|
197
|
+
// `deferredPaths`) passes through compiled for the child's own controller. `!ref` sentinels are
|
|
151
198
|
// rewritten to the `{kind, name, alias?}` injection shape here — Phase 2.5
|
|
152
199
|
// (`resolveRefSentinels`) does not descend into template bodies, so the
|
|
153
200
|
// child context's Phase 5 injection would otherwise see an unrecognized
|
|
154
201
|
// sentinel and leave the slot unresolved. Kind is left empty: injection
|
|
155
202
|
// dispatches by name and recovers the kind from the resolved instance.
|
|
156
|
-
const expandSelf = (value: any): any => {
|
|
203
|
+
const expandSelf = (value: any, path: string, deferred: CelEvalSites): any => {
|
|
157
204
|
if (isCompiledValue(value)) {
|
|
158
|
-
if (
|
|
205
|
+
if (isDeferredPath(deferred, path) && referencesBeyondSelf(value)) return value;
|
|
159
206
|
// A pure `self.<path>` access (e.g. a `connection: !ref` passed down) is
|
|
160
207
|
// resolved by navigating the resource directly. Going through CEL would
|
|
161
208
|
// re-emit the value through CEL's output type-checker, which rejects live
|
|
@@ -163,10 +210,11 @@ export function createTemplateController(definition: {
|
|
|
163
210
|
// a consumer wired in could never reach a child's slot. Complex self
|
|
164
211
|
// expressions (string building) still evaluate via CEL, where they yield
|
|
165
212
|
// CEL-safe scalars.
|
|
166
|
-
const
|
|
167
|
-
|
|
213
|
+
const selfPath =
|
|
214
|
+
typeof value.source === "string" ? value.source.trim().match(SELF_PATH) : null;
|
|
215
|
+
if (selfPath) {
|
|
168
216
|
let cur: any = getSelf();
|
|
169
|
-
for (const key of
|
|
217
|
+
for (const key of selfPath[1]!.split(".").slice(1)) cur = cur?.[key];
|
|
170
218
|
return cur;
|
|
171
219
|
}
|
|
172
220
|
// CEL cannot read a member off a live instance, and a ref slot holds
|
|
@@ -188,15 +236,54 @@ export function createTemplateController(definition: {
|
|
|
188
236
|
const name = alias === "Self" ? source.slice(dot + 1) : source;
|
|
189
237
|
return { kind: siblingKinds.get(name) ?? "", name };
|
|
190
238
|
}
|
|
191
|
-
if (Array.isArray(value))
|
|
239
|
+
if (Array.isArray(value)) {
|
|
240
|
+
return value.map((item, i) => expandSelf(item, `${path}[${i}]`, deferred));
|
|
241
|
+
}
|
|
192
242
|
if (value !== null && typeof value === "object") {
|
|
193
243
|
const out: Record<string, unknown> = {};
|
|
194
|
-
for (const [k, v] of Object.entries(value))
|
|
244
|
+
for (const [k, v] of Object.entries(value)) {
|
|
245
|
+
out[k] = expandSelf(v, path ? `${path}.${k}` : k, deferred);
|
|
246
|
+
}
|
|
195
247
|
return out;
|
|
196
248
|
}
|
|
197
249
|
return value;
|
|
198
250
|
};
|
|
199
251
|
|
|
252
|
+
/**
|
|
253
|
+
* The nested kind's own CEL evaluation sites, resolved once per body. The
|
|
254
|
+
* kind is written in the DEFINING library's alias scope, so it is
|
|
255
|
+
* resolved there — the consumer has never heard of the alias.
|
|
256
|
+
*
|
|
257
|
+
* The INHERITANCE-RESOLVED schema, merged with the capability abstract's,
|
|
258
|
+
* exactly as the analyzer half reads it (`effectiveSchemaOf` in
|
|
259
|
+
* `cel-scope.ts`). A nested kind that inherits a CEL region from an
|
|
260
|
+
* `extends` parent — or gets one implicitly from `Telo.Provider` — would
|
|
261
|
+
* otherwise be covered on the static side and not here, which is the two
|
|
262
|
+
* halves disagreeing about when a node is evaluated.
|
|
263
|
+
*/
|
|
264
|
+
const deferredPathsFor = (kind: unknown): CelEvalSites => {
|
|
265
|
+
if (typeof kind !== "string") return NO_CEL_EVAL_SITES;
|
|
266
|
+
const cached = deferredByKind.get(kind);
|
|
267
|
+
if (cached) return cached;
|
|
268
|
+
const resolveDef = (k: string): ResourceDefinition | undefined =>
|
|
269
|
+
definingContext.getDefinition?.(definingContext.kindResolver?.(k) ?? k) ??
|
|
270
|
+
definingContext.getDefinition?.(k);
|
|
271
|
+
const def = resolveDef(kind);
|
|
272
|
+
const capability = def?.capability;
|
|
273
|
+
const sites = def
|
|
274
|
+
? mergeCelEvalSites(
|
|
275
|
+
celEvalSites(effectiveAuthorSchema(def, resolveDef) as Record<string, any>),
|
|
276
|
+
celEvalSites(
|
|
277
|
+
(capability ? resolveDef(capability)?.schema : undefined) as
|
|
278
|
+
| Record<string, any>
|
|
279
|
+
| undefined,
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
: NO_CEL_EVAL_SITES;
|
|
283
|
+
deferredByKind.set(kind, sites);
|
|
284
|
+
return sites;
|
|
285
|
+
};
|
|
286
|
+
|
|
200
287
|
// init() may run more than once: when a child's local ref names a sibling
|
|
201
288
|
// not yet initialized, child init defers with ERR_LOCAL_REF_PENDING and the
|
|
202
289
|
// outer multi-pass loop retries this resource. Registration must happen
|
|
@@ -204,6 +291,10 @@ export function createTemplateController(definition: {
|
|
|
204
291
|
// children are skipped, still-pending ones advance).
|
|
205
292
|
let registered = false;
|
|
206
293
|
|
|
294
|
+
/** Memo per nested kind — a body is expanded once, but a template kind is
|
|
295
|
+
* instantiated many times across an application. */
|
|
296
|
+
const deferredByKind = new Map<string, CelEvalSites>();
|
|
297
|
+
|
|
207
298
|
return {
|
|
208
299
|
// The template's own resources are its allocation, and tearing the child
|
|
209
300
|
// context down is the inverse. `init()` still resumes rather than
|
|
@@ -212,8 +303,17 @@ export function createTemplateController(definition: {
|
|
|
212
303
|
init: (templateCtx) =>
|
|
213
304
|
templateCtx.effect("template resources", async () => {
|
|
214
305
|
if (!registered) {
|
|
306
|
+
// `self` is in scope for the whole body, not only for what init()
|
|
307
|
+
// resolves: a node the nested kind evaluates later (a step's
|
|
308
|
+
// `inputs`, a route's `returns`) may read `self` beside the
|
|
309
|
+
// call-time names its own controller binds. Bound as the
|
|
310
|
+
// published-reading view, so `self.<ref>.<field>` answers exactly
|
|
311
|
+
// as `resources.<name>.<field>` does.
|
|
312
|
+
childContext.bindContextValue?.("self", celSelfView(getSelf()));
|
|
215
313
|
for (const template of definition.resources ?? []) {
|
|
216
|
-
childContext.registerManifest(
|
|
314
|
+
childContext.registerManifest(
|
|
315
|
+
expandSelf(template, "", deferredPathsFor(template?.kind)),
|
|
316
|
+
);
|
|
217
317
|
}
|
|
218
318
|
registered = true;
|
|
219
319
|
}
|
|
@@ -82,6 +82,12 @@ const SCHEMA_AS_CONTRACT_KINDS = new Set(["Telo.Definition", "Telo.Abstract", "T
|
|
|
82
82
|
function collectResourceRefs(resource: ResourceManifest): ResourceRef[] {
|
|
83
83
|
const found = new Map<string, ResourceRef>();
|
|
84
84
|
const skipSchema = SCHEMA_AS_CONTRACT_KINDS.has(resource.kind as string);
|
|
85
|
+
// A re-created resource is rebuilt from the manifest as REGISTERED, but
|
|
86
|
+
// Phase-5 injection mutated that object in place on the previous pass — so a
|
|
87
|
+
// ref slot may already hold a live instance, whose object graph is cyclic.
|
|
88
|
+
// The walk is a best-effort edge collection for failure attribution, so it
|
|
89
|
+
// stops at anything it has already seen rather than recursing forever.
|
|
90
|
+
const seen = new WeakSet<object>();
|
|
85
91
|
const visit = (value: unknown): void => {
|
|
86
92
|
if (isResolvedRef(value)) {
|
|
87
93
|
const key = `${value.alias ?? ""}::${value.name}`;
|
|
@@ -89,8 +95,12 @@ function collectResourceRefs(resource: ResourceManifest): ResourceRef[] {
|
|
|
89
95
|
return;
|
|
90
96
|
}
|
|
91
97
|
if (Array.isArray(value)) {
|
|
98
|
+
if (seen.has(value)) return;
|
|
99
|
+
seen.add(value);
|
|
92
100
|
for (const item of value) visit(item);
|
|
93
101
|
} else if (value && typeof value === "object") {
|
|
102
|
+
if (seen.has(value)) return;
|
|
103
|
+
seen.add(value);
|
|
94
104
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
95
105
|
if (k === "metadata" || (skipSchema && k === "schema")) continue;
|
|
96
106
|
visit(v);
|
|
@@ -484,6 +494,10 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
484
494
|
parent: IEvaluationContext | undefined = undefined;
|
|
485
495
|
readonly children: IEvaluationContext[] = [];
|
|
486
496
|
|
|
497
|
+
/** Where this node sits in its parent's teardown cascade — ascending, default
|
|
498
|
+
* 0, reverse-registration within a tier. See `childTeardownOrder`. */
|
|
499
|
+
teardownPriority: number | undefined = undefined;
|
|
500
|
+
|
|
487
501
|
/** Current lifecycle state of this context node. */
|
|
488
502
|
state: LifecycleState = "Pending";
|
|
489
503
|
|
|
@@ -515,6 +529,84 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
515
529
|
* Their re-creation is not progress — see the create sub-phase. */
|
|
516
530
|
private readonly recreatedResources = new Set<string>();
|
|
517
531
|
|
|
532
|
+
/**
|
|
533
|
+
* Instances this context can reach by name but does NOT own — a library's
|
|
534
|
+
* declared `resources:` inputs, bound here by the import that handed them
|
|
535
|
+
* down.
|
|
536
|
+
*
|
|
537
|
+
* **Borrowed, not owned.** The instance's effect frame belongs to the scope
|
|
538
|
+
* that DECLARED it, so this context must never include it in its own
|
|
539
|
+
* teardown: a library tearing one down would close the application's
|
|
540
|
+
* connection out from under everything else still using it.
|
|
541
|
+
*/
|
|
542
|
+
protected readonly borrowedResources = new Set<string>();
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Bind an instance this context does not own under `name`, and mirror its
|
|
546
|
+
* published reading into this context's `resources` scope so
|
|
547
|
+
* `resources.<name>.<field>` reads here exactly as it does where the resource
|
|
548
|
+
* is declared.
|
|
549
|
+
*
|
|
550
|
+
* The mirror is a subscription rather than a copy because a published value is
|
|
551
|
+
* a READING: the owner republishes after `run()`, after every `invoke()` and
|
|
552
|
+
* on every `setStatus()`, and a snapshot taken once at binding would go stale
|
|
553
|
+
* at the first of those — silently, since nothing downstream can tell a stale
|
|
554
|
+
* reading from a current one.
|
|
555
|
+
*/
|
|
556
|
+
adoptBorrowedResource(
|
|
557
|
+
name: string,
|
|
558
|
+
resource: ResourceManifest,
|
|
559
|
+
instance: ResourceInstance,
|
|
560
|
+
owner: EvaluationContext,
|
|
561
|
+
): void | (() => void) {
|
|
562
|
+
this.resourceInstances.set(name, { resource, instance });
|
|
563
|
+
this.borrowedResources.add(name);
|
|
564
|
+
this.declaredManifests.set(name, resource);
|
|
565
|
+
const unmirror = owner.mirrorPublications(resource.metadata.name as string, (props) =>
|
|
566
|
+
this.onResourceSnapshotted(name, props),
|
|
567
|
+
);
|
|
568
|
+
// The INVERSE, returned rather than performed: an import whose `init()`
|
|
569
|
+
// fails is discarded and re-created on the next pass, so a subscription left
|
|
570
|
+
// behind would be appended again on every pass — unbounded — and would keep
|
|
571
|
+
// a dead child context reachable from the live owner. The binding is not on
|
|
572
|
+
// this context's teardown path either (`teardownOrder` filters a borrowed
|
|
573
|
+
// name out, and that is also the only place an entry is deleted), so
|
|
574
|
+
// undoing it has to be stated here.
|
|
575
|
+
return () => {
|
|
576
|
+
unmirror();
|
|
577
|
+
this.borrowedResources.delete(name);
|
|
578
|
+
this.resourceInstances.delete(name);
|
|
579
|
+
this.declaredManifests.delete(name);
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Publication mirrors registered by {@link adoptBorrowedResource}, by the
|
|
584
|
+
* OWNER's name for the resource. */
|
|
585
|
+
private readonly publicationMirrors = new Map<
|
|
586
|
+
string,
|
|
587
|
+
Array<(props: Record<string, unknown>) => void>
|
|
588
|
+
>();
|
|
589
|
+
|
|
590
|
+
/** Register a mirror and replay the current reading, so a borrower that binds
|
|
591
|
+
* after the owner has already published does not wait for the next one.
|
|
592
|
+
* Returns the unsubscribe — a subscription with no way to end it outlives
|
|
593
|
+
* whatever registered it. */
|
|
594
|
+
mirrorPublications(name: string, sink: (props: Record<string, unknown>) => void): () => void {
|
|
595
|
+
const bucket = this.publicationMirrors.get(name);
|
|
596
|
+
if (bucket) bucket.push(sink);
|
|
597
|
+
else this.publicationMirrors.set(name, [sink]);
|
|
598
|
+
const entry = this.resourceInstances.get(name) ?? this.createdInstances.get(name);
|
|
599
|
+
const current = entry ? publishedByInstance.get(entry.instance) : undefined;
|
|
600
|
+
if (current) sink(current);
|
|
601
|
+
return () => {
|
|
602
|
+
const sinks = this.publicationMirrors.get(name);
|
|
603
|
+
if (!sinks) return;
|
|
604
|
+
const at = sinks.indexOf(sink);
|
|
605
|
+
if (at >= 0) sinks.splice(at, 1);
|
|
606
|
+
if (sinks.length === 0) this.publicationMirrors.delete(name);
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
518
610
|
/** Resources queued for initialization on this context node. */
|
|
519
611
|
private pendingResources: ResourceManifest[] = [];
|
|
520
612
|
|
|
@@ -684,6 +776,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
684
776
|
});
|
|
685
777
|
publishedByInstance.set(entry.instance, props);
|
|
686
778
|
this.onResourceSnapshotted(name, props);
|
|
779
|
+
for (const sink of this.publicationMirrors.get(name) ?? []) sink(props);
|
|
687
780
|
}
|
|
688
781
|
|
|
689
782
|
get context(): Record<string, unknown> {
|
|
@@ -801,6 +894,21 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
801
894
|
return child;
|
|
802
895
|
}
|
|
803
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Bind a name into this context's own CEL scope.
|
|
899
|
+
*
|
|
900
|
+
* A copy is written rather than a mutation: `spawnChildContext` hands the
|
|
901
|
+
* child the PARENT's context object by reference, so mutating it in place
|
|
902
|
+
* would put the binding in the parent's scope as well.
|
|
903
|
+
*
|
|
904
|
+
* Used by a template to put `self` in scope for the body it creates, so a node
|
|
905
|
+
* the nested kind evaluates later can read the enclosing resource's
|
|
906
|
+
* configuration beside the call-time names that kind binds.
|
|
907
|
+
*/
|
|
908
|
+
bindContextValue(name: string, value: unknown): void {
|
|
909
|
+
this._context = { ...this._context, [name]: value };
|
|
910
|
+
}
|
|
911
|
+
|
|
804
912
|
/** Spawn a fresh child context attached to this node — the isolated scope a
|
|
805
913
|
* templated definition registers its `resources:` into. Rooting it on the
|
|
806
914
|
* context that DEFINED the template (not the consumer that instantiated the
|
|
@@ -1216,7 +1324,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1216
1324
|
this.state = "Draining";
|
|
1217
1325
|
const failures: Array<{ resource: string; error: unknown }> = [];
|
|
1218
1326
|
|
|
1219
|
-
for (const child of
|
|
1327
|
+
for (const child of this.childTeardownOrder()) {
|
|
1220
1328
|
try {
|
|
1221
1329
|
await child.teardownResources();
|
|
1222
1330
|
} catch (err) {
|
|
@@ -1294,6 +1402,28 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1294
1402
|
}
|
|
1295
1403
|
}
|
|
1296
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* Child contexts in teardown order: ascending `teardownPriority` (default 0),
|
|
1407
|
+
* with the base reverse-registration order preserved within each tier.
|
|
1408
|
+
*
|
|
1409
|
+
* The same rule `teardownOrder` applies to resource instances, and for the
|
|
1410
|
+
* same reason: reverse registration is reverse init order in the happy path,
|
|
1411
|
+
* but a node that must reliably outlive the rest has to say so rather than
|
|
1412
|
+
* depend on when it happened to be created. A `lifecycle: shared` library is
|
|
1413
|
+
* registered when the FIRST import reaches it — which, for an import declared
|
|
1414
|
+
* inside another library, is after that library's own context — so reverse
|
|
1415
|
+
* registration would tear the singleton down while a borrower still holds it.
|
|
1416
|
+
*/
|
|
1417
|
+
private childTeardownOrder(): IEvaluationContext[] {
|
|
1418
|
+
return [...this.children]
|
|
1419
|
+
.reverse()
|
|
1420
|
+
.sort(
|
|
1421
|
+
(a, b) =>
|
|
1422
|
+
((a as { teardownPriority?: number }).teardownPriority ?? 0) -
|
|
1423
|
+
((b as { teardownPriority?: number }).teardownPriority ?? 0),
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1297
1427
|
/**
|
|
1298
1428
|
* Resource instances in teardown order: ascending `teardownPriority`, with the
|
|
1299
1429
|
* base reverse-insertion order preserved within each priority tier.
|
|
@@ -1308,7 +1438,11 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1308
1438
|
* instance shape.
|
|
1309
1439
|
*/
|
|
1310
1440
|
private teardownOrder(): Array<[string, { resource: any; instance: any }]> {
|
|
1311
|
-
|
|
1441
|
+
// A borrowed instance is torn down by the scope that declared it, never
|
|
1442
|
+
// here — see `borrowedResources`.
|
|
1443
|
+
const entries = [...this.resourceInstances.entries()]
|
|
1444
|
+
.filter(([name]) => !this.borrowedResources.has(name))
|
|
1445
|
+
.reverse();
|
|
1312
1446
|
// Stable sort by priority (default 0); Array.prototype.sort is stable, so
|
|
1313
1447
|
// the reverse-insertion order survives within each tier.
|
|
1314
1448
|
return entries.sort(
|
package/src/module-context.ts
CHANGED
|
@@ -552,6 +552,12 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
552
552
|
return `${realModule}.${suffix}`;
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
/** A module context IS the alias table, so it answers the resolver seam
|
|
556
|
+
* directly rather than inheriting one from a parent it does not have. Public
|
|
557
|
+
* because a template body's nested kind is written in the DEFINING library's
|
|
558
|
+
* scope and its controller has to resolve it there. */
|
|
559
|
+
override kindResolver = (kind: string): string => this.resolveKindSafe(kind);
|
|
560
|
+
|
|
555
561
|
protected override resolveKindSafe(kind: string): string {
|
|
556
562
|
// `resolveKind` throws for unqualified / ungated kinds — an expected signal,
|
|
557
563
|
// not a failure: a capability probe that can't resolve falls back to the raw
|