@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.
@@ -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. Read only
525
- * when init fails, to attribute each failure to its cause. */
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
- this.resourceDependencies.set(name, localDependencyNames(refs));
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 depth-first through the tree:
1177
- * 1. Tear down child contexts in reverse registration order.
1178
- * 2. Tear down own resource instances in reverse registration order,
1179
- * emitting a Teardown event for each via the injected emit callback.
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
- for (const [key, { resource, instance }] of this.teardownOrder()) {
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
- this.state = "Teardown";
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 in teardown order: ascending `teardownPriority` (default 0),
1257
- * with the base reverse-registration order preserved within each tier.
1385
+ * Child contexts for the backstop sweep: ascending `teardownPriority`
1386
+ * (default 0), reverse registration within a tier.
1258
1387
  *
1259
- * The same rule `teardownOrder` applies to resource instances, and for the
1260
- * same reason: reverse registration is reverse init order in the happy path,
1261
- * but a node that must reliably outlive the rest has to say so rather than
1262
- * depend on when it happened to be created. A `lifecycle: shared` library is
1263
- * registered when the FIRST import reaches it which, for an import declared
1264
- * inside another library, is after that library's own context so reverse
1265
- * registration would tear the singleton down while a borrower still holds it.
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`, with the
1275
- * base reverse-insertion order preserved within each priority tier.
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
- * The base order is reverse *insertion*, which is reverse init order in the
1278
- * happy path but the init loop is a multi-pass retry, so a resource that
1279
- * failed its first pass lands later in the map than its topological rank
1280
- * implies. That makes the dependency graph an unreliable way to say "last".
1281
- * A resource that must reliably outlive the rest declares it directly via
1282
- * `teardownPriority` (log sinks set `TEARDOWN_LAST`), so the generic teardown
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
- // Stable sort by priority (default 0); Array.prototype.sort is stable, so
1293
- // the reverse-insertion order survives within each tier.
1294
- return entries.sort(([, a], [, b]) => (a.instance?.teardownPriority ?? 0) -
1295
- (b.instance?.teardownPriority ?? 0));
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