@go-to-k/cdkd 0.1.0 → 0.3.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.
Binary file
package/dist/index.js CHANGED
@@ -3139,6 +3139,62 @@ var TemplateParser = class {
3139
3139
 
3140
3140
  // src/analyzer/dag-builder.ts
3141
3141
  import graphlib from "graphlib";
3142
+
3143
+ // src/analyzer/lambda-vpc-deps.ts
3144
+ function extractLambdaVpcDeleteDeps(resources) {
3145
+ const edges = [];
3146
+ const seen = /* @__PURE__ */ new Set();
3147
+ for (const [lambdaId, resource] of Object.entries(resources)) {
3148
+ if (resource.Type !== "AWS::Lambda::Function")
3149
+ continue;
3150
+ const vpcConfig = (resource.Properties ?? {})["VpcConfig"];
3151
+ if (!isObject(vpcConfig))
3152
+ continue;
3153
+ const targets = /* @__PURE__ */ new Set();
3154
+ collectRefIds(vpcConfig["SubnetIds"], targets);
3155
+ collectRefIds(vpcConfig["SecurityGroupIds"], targets);
3156
+ for (const targetId of targets) {
3157
+ if (targetId === lambdaId)
3158
+ continue;
3159
+ if (!(targetId in resources))
3160
+ continue;
3161
+ const key = `${lambdaId}\0${targetId}`;
3162
+ if (seen.has(key))
3163
+ continue;
3164
+ seen.add(key);
3165
+ edges.push({ before: lambdaId, after: targetId });
3166
+ }
3167
+ }
3168
+ return edges;
3169
+ }
3170
+ function isObject(v) {
3171
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3172
+ }
3173
+ function collectRefIds(value, out) {
3174
+ if (value === null || value === void 0)
3175
+ return;
3176
+ if (Array.isArray(value)) {
3177
+ for (const item of value)
3178
+ collectRefIds(item, out);
3179
+ return;
3180
+ }
3181
+ if (!isObject(value))
3182
+ return;
3183
+ if (typeof value["Ref"] === "string") {
3184
+ const ref = value["Ref"];
3185
+ if (!ref.startsWith("AWS::"))
3186
+ out.add(ref);
3187
+ return;
3188
+ }
3189
+ if (Array.isArray(value["Fn::GetAtt"])) {
3190
+ const arr = value["Fn::GetAtt"];
3191
+ if (typeof arr[0] === "string")
3192
+ out.add(arr[0]);
3193
+ return;
3194
+ }
3195
+ }
3196
+
3197
+ // src/analyzer/dag-builder.ts
3142
3198
  var { Graph, alg } = graphlib;
3143
3199
  var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
3144
3200
  "AWS::IAM::Policy",
@@ -3186,6 +3242,7 @@ var DagBuilder = class {
3186
3242
  }
3187
3243
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
3188
3244
  edgeCount += this.addCustomResourcePolicyEdges(graph, template);
3245
+ edgeCount += this.addLambdaVpcEdges(graph, template);
3189
3246
  if (!alg.isAcyclic(graph)) {
3190
3247
  const cycles = this.findCycles(graph);
3191
3248
  throw new DependencyError(
@@ -3378,6 +3435,41 @@ var DagBuilder = class {
3378
3435
  }
3379
3436
  return added;
3380
3437
  }
3438
+ /**
3439
+ * Add edges from Subnets / SecurityGroups referenced by an
3440
+ * AWS::Lambda::Function VpcConfig to the Lambda itself.
3441
+ *
3442
+ * Same direction as a normal `Ref`-derived edge (Subnet -> Lambda), so for
3443
+ * deploy this just duplicates what extractDependencies already produced.
3444
+ * The point is robustness: if a future template massages the VpcConfig
3445
+ * shape in a way the recursive extractor doesn't anticipate, this pass
3446
+ * still ties the Lambda to its networking resources so that the
3447
+ * deletion-time reverse traversal continues to delete Lambda before
3448
+ * Subnet / SecurityGroup.
3449
+ *
3450
+ * Returns the number of NEW edges added (existing edges are skipped).
3451
+ */
3452
+ addLambdaVpcEdges(graph, template) {
3453
+ const edges = extractLambdaVpcDeleteDeps(template.Resources);
3454
+ if (edges.length === 0)
3455
+ return 0;
3456
+ let added = 0;
3457
+ for (const edge of edges) {
3458
+ const depId = edge.after;
3459
+ const dependentId = edge.before;
3460
+ if (!graph.hasNode(depId) || !graph.hasNode(dependentId))
3461
+ continue;
3462
+ if (graph.hasEdge(depId, dependentId))
3463
+ continue;
3464
+ graph.setEdge(depId, dependentId);
3465
+ added++;
3466
+ this.logger.debug(`Added implicit edge (lambda vpc): ${depId} -> ${dependentId}`);
3467
+ }
3468
+ if (added > 0) {
3469
+ this.logger.debug(`Added ${added} implicit edges for Lambda VpcConfig`);
3470
+ }
3471
+ return added;
3472
+ }
3381
3473
  isCustomResourceType(type) {
3382
3474
  return type === "AWS::CloudFormation::CustomResource" || type.startsWith("Custom::");
3383
3475
  }
@@ -6993,15 +7085,141 @@ var IAMRoleProvider = class {
6993
7085
  }
6994
7086
  };
6995
7087
 
7088
+ // src/deployment/dag-executor.ts
7089
+ var DagExecutor = class {
7090
+ nodes = /* @__PURE__ */ new Map();
7091
+ logger = getLogger().child("DagExecutor");
7092
+ add(node) {
7093
+ this.nodes.set(node.id, node);
7094
+ }
7095
+ has(id) {
7096
+ return this.nodes.has(id);
7097
+ }
7098
+ size() {
7099
+ return this.nodes.size;
7100
+ }
7101
+ values() {
7102
+ return this.nodes.values();
7103
+ }
7104
+ async execute(concurrency, fn, cancelled = () => false) {
7105
+ let active = 0;
7106
+ const errors = [];
7107
+ return new Promise((resolve4, reject) => {
7108
+ const dispatch = () => {
7109
+ let changed = true;
7110
+ while (changed) {
7111
+ changed = false;
7112
+ for (const node of this.nodes.values()) {
7113
+ if (node.state !== "pending")
7114
+ continue;
7115
+ const hasFailedDep = [...node.dependencies].some((depId) => {
7116
+ const dep = this.nodes.get(depId);
7117
+ return dep && (dep.state === "failed" || dep.state === "skipped");
7118
+ });
7119
+ if (hasFailedDep) {
7120
+ node.state = "skipped";
7121
+ changed = true;
7122
+ this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
7123
+ }
7124
+ }
7125
+ }
7126
+ const ready = [];
7127
+ for (const node of this.nodes.values()) {
7128
+ if (node.state !== "pending")
7129
+ continue;
7130
+ const depsReady = [...node.dependencies].every((depId) => {
7131
+ const dep = this.nodes.get(depId);
7132
+ return !dep || dep.state === "completed";
7133
+ });
7134
+ if (depsReady)
7135
+ ready.push(node);
7136
+ }
7137
+ if (!cancelled()) {
7138
+ for (const node of ready) {
7139
+ if (active >= concurrency)
7140
+ break;
7141
+ node.state = "running";
7142
+ active++;
7143
+ fn(node).then(() => {
7144
+ node.state = "completed";
7145
+ }).catch((error) => {
7146
+ node.state = "failed";
7147
+ errors.push({ id: node.id, error });
7148
+ }).finally(() => {
7149
+ active--;
7150
+ dispatch();
7151
+ });
7152
+ }
7153
+ }
7154
+ if (active === 0) {
7155
+ if (errors.length > 0) {
7156
+ reject(errors[0].error);
7157
+ return;
7158
+ }
7159
+ const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
7160
+ if (stillPending && !cancelled()) {
7161
+ const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
7162
+ reject(
7163
+ new Error(
7164
+ `Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
7165
+ )
7166
+ );
7167
+ return;
7168
+ }
7169
+ resolve4();
7170
+ }
7171
+ };
7172
+ dispatch();
7173
+ });
7174
+ }
7175
+ };
7176
+
7177
+ // src/analyzer/implicit-delete-deps.ts
7178
+ var IMPLICIT_DELETE_DEPENDENCIES = {
7179
+ // IGW must be deleted AFTER VPCGatewayAttachment
7180
+ "AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
7181
+ // EventBus must be deleted AFTER Rules on that bus
7182
+ "AWS::Events::EventBus": ["AWS::Events::Rule"],
7183
+ // Athena workgroup must be deleted AFTER its named queries
7184
+ "AWS::Athena::WorkGroup": ["AWS::Athena::NamedQuery"],
7185
+ // CloudFront managed-policy-style resources must be deleted AFTER
7186
+ // any Distribution that references them
7187
+ "AWS::CloudFront::ResponseHeadersPolicy": ["AWS::CloudFront::Distribution"],
7188
+ "AWS::CloudFront::CachePolicy": ["AWS::CloudFront::Distribution"],
7189
+ "AWS::CloudFront::OriginAccessControl": ["AWS::CloudFront::Distribution"],
7190
+ // VPC must be deleted AFTER all VPC-dependent resources
7191
+ "AWS::EC2::VPC": [
7192
+ "AWS::EC2::Subnet",
7193
+ "AWS::EC2::SecurityGroup",
7194
+ "AWS::EC2::InternetGateway",
7195
+ "AWS::EC2::EgressOnlyInternetGateway",
7196
+ "AWS::EC2::VPCGatewayAttachment",
7197
+ "AWS::EC2::RouteTable"
7198
+ ],
7199
+ // Subnet must be deleted AFTER any Lambda that may still hold an ENI
7200
+ // in it. Lambda DELETE returns immediately but the ENI is detached
7201
+ // asynchronously by AWS, so deleting the Subnet first races the detach
7202
+ // and yields "DependencyViolation".
7203
+ "AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation", "AWS::Lambda::Function"],
7204
+ // RouteTable must be deleted AFTER Route and Association
7205
+ "AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
7206
+ // SecurityGroup must be deleted AFTER any Lambda whose ENI is bound
7207
+ // to it (same ENI-detach race as Subnet above).
7208
+ "AWS::EC2::SecurityGroup": [
7209
+ "AWS::EC2::SecurityGroupIngress",
7210
+ "AWS::EC2::SecurityGroupEgress",
7211
+ "AWS::Lambda::Function"
7212
+ ]
7213
+ };
7214
+
6996
7215
  // src/deployment/deploy-engine.ts
6997
- import pLimit from "p-limit";
6998
7216
  var InterruptedError = class extends Error {
6999
7217
  constructor() {
7000
7218
  super("Deployment interrupted by user (Ctrl+C)");
7001
7219
  this.name = "InterruptedError";
7002
7220
  }
7003
7221
  };
7004
- var DeployEngine = class _DeployEngine {
7222
+ var DeployEngine = class {
7005
7223
  constructor(stateBackend, lockManager, dagBuilder, diffCalculator, providerRegistry, options = {}, stackRegion) {
7006
7224
  this.stateBackend = stateBackend;
7007
7225
  this.lockManager = lockManager;
@@ -7127,6 +7345,7 @@ var DeployEngine = class _DeployEngine {
7127
7345
  template,
7128
7346
  currentState,
7129
7347
  changes,
7348
+ dag,
7130
7349
  executionLevels,
7131
7350
  stackName,
7132
7351
  parameterValues,
@@ -7159,13 +7378,16 @@ var DeployEngine = class _DeployEngine {
7159
7378
  }
7160
7379
  }
7161
7380
  /**
7162
- * Execute deployment by processing resources in DAG order
7381
+ * Execute deployment by processing resources via event-driven DAG dispatch.
7163
7382
  *
7164
- * Important: DELETE operations are executed in reverse dependency order,
7165
- * while CREATE/UPDATE follow normal dependency order.
7166
- */
7167
- async executeDeployment(template, currentState, changes, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
7168
- const limit = pLimit(this.options.concurrency);
7383
+ * - CREATE/UPDATE follow forward dependency order (a node starts as soon as
7384
+ * ALL of its dependencies are completed — does not wait for unrelated
7385
+ * siblings in the same "level")
7386
+ * - DELETE follows reverse dependency order (a node starts as soon as all
7387
+ * resources that depend ON it have finished deleting)
7388
+ */
7389
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
7390
+ const concurrency = this.options.concurrency;
7169
7391
  const newResources = { ...currentState.resources };
7170
7392
  const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
7171
7393
  const completedOperations = [];
@@ -7196,32 +7418,36 @@ var DeployEngine = class _DeployEngine {
7196
7418
  Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
7197
7419
  );
7198
7420
  try {
7199
- for (let levelIndex = 0; levelIndex < executionLevels.length; levelIndex++) {
7200
- if (this.interrupted) {
7201
- throw new InterruptedError();
7202
- }
7203
- const levelNodes = executionLevels[levelIndex];
7204
- if (!levelNodes)
7421
+ const createUpdateIds = [];
7422
+ for (const [id, change] of changes.entries()) {
7423
+ if (deleteChanges.has(id))
7205
7424
  continue;
7206
- const level = levelNodes.filter((id) => {
7207
- if (deleteChanges.has(id))
7208
- return false;
7209
- const change = changes.get(id);
7210
- return !!change && change.changeType !== "NO_CHANGE";
7211
- });
7212
- if (level.length === 0)
7425
+ if (change.changeType === "NO_CHANGE")
7213
7426
  continue;
7427
+ createUpdateIds.push(id);
7428
+ }
7429
+ if (createUpdateIds.length > 0) {
7214
7430
  this.logger.info(
7215
- `Level ${levelIndex + 1}/${executionLevels.length} (${level.length} resources)`
7431
+ `Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
7216
7432
  );
7217
- const results = await Promise.allSettled(
7218
- level.map(
7219
- (logicalId) => limit(async () => {
7220
- const change = changes.get(logicalId);
7221
- if (!change || change.changeType === "NO_CHANGE") {
7222
- this.logger.debug(`Skipping ${logicalId} (no change)`);
7223
- return;
7224
- }
7433
+ const createUpdateExecutor = new DagExecutor();
7434
+ const provisionable = new Set(createUpdateIds);
7435
+ for (const id of createUpdateIds) {
7436
+ const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
7437
+ const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
7438
+ createUpdateExecutor.add({
7439
+ id,
7440
+ dependencies: deps,
7441
+ state: "pending",
7442
+ data: changes.get(id)
7443
+ });
7444
+ }
7445
+ try {
7446
+ await createUpdateExecutor.execute(
7447
+ concurrency,
7448
+ async (node) => {
7449
+ const logicalId = node.id;
7450
+ const change = node.data;
7225
7451
  const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7226
7452
  try {
7227
7453
  await this.provisionResource(
@@ -7248,30 +7474,36 @@ var DeployEngine = class _DeployEngine {
7248
7474
  properties: newResources[logicalId]?.properties
7249
7475
  });
7250
7476
  saveStateAfterResource(logicalId);
7251
- })
7252
- )
7253
- );
7254
- await saveChain;
7255
- const failures = results.filter((r) => r.status === "rejected");
7256
- if (failures.length > 0) {
7257
- throw failures[0].reason;
7477
+ },
7478
+ () => this.interrupted
7479
+ );
7480
+ } finally {
7481
+ await saveChain;
7482
+ }
7483
+ if (this.interrupted && this.hasPending(createUpdateExecutor)) {
7484
+ throw new InterruptedError();
7258
7485
  }
7259
7486
  }
7260
7487
  if (deleteChanges.size > 0) {
7261
7488
  this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
7262
- const deletionLevels = this.buildDeletionLevels(deleteChanges, currentState);
7263
- for (let levelIndex = 0; levelIndex < deletionLevels.length; levelIndex++) {
7264
- if (this.interrupted) {
7265
- throw new InterruptedError();
7266
- }
7267
- const level = deletionLevels[levelIndex];
7268
- if (level.length === 0)
7269
- continue;
7270
- const deleteResults = await Promise.allSettled(
7271
- level.map(
7272
- (logicalId) => limit(async () => {
7273
- const change = changes.get(logicalId);
7274
- const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7489
+ const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
7490
+ const deleteExecutor = new DagExecutor();
7491
+ for (const id of deleteChanges) {
7492
+ deleteExecutor.add({
7493
+ id,
7494
+ dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
7495
+ state: "pending",
7496
+ data: changes.get(id)
7497
+ });
7498
+ }
7499
+ try {
7500
+ await deleteExecutor.execute(
7501
+ concurrency,
7502
+ async (node) => {
7503
+ const logicalId = node.id;
7504
+ const change = node.data;
7505
+ const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7506
+ try {
7275
7507
  await this.provisionResource(
7276
7508
  logicalId,
7277
7509
  change,
@@ -7283,23 +7515,25 @@ var DeployEngine = class _DeployEngine {
7283
7515
  actualCounts,
7284
7516
  progress
7285
7517
  );
7286
- completedOperations.push({
7287
- logicalId,
7288
- changeType: "DELETE",
7289
- resourceType: change.resourceType,
7290
- previousState
7291
- });
7292
- saveStateAfterResource(logicalId);
7293
- })
7294
- )
7518
+ } catch (provisionError) {
7519
+ this.interrupted = true;
7520
+ throw provisionError;
7521
+ }
7522
+ completedOperations.push({
7523
+ logicalId,
7524
+ changeType: "DELETE",
7525
+ resourceType: change.resourceType,
7526
+ previousState
7527
+ });
7528
+ saveStateAfterResource(logicalId);
7529
+ },
7530
+ () => this.interrupted
7295
7531
  );
7532
+ } finally {
7296
7533
  await saveChain;
7297
- const deleteFailures = deleteResults.filter(
7298
- (r) => r.status === "rejected"
7299
- );
7300
- if (deleteFailures.length > 0) {
7301
- throw deleteFailures[0].reason;
7302
- }
7534
+ }
7535
+ if (this.interrupted && this.hasPending(deleteExecutor)) {
7536
+ throw new InterruptedError();
7303
7537
  }
7304
7538
  }
7305
7539
  } catch (error) {
@@ -7393,12 +7627,12 @@ var DeployEngine = class _DeployEngine {
7393
7627
  * - UPDATE → update back to previous properties
7394
7628
  * - DELETE → cannot rollback (resource already deleted), log warning
7395
7629
  *
7396
- * Resources created in the same DAG level may have dependencies between them
7397
- * (e.g., IAM Policy depends on IAM Role). When rolling back CREATEs (deleting),
7398
- * dependent resources must be deleted before their dependencies. This method
7399
- * sorts CREATE rollback operations using dependency information from state,
7400
- * then processes UPDATE/DELETE rollbacks, and finally processes sorted CREATE
7401
- * rollback deletions.
7630
+ * Resources completed concurrently in the dispatcher may have dependencies
7631
+ * between them (e.g., IAM Policy depends on IAM Role). When rolling back
7632
+ * CREATEs (deleting), dependent resources must be deleted before their
7633
+ * dependencies. This method sorts CREATE rollback operations using dependency
7634
+ * information from state, then processes UPDATE/DELETE rollbacks, and finally
7635
+ * processes sorted CREATE rollback deletions.
7402
7636
  */
7403
7637
  async performRollback(completedOperations, stateResources, _stackName) {
7404
7638
  if (completedOperations.length === 0) {
@@ -7431,7 +7665,7 @@ var DeployEngine = class _DeployEngine {
7431
7665
  * Sort CREATE rollback operations so that resources depending on others
7432
7666
  * are deleted first (reverse dependency order).
7433
7667
  *
7434
- * Uses state dependencies to build deletion levels, similar to buildDeletionLevels.
7668
+ * Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
7435
7669
  */
7436
7670
  sortRollbackCreates(createOps, stateResources) {
7437
7671
  const opMap = /* @__PURE__ */ new Map();
@@ -7822,48 +8056,34 @@ var DeployEngine = class _DeployEngine {
7822
8056
  const deps = parser.extractDependencies(resource);
7823
8057
  return deps.size > 0 ? [...deps] : void 0;
7824
8058
  }
8059
+ // Type-based implicit deletion ordering rules are defined in
8060
+ // src/analyzer/implicit-delete-deps.ts so the deploy DELETE phase and the
8061
+ // standalone destroy command apply the same rules.
7825
8062
  /**
7826
- * Implicit dependency map for correct deletion order.
8063
+ * Build a per-resource map of "must be deleted before me" dependencies for
8064
+ * the DELETE phase, derived from state-recorded dependencies plus implicit
8065
+ * type-based ordering rules.
7827
8066
  *
7828
- * Key = resource type that must be deleted AFTER all value types are deleted.
7829
- * Value = resource types that must be deleted BEFORE the key type.
7830
- *
7831
- * Example: InternetGateway depends on VPCGatewayAttachment being deleted first,
7832
- * because AWS won't let you delete an IGW while it's still attached to a VPC.
7833
- */
7834
- static IMPLICIT_DELETE_DEPENDENCIES = {
7835
- // IGW must be deleted AFTER VPCGatewayAttachment
7836
- "AWS::EC2::InternetGateway": ["AWS::EC2::VPCGatewayAttachment"],
7837
- // EventBus must be deleted AFTER Rules on that bus
7838
- "AWS::Events::EventBus": ["AWS::Events::Rule"],
7839
- // VPC must be deleted AFTER all VPC-dependent resources
7840
- "AWS::EC2::VPC": [
7841
- "AWS::EC2::Subnet",
7842
- "AWS::EC2::SecurityGroup",
7843
- "AWS::EC2::InternetGateway",
7844
- "AWS::EC2::EgressOnlyInternetGateway",
7845
- "AWS::EC2::VPCGatewayAttachment",
7846
- "AWS::EC2::RouteTable"
7847
- ],
7848
- // Subnet must be deleted AFTER RouteTableAssociation
7849
- "AWS::EC2::Subnet": ["AWS::EC2::SubnetRouteTableAssociation"],
7850
- // RouteTable must be deleted AFTER Route and Association
7851
- "AWS::EC2::RouteTable": ["AWS::EC2::Route", "AWS::EC2::SubnetRouteTableAssociation"],
7852
- // SecurityGroup must be deleted AFTER SecurityGroupIngress/Egress
7853
- "AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
7854
- };
8067
+ * For a resource X, the returned set contains every resource Y such that Y
8068
+ * must finish deleting before X starts i.e., Y depends on X (or is otherwise
8069
+ * required to vanish first per implicit type rules).
8070
+ */
7855
8071
  /**
7856
- * Build deletion levels from state dependencies (reverse topological order).
7857
- * Resources that are depended upon by others are deleted LAST.
8072
+ * Returns true if the executor still has un-started pending nodes —
8073
+ * used to distinguish "SIGINT cancelled real work" from "SIGINT landed
8074
+ * after all nodes already completed" (the latter should not error).
7858
8075
  */
7859
- buildDeletionLevels(deleteIds, state) {
8076
+ hasPending(executor) {
8077
+ for (const node of executor.values()) {
8078
+ if (node.state === "pending")
8079
+ return true;
8080
+ }
8081
+ return false;
8082
+ }
8083
+ buildDeletionDependencies(deleteIds, state) {
7860
8084
  const dependedBy = /* @__PURE__ */ new Map();
7861
- const inDegree = /* @__PURE__ */ new Map();
7862
8085
  for (const id of deleteIds) {
7863
- if (!dependedBy.has(id))
7864
- dependedBy.set(id, /* @__PURE__ */ new Set());
7865
- if (!inDegree.has(id))
7866
- inDegree.set(id, 0);
8086
+ dependedBy.set(id, /* @__PURE__ */ new Set());
7867
8087
  }
7868
8088
  for (const id of deleteIds) {
7869
8089
  const resource = state.resources[id];
@@ -7872,38 +8092,11 @@ var DeployEngine = class _DeployEngine {
7872
8092
  for (const dep of resource.dependencies) {
7873
8093
  if (!deleteIds.has(dep))
7874
8094
  continue;
7875
- if (!dependedBy.has(dep))
7876
- dependedBy.set(dep, /* @__PURE__ */ new Set());
7877
8095
  dependedBy.get(dep).add(id);
7878
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
7879
8096
  }
7880
8097
  }
7881
8098
  this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
7882
- const levels = [];
7883
- let remaining = new Set(deleteIds);
7884
- while (remaining.size > 0) {
7885
- const level = [];
7886
- for (const id of remaining) {
7887
- const dependents = dependedBy.get(id);
7888
- const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
7889
- if (!hasPendingDependents) {
7890
- level.push(id);
7891
- }
7892
- }
7893
- if (level.length === 0) {
7894
- this.logger.warn(
7895
- `Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
7896
- );
7897
- levels.push([...remaining]);
7898
- break;
7899
- }
7900
- levels.push(level);
7901
- remaining = new Set([...remaining].filter((id) => !level.includes(id)));
7902
- }
7903
- this.logger.debug(
7904
- `Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
7905
- );
7906
- return levels;
8099
+ return dependedBy;
7907
8100
  }
7908
8101
  /**
7909
8102
  * Add implicit delete dependency edges based on resource type relationships.
@@ -7931,7 +8124,7 @@ var DeployEngine = class _DeployEngine {
7931
8124
  const resource = state.resources[id];
7932
8125
  if (!resource)
7933
8126
  continue;
7934
- const mustDeleteAfter = _DeployEngine.IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
8127
+ const mustDeleteAfter = IMPLICIT_DELETE_DEPENDENCIES[resource.resourceType];
7935
8128
  if (!mustDeleteAfter)
7936
8129
  continue;
7937
8130
  for (const depType of mustDeleteAfter) {