@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
|
@@ -2,8 +2,9 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { formatSpanCounter } from "./logging/span-id.js";
|
|
3
3
|
import { deriveContext, getRefIdentity, isCompiledValue, isInvokeError, isCancellationError, isSuspension, resourceKey, UNCANCELLABLE_CONTEXT, } from "@telorun/sdk";
|
|
4
4
|
import { RuntimeError } from "@telorun/sdk";
|
|
5
|
-
import { evalPathCovers } from "@telorun/analyzer";
|
|
5
|
+
import { celResourceReads, evalPathCovers } from "@telorun/analyzer";
|
|
6
6
|
import { effectOwnerOf, executeReturnedChain } from "./effect-scope.js";
|
|
7
|
+
import { impactClosure, reverseTopologicalOrder } from "./resource-edges.js";
|
|
7
8
|
import { REDACTED, redactSensitive, sensitivePathsOfInstance, } from "./instance-sensitive-paths.js";
|
|
8
9
|
import { classifyInitFailures, isDeferral, renderInitFailureText, summarizeInitFailures, } from "./init-failure-diagnostics.js";
|
|
9
10
|
import { acceptReportedStatus, buildPublishedProps, diagnoseObservedStateAccess, } from "./observed-state.js";
|
|
@@ -432,6 +433,24 @@ export class EvaluationContext {
|
|
|
432
433
|
* they are reported with both the init error and the refusing inverse.
|
|
433
434
|
*/
|
|
434
435
|
withheldResources = new Set();
|
|
436
|
+
/**
|
|
437
|
+
* Keys whose instance was REMOVED BY AN UNWIND — the fact a dispatch that
|
|
438
|
+
* misses actually needs, as opposed to the context-wide state that only
|
|
439
|
+
* approximates it.
|
|
440
|
+
*
|
|
441
|
+
* A miss because the resource went away is the runtime withdrawing, not a
|
|
442
|
+
* manifest defect, and the two want opposite follow-ups (see `invoke`). Both
|
|
443
|
+
* paths that withdraw one go through `unwindEach`, the single removal site, so
|
|
444
|
+
* recording it there covers a shutdown AND a reconciliation — where a check
|
|
445
|
+
* against `state` would have converted the first and missed the second, since
|
|
446
|
+
* `unwindResources` deliberately leaves the state untouched. The second is the
|
|
447
|
+
* one that fires on every watch-session save.
|
|
448
|
+
*
|
|
449
|
+
* Cleared when the key is registered again: after a reconcile the name means a
|
|
450
|
+
* live resource, and a stale entry would report a genuine missing-resource
|
|
451
|
+
* defect as a cancellation.
|
|
452
|
+
*/
|
|
453
|
+
unwoundResources = new Set();
|
|
435
454
|
/** Resources discarded after a failed `init()` and re-queued for creation.
|
|
436
455
|
* Their re-creation is not progress — see the create sub-phase. */
|
|
437
456
|
recreatedResources = new Set();
|
|
@@ -460,6 +479,7 @@ export class EvaluationContext {
|
|
|
460
479
|
*/
|
|
461
480
|
adoptBorrowedResource(name, resource, instance, owner) {
|
|
462
481
|
this.resourceInstances.set(name, { resource, instance });
|
|
482
|
+
this.unwoundResources.delete(name);
|
|
463
483
|
this.borrowedResources.add(name);
|
|
464
484
|
this.declaredManifests.set(name, resource);
|
|
465
485
|
const unmirror = owner.mirrorPublications(resource.metadata.name, (props) => this.onResourceSnapshotted(name, props));
|
|
@@ -521,9 +541,29 @@ export class EvaluationContext {
|
|
|
521
541
|
declaredManifests = new Map();
|
|
522
542
|
/** Per-resource dependency names, captured at create() time — BEFORE Phase-5
|
|
523
543
|
* injection swaps refs for live instances, so the walk sees plain objects and
|
|
524
|
-
* cannot wander into a controller's (possibly cyclic) object graph.
|
|
525
|
-
*
|
|
544
|
+
* cannot wander into a controller's (possibly cyclic) object graph.
|
|
545
|
+
*
|
|
546
|
+
* Read twice, and RETAINED for the context's lifetime because of the second
|
|
547
|
+
* reader: to attribute an init failure to its cause, and to order teardown
|
|
548
|
+
* (`teardownOrder`) so a consumer's inverses run while the resources it holds
|
|
549
|
+
* are still alive. */
|
|
526
550
|
resourceDependencies = new Map();
|
|
551
|
+
/**
|
|
552
|
+
* Names resolved by NAME during initialization, rather than through a
|
|
553
|
+
* declared reference slot — so a resource somebody may be holding, with no
|
|
554
|
+
* edge recording who.
|
|
555
|
+
*
|
|
556
|
+
* The set is of TARGETS, not of pairs: the door that records
|
|
557
|
+
* (`ModuleContext.getInstance`) is reached as `ctx.moduleContext`, which every
|
|
558
|
+
* resource of the module shares, so there is no caller to attribute the read
|
|
559
|
+
* to. That is enough for the one decision that depends on it — see
|
|
560
|
+
* {@link impactedBy}.
|
|
561
|
+
*/
|
|
562
|
+
opaquelyRead = new Set();
|
|
563
|
+
/** Record a by-name resolution. Called by the recording door only. */
|
|
564
|
+
recordOpaqueRead(name) {
|
|
565
|
+
this.opaquelyRead.add(name);
|
|
566
|
+
}
|
|
527
567
|
/**
|
|
528
568
|
* Optional hook called between create() and init() for each resource.
|
|
529
569
|
* Set by the kernel to inject live instances into reference fields.
|
|
@@ -730,6 +770,32 @@ export class EvaluationContext {
|
|
|
730
770
|
this.pendingResources.push(resource);
|
|
731
771
|
this.declaredManifests.set(name, resource);
|
|
732
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* Forget a declaration entirely — the inverse of {@link registerManifest}.
|
|
775
|
+
*
|
|
776
|
+
* Reconciliation's other half: {@link unwindResources} disposes the INSTANCE,
|
|
777
|
+
* and this clears everything keyed by the name so the same name can be
|
|
778
|
+
* declared again. Without it `registerManifest` refuses with
|
|
779
|
+
* `ERR_DUPLICATE_RESOURCE`, which is the right answer for a manifest
|
|
780
|
+
* declaring one name twice and the wrong one for a second load of the same
|
|
781
|
+
* manifest.
|
|
782
|
+
*
|
|
783
|
+
* Every per-name record goes, not just the declaration: a resource left in
|
|
784
|
+
* `withheldResources` would be skipped by the init loop for the life of the
|
|
785
|
+
* kernel, and a stale `createdInstances` entry would have the loop initialize
|
|
786
|
+
* the object built from the PREVIOUS declaration.
|
|
787
|
+
*/
|
|
788
|
+
deregisterManifest(name) {
|
|
789
|
+
this.declaredManifests.delete(name);
|
|
790
|
+
this.resourceDependencies.delete(name);
|
|
791
|
+
this.createdInstances.delete(name);
|
|
792
|
+
this.withheldResources.delete(name);
|
|
793
|
+
this.recreatedResources.delete(name);
|
|
794
|
+
this.opaquelyRead.delete(name);
|
|
795
|
+
const pending = this.pendingResources.findIndex((r) => r.metadata?.name === name);
|
|
796
|
+
if (pending >= 0)
|
|
797
|
+
this.pendingResources.splice(pending, 1);
|
|
798
|
+
}
|
|
733
799
|
/**
|
|
734
800
|
* The manifest a name was DECLARED with, resolved scope-local first and then
|
|
735
801
|
* up the enclosing chain — the order `getInstance` and the CEL `resources`
|
|
@@ -890,7 +956,12 @@ export class EvaluationContext {
|
|
|
890
956
|
progress = true;
|
|
891
957
|
const createdRes = created.resource;
|
|
892
958
|
const refs = collectResourceRefs(createdRes);
|
|
893
|
-
|
|
959
|
+
// `resource` rather than `createdRes`: the registered declaration
|
|
960
|
+
// still holds its expressions, while the created copy holds the
|
|
961
|
+
// values they were expanded to.
|
|
962
|
+
this.resourceDependencies.set(name, [
|
|
963
|
+
...new Set([...localDependencyNames(refs), ...celResourceReads(resource)]),
|
|
964
|
+
]);
|
|
894
965
|
const payload = {
|
|
895
966
|
resource: {
|
|
896
967
|
kind: createdRes.kind,
|
|
@@ -1000,10 +1071,11 @@ export class EvaluationContext {
|
|
|
1000
1071
|
// so it resolves fine from here.
|
|
1001
1072
|
await this.publishSnapshot(name);
|
|
1002
1073
|
this.resourceInstances.set(name, { resource, instance });
|
|
1074
|
+
// Live again: a reconcile re-initializes what it unwound, and leaving
|
|
1075
|
+
// the mark would report a genuine missing-resource defect at this name
|
|
1076
|
+
// as a cancellation for the rest of the process.
|
|
1077
|
+
this.unwoundResources.delete(name);
|
|
1003
1078
|
this.createdInstances.delete(name);
|
|
1004
|
-
// Read only on failure, and this one succeeded — drop it rather than
|
|
1005
|
-
// holding a dep-name array per resource for the context's lifetime.
|
|
1006
|
-
this.resourceDependencies.delete(name);
|
|
1007
1079
|
errors.delete(name);
|
|
1008
1080
|
progress = true;
|
|
1009
1081
|
await this.emit(`${resource.kind}.${resource.metadata.name}.Initialized`, {
|
|
@@ -1173,15 +1245,32 @@ export class EvaluationContext {
|
|
|
1173
1245
|
};
|
|
1174
1246
|
}
|
|
1175
1247
|
/**
|
|
1176
|
-
* Cascade teardown
|
|
1177
|
-
* 1. Tear down
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1248
|
+
* Cascade teardown through the tree:
|
|
1249
|
+
* 1. Tear down own resource instances in {@link teardownOrder}, emitting a
|
|
1250
|
+
* Teardown event for each via the injected emit callback.
|
|
1251
|
+
* 2. Sweep any child context still standing, in {@link childTeardownOrder}.
|
|
1252
|
+
*
|
|
1253
|
+
* **Own resources go FIRST, and the child sweep is a backstop.** A child
|
|
1254
|
+
* context that belongs to a resource is torn down by that resource's own
|
|
1255
|
+
* inverse — an import's `init()` returns `child.teardownResources()`, and so
|
|
1256
|
+
* does a template's — so it already unwinds at its owner's position in step 1,
|
|
1257
|
+
* which is the position the edges put it at. Running a child cascade ahead of
|
|
1258
|
+
* step 1 tore every imported library down before any resource of THIS context,
|
|
1259
|
+
* so an app resource holding `!ref Alias.name` unwound after its provider was
|
|
1260
|
+
* already gone, and the owner's inverse then found nothing left to do.
|
|
1261
|
+
*
|
|
1262
|
+
* Sweeping afterwards rather than not at all is what still reclaims a context
|
|
1263
|
+
* no inverse claims: a `lifecycle: shared` library, which is spawned under the
|
|
1264
|
+
* root and deliberately gives no importer a claim on it, and an import whose
|
|
1265
|
+
* `init()` never ran to register one. `teardownResources` is idempotent, so a
|
|
1266
|
+
* context already taken down in step 1 costs the sweep nothing.
|
|
1180
1267
|
*/
|
|
1181
1268
|
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
1182
1269
|
async teardownResources() {
|
|
1183
1270
|
this.state = "Draining";
|
|
1184
|
-
const failures =
|
|
1271
|
+
const failures = await this.unwindEach(this.teardownOrder());
|
|
1272
|
+
// The backstop: whatever no resource's inverse claimed. Everything an import
|
|
1273
|
+
// or a template owns is already down, so this is a no-op for it.
|
|
1185
1274
|
for (const child of this.childTeardownOrder()) {
|
|
1186
1275
|
try {
|
|
1187
1276
|
await child.teardownResources();
|
|
@@ -1190,7 +1279,48 @@ export class EvaluationContext {
|
|
|
1190
1279
|
failures.push({ resource: "(child context)", error: err });
|
|
1191
1280
|
}
|
|
1192
1281
|
}
|
|
1193
|
-
|
|
1282
|
+
this.state = "Teardown";
|
|
1283
|
+
this.raiseTeardownFailures(failures);
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Unwind SOME of this context's resources and leave the rest running.
|
|
1287
|
+
*
|
|
1288
|
+
* The reconciliation half of teardown: a host that has decided which
|
|
1289
|
+
* declarations moved unwinds exactly {@link impactedBy}'s answer, then
|
|
1290
|
+
* re-registers and re-initializes. Ordering is {@link teardownOrder}
|
|
1291
|
+
* restricted to the selection, so a consumer still unwinds before what it
|
|
1292
|
+
* holds — and the selection being closed under holders is what makes that
|
|
1293
|
+
* true of the resources left standing as well, since none of them holds
|
|
1294
|
+
* anything in it.
|
|
1295
|
+
*
|
|
1296
|
+
* The context keeps its state: it is neither draining nor torn down, and no
|
|
1297
|
+
* child context is swept, because a child belonging to an unwound import goes
|
|
1298
|
+
* down with that import's own inverse exactly as it does at teardown.
|
|
1299
|
+
*
|
|
1300
|
+
* A name with no live instance is skipped rather than reported — a resource
|
|
1301
|
+
* that failed to initialize has nothing to unwind, and a host asking for it is
|
|
1302
|
+
* asking about a declaration, not about an instance.
|
|
1303
|
+
*/
|
|
1304
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
1305
|
+
async unwindResources(names) {
|
|
1306
|
+
const selected = this.teardownOrder().filter(([, entry]) => names.has(entry.resource.metadata.name));
|
|
1307
|
+
this.raiseTeardownFailures(await this.unwindEach(selected));
|
|
1308
|
+
}
|
|
1309
|
+
raiseTeardownFailures(failures) {
|
|
1310
|
+
if (failures.length === 0)
|
|
1311
|
+
return;
|
|
1312
|
+
throw new RuntimeError("ERR_TEARDOWN_FAILED", `${failures.length} resource(s) failed during teardown`, failures.map(({ resource, error }) => ({
|
|
1313
|
+
severity: "error",
|
|
1314
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1315
|
+
resource,
|
|
1316
|
+
})));
|
|
1317
|
+
}
|
|
1318
|
+
/** Unwind the given entries in the order supplied, aggregating what refused.
|
|
1319
|
+
* Failures are returned rather than thrown so one refusing inverse cannot
|
|
1320
|
+
* strand the resources after it — the log sinks above all. */
|
|
1321
|
+
async unwindEach(entries) {
|
|
1322
|
+
const failures = [];
|
|
1323
|
+
for (const [key, { resource, instance }] of entries) {
|
|
1194
1324
|
const label = `${resource.kind}.${resource.metadata.name}`;
|
|
1195
1325
|
// A reading belongs to the run that produced it. The WeakMap would drop it
|
|
1196
1326
|
// with the instance anyway; clearing here also covers an instance something
|
|
@@ -1242,27 +1372,30 @@ export class EvaluationContext {
|
|
|
1242
1372
|
failures.push({ resource: `${label} (Teardown event)`, error: err });
|
|
1243
1373
|
}
|
|
1244
1374
|
this.resourceInstances.delete(key);
|
|
1375
|
+
// The single removal site, so it is also where "this went away because the
|
|
1376
|
+
// runtime withdrew it" is recorded — the fact `invoke` reports a miss by.
|
|
1377
|
+
this.unwoundResources.add(key);
|
|
1378
|
+
// A torn-down resource must stop being readable: a CEL expansion that still
|
|
1379
|
+
// found its reading would bake a value nothing is serving any more.
|
|
1380
|
+
this.clearPublishedReading(resource.metadata.name);
|
|
1245
1381
|
}
|
|
1246
|
-
|
|
1247
|
-
if (failures.length > 0) {
|
|
1248
|
-
throw new RuntimeError("ERR_TEARDOWN_FAILED", `${failures.length} resource(s) failed during teardown`, failures.map(({ resource, error }) => ({
|
|
1249
|
-
severity: "error",
|
|
1250
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1251
|
-
resource,
|
|
1252
|
-
})));
|
|
1253
|
-
}
|
|
1382
|
+
return failures;
|
|
1254
1383
|
}
|
|
1255
1384
|
/**
|
|
1256
|
-
* Child contexts
|
|
1257
|
-
*
|
|
1385
|
+
* Child contexts for the backstop sweep: ascending `teardownPriority`
|
|
1386
|
+
* (default 0), reverse registration within a tier.
|
|
1258
1387
|
*
|
|
1259
|
-
*
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1388
|
+
* What reaches the sweep is a context no resource's inverse claimed, which in
|
|
1389
|
+
* practice is the `lifecycle: shared` libraries. Those are spawned under the
|
|
1390
|
+
* ROOT and give no importer a claim, so nothing but this orders them — and
|
|
1391
|
+
* reverse registration alone does not, since a singleton is registered when
|
|
1392
|
+
* the FIRST import reaches it, which for an import declared inside another
|
|
1393
|
+
* library is after that library's own context. `TEARDOWN_LAST` on the context
|
|
1394
|
+
* is what keeps a singleton alive until the libraries borrowing it have gone.
|
|
1395
|
+
*
|
|
1396
|
+
* Its protection now stops at library-against-library. Every resource of the
|
|
1397
|
+
* context that owns the sweep has already unwound by the time it runs, which
|
|
1398
|
+
* is the ordering the sweep used to invert.
|
|
1266
1399
|
*/
|
|
1267
1400
|
childTeardownOrder() {
|
|
1268
1401
|
return [...this.children]
|
|
@@ -1271,17 +1404,17 @@ export class EvaluationContext {
|
|
|
1271
1404
|
(b.teardownPriority ?? 0));
|
|
1272
1405
|
}
|
|
1273
1406
|
/**
|
|
1274
|
-
* Resource instances in teardown order: ascending `teardownPriority
|
|
1275
|
-
*
|
|
1407
|
+
* Resource instances in teardown order: ascending `teardownPriority` as a hard
|
|
1408
|
+
* tier, and within a tier a consumer before every resource it holds
|
|
1409
|
+
* ({@link reverseTopologicalOrder} over the create-time edges), with reverse
|
|
1410
|
+
* insertion as the tiebreak.
|
|
1276
1411
|
*
|
|
1277
|
-
*
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
*
|
|
1283
|
-
* path orders by a declared number rather than sniffing any one subsystem's
|
|
1284
|
-
* instance shape.
|
|
1412
|
+
* `teardownPriority` is a TIER rather than another edge because it is the
|
|
1413
|
+
* author's statement about an edge nothing captured: a log sink is reached
|
|
1414
|
+
* through `ctx.log` rather than through a ref slot, so no walk of the manifest
|
|
1415
|
+
* can find the resources that will log on the way down. Letting topology
|
|
1416
|
+
* reorder across tiers would let one discovered edge override a declaration
|
|
1417
|
+
* made precisely because the edges are not all discoverable.
|
|
1285
1418
|
*/
|
|
1286
1419
|
teardownOrder() {
|
|
1287
1420
|
// A borrowed instance is torn down by the scope that declared it, never
|
|
@@ -1289,10 +1422,93 @@ export class EvaluationContext {
|
|
|
1289
1422
|
const entries = [...this.resourceInstances.entries()]
|
|
1290
1423
|
.filter(([name]) => !this.borrowedResources.has(name))
|
|
1291
1424
|
.reverse();
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1425
|
+
const tiers = new Map();
|
|
1426
|
+
for (const entry of entries) {
|
|
1427
|
+
const priority = entry[1].instance?.teardownPriority ?? 0;
|
|
1428
|
+
const tier = tiers.get(priority);
|
|
1429
|
+
if (tier)
|
|
1430
|
+
tier.push(entry);
|
|
1431
|
+
else
|
|
1432
|
+
tiers.set(priority, [entry]);
|
|
1433
|
+
}
|
|
1434
|
+
return [...tiers.keys()]
|
|
1435
|
+
.sort((a, b) => a - b)
|
|
1436
|
+
.flatMap((priority) => reverseTopologicalOrder(tiers.get(priority), (value) => value.resource.metadata.name, (name) => this.resourceDependencies.get(name)));
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Every resource of this context that becomes invalid when `names` do — the
|
|
1440
|
+
* named resources plus everything that transitively holds one
|
|
1441
|
+
* ({@link impactClosure} over the same create-time edges teardown reads).
|
|
1442
|
+
*
|
|
1443
|
+
* What a reconciliation unwinds. A cross-module reference projects onto the
|
|
1444
|
+
* local `Telo.Import` (`localDependencyNames`), so a change inside an
|
|
1445
|
+
* imported library reaches this context as its import being impacted, and the
|
|
1446
|
+
* library goes down with that import's own inverse — which is why this
|
|
1447
|
+
* answers for one context rather than walking the tree.
|
|
1448
|
+
*
|
|
1449
|
+
* **`opaque` is what the answer cannot cover.** A name resolved by NAME during
|
|
1450
|
+
* initialization ({@link opaquelyRead}) may be held by a resource no edge
|
|
1451
|
+
* names, so a closure reaching one is not an answer at all. Those names are
|
|
1452
|
+
* reported rather than absorbed: expanding the set to "every resource here"
|
|
1453
|
+
* would sweep in the module document, which is not a resource a caller can
|
|
1454
|
+
* unwind and re-register, and would present a whole-context rebuild as a
|
|
1455
|
+
* narrowing. The caller escalates, and can say which resource forced it.
|
|
1456
|
+
*/
|
|
1457
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering
|
|
1458
|
+
impactedBy(names) {
|
|
1459
|
+
const impacted = impactClosure(names, this.resourceDependencies);
|
|
1460
|
+
const opaque = [...impacted].filter((name) => this.opaquelyRead.has(name));
|
|
1461
|
+
return { impacted, opaque };
|
|
1462
|
+
}
|
|
1463
|
+
/** Whether this resource's `run()` has been dispatched. A rebuilt resource
|
|
1464
|
+
* that had been started is one nothing will start again — boot targets run
|
|
1465
|
+
* once — so a caller reconciling has to escalate rather than leave it
|
|
1466
|
+
* constructed and idle. */
|
|
1467
|
+
wasStarted(name) {
|
|
1468
|
+
const instance = this.resourceInstances.get(name)?.instance;
|
|
1469
|
+
return instance !== undefined && startedInstances.has(instance);
|
|
1470
|
+
}
|
|
1471
|
+
/** Drop a resource's published reading. A context with no `resources` scope of
|
|
1472
|
+
* its own has nothing to drop; `ModuleContext` overrides. */
|
|
1473
|
+
clearPublishedReading(name) {
|
|
1474
|
+
void name;
|
|
1475
|
+
}
|
|
1476
|
+
/** The declaration registered under `name` in THIS context, without the
|
|
1477
|
+
* walk up the enclosing chain a name lookup does. */
|
|
1478
|
+
declaredManifestFor(name) {
|
|
1479
|
+
return this.declaredManifests.get(name);
|
|
1480
|
+
}
|
|
1481
|
+
/** Replace a declaration in place, without queueing the resource for
|
|
1482
|
+
* creation. For a survivor of a reconciliation: its declaration is
|
|
1483
|
+
* content-identical by construction, but the object carries fresh loader
|
|
1484
|
+
* stamps (`metadata.sourceLine` above all), and a diagnostic anchored on the
|
|
1485
|
+
* stale one points at a pre-edit line. */
|
|
1486
|
+
refreshManifest(name, resource) {
|
|
1487
|
+
if (this.declaredManifests.has(name))
|
|
1488
|
+
this.declaredManifests.set(name, resource);
|
|
1489
|
+
}
|
|
1490
|
+
/** Re-open an initialized context for another initialization pass.
|
|
1491
|
+
*
|
|
1492
|
+
* The transition is the context's own, not a field a caller assigns: while it
|
|
1493
|
+
* is open, a reference to a resource that has not been rebuilt yet must
|
|
1494
|
+
* produce the deferral the init loop retries on rather than a hard
|
|
1495
|
+
* not-found, and leaving it open after a failed pass turns every later
|
|
1496
|
+
* lookup into that deferral with no pass coming. */
|
|
1497
|
+
reopenForInitialization() {
|
|
1498
|
+
if (this.state === "Initialized")
|
|
1499
|
+
this.state = "Validated";
|
|
1500
|
+
}
|
|
1501
|
+
/** Close a pass opened by {@link reopenForInitialization} that did not reach
|
|
1502
|
+
* the end of `initializeResources`. */
|
|
1503
|
+
closeInitialization() {
|
|
1504
|
+
if (this.state === "Validated")
|
|
1505
|
+
this.state = "Initialized";
|
|
1506
|
+
}
|
|
1507
|
+
/** Names a resource resolved by NAME while this context was initializing, so
|
|
1508
|
+
* no edge records who is holding them. Read by {@link impactedBy}; exposed so
|
|
1509
|
+
* a host can report why a reconciliation could not be narrowed. */
|
|
1510
|
+
opaqueReads() {
|
|
1511
|
+
return this.opaquelyRead;
|
|
1296
1512
|
}
|
|
1297
1513
|
transientChild(context) {
|
|
1298
1514
|
return new EvaluationContext(this.source, { ...this.context, ...context }, this._createInstance, this._secretValues, this.emit);
|
|
@@ -1306,6 +1522,45 @@ export class EvaluationContext {
|
|
|
1306
1522
|
async invoke(kind, name, inputs, ctx) {
|
|
1307
1523
|
const entry = this.resourceInstances.get(name);
|
|
1308
1524
|
if (!entry) {
|
|
1525
|
+
// A MISS ON A RESOURCE THE RUNTIME WITHDREW is the runtime going away, not
|
|
1526
|
+
// a manifest defect, and the two want opposite follow-ups. An unwind
|
|
1527
|
+
// removes each instance as it goes, so work still in flight — a detached
|
|
1528
|
+
// task the kernel waited for and then abandoned, above all — finds an
|
|
1529
|
+
// emptying map and would otherwise be told its target does not exist. That
|
|
1530
|
+
// verdict is durable where the withdrawal is not: a durable run records it
|
|
1531
|
+
// as `failed`, which is terminal, so one ordinary Ctrl-C leaves a run id
|
|
1532
|
+
// nothing will ever pick up again — from the one feature whose whole
|
|
1533
|
+
// purpose is surviving that.
|
|
1534
|
+
//
|
|
1535
|
+
// Reported as a cancellation because that is what it is, and because every
|
|
1536
|
+
// consumer already handles one correctly: a durable body leaves the run
|
|
1537
|
+
// `running` for the resumer, and a step's retry budget is not spent
|
|
1538
|
+
// re-issuing a call the runtime has no intention of answering.
|
|
1539
|
+
//
|
|
1540
|
+
// Keyed on `unwoundResources` — the recorded FACT — rather than on this
|
|
1541
|
+
// context's state, which is only a proxy for it and misses the
|
|
1542
|
+
// reconciliation path entirely: `unwindResources` withdraws an instance and
|
|
1543
|
+
// deliberately leaves the state alone, and that is the path a watch session
|
|
1544
|
+
// takes on every save. Only the MISS is converted; a resource still in the
|
|
1545
|
+
// map is dispatched as before, because a teardown-time flush is legitimate
|
|
1546
|
+
// work and refusing it would break shutdown to protect it.
|
|
1547
|
+
if (this.unwoundResources.has(name)) {
|
|
1548
|
+
const reason = this.state === "Draining" || this.state === "Teardown"
|
|
1549
|
+
? "the runtime is shutting down"
|
|
1550
|
+
: "the resource was unwound while the runtime reconciled";
|
|
1551
|
+
// The SPAN payload, not an ad-hoc object: every consumer of this event
|
|
1552
|
+
// reads `ref.kind` (the debug UI's kind facet) and `outcome` (its graph
|
|
1553
|
+
// nodes and outcome tally) off the trace shape, so a payload carrying
|
|
1554
|
+
// neither is an event that fires and is invisible — which is worse than
|
|
1555
|
+
// no event, and precisely the cancellation an operator is looking for
|
|
1556
|
+
// during a watch-session save. No span ids: this refusal happens before
|
|
1557
|
+
// any span is opened, and `tracePayload` omits an undefined one.
|
|
1558
|
+
await this.emit(`${name}.InvokeCancelled`, this.tracePayload(kind, name, undefined, undefined, undefined, "invoke", "end", "cancelled", {
|
|
1559
|
+
reason,
|
|
1560
|
+
}));
|
|
1561
|
+
throw new RuntimeError("ERR_INVOKE_CANCELLED", `Invoke ${kind}.${name} was cancelled: ${reason} and the resource has already ` +
|
|
1562
|
+
`been torn down.`);
|
|
1563
|
+
}
|
|
1309
1564
|
throw new RuntimeError("ERR_RESOURCE_NOT_FOUND", `Resource not found for invocation: ${kind}.${name}. Available resources: ${[...this.resourceInstances.keys()].join(", ")}`);
|
|
1310
1565
|
}
|
|
1311
1566
|
// A `!ref` whose kind couldn't be determined at resolve time (e.g. a
|