@telorun/kernel 0.85.0 → 0.87.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/evaluation-context.d.ts +176 -25
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +299 -44
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts +60 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +252 -3
- package/dist/kernel.js.map +1 -1
- package/dist/module-context.d.ts +31 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +41 -1
- package/dist/module-context.js.map +1 -1
- package/dist/reconcile.d.ts +42 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +58 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/resource-edges.d.ts +58 -0
- package/dist/resource-edges.d.ts.map +1 -0
- package/dist/resource-edges.js +110 -0
- package/dist/resource-edges.js.map +1 -0
- package/dist/runtime-seam.js +2 -2
- package/dist/runtime-seam.js.map +1 -1
- package/package.json +3 -3
- package/src/evaluation-context.ts +330 -53
- package/src/kernel.ts +312 -3
- package/src/module-context.ts +41 -1
- package/src/reconcile.ts +83 -0
- package/src/resource-edges.ts +114 -0
- package/src/runtime-seam.ts +4 -4
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
type Tracer,
|
|
29
29
|
} from "@telorun/sdk";
|
|
30
30
|
import { RuntimeError } from "@telorun/sdk";
|
|
31
|
-
import { evalPathCovers } from "@telorun/analyzer";
|
|
31
|
+
import { celResourceReads, evalPathCovers } from "@telorun/analyzer";
|
|
32
32
|
import { effectOwnerOf, executeReturnedChain } from "./effect-scope.js";
|
|
33
|
+
import { impactClosure, reverseTopologicalOrder } from "./resource-edges.js";
|
|
33
34
|
import {
|
|
34
35
|
REDACTED,
|
|
35
36
|
redactSensitive,
|
|
@@ -536,6 +537,25 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
536
537
|
*/
|
|
537
538
|
private readonly withheldResources = new Set<string>();
|
|
538
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Keys whose instance was REMOVED BY AN UNWIND — the fact a dispatch that
|
|
542
|
+
* misses actually needs, as opposed to the context-wide state that only
|
|
543
|
+
* approximates it.
|
|
544
|
+
*
|
|
545
|
+
* A miss because the resource went away is the runtime withdrawing, not a
|
|
546
|
+
* manifest defect, and the two want opposite follow-ups (see `invoke`). Both
|
|
547
|
+
* paths that withdraw one go through `unwindEach`, the single removal site, so
|
|
548
|
+
* recording it there covers a shutdown AND a reconciliation — where a check
|
|
549
|
+
* against `state` would have converted the first and missed the second, since
|
|
550
|
+
* `unwindResources` deliberately leaves the state untouched. The second is the
|
|
551
|
+
* one that fires on every watch-session save.
|
|
552
|
+
*
|
|
553
|
+
* Cleared when the key is registered again: after a reconcile the name means a
|
|
554
|
+
* live resource, and a stale entry would report a genuine missing-resource
|
|
555
|
+
* defect as a cancellation.
|
|
556
|
+
*/
|
|
557
|
+
private readonly unwoundResources = new Set<string>();
|
|
558
|
+
|
|
539
559
|
/** Resources discarded after a failed `init()` and re-queued for creation.
|
|
540
560
|
* Their re-creation is not progress — see the create sub-phase. */
|
|
541
561
|
private readonly recreatedResources = new Set<string>();
|
|
@@ -571,6 +591,7 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
571
591
|
owner: EvaluationContext,
|
|
572
592
|
): void | (() => void) {
|
|
573
593
|
this.resourceInstances.set(name, { resource, instance });
|
|
594
|
+
this.unwoundResources.delete(name);
|
|
574
595
|
this.borrowedResources.add(name);
|
|
575
596
|
this.declaredManifests.set(name, resource);
|
|
576
597
|
const unmirror = owner.mirrorPublications(resource.metadata.name as string, (props) =>
|
|
@@ -636,10 +657,32 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
636
657
|
|
|
637
658
|
/** Per-resource dependency names, captured at create() time — BEFORE Phase-5
|
|
638
659
|
* injection swaps refs for live instances, so the walk sees plain objects and
|
|
639
|
-
* cannot wander into a controller's (possibly cyclic) object graph.
|
|
640
|
-
*
|
|
660
|
+
* cannot wander into a controller's (possibly cyclic) object graph.
|
|
661
|
+
*
|
|
662
|
+
* Read twice, and RETAINED for the context's lifetime because of the second
|
|
663
|
+
* reader: to attribute an init failure to its cause, and to order teardown
|
|
664
|
+
* (`teardownOrder`) so a consumer's inverses run while the resources it holds
|
|
665
|
+
* are still alive. */
|
|
641
666
|
private readonly resourceDependencies = new Map<string, string[]>();
|
|
642
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Names resolved by NAME during initialization, rather than through a
|
|
670
|
+
* declared reference slot — so a resource somebody may be holding, with no
|
|
671
|
+
* edge recording who.
|
|
672
|
+
*
|
|
673
|
+
* The set is of TARGETS, not of pairs: the door that records
|
|
674
|
+
* (`ModuleContext.getInstance`) is reached as `ctx.moduleContext`, which every
|
|
675
|
+
* resource of the module shares, so there is no caller to attribute the read
|
|
676
|
+
* to. That is enough for the one decision that depends on it — see
|
|
677
|
+
* {@link impactedBy}.
|
|
678
|
+
*/
|
|
679
|
+
protected readonly opaquelyRead = new Set<string>();
|
|
680
|
+
|
|
681
|
+
/** Record a by-name resolution. Called by the recording door only. */
|
|
682
|
+
protected recordOpaqueRead(name: string): void {
|
|
683
|
+
this.opaquelyRead.add(name);
|
|
684
|
+
}
|
|
685
|
+
|
|
643
686
|
/**
|
|
644
687
|
* Optional hook called between create() and init() for each resource.
|
|
645
688
|
* Set by the kernel to inject live instances into reference fields.
|
|
@@ -876,6 +919,32 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
876
919
|
this.declaredManifests.set(name, resource);
|
|
877
920
|
}
|
|
878
921
|
|
|
922
|
+
/**
|
|
923
|
+
* Forget a declaration entirely — the inverse of {@link registerManifest}.
|
|
924
|
+
*
|
|
925
|
+
* Reconciliation's other half: {@link unwindResources} disposes the INSTANCE,
|
|
926
|
+
* and this clears everything keyed by the name so the same name can be
|
|
927
|
+
* declared again. Without it `registerManifest` refuses with
|
|
928
|
+
* `ERR_DUPLICATE_RESOURCE`, which is the right answer for a manifest
|
|
929
|
+
* declaring one name twice and the wrong one for a second load of the same
|
|
930
|
+
* manifest.
|
|
931
|
+
*
|
|
932
|
+
* Every per-name record goes, not just the declaration: a resource left in
|
|
933
|
+
* `withheldResources` would be skipped by the init loop for the life of the
|
|
934
|
+
* kernel, and a stale `createdInstances` entry would have the loop initialize
|
|
935
|
+
* the object built from the PREVIOUS declaration.
|
|
936
|
+
*/
|
|
937
|
+
deregisterManifest(name: string): void {
|
|
938
|
+
this.declaredManifests.delete(name);
|
|
939
|
+
this.resourceDependencies.delete(name);
|
|
940
|
+
this.createdInstances.delete(name);
|
|
941
|
+
this.withheldResources.delete(name);
|
|
942
|
+
this.recreatedResources.delete(name);
|
|
943
|
+
this.opaquelyRead.delete(name);
|
|
944
|
+
const pending = this.pendingResources.findIndex((r) => r.metadata?.name === name);
|
|
945
|
+
if (pending >= 0) this.pendingResources.splice(pending, 1);
|
|
946
|
+
}
|
|
947
|
+
|
|
879
948
|
/**
|
|
880
949
|
* The manifest a name was DECLARED with, resolved scope-local first and then
|
|
881
950
|
* up the enclosing chain — the order `getInstance` and the CEL `resources`
|
|
@@ -1048,7 +1117,12 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1048
1117
|
if (!this.recreatedResources.has(name)) progress = true;
|
|
1049
1118
|
const createdRes = created.resource;
|
|
1050
1119
|
const refs = collectResourceRefs(createdRes);
|
|
1051
|
-
|
|
1120
|
+
// `resource` rather than `createdRes`: the registered declaration
|
|
1121
|
+
// still holds its expressions, while the created copy holds the
|
|
1122
|
+
// values they were expanded to.
|
|
1123
|
+
this.resourceDependencies.set(name, [
|
|
1124
|
+
...new Set([...localDependencyNames(refs), ...celResourceReads(resource)]),
|
|
1125
|
+
]);
|
|
1052
1126
|
const payload: Record<string, unknown> = {
|
|
1053
1127
|
resource: {
|
|
1054
1128
|
kind: createdRes.kind,
|
|
@@ -1163,10 +1237,11 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1163
1237
|
// so it resolves fine from here.
|
|
1164
1238
|
await this.publishSnapshot(name);
|
|
1165
1239
|
this.resourceInstances.set(name, { resource, instance });
|
|
1240
|
+
// Live again: a reconcile re-initializes what it unwound, and leaving
|
|
1241
|
+
// the mark would report a genuine missing-resource defect at this name
|
|
1242
|
+
// as a cancellation for the rest of the process.
|
|
1243
|
+
this.unwoundResources.delete(name);
|
|
1166
1244
|
this.createdInstances.delete(name);
|
|
1167
|
-
// Read only on failure, and this one succeeded — drop it rather than
|
|
1168
|
-
// holding a dep-name array per resource for the context's lifetime.
|
|
1169
|
-
this.resourceDependencies.delete(name);
|
|
1170
1245
|
errors.delete(name);
|
|
1171
1246
|
progress = true;
|
|
1172
1247
|
await this.emit(`${resource.kind}.${resource.metadata.name}.Initialized`, {
|
|
@@ -1365,16 +1440,33 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1365
1440
|
}
|
|
1366
1441
|
|
|
1367
1442
|
/**
|
|
1368
|
-
* Cascade teardown
|
|
1369
|
-
* 1. Tear down
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1443
|
+
* Cascade teardown through the tree:
|
|
1444
|
+
* 1. Tear down own resource instances in {@link teardownOrder}, emitting a
|
|
1445
|
+
* Teardown event for each via the injected emit callback.
|
|
1446
|
+
* 2. Sweep any child context still standing, in {@link childTeardownOrder}.
|
|
1447
|
+
*
|
|
1448
|
+
* **Own resources go FIRST, and the child sweep is a backstop.** A child
|
|
1449
|
+
* context that belongs to a resource is torn down by that resource's own
|
|
1450
|
+
* inverse — an import's `init()` returns `child.teardownResources()`, and so
|
|
1451
|
+
* does a template's — so it already unwinds at its owner's position in step 1,
|
|
1452
|
+
* which is the position the edges put it at. Running a child cascade ahead of
|
|
1453
|
+
* step 1 tore every imported library down before any resource of THIS context,
|
|
1454
|
+
* so an app resource holding `!ref Alias.name` unwound after its provider was
|
|
1455
|
+
* already gone, and the owner's inverse then found nothing left to do.
|
|
1456
|
+
*
|
|
1457
|
+
* Sweeping afterwards rather than not at all is what still reclaims a context
|
|
1458
|
+
* no inverse claims: a `lifecycle: shared` library, which is spawned under the
|
|
1459
|
+
* root and deliberately gives no importer a claim on it, and an import whose
|
|
1460
|
+
* `init()` never ran to register one. `teardownResources` is idempotent, so a
|
|
1461
|
+
* context already taken down in step 1 costs the sweep nothing.
|
|
1372
1462
|
*/
|
|
1373
1463
|
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
1374
1464
|
async teardownResources(): Promise<void> {
|
|
1375
1465
|
this.state = "Draining";
|
|
1376
|
-
const failures
|
|
1466
|
+
const failures = await this.unwindEach(this.teardownOrder());
|
|
1377
1467
|
|
|
1468
|
+
// The backstop: whatever no resource's inverse claimed. Everything an import
|
|
1469
|
+
// or a template owns is already down, so this is a no-op for it.
|
|
1378
1470
|
for (const child of this.childTeardownOrder()) {
|
|
1379
1471
|
try {
|
|
1380
1472
|
await child.teardownResources();
|
|
@@ -1383,7 +1475,59 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1383
1475
|
}
|
|
1384
1476
|
}
|
|
1385
1477
|
|
|
1386
|
-
|
|
1478
|
+
this.state = "Teardown";
|
|
1479
|
+
this.raiseTeardownFailures(failures);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Unwind SOME of this context's resources and leave the rest running.
|
|
1484
|
+
*
|
|
1485
|
+
* The reconciliation half of teardown: a host that has decided which
|
|
1486
|
+
* declarations moved unwinds exactly {@link impactedBy}'s answer, then
|
|
1487
|
+
* re-registers and re-initializes. Ordering is {@link teardownOrder}
|
|
1488
|
+
* restricted to the selection, so a consumer still unwinds before what it
|
|
1489
|
+
* holds — and the selection being closed under holders is what makes that
|
|
1490
|
+
* true of the resources left standing as well, since none of them holds
|
|
1491
|
+
* anything in it.
|
|
1492
|
+
*
|
|
1493
|
+
* The context keeps its state: it is neither draining nor torn down, and no
|
|
1494
|
+
* child context is swept, because a child belonging to an unwound import goes
|
|
1495
|
+
* down with that import's own inverse exactly as it does at teardown.
|
|
1496
|
+
*
|
|
1497
|
+
* A name with no live instance is skipped rather than reported — a resource
|
|
1498
|
+
* that failed to initialize has nothing to unwind, and a host asking for it is
|
|
1499
|
+
* asking about a declaration, not about an instance.
|
|
1500
|
+
*/
|
|
1501
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
1502
|
+
async unwindResources(names: ReadonlySet<string>): Promise<void> {
|
|
1503
|
+
const selected = this.teardownOrder().filter(([, entry]) =>
|
|
1504
|
+
names.has(entry.resource.metadata.name as string),
|
|
1505
|
+
);
|
|
1506
|
+
this.raiseTeardownFailures(await this.unwindEach(selected));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
private raiseTeardownFailures(failures: Array<{ resource: string; error: unknown }>): void {
|
|
1510
|
+
if (failures.length === 0) return;
|
|
1511
|
+
throw new RuntimeError(
|
|
1512
|
+
"ERR_TEARDOWN_FAILED",
|
|
1513
|
+
`${failures.length} resource(s) failed during teardown`,
|
|
1514
|
+
failures.map(({ resource, error }) => ({
|
|
1515
|
+
severity: "error" as const,
|
|
1516
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1517
|
+
resource,
|
|
1518
|
+
})),
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/** Unwind the given entries in the order supplied, aggregating what refused.
|
|
1523
|
+
* Failures are returned rather than thrown so one refusing inverse cannot
|
|
1524
|
+
* strand the resources after it — the log sinks above all. */
|
|
1525
|
+
private async unwindEach(
|
|
1526
|
+
entries: Array<readonly [string, { resource: any; instance: any }]>,
|
|
1527
|
+
): Promise<Array<{ resource: string; error: unknown }>> {
|
|
1528
|
+
const failures: Array<{ resource: string; error: unknown }> = [];
|
|
1529
|
+
|
|
1530
|
+
for (const [key, { resource, instance }] of entries) {
|
|
1387
1531
|
const label = `${resource.kind}.${resource.metadata.name}`;
|
|
1388
1532
|
// A reading belongs to the run that produced it. The WeakMap would drop it
|
|
1389
1533
|
// with the instance anyway; clearing here also covers an instance something
|
|
@@ -1436,34 +1580,32 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1436
1580
|
failures.push({ resource: `${label} (Teardown event)`, error: err });
|
|
1437
1581
|
}
|
|
1438
1582
|
this.resourceInstances.delete(key);
|
|
1583
|
+
// The single removal site, so it is also where "this went away because the
|
|
1584
|
+
// runtime withdrew it" is recorded — the fact `invoke` reports a miss by.
|
|
1585
|
+
this.unwoundResources.add(key);
|
|
1586
|
+
// A torn-down resource must stop being readable: a CEL expansion that still
|
|
1587
|
+
// found its reading would bake a value nothing is serving any more.
|
|
1588
|
+
this.clearPublishedReading(resource.metadata.name as string);
|
|
1439
1589
|
}
|
|
1440
1590
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
if (failures.length > 0) {
|
|
1444
|
-
throw new RuntimeError(
|
|
1445
|
-
"ERR_TEARDOWN_FAILED",
|
|
1446
|
-
`${failures.length} resource(s) failed during teardown`,
|
|
1447
|
-
failures.map(({ resource, error }) => ({
|
|
1448
|
-
severity: "error" as const,
|
|
1449
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1450
|
-
resource,
|
|
1451
|
-
})),
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1591
|
+
return failures;
|
|
1454
1592
|
}
|
|
1455
1593
|
|
|
1456
1594
|
/**
|
|
1457
|
-
* Child contexts
|
|
1458
|
-
*
|
|
1595
|
+
* Child contexts for the backstop sweep: ascending `teardownPriority`
|
|
1596
|
+
* (default 0), reverse registration within a tier.
|
|
1597
|
+
*
|
|
1598
|
+
* What reaches the sweep is a context no resource's inverse claimed, which in
|
|
1599
|
+
* practice is the `lifecycle: shared` libraries. Those are spawned under the
|
|
1600
|
+
* ROOT and give no importer a claim, so nothing but this orders them — and
|
|
1601
|
+
* reverse registration alone does not, since a singleton is registered when
|
|
1602
|
+
* the FIRST import reaches it, which for an import declared inside another
|
|
1603
|
+
* library is after that library's own context. `TEARDOWN_LAST` on the context
|
|
1604
|
+
* is what keeps a singleton alive until the libraries borrowing it have gone.
|
|
1459
1605
|
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
* depend on when it happened to be created. A `lifecycle: shared` library is
|
|
1464
|
-
* registered when the FIRST import reaches it — which, for an import declared
|
|
1465
|
-
* inside another library, is after that library's own context — so reverse
|
|
1466
|
-
* registration would tear the singleton down while a borrower still holds it.
|
|
1606
|
+
* Its protection now stops at library-against-library. Every resource of the
|
|
1607
|
+
* context that owns the sweep has already unwound by the time it runs, which
|
|
1608
|
+
* is the ordering the sweep used to invert.
|
|
1467
1609
|
*/
|
|
1468
1610
|
private childTeardownOrder(): IEvaluationContext[] {
|
|
1469
1611
|
return [...this.children]
|
|
@@ -1476,31 +1618,120 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1476
1618
|
}
|
|
1477
1619
|
|
|
1478
1620
|
/**
|
|
1479
|
-
* Resource instances in teardown order: ascending `teardownPriority
|
|
1480
|
-
*
|
|
1621
|
+
* Resource instances in teardown order: ascending `teardownPriority` as a hard
|
|
1622
|
+
* tier, and within a tier a consumer before every resource it holds
|
|
1623
|
+
* ({@link reverseTopologicalOrder} over the create-time edges), with reverse
|
|
1624
|
+
* insertion as the tiebreak.
|
|
1481
1625
|
*
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
*
|
|
1488
|
-
* path orders by a declared number rather than sniffing any one subsystem's
|
|
1489
|
-
* instance shape.
|
|
1626
|
+
* `teardownPriority` is a TIER rather than another edge because it is the
|
|
1627
|
+
* author's statement about an edge nothing captured: a log sink is reached
|
|
1628
|
+
* through `ctx.log` rather than through a ref slot, so no walk of the manifest
|
|
1629
|
+
* can find the resources that will log on the way down. Letting topology
|
|
1630
|
+
* reorder across tiers would let one discovered edge override a declaration
|
|
1631
|
+
* made precisely because the edges are not all discoverable.
|
|
1490
1632
|
*/
|
|
1491
|
-
private teardownOrder(): Array<[string, { resource: any; instance: any }]> {
|
|
1633
|
+
private teardownOrder(): Array<readonly [string, { resource: any; instance: any }]> {
|
|
1492
1634
|
// A borrowed instance is torn down by the scope that declared it, never
|
|
1493
1635
|
// here — see `borrowedResources`.
|
|
1494
1636
|
const entries = [...this.resourceInstances.entries()]
|
|
1495
1637
|
.filter(([name]) => !this.borrowedResources.has(name))
|
|
1496
1638
|
.reverse();
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1639
|
+
const tiers = new Map<number, Array<readonly [string, { resource: any; instance: any }]>>();
|
|
1640
|
+
for (const entry of entries) {
|
|
1641
|
+
const priority = (entry[1].instance as { teardownPriority?: number })?.teardownPriority ?? 0;
|
|
1642
|
+
const tier = tiers.get(priority);
|
|
1643
|
+
if (tier) tier.push(entry);
|
|
1644
|
+
else tiers.set(priority, [entry]);
|
|
1645
|
+
}
|
|
1646
|
+
return [...tiers.keys()]
|
|
1647
|
+
.sort((a, b) => a - b)
|
|
1648
|
+
.flatMap((priority) =>
|
|
1649
|
+
reverseTopologicalOrder(
|
|
1650
|
+
tiers.get(priority)!,
|
|
1651
|
+
(value) => value.resource.metadata.name as string,
|
|
1652
|
+
(name) => this.resourceDependencies.get(name),
|
|
1653
|
+
),
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Every resource of this context that becomes invalid when `names` do — the
|
|
1659
|
+
* named resources plus everything that transitively holds one
|
|
1660
|
+
* ({@link impactClosure} over the same create-time edges teardown reads).
|
|
1661
|
+
*
|
|
1662
|
+
* What a reconciliation unwinds. A cross-module reference projects onto the
|
|
1663
|
+
* local `Telo.Import` (`localDependencyNames`), so a change inside an
|
|
1664
|
+
* imported library reaches this context as its import being impacted, and the
|
|
1665
|
+
* library goes down with that import's own inverse — which is why this
|
|
1666
|
+
* answers for one context rather than walking the tree.
|
|
1667
|
+
*
|
|
1668
|
+
* **`opaque` is what the answer cannot cover.** A name resolved by NAME during
|
|
1669
|
+
* initialization ({@link opaquelyRead}) may be held by a resource no edge
|
|
1670
|
+
* names, so a closure reaching one is not an answer at all. Those names are
|
|
1671
|
+
* reported rather than absorbed: expanding the set to "every resource here"
|
|
1672
|
+
* would sweep in the module document, which is not a resource a caller can
|
|
1673
|
+
* unwind and re-register, and would present a whole-context rebuild as a
|
|
1674
|
+
* narrowing. The caller escalates, and can say which resource forced it.
|
|
1675
|
+
*/
|
|
1676
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
1677
|
+
impactedBy(names: Iterable<string>): { impacted: Set<string>; opaque: string[] } {
|
|
1678
|
+
const impacted = impactClosure(names, this.resourceDependencies);
|
|
1679
|
+
const opaque = [...impacted].filter((name) => this.opaquelyRead.has(name));
|
|
1680
|
+
return { impacted, opaque };
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/** Whether this resource's `run()` has been dispatched. A rebuilt resource
|
|
1684
|
+
* that had been started is one nothing will start again — boot targets run
|
|
1685
|
+
* once — so a caller reconciling has to escalate rather than leave it
|
|
1686
|
+
* constructed and idle. */
|
|
1687
|
+
wasStarted(name: string): boolean {
|
|
1688
|
+
const instance = this.resourceInstances.get(name)?.instance;
|
|
1689
|
+
return instance !== undefined && startedInstances.has(instance);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/** Drop a resource's published reading. A context with no `resources` scope of
|
|
1693
|
+
* its own has nothing to drop; `ModuleContext` overrides. */
|
|
1694
|
+
clearPublishedReading(name: string): void {
|
|
1695
|
+
void name;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/** The declaration registered under `name` in THIS context, without the
|
|
1699
|
+
* walk up the enclosing chain a name lookup does. */
|
|
1700
|
+
declaredManifestFor(name: string): ResourceManifest | undefined {
|
|
1701
|
+
return this.declaredManifests.get(name);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/** Replace a declaration in place, without queueing the resource for
|
|
1705
|
+
* creation. For a survivor of a reconciliation: its declaration is
|
|
1706
|
+
* content-identical by construction, but the object carries fresh loader
|
|
1707
|
+
* stamps (`metadata.sourceLine` above all), and a diagnostic anchored on the
|
|
1708
|
+
* stale one points at a pre-edit line. */
|
|
1709
|
+
refreshManifest(name: string, resource: ResourceManifest): void {
|
|
1710
|
+
if (this.declaredManifests.has(name)) this.declaredManifests.set(name, resource);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
/** Re-open an initialized context for another initialization pass.
|
|
1714
|
+
*
|
|
1715
|
+
* The transition is the context's own, not a field a caller assigns: while it
|
|
1716
|
+
* is open, a reference to a resource that has not been rebuilt yet must
|
|
1717
|
+
* produce the deferral the init loop retries on rather than a hard
|
|
1718
|
+
* not-found, and leaving it open after a failed pass turns every later
|
|
1719
|
+
* lookup into that deferral with no pass coming. */
|
|
1720
|
+
reopenForInitialization(): void {
|
|
1721
|
+
if (this.state === "Initialized") this.state = "Validated";
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/** Close a pass opened by {@link reopenForInitialization} that did not reach
|
|
1725
|
+
* the end of `initializeResources`. */
|
|
1726
|
+
closeInitialization(): void {
|
|
1727
|
+
if (this.state === "Validated") this.state = "Initialized";
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
/** Names a resource resolved by NAME while this context was initializing, so
|
|
1731
|
+
* no edge records who is holding them. Read by {@link impactedBy}; exposed so
|
|
1732
|
+
* a host can report why a reconciliation could not be narrowed. */
|
|
1733
|
+
opaqueReads(): ReadonlySet<string> {
|
|
1734
|
+
return this.opaquelyRead;
|
|
1504
1735
|
}
|
|
1505
1736
|
|
|
1506
1737
|
transientChild(context: Record<string, any>): EvaluationContext {
|
|
@@ -1528,6 +1759,52 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
1528
1759
|
const entry = this.resourceInstances.get(name);
|
|
1529
1760
|
|
|
1530
1761
|
if (!entry) {
|
|
1762
|
+
// A MISS ON A RESOURCE THE RUNTIME WITHDREW is the runtime going away, not
|
|
1763
|
+
// a manifest defect, and the two want opposite follow-ups. An unwind
|
|
1764
|
+
// removes each instance as it goes, so work still in flight — a detached
|
|
1765
|
+
// task the kernel waited for and then abandoned, above all — finds an
|
|
1766
|
+
// emptying map and would otherwise be told its target does not exist. That
|
|
1767
|
+
// verdict is durable where the withdrawal is not: a durable run records it
|
|
1768
|
+
// as `failed`, which is terminal, so one ordinary Ctrl-C leaves a run id
|
|
1769
|
+
// nothing will ever pick up again — from the one feature whose whole
|
|
1770
|
+
// purpose is surviving that.
|
|
1771
|
+
//
|
|
1772
|
+
// Reported as a cancellation because that is what it is, and because every
|
|
1773
|
+
// consumer already handles one correctly: a durable body leaves the run
|
|
1774
|
+
// `running` for the resumer, and a step's retry budget is not spent
|
|
1775
|
+
// re-issuing a call the runtime has no intention of answering.
|
|
1776
|
+
//
|
|
1777
|
+
// Keyed on `unwoundResources` — the recorded FACT — rather than on this
|
|
1778
|
+
// context's state, which is only a proxy for it and misses the
|
|
1779
|
+
// reconciliation path entirely: `unwindResources` withdraws an instance and
|
|
1780
|
+
// deliberately leaves the state alone, and that is the path a watch session
|
|
1781
|
+
// takes on every save. Only the MISS is converted; a resource still in the
|
|
1782
|
+
// map is dispatched as before, because a teardown-time flush is legitimate
|
|
1783
|
+
// work and refusing it would break shutdown to protect it.
|
|
1784
|
+
if (this.unwoundResources.has(name)) {
|
|
1785
|
+
const reason =
|
|
1786
|
+
this.state === "Draining" || this.state === "Teardown"
|
|
1787
|
+
? "the runtime is shutting down"
|
|
1788
|
+
: "the resource was unwound while the runtime reconciled";
|
|
1789
|
+
// The SPAN payload, not an ad-hoc object: every consumer of this event
|
|
1790
|
+
// reads `ref.kind` (the debug UI's kind facet) and `outcome` (its graph
|
|
1791
|
+
// nodes and outcome tally) off the trace shape, so a payload carrying
|
|
1792
|
+
// neither is an event that fires and is invisible — which is worse than
|
|
1793
|
+
// no event, and precisely the cancellation an operator is looking for
|
|
1794
|
+
// during a watch-session save. No span ids: this refusal happens before
|
|
1795
|
+
// any span is opened, and `tracePayload` omits an undefined one.
|
|
1796
|
+
await this.emit(
|
|
1797
|
+
`${name}.InvokeCancelled`,
|
|
1798
|
+
this.tracePayload(kind, name, undefined, undefined, undefined, "invoke", "end", "cancelled", {
|
|
1799
|
+
reason,
|
|
1800
|
+
}),
|
|
1801
|
+
);
|
|
1802
|
+
throw new RuntimeError(
|
|
1803
|
+
"ERR_INVOKE_CANCELLED",
|
|
1804
|
+
`Invoke ${kind}.${name} was cancelled: ${reason} and the resource has already ` +
|
|
1805
|
+
`been torn down.`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1531
1808
|
throw new RuntimeError(
|
|
1532
1809
|
"ERR_RESOURCE_NOT_FOUND",
|
|
1533
1810
|
`Resource not found for invocation: ${kind}.${name}. Available resources: ${[...this.resourceInstances.keys()].join(", ")}`,
|