alchemy 2.0.0-beta.1 → 2.0.0-beta.2

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.
Files changed (129) hide show
  1. package/bin/alchemy-effect.js +31 -16
  2. package/bin/alchemy-effect.js.map +1 -1
  3. package/bin/alchemy-effect.sh +2 -3
  4. package/bin/alchemy-effect.ts +3 -3
  5. package/lib/cli/index.d.ts +39 -36
  6. package/lib/cli/index.d.ts.map +1 -1
  7. package/package.json +1 -1
  8. package/src/AWS/ACM/Certificate.ts +49 -46
  9. package/src/AWS/AutoScaling/AutoScalingGroup.ts +4 -1
  10. package/src/AWS/AutoScaling/LaunchTemplate.ts +3 -1
  11. package/src/AWS/AutoScaling/ScalingPolicy.ts +4 -1
  12. package/src/AWS/CloudFront/Distribution.ts +4 -1
  13. package/src/AWS/CloudFront/Function.ts +4 -1
  14. package/src/AWS/CloudFront/Invalidation.ts +4 -1
  15. package/src/AWS/CloudFront/KeyValueStore.ts +4 -1
  16. package/src/AWS/CloudFront/KvEntries.ts +4 -1
  17. package/src/AWS/CloudFront/KvRoutesUpdate.ts +4 -1
  18. package/src/AWS/CloudFront/OriginAccessControl.ts +4 -1
  19. package/src/AWS/CloudWatch/Alarm.ts +4 -1
  20. package/src/AWS/CloudWatch/AlarmMuteRule.ts +4 -1
  21. package/src/AWS/CloudWatch/AnomalyDetector.ts +4 -1
  22. package/src/AWS/CloudWatch/CompositeAlarm.ts +4 -1
  23. package/src/AWS/CloudWatch/Dashboard.ts +4 -1
  24. package/src/AWS/CloudWatch/InsightRule.ts +4 -1
  25. package/src/AWS/CloudWatch/MetricStream.ts +4 -1
  26. package/src/AWS/DynamoDB/Table.ts +3 -1
  27. package/src/AWS/EC2/EIP.ts +4 -1
  28. package/src/AWS/EC2/EgressOnlyInternetGateway.ts +4 -1
  29. package/src/AWS/EC2/Instance.ts +3 -1
  30. package/src/AWS/EC2/InternetGateway.ts +4 -1
  31. package/src/AWS/EC2/NatGateway.ts +4 -1
  32. package/src/AWS/EC2/NetworkAcl.ts +4 -1
  33. package/src/AWS/EC2/NetworkAclAssociation.ts +4 -1
  34. package/src/AWS/EC2/NetworkAclEntry.ts +4 -1
  35. package/src/AWS/EC2/Route.ts +4 -1
  36. package/src/AWS/EC2/RouteTable.ts +4 -1
  37. package/src/AWS/EC2/RouteTableAssociation.ts +4 -1
  38. package/src/AWS/EC2/SecurityGroup.ts +4 -1
  39. package/src/AWS/EC2/SecurityGroupRule.ts +4 -1
  40. package/src/AWS/EC2/Subnet.ts +4 -1
  41. package/src/AWS/EC2/Vpc.ts +4 -1
  42. package/src/AWS/EC2/VpcEndpoint.ts +4 -1
  43. package/src/AWS/ECR/Repository.ts +4 -1
  44. package/src/AWS/ECS/Cluster.ts +4 -1
  45. package/src/AWS/ECS/Service.ts +4 -1
  46. package/src/AWS/ECS/Task.ts +3 -1
  47. package/src/AWS/EKS/AccessEntry.ts +4 -1
  48. package/src/AWS/EKS/Addon.ts +4 -1
  49. package/src/AWS/EKS/Cluster.ts +3 -1
  50. package/src/AWS/EKS/PodIdentityAssociation.ts +4 -1
  51. package/src/AWS/ELBv2/Listener.ts +4 -1
  52. package/src/AWS/ELBv2/LoadBalancer.ts +4 -1
  53. package/src/AWS/ELBv2/TargetGroup.ts +4 -1
  54. package/src/AWS/EventBridge/EventBus.ts +4 -1
  55. package/src/AWS/EventBridge/Permission.ts +4 -1
  56. package/src/AWS/EventBridge/Rule.ts +4 -1
  57. package/src/AWS/IAM/AccessKey.ts +4 -1
  58. package/src/AWS/IAM/AccountAlias.ts +4 -1
  59. package/src/AWS/IAM/AccountPasswordPolicy.ts +4 -1
  60. package/src/AWS/IAM/Group.ts +4 -1
  61. package/src/AWS/IAM/GroupMembership.ts +4 -1
  62. package/src/AWS/IAM/InstanceProfile.ts +4 -1
  63. package/src/AWS/IAM/LoginProfile.ts +4 -1
  64. package/src/AWS/IAM/OpenIDConnectProvider.ts +4 -1
  65. package/src/AWS/IAM/Policy.ts +4 -1
  66. package/src/AWS/IAM/Role.ts +4 -1
  67. package/src/AWS/IAM/SAMLProvider.ts +4 -1
  68. package/src/AWS/IAM/SSHPublicKey.ts +4 -1
  69. package/src/AWS/IAM/ServerCertificate.ts +4 -1
  70. package/src/AWS/IAM/ServiceSpecificCredential.ts +4 -1
  71. package/src/AWS/IAM/SigningCertificate.ts +4 -1
  72. package/src/AWS/IAM/User.ts +4 -1
  73. package/src/AWS/IAM/VirtualMFADevice.ts +4 -1
  74. package/src/AWS/IdentityCenter/AccountAssignment.ts +4 -1
  75. package/src/AWS/IdentityCenter/Group.ts +4 -1
  76. package/src/AWS/IdentityCenter/Instance.ts +4 -1
  77. package/src/AWS/IdentityCenter/PermissionSet.ts +4 -1
  78. package/src/AWS/Kinesis/Stream.ts +4 -1
  79. package/src/AWS/Kinesis/StreamConsumer.ts +4 -1
  80. package/src/AWS/Lambda/EventSourceMapping.ts +4 -1
  81. package/src/AWS/Lambda/Function.ts +3 -1
  82. package/src/AWS/Lambda/Permission.ts +4 -1
  83. package/src/AWS/Logs/LogGroup.ts +4 -1
  84. package/src/AWS/Organizations/Account.ts +4 -1
  85. package/src/AWS/Organizations/DelegatedAdministrator.ts +4 -1
  86. package/src/AWS/Organizations/Organization.ts +4 -1
  87. package/src/AWS/Organizations/OrganizationResourcePolicy.ts +4 -1
  88. package/src/AWS/Organizations/OrganizationalUnit.ts +4 -1
  89. package/src/AWS/Organizations/Policy.ts +4 -1
  90. package/src/AWS/Organizations/PolicyAttachment.ts +4 -1
  91. package/src/AWS/Organizations/Root.ts +4 -1
  92. package/src/AWS/Organizations/RootPolicyType.ts +4 -1
  93. package/src/AWS/Organizations/TrustedServiceAccess.ts +4 -1
  94. package/src/AWS/Providers.ts +484 -438
  95. package/src/AWS/RDS/DBCluster.ts +4 -1
  96. package/src/AWS/RDS/DBClusterEndpoint.ts +4 -1
  97. package/src/AWS/RDS/DBClusterParameterGroup.ts +4 -1
  98. package/src/AWS/RDS/DBInstance.ts +4 -1
  99. package/src/AWS/RDS/DBParameterGroup.ts +4 -1
  100. package/src/AWS/RDS/DBProxy.ts +4 -1
  101. package/src/AWS/RDS/DBProxyEndpoint.ts +4 -1
  102. package/src/AWS/RDS/DBProxyTargetGroup.ts +4 -1
  103. package/src/AWS/RDS/DBSubnetGroup.ts +4 -1
  104. package/src/AWS/Route53/Record.ts +4 -1
  105. package/src/AWS/S3/Bucket.ts +3 -1
  106. package/src/AWS/SNS/Subscription.ts +4 -1
  107. package/src/AWS/SNS/Topic.ts +4 -1
  108. package/src/AWS/SQS/Queue.ts +3 -1
  109. package/src/AWS/Scheduler/Schedule.ts +4 -1
  110. package/src/AWS/Scheduler/ScheduleGroup.ts +4 -1
  111. package/src/AWS/SecretsManager/Secret.ts +4 -1
  112. package/src/AWS/Website/AssetDeployment.ts +4 -1
  113. package/src/Apply.ts +3 -3
  114. package/src/Binding.ts +18 -17
  115. package/src/Cloudflare/Container/ContainerApplication.ts +3 -1
  116. package/src/Cloudflare/D1/D1Database.ts +4 -1
  117. package/src/Cloudflare/KV/KVNamespace.ts +7 -4
  118. package/src/Cloudflare/Providers.ts +62 -60
  119. package/src/Cloudflare/R2/R2Bucket.ts +4 -1
  120. package/src/Cloudflare/R2/R2BucketBinding.ts +26 -31
  121. package/src/Cloudflare/Workers/Worker.ts +3 -1
  122. package/src/Cloudflare/Workers/Workflow.ts +3 -1
  123. package/src/Plan.ts +595 -596
  124. package/src/Platform.ts +9 -8
  125. package/src/Provider.ts +145 -23
  126. package/src/Resource.ts +10 -3
  127. package/src/Stack.ts +1 -1
  128. package/src/Util/service.ts +1 -1
  129. package/src/index.ts +10 -7
package/src/Plan.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as Data from "effect/Data";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as Option from "effect/Option";
3
4
  import { asEffect } from ".//Util/types.ts";
4
5
  import {
5
6
  Artifacts,
@@ -8,7 +9,6 @@ import {
8
9
  ensureArtifactStore,
9
10
  makeScopedArtifacts,
10
11
  } from "./Artifacts.ts";
11
- import * as Option from "effect/Option";
12
12
  import {
13
13
  diffBindings,
14
14
  havePropsChanged,
@@ -19,7 +19,7 @@ import { parseFqn } from "./FQN.ts";
19
19
  import { InstanceId } from "./InstanceId.ts";
20
20
  import * as Output from "./Output.ts";
21
21
  import {
22
- getProviderByType,
22
+ findProviderByType,
23
23
  Provider,
24
24
  type ProviderService,
25
25
  } from "./Provider.ts";
@@ -157,653 +157,652 @@ export const make = <A>(
157
157
  options: MakePlanOptions = {},
158
158
  ): Effect.Effect<Plan<A>, never, State> =>
159
159
  // @ts-expect-error
160
- ensureArtifactStore(
161
- Effect.gen(function* () {
162
- const state = yield* State;
163
-
164
- const resources = Object.values(stack.resources);
165
-
166
- // TODO(sam): rename terminology to Stack
167
- const stackName = stack.name;
168
- const stage = stack.stage;
169
-
170
- const resourceFqns = yield* state.list({
171
- stack: stackName,
172
- stage: stage,
173
- });
174
- const oldResources = yield* Effect.all(
175
- resourceFqns.map((fqn) =>
176
- state.get({ stack: stackName, stage: stage, fqn }),
177
- ),
178
- { concurrency: "unbounded" },
179
- );
180
-
181
- const resolvedResources: Record<string, Effect.Effect<any>> = {};
182
-
183
- const resolveResource = (
184
- resourceExpr: Output.ResourceExpr<any, any>,
185
- ): Effect.Effect<any> =>
186
- Effect.gen(function* () {
187
- // @ts-expect-error
188
- return yield* (resolvedResources[resourceExpr.src.FQN] ??=
189
- yield* Effect.cached(
190
- Effect.gen(function* () {
191
- const resource = resourceExpr.src;
192
-
193
- const provider = yield* Provider(resource.Type);
194
- const props = yield* resolveInput(resource.Props);
195
- const oldState = yield* state.get({
196
- stack: stackName,
197
- stage: stage,
198
- fqn: resource.FQN,
199
- });
200
-
201
- if (!oldState || oldState.status === "creating") {
202
- return resourceExpr;
203
- }
204
-
205
- const oldProps =
206
- oldState.status === "updating"
207
- ? oldState.old.props
208
- : oldState.props;
209
-
210
- const oldBindings = oldState.bindings ?? [];
211
- const newBindings = stack.bindings[resource.FQN] ?? [];
212
-
213
- const diff = yield* provider.diff
214
- ? provider
215
- .diff({
216
- id: resource.LogicalId,
217
- olds: oldProps,
218
- instanceId: oldState.instanceId,
219
- news: props,
220
- output: oldState.attr,
221
- oldBindings,
222
- newBindings,
223
- })
224
- .pipe(providePlanScope(resource.FQN, oldState.instanceId))
225
- : Effect.succeed(undefined);
226
-
227
- const stables: string[] = [
228
- ...(provider.stables ?? []),
229
- ...(diff?.stables ?? []),
230
- ];
231
-
232
- const withStables = (output: any) =>
233
- stables.length > 0
234
- ? new Output.ResourceExpr(
235
- resourceExpr.src,
236
- Object.fromEntries(
237
- stables.map((stable) => [stable, output?.[stable]]),
238
- ),
239
- )
240
- : // if there are no stable properties, treat every property as changed
241
- resourceExpr;
242
-
243
- if (diff == null) {
244
- if (havePropsChanged(oldProps, props)) {
245
- // the props have changed but the provider did not provide any hints as to what is stable
246
- // so we must assume everything has changed
247
- return withStables(oldState?.attr);
248
- }
249
- } else if (diff.action === "update") {
250
- return withStables(oldState?.attr);
251
- } else if (diff.action === "replace") {
252
- return resourceExpr;
253
- }
254
- if (
255
- oldState.status === "created" ||
256
- oldState.status === "updated" ||
257
- oldState.status === "replaced"
258
- ) {
259
- // we can safely return the attributes if we know they have stabilized
260
- return oldState?.attr;
261
- } else {
262
- // we must assume the resource doesn't exist if it hasn't stabilized
263
- return resourceExpr;
264
- }
265
- }),
266
- ));
267
- });
268
-
269
- const resolveInput = (input: any): Effect.Effect<any> =>
270
- Effect.gen(function* () {
271
- if (!input) {
272
- return input;
273
- } else if (Output.isExpr(input)) {
274
- return yield* resolveOutput(input);
275
- } else if (Array.isArray(input)) {
276
- return yield* Effect.all(input.map(resolveInput), {
277
- concurrency: "unbounded",
278
- });
279
- } else if (isResource(input)) {
280
- // Resource objects have dynamic properties (path, hash, etc.) that are
281
- // created on-demand by a Proxy getter and aren't enumerable via Object.entries.
282
- // Resolve the ResourceExpr to get the actual resource output, then continue
283
- // resolving any nested outputs in the result.
284
- const resourceExpr = Output.of(input);
285
- const resolved = yield* resolveOutput(resourceExpr);
286
- return yield* resolveInput(resolved);
287
- } else if (typeof input === "object") {
288
- return Object.fromEntries(
289
- yield* Effect.all(
290
- Object.entries(input).map(([key, value]) =>
291
- resolveInput(value).pipe(Effect.map((value) => [key, value])),
292
- ),
293
- { concurrency: "unbounded" },
294
- ),
295
- );
296
- }
297
- return input;
298
- });
299
-
300
- const resolveOutput = (expr: Output.Expr<any>): Effect.Effect<any> =>
301
- Effect.gen(function* () {
302
- if (Output.isResourceExpr(expr)) {
303
- return yield* resolveResource(expr);
304
- } else if (Output.isPropExpr(expr)) {
305
- const upstream = yield* resolveOutput(expr.expr);
306
- return upstream?.[expr.identifier];
307
- } else if (Output.isApplyExpr(expr)) {
308
- const upstream = yield* resolveOutput(expr.expr);
309
- return Output.hasOutputs(upstream) ? expr : expr.f(upstream);
310
- } else if (Output.isEffectExpr(expr)) {
311
- const upstream = yield* resolveOutput(expr.expr);
312
- return Output.hasOutputs(upstream) ? expr : yield* expr.f(upstream);
313
- } else if (Output.isAllExpr(expr)) {
314
- return yield* Effect.all(expr.outs.map(resolveOutput), {
315
- concurrency: "unbounded",
316
- });
317
- } else if (Output.isLiteralExpr(expr)) {
318
- return expr.value;
319
- }
320
- return yield* Effect.die(new Error("Not implemented yet"));
321
- });
322
-
323
- // map of resource FQN -> its downstream dependencies (resources that depend on it)
324
- const oldDownstreamDependencies: {
325
- [fqn: string]: string[];
326
- } = Object.fromEntries(
327
- oldResources
328
- .filter((resource) => !!resource)
329
- .map((resource) => [resource.fqn, resource.downstream]),
330
- );
331
-
332
- // Build a set of FQNs for the new resources to detect orphans
333
- const newResourceFqns = new Set(resources.map((r) => r.FQN));
334
-
335
- // Map FQN -> list of upstream FQNs (resources this one depends on via props)
336
- const newUpstreamDependencies: {
337
- [fqn: string]: string[];
338
- } = Object.fromEntries(
339
- resources.map((resource) => [
340
- resource.FQN,
341
- Object.values(Output.upstreamAny(resource.Props)).map((r) => r.FQN),
342
- ]),
343
- );
344
-
345
- // Map FQN -> list of upstream FQNs from bindings
346
- const bindingUpstreamDependencies: {
347
- [fqn: string]: string[];
348
- } = Object.fromEntries(
349
- resources.map((resource) => [
350
- resource.FQN,
351
- Object.values(
352
- Output.upstreamAny(stack.bindings[resource.FQN] ?? []),
353
- ).map((r) => r.FQN),
354
- ]),
355
- );
356
-
357
- // Combined prop + binding upstream for the desired graph, including
358
- // references to resources outside the current graph so delete validation can
359
- // tell whether any surviving resource still points at an orphan.
360
- const rawUpstreamDependencies: {
361
- [fqn: string]: string[];
362
- } = Object.fromEntries(
363
- resources.map((resource) => {
364
- const fqn = resource.FQN;
365
- const propDeps = newUpstreamDependencies[fqn] ?? [];
366
- const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
367
- return [fqn, [...new Set([...propDeps, ...bindDeps])]];
368
- }),
369
- );
370
-
371
- // Combined prop + binding upstream, filtered to resources in this graph for
372
- // scheduling and cycle detection.
373
- const allUpstreamDependencies: {
374
- [fqn: string]: string[];
375
- } = Object.fromEntries(
376
- resources.map((resource) => {
377
- const fqn = resource.FQN;
378
- const deps = rawUpstreamDependencies[fqn] ?? [];
379
- return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
380
- }),
381
- );
382
-
383
- // Map FQN -> list of downstream FQNs (resources that depend on this one)
384
- const newDownstreamDependencies: {
385
- [fqn: string]: string[];
386
- } = Object.fromEntries(
387
- resources.map((resource) => [
388
- resource.FQN,
389
- Object.entries(newUpstreamDependencies)
390
- .filter(([_, upstream]) => upstream.includes(resource.FQN))
391
- .map(([depFqn]) => depFqn),
392
- ]),
393
- );
394
-
395
- const resourceGraph = Object.fromEntries(
396
- (yield* Effect.all(
397
- resources.map(
398
- Effect.fn(function* (resource) {
399
- const provider = yield* Provider(resource.Type);
400
- const id = resource.LogicalId;
401
- const fqn = resource.FQN;
402
- const news = yield* resolveInput(resource.Props);
403
- const downstream = newDownstreamDependencies[fqn] ?? [];
404
-
405
- const newBindings: ResourceBinding[] = yield* resolveInput(
406
- stack.bindings[fqn] ?? [],
407
- );
160
+ Effect.gen(function* () {
161
+ const state = yield* State;
162
+
163
+ const resources = Object.values(stack.resources);
164
+
165
+ // TODO(sam): rename terminology to Stack
166
+ const stackName = stack.name;
167
+ const stage = stack.stage;
168
+
169
+ const resourceFqns = yield* state.list({
170
+ stack: stackName,
171
+ stage: stage,
172
+ });
173
+ const oldResources = yield* Effect.all(
174
+ resourceFqns.map((fqn) =>
175
+ state.get({ stack: stackName, stage: stage, fqn }),
176
+ ),
177
+ { concurrency: "unbounded" },
178
+ );
179
+
180
+ const resolvedResources: Record<string, Effect.Effect<any>> = {};
181
+
182
+ const resolveResource = (
183
+ resourceExpr: Output.ResourceExpr<any, any>,
184
+ ): Effect.Effect<any> =>
185
+ Effect.gen(function* () {
186
+ // @ts-expect-error
187
+ return yield* (resolvedResources[resourceExpr.src.FQN] ??=
188
+ yield* Effect.cached(
189
+ Effect.gen(function* () {
190
+ const resource = resourceExpr.src;
191
+
192
+ const provider = yield* findProviderByType(resource.Type);
193
+ const props = yield* resolveInput(resource.Props);
408
194
  const oldState = yield* state.get({
409
195
  stack: stackName,
410
196
  stage: stage,
411
- fqn,
197
+ fqn: resource.FQN,
412
198
  });
413
- const oldBindings = oldState?.bindings ?? [];
414
- const bindingDiffs = diffBindings(oldBindings, newBindings);
415
-
416
- const Node = <T extends Apply>(
417
- node: Omit<
418
- T,
419
- "provider" | "resource" | "bindings" | "downstream"
420
- >,
421
- ) =>
422
- ({
423
- ...node,
424
- provider,
425
- resource,
426
- bindings: bindingDiffs,
427
- downstream,
428
- }) as any as T;
429
-
430
- // Plan against the persisted state we have, not the ideal final state we
431
- // hoped to reach last time. Recovery is expressed by mapping each
432
- // intermediate state back onto a fresh CRUD action.
433
- if (oldState === undefined) {
434
- return Node<Create>({
435
- action: "create",
436
- props: news,
437
- state: oldState,
438
- });
439
- } else if (
440
- oldState.status === "creating" &&
441
- oldState.attr === undefined
442
- ) {
443
- // A create may have succeeded before state persistence failed. If the
444
- // provider can recover an attribute snapshot, keep driving the same
445
- // create instead of starting over blindly.
446
- if (provider.read) {
447
- const attr = yield* provider
448
- .read({
449
- id,
199
+
200
+ if (!oldState || oldState.status === "creating") {
201
+ return resourceExpr;
202
+ }
203
+
204
+ const oldProps =
205
+ oldState.status === "updating"
206
+ ? oldState.old.props
207
+ : oldState.props;
208
+
209
+ const oldBindings = oldState.bindings ?? [];
210
+ const newBindings = stack.bindings[resource.FQN] ?? [];
211
+
212
+ const diff = yield* provider.diff
213
+ ? provider
214
+ .diff({
215
+ id: resource.LogicalId,
216
+ olds: oldProps,
450
217
  instanceId: oldState.instanceId,
451
- olds: oldState.props,
218
+ news: props,
452
219
  output: oldState.attr,
220
+ oldBindings,
221
+ newBindings,
453
222
  })
454
- .pipe(providePlanScope(fqn, oldState.instanceId));
455
- if (attr) {
456
- return Node<Create>({
457
- action: "create",
458
- props: news,
459
- state: { ...oldState, attr },
460
- });
461
- }
223
+ .pipe(providePlanScope(resource.FQN, oldState.instanceId))
224
+ : Effect.succeed(undefined);
225
+
226
+ const stables: string[] = [
227
+ ...(provider.stables ?? []),
228
+ ...(diff?.stables ?? []),
229
+ ];
230
+
231
+ const withStables = (output: any) =>
232
+ stables.length > 0
233
+ ? new Output.ResourceExpr(
234
+ resourceExpr.src,
235
+ Object.fromEntries(
236
+ stables.map((stable) => [stable, output?.[stable]]),
237
+ ),
238
+ )
239
+ : // if there are no stable properties, treat every property as changed
240
+ resourceExpr;
241
+
242
+ if (diff == null) {
243
+ if (havePropsChanged(oldProps, props)) {
244
+ // the props have changed but the provider did not provide any hints as to what is stable
245
+ // so we must assume everything has changed
246
+ return withStables(oldState?.attr);
462
247
  }
248
+ } else if (diff.action === "update") {
249
+ return withStables(oldState?.attr);
250
+ } else if (diff.action === "replace") {
251
+ return resourceExpr;
252
+ }
253
+ if (
254
+ oldState.status === "created" ||
255
+ oldState.status === "updated" ||
256
+ oldState.status === "replaced"
257
+ ) {
258
+ // we can safely return the attributes if we know they have stabilized
259
+ return oldState?.attr;
260
+ } else {
261
+ // we must assume the resource doesn't exist if it hasn't stabilized
262
+ return resourceExpr;
463
263
  }
264
+ }),
265
+ ));
266
+ });
464
267
 
465
- // Diff against whatever props represent the best-known current attempt.
466
- // For replacement recovery that means the top-level replacement props,
467
- // not the older generations stored under `old`.
468
- const oldProps = oldState.props;
268
+ const resolveInput = (input: any): Effect.Effect<any> =>
269
+ Effect.gen(function* () {
270
+ if (!input) {
271
+ return input;
272
+ } else if (Output.isExpr(input)) {
273
+ return yield* resolveOutput(input);
274
+ } else if (Array.isArray(input)) {
275
+ return yield* Effect.all(input.map(resolveInput), {
276
+ concurrency: "unbounded",
277
+ });
278
+ } else if (isResource(input)) {
279
+ // Resource objects have dynamic properties (path, hash, etc.) that are
280
+ // created on-demand by a Proxy getter and aren't enumerable via Object.entries.
281
+ // Resolve the ResourceExpr to get the actual resource output, then continue
282
+ // resolving any nested outputs in the result.
283
+ const resourceExpr = Output.of(input);
284
+ const resolved = yield* resolveOutput(resourceExpr);
285
+ return yield* resolveInput(resolved);
286
+ } else if (typeof input === "object") {
287
+ return Object.fromEntries(
288
+ yield* Effect.all(
289
+ Object.entries(input).map(([key, value]) =>
290
+ resolveInput(value).pipe(Effect.map((value) => [key, value])),
291
+ ),
292
+ { concurrency: "unbounded" },
293
+ ),
294
+ );
295
+ }
296
+ return input;
297
+ });
469
298
 
470
- const diff = yield* asEffect(
471
- provider
472
- ?.diff?.({
299
+ const resolveOutput = (expr: Output.Expr<any>): Effect.Effect<any> =>
300
+ Effect.gen(function* () {
301
+ if (Output.isResourceExpr(expr)) {
302
+ return yield* resolveResource(expr);
303
+ } else if (Output.isPropExpr(expr)) {
304
+ const upstream = yield* resolveOutput(expr.expr);
305
+ return upstream?.[expr.identifier];
306
+ } else if (Output.isApplyExpr(expr)) {
307
+ const upstream = yield* resolveOutput(expr.expr);
308
+ return Output.hasOutputs(upstream) ? expr : expr.f(upstream);
309
+ } else if (Output.isEffectExpr(expr)) {
310
+ const upstream = yield* resolveOutput(expr.expr);
311
+ return Output.hasOutputs(upstream) ? expr : yield* expr.f(upstream);
312
+ } else if (Output.isAllExpr(expr)) {
313
+ return yield* Effect.all(expr.outs.map(resolveOutput), {
314
+ concurrency: "unbounded",
315
+ });
316
+ } else if (Output.isLiteralExpr(expr)) {
317
+ return expr.value;
318
+ }
319
+ return yield* Effect.die(new Error("Not implemented yet"));
320
+ });
321
+
322
+ // map of resource FQN -> its downstream dependencies (resources that depend on it)
323
+ const oldDownstreamDependencies: {
324
+ [fqn: string]: string[];
325
+ } = Object.fromEntries(
326
+ oldResources
327
+ .filter((resource) => !!resource)
328
+ .map((resource) => [resource.fqn, resource.downstream]),
329
+ );
330
+
331
+ // Build a set of FQNs for the new resources to detect orphans
332
+ const newResourceFqns = new Set(resources.map((r) => r.FQN));
333
+
334
+ // Map FQN -> list of upstream FQNs (resources this one depends on via props)
335
+ const newUpstreamDependencies: {
336
+ [fqn: string]: string[];
337
+ } = Object.fromEntries(
338
+ resources.map((resource) => [
339
+ resource.FQN,
340
+ Object.values(Output.upstreamAny(resource.Props)).map((r) => r.FQN),
341
+ ]),
342
+ );
343
+
344
+ // Map FQN -> list of upstream FQNs from bindings
345
+ const bindingUpstreamDependencies: {
346
+ [fqn: string]: string[];
347
+ } = Object.fromEntries(
348
+ resources.map((resource) => [
349
+ resource.FQN,
350
+ Object.values(
351
+ Output.upstreamAny(stack.bindings[resource.FQN] ?? []),
352
+ ).map((r) => r.FQN),
353
+ ]),
354
+ );
355
+
356
+ // Combined prop + binding upstream for the desired graph, including
357
+ // references to resources outside the current graph so delete validation can
358
+ // tell whether any surviving resource still points at an orphan.
359
+ const rawUpstreamDependencies: {
360
+ [fqn: string]: string[];
361
+ } = Object.fromEntries(
362
+ resources.map((resource) => {
363
+ const fqn = resource.FQN;
364
+ const propDeps = newUpstreamDependencies[fqn] ?? [];
365
+ const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
366
+ return [fqn, [...new Set([...propDeps, ...bindDeps])]];
367
+ }),
368
+ );
369
+
370
+ // Combined prop + binding upstream, filtered to resources in this graph for
371
+ // scheduling and cycle detection.
372
+ const allUpstreamDependencies: {
373
+ [fqn: string]: string[];
374
+ } = Object.fromEntries(
375
+ resources.map((resource) => {
376
+ const fqn = resource.FQN;
377
+ const deps = rawUpstreamDependencies[fqn] ?? [];
378
+ return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
379
+ }),
380
+ );
381
+
382
+ // Map FQN -> list of downstream FQNs (resources that depend on this one)
383
+ const newDownstreamDependencies: {
384
+ [fqn: string]: string[];
385
+ } = Object.fromEntries(
386
+ resources.map((resource) => [
387
+ resource.FQN,
388
+ Object.entries(newUpstreamDependencies)
389
+ .filter(([_, upstream]) => upstream.includes(resource.FQN))
390
+ .map(([depFqn]) => depFqn),
391
+ ]),
392
+ );
393
+
394
+ const resourceGraph = Object.fromEntries(
395
+ (yield* Effect.all(
396
+ resources.map(
397
+ Effect.fn(function* (resource) {
398
+ const provider = yield* findProviderByType(resource.Type);
399
+ const id = resource.LogicalId;
400
+ const fqn = resource.FQN;
401
+ const news = yield* resolveInput(resource.Props);
402
+ const downstream = newDownstreamDependencies[fqn] ?? [];
403
+
404
+ const newBindings: ResourceBinding[] = yield* resolveInput(
405
+ stack.bindings[fqn] ?? [],
406
+ );
407
+ const oldState = yield* state.get({
408
+ stack: stackName,
409
+ stage: stage,
410
+ fqn,
411
+ });
412
+ const oldBindings = oldState?.bindings ?? [];
413
+ const bindingDiffs = diffBindings(oldBindings, newBindings);
414
+
415
+ const Node = <T extends Apply>(
416
+ node: Omit<
417
+ T,
418
+ "provider" | "resource" | "bindings" | "downstream"
419
+ >,
420
+ ) =>
421
+ ({
422
+ ...node,
423
+ provider,
424
+ resource,
425
+ bindings: bindingDiffs,
426
+ downstream,
427
+ }) as any as T;
428
+
429
+ // Plan against the persisted state we have, not the ideal final state we
430
+ // hoped to reach last time. Recovery is expressed by mapping each
431
+ // intermediate state back onto a fresh CRUD action.
432
+ if (oldState === undefined) {
433
+ return Node<Create>({
434
+ action: "create",
435
+ props: news,
436
+ state: oldState,
437
+ });
438
+ } else if (
439
+ oldState.status === "creating" &&
440
+ oldState.attr === undefined
441
+ ) {
442
+ // A create may have succeeded before state persistence failed. If the
443
+ // provider can recover an attribute snapshot, keep driving the same
444
+ // create instead of starting over blindly.
445
+ if (provider.read) {
446
+ const attr = yield* provider
447
+ .read({
473
448
  id,
474
- olds: oldProps,
475
449
  instanceId: oldState.instanceId,
450
+ olds: oldState.props,
476
451
  output: oldState.attr,
477
- news,
478
- oldBindings,
479
- newBindings,
480
452
  })
481
- .pipe(providePlanScope(fqn, oldState.instanceId)),
482
- ).pipe(
483
- Effect.map(
484
- (diff) =>
485
- diff ??
486
- ({
487
- action:
488
- havePropsChanged(oldProps, news) ||
489
- bindingDiffs.some((b) => b.action !== "noop")
490
- ? "update"
491
- : "noop",
492
- } as UpdateDiff | NoopDiff),
493
- ),
494
- Effect.map((diff) =>
495
- options.force && diff.action === "noop"
496
- ? ({
497
- action: "update",
498
- } satisfies UpdateDiff)
499
- : diff,
500
- ),
501
- );
502
-
503
- if (oldState.status === "creating") {
504
- if (diff.action === "noop") {
505
- // we're in the creating state and props are un-changed
506
- // let's just continue where we left off
507
- return Node<Create>({
508
- action: "create",
509
- props: news,
510
- state: oldState,
511
- });
512
- } else if (diff.action === "update") {
513
- // props have changed in a way that is updatable
514
- // again, just continue with the create
515
- // TODO(sam): should we maybe try an update instead?
453
+ .pipe(providePlanScope(fqn, oldState.instanceId));
454
+ if (attr) {
516
455
  return Node<Create>({
517
456
  action: "create",
518
457
  props: news,
519
- state: oldState,
520
- });
521
- } else {
522
- // props have changed in an incompatible way
523
- // because it's possible that an un-updatable resource has already been created
524
- // we must use a replace step to create a new one and delete the potential old one
525
- return Node<Replace>({
526
- action: "replace",
527
- props: news,
528
- deleteFirst: diff.deleteFirst ?? false,
529
- state: oldState,
530
- });
531
- }
532
- } else if (oldState.status === "updating") {
533
- // Updating already targets the live resource, so noop/update both mean
534
- // "finish the interrupted update". Only a replace diff escalates it
535
- // into a fresh replacement.
536
- if (diff.action === "update" || diff.action === "noop") {
537
- // we can continue where we left off
538
- return Node<Update>({
539
- action: "update",
540
- props: news,
541
- state: oldState,
542
- });
543
- } else {
544
- // we started to update a resource but now believe we should replace it
545
- return Node<Replace>({
546
- action: "replace",
547
- deleteFirst: diff.deleteFirst ?? false,
548
- props: news,
549
- // TODO(sam): can Apply handle replacements when the oldState is UpdatingResourceState?
550
- // -> or should we do a provider.read to try and reconcile back to UpdatedResourceState?
551
- state: oldState,
552
- });
553
- }
554
- } else if (oldState.status === "replacing") {
555
- // The replacement candidate is still being created. Noop/update keep
556
- // driving the same generation; replace means that candidate itself is
557
- // now obsolete and must be wrapped in a new outer generation.
558
- if (diff.action === "noop") {
559
- // this is the stable case - noop means just continue with the replacement
560
- return Node<Replace>({
561
- action: "replace",
562
- deleteFirst: oldState.deleteFirst,
563
- props: news,
564
- state: oldState,
565
- });
566
- } else if (diff.action === "update") {
567
- // potential problem here - the props have changed since we tried to replace,
568
- // but not enough to trigger another replacement. the resource provider should
569
- // be designed as idempotent to converge to the right state when creating the new resource
570
- // the newly generated instanceId is intended to assist with this
571
- return Node<Replace>({
572
- action: "replace",
573
- deleteFirst: oldState.deleteFirst,
574
- props: news,
575
- state: oldState,
576
- });
577
- } else {
578
- // The in-flight replacement candidate itself now needs replacement.
579
- // Mark this as a restart so Apply creates a fresh generation instead
580
- // of resuming the old replacement instance.
581
- return Node<Replace>({
582
- restart: true,
583
- action: "replace",
584
- deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
585
- props: news,
586
- state: oldState,
587
- });
588
- }
589
- } else if (oldState.status === "replaced") {
590
- // The new resource already exists. Noop means "just let GC finish",
591
- // update means "mutate the current replacement before GC finishes",
592
- // and replace means "the current replacement also became obsolete".
593
- if (diff.action === "noop") {
594
- // this is the stable case - noop means just continue cleaning up the replacement
595
- return Node<Replace>({
596
- action: "replace",
597
- deleteFirst: oldState.deleteFirst,
598
- props: news,
599
- state: oldState,
600
- });
601
- } else if (diff.action === "update") {
602
- // the replacement has been created but now also needs to be updated
603
- // the resource provider should:
604
- // 1. Update the newly created replacement resource
605
- // 2. Then proceed as normal to delete the replaced resources (after all downstream references are updated)
606
- return Node<Update>({
607
- action: "update",
608
- props: news,
609
- state: oldState,
610
- });
611
- } else {
612
- // Cleanup is still pending, but the current "new" resource has already
613
- // become obsolete. Start another replacement generation and preserve
614
- // the existing replaced node as part of the recursive old chain.
615
- return Node<Replace>({
616
- restart: true,
617
- action: "replace",
618
- deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
619
- props: news,
620
- state: oldState,
458
+ state: { ...oldState, attr },
621
459
  });
622
460
  }
623
- } else if (oldState.status === "deleting") {
624
- // we're in a partially deleted state, it is unclear whether it was or was not deleted
625
- // so continue by re-creating it with the same instanceId and desired props
461
+ }
462
+ }
463
+
464
+ // Diff against whatever props represent the best-known current attempt.
465
+ // For replacement recovery that means the top-level replacement props,
466
+ // not the older generations stored under `old`.
467
+ const oldProps = oldState.props;
468
+
469
+ const diff = yield* asEffect(
470
+ provider
471
+ ?.diff?.({
472
+ id,
473
+ olds: oldProps,
474
+ instanceId: oldState.instanceId,
475
+ output: oldState.attr,
476
+ news,
477
+ oldBindings,
478
+ newBindings,
479
+ })
480
+ .pipe(providePlanScope(fqn, oldState.instanceId)),
481
+ ).pipe(
482
+ Effect.map(
483
+ (diff) =>
484
+ diff ??
485
+ ({
486
+ action:
487
+ havePropsChanged(oldProps, news) ||
488
+ bindingDiffs.some((b) => b.action !== "noop")
489
+ ? "update"
490
+ : "noop",
491
+ } as UpdateDiff | NoopDiff),
492
+ ),
493
+ Effect.map((diff) =>
494
+ options.force && diff.action === "noop"
495
+ ? ({
496
+ action: "update",
497
+ } satisfies UpdateDiff)
498
+ : diff,
499
+ ),
500
+ );
501
+
502
+ if (oldState.status === "creating") {
503
+ if (diff.action === "noop") {
504
+ // we're in the creating state and props are un-changed
505
+ // let's just continue where we left off
626
506
  return Node<Create>({
627
507
  action: "create",
628
508
  props: news,
629
- state: {
630
- ...oldState,
631
- status: "creating",
632
- props: news,
633
- },
509
+ state: oldState,
634
510
  });
635
511
  } else if (diff.action === "update") {
636
- // Stable created/updated resources follow the normal CRUD mapping.
512
+ // props have changed in a way that is updatable
513
+ // again, just continue with the create
514
+ // TODO(sam): should we maybe try an update instead?
515
+ return Node<Create>({
516
+ action: "create",
517
+ props: news,
518
+ state: oldState,
519
+ });
520
+ } else {
521
+ // props have changed in an incompatible way
522
+ // because it's possible that an un-updatable resource has already been created
523
+ // we must use a replace step to create a new one and delete the potential old one
524
+ return Node<Replace>({
525
+ action: "replace",
526
+ props: news,
527
+ deleteFirst: diff.deleteFirst ?? false,
528
+ state: oldState,
529
+ });
530
+ }
531
+ } else if (oldState.status === "updating") {
532
+ // Updating already targets the live resource, so noop/update both mean
533
+ // "finish the interrupted update". Only a replace diff escalates it
534
+ // into a fresh replacement.
535
+ if (diff.action === "update" || diff.action === "noop") {
536
+ // we can continue where we left off
637
537
  return Node<Update>({
638
538
  action: "update",
639
539
  props: news,
640
540
  state: oldState,
641
541
  });
642
- } else if (diff.action === "replace") {
542
+ } else {
543
+ // we started to update a resource but now believe we should replace it
544
+ return Node<Replace>({
545
+ action: "replace",
546
+ deleteFirst: diff.deleteFirst ?? false,
547
+ props: news,
548
+ // TODO(sam): can Apply handle replacements when the oldState is UpdatingResourceState?
549
+ // -> or should we do a provider.read to try and reconcile back to UpdatedResourceState?
550
+ state: oldState,
551
+ });
552
+ }
553
+ } else if (oldState.status === "replacing") {
554
+ // The replacement candidate is still being created. Noop/update keep
555
+ // driving the same generation; replace means that candidate itself is
556
+ // now obsolete and must be wrapped in a new outer generation.
557
+ if (diff.action === "noop") {
558
+ // this is the stable case - noop means just continue with the replacement
559
+ return Node<Replace>({
560
+ action: "replace",
561
+ deleteFirst: oldState.deleteFirst,
562
+ props: news,
563
+ state: oldState,
564
+ });
565
+ } else if (diff.action === "update") {
566
+ // potential problem here - the props have changed since we tried to replace,
567
+ // but not enough to trigger another replacement. the resource provider should
568
+ // be designed as idempotent to converge to the right state when creating the new resource
569
+ // the newly generated instanceId is intended to assist with this
643
570
  return Node<Replace>({
644
571
  action: "replace",
572
+ deleteFirst: oldState.deleteFirst,
645
573
  props: news,
646
574
  state: oldState,
647
- deleteFirst: diff?.deleteFirst ?? false,
648
575
  });
649
576
  } else {
650
- return Node<NoopUpdate>({
651
- action: "noop",
577
+ // The in-flight replacement candidate itself now needs replacement.
578
+ // Mark this as a restart so Apply creates a fresh generation instead
579
+ // of resuming the old replacement instance.
580
+ return Node<Replace>({
581
+ restart: true,
582
+ action: "replace",
583
+ deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
584
+ props: news,
652
585
  state: oldState,
653
586
  });
654
587
  }
655
- }),
588
+ } else if (oldState.status === "replaced") {
589
+ // The new resource already exists. Noop means "just let GC finish",
590
+ // update means "mutate the current replacement before GC finishes",
591
+ // and replace means "the current replacement also became obsolete".
592
+ if (diff.action === "noop") {
593
+ // this is the stable case - noop means just continue cleaning up the replacement
594
+ return Node<Replace>({
595
+ action: "replace",
596
+ deleteFirst: oldState.deleteFirst,
597
+ props: news,
598
+ state: oldState,
599
+ });
600
+ } else if (diff.action === "update") {
601
+ // the replacement has been created but now also needs to be updated
602
+ // the resource provider should:
603
+ // 1. Update the newly created replacement resource
604
+ // 2. Then proceed as normal to delete the replaced resources (after all downstream references are updated)
605
+ return Node<Update>({
606
+ action: "update",
607
+ props: news,
608
+ state: oldState,
609
+ });
610
+ } else {
611
+ // Cleanup is still pending, but the current "new" resource has already
612
+ // become obsolete. Start another replacement generation and preserve
613
+ // the existing replaced node as part of the recursive old chain.
614
+ return Node<Replace>({
615
+ restart: true,
616
+ action: "replace",
617
+ deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
618
+ props: news,
619
+ state: oldState,
620
+ });
621
+ }
622
+ } else if (oldState.status === "deleting") {
623
+ // we're in a partially deleted state, it is unclear whether it was or was not deleted
624
+ // so continue by re-creating it with the same instanceId and desired props
625
+ return Node<Create>({
626
+ action: "create",
627
+ props: news,
628
+ state: {
629
+ ...oldState,
630
+ status: "creating",
631
+ props: news,
632
+ },
633
+ });
634
+ } else if (diff.action === "update") {
635
+ // Stable created/updated resources follow the normal CRUD mapping.
636
+ return Node<Update>({
637
+ action: "update",
638
+ props: news,
639
+ state: oldState,
640
+ });
641
+ } else if (diff.action === "replace") {
642
+ return Node<Replace>({
643
+ action: "replace",
644
+ props: news,
645
+ state: oldState,
646
+ deleteFirst: diff?.deleteFirst ?? false,
647
+ });
648
+ } else {
649
+ return Node<NoopUpdate>({
650
+ action: "noop",
651
+ state: oldState,
652
+ });
653
+ }
654
+ }),
655
+ ),
656
+ { concurrency: "unbounded" },
657
+ )).map((update) => [update.resource.FQN, update]),
658
+ ) as Plan["resources"];
659
+
660
+ // Detect unsatisfiable dependency cycles among create/replace nodes.
661
+ // Update/noop nodes signal their Deferred before waitForDeps so they
662
+ // cannot deadlock. Create/replace nodes only signal early when they
663
+ // have a precreate handler. Simulate the concurrent execution:
664
+ // precreate nodes are immediately "resolved", then iteratively resolve
665
+ // any node whose deps are all resolved. Remaining nodes would deadlock.
666
+ {
667
+ const createReplaceNodes = new Set(
668
+ Object.entries(resourceGraph)
669
+ .filter(
670
+ ([_, node]) =>
671
+ node.action === "create" || node.action === "replace",
672
+ )
673
+ .map(([fqn]) => fqn),
674
+ );
675
+
676
+ if (createReplaceNodes.size > 0) {
677
+ const hasPrecreate = new Set(
678
+ [...createReplaceNodes].filter(
679
+ (fqn) => !!resourceGraph[fqn]?.provider?.precreate,
656
680
  ),
657
- { concurrency: "unbounded" },
658
- )).map((update) => [update.resource.FQN, update]),
659
- ) as Plan["resources"];
660
-
661
- // Detect unsatisfiable dependency cycles among create/replace nodes.
662
- // Update/noop nodes signal their Deferred before waitForDeps so they
663
- // cannot deadlock. Create/replace nodes only signal early when they
664
- // have a precreate handler. Simulate the concurrent execution:
665
- // precreate nodes are immediately "resolved", then iteratively resolve
666
- // any node whose deps are all resolved. Remaining nodes would deadlock.
667
- {
668
- const createReplaceNodes = new Set(
669
- Object.entries(resourceGraph)
670
- .filter(
671
- ([_, node]) =>
672
- node.action === "create" || node.action === "replace",
673
- )
674
- .map(([fqn]) => fqn),
675
681
  );
676
682
 
677
- if (createReplaceNodes.size > 0) {
678
- const hasPrecreate = new Set(
679
- [...createReplaceNodes].filter(
680
- (fqn) => !!resourceGraph[fqn]?.provider?.precreate,
681
- ),
682
- );
683
-
684
- const resolved = new Set(hasPrecreate);
685
- let changed = true;
686
- while (changed) {
687
- changed = false;
688
- for (const fqn of createReplaceNodes) {
689
- if (resolved.has(fqn)) continue;
690
- const deps = (allUpstreamDependencies[fqn] ?? []).filter((dep) =>
691
- createReplaceNodes.has(dep),
692
- );
693
- if (deps.every((dep) => resolved.has(dep))) {
694
- resolved.add(fqn);
695
- changed = true;
696
- }
683
+ const resolved = new Set(hasPrecreate);
684
+ let changed = true;
685
+ while (changed) {
686
+ changed = false;
687
+ for (const fqn of createReplaceNodes) {
688
+ if (resolved.has(fqn)) continue;
689
+ const deps = (allUpstreamDependencies[fqn] ?? []).filter((dep) =>
690
+ createReplaceNodes.has(dep),
691
+ );
692
+ if (deps.every((dep) => resolved.has(dep))) {
693
+ resolved.add(fqn);
694
+ changed = true;
697
695
  }
698
696
  }
697
+ }
699
698
 
700
- const deadlocked = [...createReplaceNodes].filter(
701
- (fqn) => !resolved.has(fqn),
699
+ const deadlocked = [...createReplaceNodes].filter(
700
+ (fqn) => !resolved.has(fqn),
701
+ );
702
+ if (deadlocked.length > 0) {
703
+ const missingPrecreate = deadlocked.filter(
704
+ (fqn) => !hasPrecreate.has(fqn),
705
+ );
706
+ return yield* Effect.die(
707
+ new UnsatisfiedResourceCycle({
708
+ message:
709
+ `Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. ` +
710
+ `Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. ` +
711
+ `All resources in a dependency cycle must implement precreate to allow early signaling.`,
712
+ cycle: deadlocked,
713
+ missingPrecreate,
714
+ }),
702
715
  );
703
- if (deadlocked.length > 0) {
704
- const missingPrecreate = deadlocked.filter(
705
- (fqn) => !hasPrecreate.has(fqn),
706
- );
707
- return yield* Effect.die(
708
- new UnsatisfiedResourceCycle({
709
- message:
710
- `Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. ` +
711
- `Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. ` +
712
- `All resources in a dependency cycle must implement precreate to allow early signaling.`,
713
- cycle: deadlocked,
714
- missingPrecreate,
715
- }),
716
- );
717
- }
718
716
  }
719
717
  }
718
+ }
720
719
 
721
- const deletions = Object.fromEntries(
722
- (yield* Effect.all(
723
- (yield* state.list({ stack: stackName, stage: stage })).map(
724
- Effect.fn(function* (fqn) {
725
- // Check if this FQN is in the new resources
726
- if (newResourceFqns.has(fqn)) {
727
- return;
728
- }
729
- const oldState = yield* state.get({
730
- stack: stackName,
731
- stage: stage,
732
- fqn,
733
- });
734
- let attr: any = oldState?.attr;
735
- if (oldState) {
736
- const { logicalId } = parseFqn(fqn);
737
- const resourceType = oldState.resourceType;
738
- const provider = yield* getProviderByType(resourceType);
739
- if (oldState.attr === undefined) {
740
- if (provider.read) {
741
- attr = yield* provider
742
- .read({
743
- id: logicalId,
744
- instanceId: oldState.instanceId,
745
- olds: oldState.props as never,
746
- output: oldState.attr as never,
747
- })
748
- .pipe(providePlanScope(fqn, oldState.instanceId));
749
- }
720
+ const deletions = Object.fromEntries(
721
+ (yield* Effect.all(
722
+ (yield* state.list({ stack: stackName, stage: stage })).map(
723
+ Effect.fn(function* (fqn) {
724
+ // Check if this FQN is in the new resources
725
+ if (newResourceFqns.has(fqn)) {
726
+ return;
727
+ }
728
+ const oldState = yield* state.get({
729
+ stack: stackName,
730
+ stage: stage,
731
+ fqn,
732
+ });
733
+ let attr: any = oldState?.attr;
734
+ if (oldState) {
735
+ const { logicalId } = parseFqn(fqn);
736
+ const resourceType = oldState.resourceType;
737
+ const provider = yield* findProviderByType(resourceType);
738
+ if (oldState.attr === undefined) {
739
+ if (provider.read) {
740
+ attr = yield* provider
741
+ .read({
742
+ id: logicalId,
743
+ instanceId: oldState.instanceId,
744
+ olds: oldState.props as never,
745
+ output: oldState.attr as never,
746
+ })
747
+ .pipe(providePlanScope(fqn, oldState.instanceId));
750
748
  }
751
- return [
752
- fqn,
753
- {
754
- action: "delete",
755
- state: { ...oldState, attr },
756
- provider: provider,
757
- resource: {
758
- Namespace: oldState.namespace,
759
- FQN: fqn,
760
- LogicalId: logicalId,
761
- Type: oldState.resourceType,
762
- Attributes: attr,
763
- Props: oldState.props,
764
- Binding: undefined!,
765
- Provider: Provider(resourceType),
766
- RemovalPolicy: oldState.removalPolicy,
767
- ExecutionContext: undefined!,
768
- } as ResourceLike,
769
- downstream: oldDownstreamDependencies[fqn] ?? [],
770
- bindings: oldState.bindings.map((binding) => ({
771
- sid: binding.sid,
772
- action: "delete" as const,
773
- data: binding.data,
774
- })),
775
- } satisfies Delete,
776
- ] as const;
777
749
  }
778
- }),
779
- ),
780
- { concurrency: "unbounded" },
781
- )).filter((v) => !!v),
782
- );
783
-
784
- for (const resourceFqn of Object.keys(deletions)) {
785
- const dependencies = Object.entries(rawUpstreamDependencies)
786
- .filter(
787
- ([survivorFqn, upstream]) =>
788
- survivorFqn in resourceGraph && upstream.includes(resourceFqn),
789
- )
790
- .map(([survivorFqn]) => survivorFqn);
791
- if (dependencies.length > 0) {
792
- return yield* new DeleteResourceHasDownstreamDependencies({
793
- message: `Resource ${resourceFqn} has downstream dependencies`,
794
- resourceId: resourceFqn,
795
- dependencies,
796
- });
797
- }
750
+ return [
751
+ fqn,
752
+ {
753
+ action: "delete",
754
+ state: { ...oldState, attr },
755
+ provider: provider,
756
+ resource: {
757
+ Namespace: oldState.namespace,
758
+ FQN: fqn,
759
+ LogicalId: logicalId,
760
+ Type: oldState.resourceType,
761
+ Attributes: attr,
762
+ Props: oldState.props,
763
+ Binding: undefined!,
764
+ Provider: Provider(resourceType),
765
+ RemovalPolicy: oldState.removalPolicy,
766
+ ExecutionContext: undefined!,
767
+ Providers: undefined,
768
+ } as ResourceLike,
769
+ downstream: oldDownstreamDependencies[fqn] ?? [],
770
+ bindings: oldState.bindings.map((binding) => ({
771
+ sid: binding.sid,
772
+ action: "delete" as const,
773
+ data: binding.data,
774
+ })),
775
+ } satisfies Delete,
776
+ ] as const;
777
+ }
778
+ }),
779
+ ),
780
+ { concurrency: "unbounded" },
781
+ )).filter((v) => !!v),
782
+ );
783
+
784
+ for (const resourceFqn of Object.keys(deletions)) {
785
+ const dependencies = Object.entries(rawUpstreamDependencies)
786
+ .filter(
787
+ ([survivorFqn, upstream]) =>
788
+ survivorFqn in resourceGraph && upstream.includes(resourceFqn),
789
+ )
790
+ .map(([survivorFqn]) => survivorFqn);
791
+ if (dependencies.length > 0) {
792
+ return yield* new DeleteResourceHasDownstreamDependencies({
793
+ message: `Resource ${resourceFqn} has downstream dependencies`,
794
+ resourceId: resourceFqn,
795
+ dependencies,
796
+ });
798
797
  }
798
+ }
799
799
 
800
- return {
801
- resources: resourceGraph,
802
- deletions,
803
- output: stack.output,
804
- } satisfies Plan<A> as Plan<A>;
805
- }),
806
- );
800
+ return {
801
+ resources: resourceGraph,
802
+ deletions,
803
+ output: stack.output,
804
+ } satisfies Plan<A> as Plan<A>;
805
+ }).pipe(ensureArtifactStore);
807
806
 
808
807
  const providePlanScope =
809
808
  (fqn: string, instanceId: string) =>