alchemy-effect 0.6.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alchemy-effect.js +90 -20
- package/bin/alchemy-effect.js.map +1 -1
- package/bin/alchemy-effect.ts +27 -24
- package/lib/cli/index.d.ts +1 -0
- package/lib/cli/index.d.ts.map +1 -1
- package/package.json +13 -3
- package/src/AWS/Website/StaticSite.ts +5 -7
- package/src/AWS/Website/shared.ts +15 -3
- package/src/Apply.ts +143 -107
- package/src/Artifacts.ts +147 -0
- package/src/Build/Command.ts +21 -99
- package/src/Build/Memo.ts +187 -0
- package/src/Bundle/Bundle.ts +23 -17
- package/src/Cloudflare/Website/Vite.ts +65 -0
- package/src/Cloudflare/Website/index.ts +1 -1
- package/src/Cloudflare/Workers/Assets.ts +7 -2
- package/src/Cloudflare/Workers/Worker.ts +189 -59
- package/src/Destroy.ts +3 -2
- package/src/Output.ts +4 -0
- package/src/Plan.ts +606 -585
- package/src/Stack.ts +18 -6
- package/src/Test/Vitest.ts +20 -11
- package/src/Util/gitignore-rules-to-globs.ts +80 -0
- package/src/Cloudflare/Website/TanstackStart.ts +0 -14
package/src/Plan.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import * as Data from "effect/Data";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import { asEffect } from ".//Util/types.ts";
|
|
4
|
+
import {
|
|
5
|
+
Artifacts,
|
|
6
|
+
ArtifactStore,
|
|
7
|
+
createArtifactStore,
|
|
8
|
+
ensureArtifactStore,
|
|
9
|
+
makeScopedArtifacts,
|
|
10
|
+
} from "./Artifacts.ts";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
4
12
|
import {
|
|
5
13
|
diffBindings,
|
|
6
14
|
havePropsChanged,
|
|
@@ -144,648 +152,661 @@ export const make = <A>(
|
|
|
144
152
|
stack: StackSpec<A>,
|
|
145
153
|
): Effect.Effect<Plan<A>, never, State> =>
|
|
146
154
|
// @ts-expect-error
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
155
|
+
ensureArtifactStore(
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const state = yield* State;
|
|
158
|
+
|
|
159
|
+
const resources = Object.values(stack.resources);
|
|
160
|
+
|
|
161
|
+
// TODO(sam): rename terminology to Stack
|
|
162
|
+
const stackName = stack.name;
|
|
163
|
+
const stage = stack.stage;
|
|
164
|
+
|
|
165
|
+
const resourceFqns = yield* state.list({
|
|
166
|
+
stack: stackName,
|
|
167
|
+
stage: stage,
|
|
168
|
+
});
|
|
169
|
+
const oldResources = yield* Effect.all(
|
|
170
|
+
resourceFqns.map((fqn) =>
|
|
171
|
+
state.get({ stack: stackName, stage: stage, fqn }),
|
|
172
|
+
),
|
|
173
|
+
{ concurrency: "unbounded" },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const resolvedResources: Record<string, Effect.Effect<any>> = {};
|
|
177
|
+
|
|
178
|
+
const resolveResource = (
|
|
179
|
+
resourceExpr: Output.ResourceExpr<any, any>,
|
|
180
|
+
): Effect.Effect<any> =>
|
|
181
|
+
Effect.gen(function* () {
|
|
182
|
+
// @ts-expect-error
|
|
183
|
+
return yield* (resolvedResources[resourceExpr.src.FQN] ??=
|
|
184
|
+
yield* Effect.cached(
|
|
185
|
+
Effect.gen(function* () {
|
|
186
|
+
const resource = resourceExpr.src;
|
|
187
|
+
|
|
188
|
+
const provider = yield* Provider(resource.Type);
|
|
189
|
+
const props = yield* resolveInput(resource.Props);
|
|
190
|
+
const oldState = yield* state.get({
|
|
191
|
+
stack: stackName,
|
|
192
|
+
stage: stage,
|
|
193
|
+
fqn: resource.FQN,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!oldState || oldState.status === "creating") {
|
|
197
|
+
return resourceExpr;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const oldProps =
|
|
201
|
+
oldState.status === "updating"
|
|
202
|
+
? oldState.old.props
|
|
203
|
+
: oldState.props;
|
|
204
|
+
|
|
205
|
+
const oldBindings = oldState.bindings ?? [];
|
|
206
|
+
const newBindings = stack.bindings[resource.FQN] ?? [];
|
|
207
|
+
|
|
208
|
+
const diff = yield* provider.diff
|
|
209
|
+
? provider
|
|
210
|
+
.diff({
|
|
211
|
+
id: resource.LogicalId,
|
|
212
|
+
olds: oldProps,
|
|
213
|
+
instanceId: oldState.instanceId,
|
|
214
|
+
news: props,
|
|
215
|
+
output: oldState.attr,
|
|
216
|
+
oldBindings,
|
|
217
|
+
newBindings,
|
|
218
|
+
})
|
|
219
|
+
.pipe(providePlanScope(resource.FQN, oldState.instanceId))
|
|
220
|
+
: Effect.succeed(undefined);
|
|
221
|
+
|
|
222
|
+
const stables: string[] = [
|
|
223
|
+
...(provider.stables ?? []),
|
|
224
|
+
...(diff?.stables ?? []),
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
const withStables = (output: any) =>
|
|
228
|
+
stables.length > 0
|
|
229
|
+
? new Output.ResourceExpr(
|
|
230
|
+
resourceExpr.src,
|
|
231
|
+
Object.fromEntries(
|
|
232
|
+
stables.map((stable) => [stable, output?.[stable]]),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
: // if there are no stable properties, treat every property as changed
|
|
236
|
+
resourceExpr;
|
|
237
|
+
|
|
238
|
+
if (diff == null) {
|
|
239
|
+
if (havePropsChanged(oldProps, props)) {
|
|
240
|
+
// the props have changed but the provider did not provide any hints as to what is stable
|
|
241
|
+
// so we must assume everything has changed
|
|
242
|
+
return withStables(oldState?.attr);
|
|
243
|
+
}
|
|
244
|
+
} else if (diff.action === "update") {
|
|
245
|
+
return withStables(oldState?.attr);
|
|
246
|
+
} else if (diff.action === "replace") {
|
|
247
|
+
return resourceExpr;
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
oldState.status === "created" ||
|
|
251
|
+
oldState.status === "updated" ||
|
|
252
|
+
oldState.status === "replaced"
|
|
253
|
+
) {
|
|
254
|
+
// we can safely return the attributes if we know they have stabilized
|
|
255
|
+
return oldState?.attr;
|
|
256
|
+
} else {
|
|
257
|
+
// we must assume the resource doesn't exist if it hasn't stabilized
|
|
258
|
+
return resourceExpr;
|
|
259
|
+
}
|
|
260
|
+
}),
|
|
261
|
+
));
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const resolveInput = (input: any): Effect.Effect<any> =>
|
|
265
|
+
Effect.gen(function* () {
|
|
266
|
+
if (!input) {
|
|
267
|
+
return input;
|
|
268
|
+
} else if (Output.isExpr(input)) {
|
|
269
|
+
return yield* resolveOutput(input);
|
|
270
|
+
} else if (Array.isArray(input)) {
|
|
271
|
+
return yield* Effect.all(input.map(resolveInput), {
|
|
272
|
+
concurrency: "unbounded",
|
|
273
|
+
});
|
|
274
|
+
} else if (isResource(input)) {
|
|
275
|
+
// Resource objects have dynamic properties (path, hash, etc.) that are
|
|
276
|
+
// created on-demand by a Proxy getter and aren't enumerable via Object.entries.
|
|
277
|
+
// Resolve the ResourceExpr to get the actual resource output, then continue
|
|
278
|
+
// resolving any nested outputs in the result.
|
|
279
|
+
const resourceExpr = Output.of(input);
|
|
280
|
+
const resolved = yield* resolveOutput(resourceExpr);
|
|
281
|
+
return yield* resolveInput(resolved);
|
|
282
|
+
} else if (typeof input === "object") {
|
|
283
|
+
return Object.fromEntries(
|
|
284
|
+
yield* Effect.all(
|
|
285
|
+
Object.entries(input).map(([key, value]) =>
|
|
286
|
+
resolveInput(value).pipe(Effect.map((value) => [key, value])),
|
|
287
|
+
),
|
|
288
|
+
{ concurrency: "unbounded" },
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return input;
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const resolveOutput = (expr: Output.Expr<any>): Effect.Effect<any> =>
|
|
296
|
+
Effect.gen(function* () {
|
|
297
|
+
if (Output.isResourceExpr(expr)) {
|
|
298
|
+
return yield* resolveResource(expr);
|
|
299
|
+
} else if (Output.isPropExpr(expr)) {
|
|
300
|
+
const upstream = yield* resolveOutput(expr.expr);
|
|
301
|
+
return upstream?.[expr.identifier];
|
|
302
|
+
} else if (Output.isApplyExpr(expr)) {
|
|
303
|
+
const upstream = yield* resolveOutput(expr.expr);
|
|
304
|
+
return Output.hasOutputs(upstream) ? expr : expr.f(upstream);
|
|
305
|
+
} else if (Output.isEffectExpr(expr)) {
|
|
306
|
+
const upstream = yield* resolveOutput(expr.expr);
|
|
307
|
+
return Output.hasOutputs(upstream) ? expr : yield* expr.f(upstream);
|
|
308
|
+
} else if (Output.isAllExpr(expr)) {
|
|
309
|
+
return yield* Effect.all(expr.outs.map(resolveOutput), {
|
|
310
|
+
concurrency: "unbounded",
|
|
311
|
+
});
|
|
312
|
+
} else if (Output.isLiteralExpr(expr)) {
|
|
313
|
+
return expr.value;
|
|
314
|
+
}
|
|
315
|
+
return yield* Effect.die(new Error("Not implemented yet"));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// map of resource FQN -> its downstream dependencies (resources that depend on it)
|
|
319
|
+
const oldDownstreamDependencies: {
|
|
320
|
+
[fqn: string]: string[];
|
|
321
|
+
} = Object.fromEntries(
|
|
322
|
+
oldResources
|
|
323
|
+
.filter((resource) => !!resource)
|
|
324
|
+
.map((resource) => [resource.fqn, resource.downstream]),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Build a set of FQNs for the new resources to detect orphans
|
|
328
|
+
const newResourceFqns = new Set(resources.map((r) => r.FQN));
|
|
329
|
+
|
|
330
|
+
// Map FQN -> list of upstream FQNs (resources this one depends on via props)
|
|
331
|
+
const newUpstreamDependencies: {
|
|
332
|
+
[fqn: string]: string[];
|
|
333
|
+
} = Object.fromEntries(
|
|
334
|
+
resources.map((resource) => [
|
|
335
|
+
resource.FQN,
|
|
336
|
+
Object.values(Output.upstreamAny(resource.Props)).map((r) => r.FQN),
|
|
337
|
+
]),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Map FQN -> list of upstream FQNs from bindings
|
|
341
|
+
const bindingUpstreamDependencies: {
|
|
342
|
+
[fqn: string]: string[];
|
|
343
|
+
} = Object.fromEntries(
|
|
344
|
+
resources.map((resource) => [
|
|
345
|
+
resource.FQN,
|
|
346
|
+
Object.values(
|
|
347
|
+
Output.upstreamAny(stack.bindings[resource.FQN] ?? []),
|
|
348
|
+
).map((r) => r.FQN),
|
|
349
|
+
]),
|
|
350
|
+
);
|
|
166
351
|
|
|
167
|
-
|
|
352
|
+
// Combined prop + binding upstream for the desired graph, including
|
|
353
|
+
// references to resources outside the current graph so delete validation can
|
|
354
|
+
// tell whether any surviving resource still points at an orphan.
|
|
355
|
+
const rawUpstreamDependencies: {
|
|
356
|
+
[fqn: string]: string[];
|
|
357
|
+
} = Object.fromEntries(
|
|
358
|
+
resources.map((resource) => {
|
|
359
|
+
const fqn = resource.FQN;
|
|
360
|
+
const propDeps = newUpstreamDependencies[fqn] ?? [];
|
|
361
|
+
const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
|
|
362
|
+
return [fqn, [...new Set([...propDeps, ...bindDeps])]];
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
168
365
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
366
|
+
// Combined prop + binding upstream, filtered to resources in this graph for
|
|
367
|
+
// scheduling and cycle detection.
|
|
368
|
+
const allUpstreamDependencies: {
|
|
369
|
+
[fqn: string]: string[];
|
|
370
|
+
} = Object.fromEntries(
|
|
371
|
+
resources.map((resource) => {
|
|
372
|
+
const fqn = resource.FQN;
|
|
373
|
+
const deps = rawUpstreamDependencies[fqn] ?? [];
|
|
374
|
+
return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
178
377
|
|
|
378
|
+
// Map FQN -> list of downstream FQNs (resources that depend on this one)
|
|
379
|
+
const newDownstreamDependencies: {
|
|
380
|
+
[fqn: string]: string[];
|
|
381
|
+
} = Object.fromEntries(
|
|
382
|
+
resources.map((resource) => [
|
|
383
|
+
resource.FQN,
|
|
384
|
+
Object.entries(newUpstreamDependencies)
|
|
385
|
+
.filter(([_, upstream]) => upstream.includes(resource.FQN))
|
|
386
|
+
.map(([depFqn]) => depFqn),
|
|
387
|
+
]),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const resourceGraph = Object.fromEntries(
|
|
391
|
+
(yield* Effect.all(
|
|
392
|
+
resources.map(
|
|
393
|
+
Effect.fn(function* (resource) {
|
|
179
394
|
const provider = yield* Provider(resource.Type);
|
|
180
|
-
const
|
|
395
|
+
const id = resource.LogicalId;
|
|
396
|
+
const fqn = resource.FQN;
|
|
397
|
+
const news = yield* resolveInput(resource.Props);
|
|
398
|
+
const downstream = newDownstreamDependencies[fqn] ?? [];
|
|
399
|
+
|
|
400
|
+
const newBindings: ResourceBinding[] = yield* resolveInput(
|
|
401
|
+
stack.bindings[fqn] ?? [],
|
|
402
|
+
);
|
|
181
403
|
const oldState = yield* state.get({
|
|
182
404
|
stack: stackName,
|
|
183
405
|
stage: stage,
|
|
184
|
-
fqn
|
|
406
|
+
fqn,
|
|
185
407
|
});
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
408
|
+
const oldBindings = oldState?.bindings ?? [];
|
|
409
|
+
const bindingDiffs = diffBindings(oldBindings, newBindings);
|
|
410
|
+
|
|
411
|
+
const Node = <T extends Apply>(
|
|
412
|
+
node: Omit<
|
|
413
|
+
T,
|
|
414
|
+
"provider" | "resource" | "bindings" | "downstream"
|
|
415
|
+
>,
|
|
416
|
+
) =>
|
|
417
|
+
({
|
|
418
|
+
...node,
|
|
419
|
+
provider,
|
|
420
|
+
resource,
|
|
421
|
+
bindings: bindingDiffs,
|
|
422
|
+
downstream,
|
|
423
|
+
}) as any as T;
|
|
424
|
+
|
|
425
|
+
// Plan against the persisted state we have, not the ideal final state we
|
|
426
|
+
// hoped to reach last time. Recovery is expressed by mapping each
|
|
427
|
+
// intermediate state back onto a fresh CRUD action.
|
|
428
|
+
if (oldState === undefined) {
|
|
429
|
+
return Node<Create>({
|
|
430
|
+
action: "create",
|
|
431
|
+
props: news,
|
|
432
|
+
state: oldState,
|
|
433
|
+
});
|
|
434
|
+
} else if (
|
|
435
|
+
oldState.status === "creating" &&
|
|
436
|
+
oldState.attr === undefined
|
|
437
|
+
) {
|
|
438
|
+
// A create may have succeeded before state persistence failed. If the
|
|
439
|
+
// provider can recover an attribute snapshot, keep driving the same
|
|
440
|
+
// create instead of starting over blindly.
|
|
441
|
+
if (provider.read) {
|
|
442
|
+
const attr = yield* provider
|
|
443
|
+
.read({
|
|
444
|
+
id,
|
|
204
445
|
instanceId: oldState.instanceId,
|
|
205
|
-
|
|
446
|
+
olds: oldState.props,
|
|
206
447
|
output: oldState.attr,
|
|
207
|
-
oldBindings,
|
|
208
|
-
newBindings,
|
|
209
448
|
})
|
|
210
|
-
.pipe(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
];
|
|
219
|
-
|
|
220
|
-
const withStables = (output: any) =>
|
|
221
|
-
stables.length > 0
|
|
222
|
-
? new Output.ResourceExpr(
|
|
223
|
-
resourceExpr.src,
|
|
224
|
-
Object.fromEntries(
|
|
225
|
-
stables.map((stable) => [stable, output?.[stable]]),
|
|
226
|
-
),
|
|
227
|
-
)
|
|
228
|
-
: // if there are no stable properties, treat every property as changed
|
|
229
|
-
resourceExpr;
|
|
230
|
-
|
|
231
|
-
if (diff == null) {
|
|
232
|
-
if (havePropsChanged(oldProps, props)) {
|
|
233
|
-
// the props have changed but the provider did not provide any hints as to what is stable
|
|
234
|
-
// so we must assume everything has changed
|
|
235
|
-
return withStables(oldState?.attr);
|
|
449
|
+
.pipe(providePlanScope(fqn, oldState.instanceId));
|
|
450
|
+
if (attr) {
|
|
451
|
+
return Node<Create>({
|
|
452
|
+
action: "create",
|
|
453
|
+
props: news,
|
|
454
|
+
state: { ...oldState, attr },
|
|
455
|
+
});
|
|
456
|
+
}
|
|
236
457
|
}
|
|
237
|
-
} else if (diff.action === "update") {
|
|
238
|
-
return withStables(oldState?.attr);
|
|
239
|
-
} else if (diff.action === "replace") {
|
|
240
|
-
return resourceExpr;
|
|
241
458
|
}
|
|
242
|
-
if (
|
|
243
|
-
oldState.status === "created" ||
|
|
244
|
-
oldState.status === "updated" ||
|
|
245
|
-
oldState.status === "replaced"
|
|
246
|
-
) {
|
|
247
|
-
// we can safely return the attributes if we know they have stabilized
|
|
248
|
-
return oldState?.attr;
|
|
249
|
-
} else {
|
|
250
|
-
// we must assume the resource doesn't exist if it hasn't stabilized
|
|
251
|
-
return resourceExpr;
|
|
252
|
-
}
|
|
253
|
-
}),
|
|
254
|
-
));
|
|
255
|
-
});
|
|
256
459
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
} else if (Output.isExpr(input)) {
|
|
262
|
-
return yield* resolveOutput(input);
|
|
263
|
-
} else if (Array.isArray(input)) {
|
|
264
|
-
return yield* Effect.all(input.map(resolveInput), {
|
|
265
|
-
concurrency: "unbounded",
|
|
266
|
-
});
|
|
267
|
-
} else if (isResource(input)) {
|
|
268
|
-
// Resource objects have dynamic properties (path, hash, etc.) that are
|
|
269
|
-
// created on-demand by a Proxy getter and aren't enumerable via Object.entries.
|
|
270
|
-
// Resolve the ResourceExpr to get the actual resource output, then continue
|
|
271
|
-
// resolving any nested outputs in the result.
|
|
272
|
-
const resourceExpr = Output.of(input);
|
|
273
|
-
const resolved = yield* resolveOutput(resourceExpr);
|
|
274
|
-
return yield* resolveInput(resolved);
|
|
275
|
-
} else if (typeof input === "object") {
|
|
276
|
-
return Object.fromEntries(
|
|
277
|
-
yield* Effect.all(
|
|
278
|
-
Object.entries(input).map(([key, value]) =>
|
|
279
|
-
resolveInput(value).pipe(Effect.map((value) => [key, value])),
|
|
280
|
-
),
|
|
281
|
-
{ concurrency: "unbounded" },
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
return input;
|
|
286
|
-
});
|
|
460
|
+
// Diff against whatever props represent the best-known current attempt.
|
|
461
|
+
// For replacement recovery that means the top-level replacement props,
|
|
462
|
+
// not the older generations stored under `old`.
|
|
463
|
+
const oldProps = oldState.props;
|
|
287
464
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return yield* resolveResource(expr);
|
|
292
|
-
} else if (Output.isPropExpr(expr)) {
|
|
293
|
-
const upstream = yield* resolveOutput(expr.expr);
|
|
294
|
-
return upstream?.[expr.identifier];
|
|
295
|
-
} else if (Output.isApplyExpr(expr)) {
|
|
296
|
-
const upstream = yield* resolveOutput(expr.expr);
|
|
297
|
-
return Output.hasOutputs(upstream) ? expr : expr.f(upstream);
|
|
298
|
-
} else if (Output.isEffectExpr(expr)) {
|
|
299
|
-
const upstream = yield* resolveOutput(expr.expr);
|
|
300
|
-
return Output.hasOutputs(upstream) ? expr : yield* expr.f(upstream);
|
|
301
|
-
} else if (Output.isAllExpr(expr)) {
|
|
302
|
-
return yield* Effect.all(expr.outs.map(resolveOutput), {
|
|
303
|
-
concurrency: "unbounded",
|
|
304
|
-
});
|
|
305
|
-
} else if (Output.isLiteralExpr(expr)) {
|
|
306
|
-
return expr.value;
|
|
307
|
-
}
|
|
308
|
-
return yield* Effect.die(new Error("Not implemented yet"));
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
// map of resource FQN -> its downstream dependencies (resources that depend on it)
|
|
312
|
-
const oldDownstreamDependencies: {
|
|
313
|
-
[fqn: string]: string[];
|
|
314
|
-
} = Object.fromEntries(
|
|
315
|
-
oldResources
|
|
316
|
-
.filter((resource) => !!resource)
|
|
317
|
-
.map((resource) => [resource.fqn, resource.downstream]),
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
// Build a set of FQNs for the new resources to detect orphans
|
|
321
|
-
const newResourceFqns = new Set(resources.map((r) => r.FQN));
|
|
322
|
-
|
|
323
|
-
// Map FQN -> list of upstream FQNs (resources this one depends on via props)
|
|
324
|
-
const newUpstreamDependencies: {
|
|
325
|
-
[fqn: string]: string[];
|
|
326
|
-
} = Object.fromEntries(
|
|
327
|
-
resources.map((resource) => [
|
|
328
|
-
resource.FQN,
|
|
329
|
-
Object.values(Output.upstreamAny(resource.Props)).map((r) => r.FQN),
|
|
330
|
-
]),
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
// Map FQN -> list of upstream FQNs from bindings
|
|
334
|
-
const bindingUpstreamDependencies: {
|
|
335
|
-
[fqn: string]: string[];
|
|
336
|
-
} = Object.fromEntries(
|
|
337
|
-
resources.map((resource) => [
|
|
338
|
-
resource.FQN,
|
|
339
|
-
Object.values(
|
|
340
|
-
Output.upstreamAny(stack.bindings[resource.FQN] ?? []),
|
|
341
|
-
).map((r) => r.FQN),
|
|
342
|
-
]),
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
// Combined prop + binding upstream for the desired graph, including
|
|
346
|
-
// references to resources outside the current graph so delete validation can
|
|
347
|
-
// tell whether any surviving resource still points at an orphan.
|
|
348
|
-
const rawUpstreamDependencies: {
|
|
349
|
-
[fqn: string]: string[];
|
|
350
|
-
} = Object.fromEntries(
|
|
351
|
-
resources.map((resource) => {
|
|
352
|
-
const fqn = resource.FQN;
|
|
353
|
-
const propDeps = newUpstreamDependencies[fqn] ?? [];
|
|
354
|
-
const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
|
|
355
|
-
return [fqn, [...new Set([...propDeps, ...bindDeps])]];
|
|
356
|
-
}),
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
// Combined prop + binding upstream, filtered to resources in this graph for
|
|
360
|
-
// scheduling and cycle detection.
|
|
361
|
-
const allUpstreamDependencies: {
|
|
362
|
-
[fqn: string]: string[];
|
|
363
|
-
} = Object.fromEntries(
|
|
364
|
-
resources.map((resource) => {
|
|
365
|
-
const fqn = resource.FQN;
|
|
366
|
-
const deps = rawUpstreamDependencies[fqn] ?? [];
|
|
367
|
-
return [fqn, deps.filter((dep) => newResourceFqns.has(dep))];
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
// Map FQN -> list of downstream FQNs (resources that depend on this one)
|
|
372
|
-
const newDownstreamDependencies: {
|
|
373
|
-
[fqn: string]: string[];
|
|
374
|
-
} = Object.fromEntries(
|
|
375
|
-
resources.map((resource) => [
|
|
376
|
-
resource.FQN,
|
|
377
|
-
Object.entries(newUpstreamDependencies)
|
|
378
|
-
.filter(([_, upstream]) => upstream.includes(resource.FQN))
|
|
379
|
-
.map(([depFqn]) => depFqn),
|
|
380
|
-
]),
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
const resourceGraph = Object.fromEntries(
|
|
384
|
-
(yield* Effect.all(
|
|
385
|
-
resources.map(
|
|
386
|
-
Effect.fn(function* (resource) {
|
|
387
|
-
const provider = yield* Provider(resource.Type);
|
|
388
|
-
const id = resource.LogicalId;
|
|
389
|
-
const fqn = resource.FQN;
|
|
390
|
-
const news = yield* resolveInput(resource.Props);
|
|
391
|
-
const downstream = newDownstreamDependencies[fqn] ?? [];
|
|
392
|
-
|
|
393
|
-
const newBindings: ResourceBinding[] = yield* resolveInput(
|
|
394
|
-
stack.bindings[fqn] ?? [],
|
|
395
|
-
);
|
|
396
|
-
const oldState = yield* state.get({
|
|
397
|
-
stack: stackName,
|
|
398
|
-
stage: stage,
|
|
399
|
-
fqn,
|
|
400
|
-
});
|
|
401
|
-
const oldBindings = oldState?.bindings ?? [];
|
|
402
|
-
const bindingDiffs = diffBindings(oldBindings, newBindings);
|
|
403
|
-
|
|
404
|
-
const Node = <T extends Apply>(
|
|
405
|
-
node: Omit<
|
|
406
|
-
T,
|
|
407
|
-
"provider" | "resource" | "bindings" | "downstream"
|
|
408
|
-
>,
|
|
409
|
-
) =>
|
|
410
|
-
({
|
|
411
|
-
...node,
|
|
412
|
-
provider,
|
|
413
|
-
resource,
|
|
414
|
-
bindings: bindingDiffs,
|
|
415
|
-
downstream,
|
|
416
|
-
}) as any as T;
|
|
417
|
-
|
|
418
|
-
// Plan against the persisted state we have, not the ideal final state we
|
|
419
|
-
// hoped to reach last time. Recovery is expressed by mapping each
|
|
420
|
-
// intermediate state back onto a fresh CRUD action.
|
|
421
|
-
if (oldState === undefined) {
|
|
422
|
-
return Node<Create>({
|
|
423
|
-
action: "create",
|
|
424
|
-
props: news,
|
|
425
|
-
state: oldState,
|
|
426
|
-
});
|
|
427
|
-
} else if (
|
|
428
|
-
oldState.status === "creating" &&
|
|
429
|
-
oldState.attr === undefined
|
|
430
|
-
) {
|
|
431
|
-
// A create may have succeeded before state persistence failed. If the
|
|
432
|
-
// provider can recover an attribute snapshot, keep driving the same
|
|
433
|
-
// create instead of starting over blindly.
|
|
434
|
-
if (provider.read) {
|
|
435
|
-
const attr = yield* provider
|
|
436
|
-
.read({
|
|
465
|
+
const diff = yield* asEffect(
|
|
466
|
+
provider
|
|
467
|
+
?.diff?.({
|
|
437
468
|
id,
|
|
469
|
+
olds: oldProps,
|
|
438
470
|
instanceId: oldState.instanceId,
|
|
439
|
-
olds: oldState.props,
|
|
440
471
|
output: oldState.attr,
|
|
472
|
+
news,
|
|
473
|
+
oldBindings,
|
|
474
|
+
newBindings,
|
|
441
475
|
})
|
|
442
|
-
.pipe(
|
|
443
|
-
|
|
476
|
+
.pipe(providePlanScope(fqn, oldState.instanceId)),
|
|
477
|
+
).pipe(
|
|
478
|
+
Effect.map(
|
|
479
|
+
(diff) =>
|
|
480
|
+
diff ??
|
|
481
|
+
({
|
|
482
|
+
action:
|
|
483
|
+
havePropsChanged(oldProps, news) ||
|
|
484
|
+
bindingDiffs.some((b) => b.action !== "noop")
|
|
485
|
+
? "update"
|
|
486
|
+
: "noop",
|
|
487
|
+
} as UpdateDiff | NoopDiff),
|
|
488
|
+
),
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
if (oldState.status === "creating") {
|
|
492
|
+
if (diff.action === "noop") {
|
|
493
|
+
// we're in the creating state and props are un-changed
|
|
494
|
+
// let's just continue where we left off
|
|
444
495
|
return Node<Create>({
|
|
445
496
|
action: "create",
|
|
446
497
|
props: news,
|
|
447
|
-
state:
|
|
498
|
+
state: oldState,
|
|
499
|
+
});
|
|
500
|
+
} else if (diff.action === "update") {
|
|
501
|
+
// props have changed in a way that is updatable
|
|
502
|
+
// again, just continue with the create
|
|
503
|
+
// TODO(sam): should we maybe try an update instead?
|
|
504
|
+
return Node<Create>({
|
|
505
|
+
action: "create",
|
|
506
|
+
props: news,
|
|
507
|
+
state: oldState,
|
|
508
|
+
});
|
|
509
|
+
} else {
|
|
510
|
+
// props have changed in an incompatible way
|
|
511
|
+
// because it's possible that an un-updatable resource has already been created
|
|
512
|
+
// we must use a replace step to create a new one and delete the potential old one
|
|
513
|
+
return Node<Replace>({
|
|
514
|
+
action: "replace",
|
|
515
|
+
props: news,
|
|
516
|
+
deleteFirst: diff.deleteFirst ?? false,
|
|
517
|
+
state: oldState,
|
|
448
518
|
});
|
|
449
519
|
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
520
|
+
} else if (oldState.status === "updating") {
|
|
521
|
+
// Updating already targets the live resource, so noop/update both mean
|
|
522
|
+
// "finish the interrupted update". Only a replace diff escalates it
|
|
523
|
+
// into a fresh replacement.
|
|
524
|
+
if (diff.action === "update" || diff.action === "noop") {
|
|
525
|
+
// we can continue where we left off
|
|
526
|
+
return Node<Update>({
|
|
527
|
+
action: "update",
|
|
528
|
+
props: news,
|
|
529
|
+
state: oldState,
|
|
530
|
+
});
|
|
531
|
+
} else {
|
|
532
|
+
// we started to update a resource but now believe we should replace it
|
|
533
|
+
return Node<Replace>({
|
|
534
|
+
action: "replace",
|
|
535
|
+
deleteFirst: diff.deleteFirst ?? false,
|
|
536
|
+
props: news,
|
|
537
|
+
// TODO(sam): can Apply handle replacements when the oldState is UpdatingResourceState?
|
|
538
|
+
// -> or should we do a provider.read to try and reconcile back to UpdatedResourceState?
|
|
539
|
+
state: oldState,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
} else if (oldState.status === "replacing") {
|
|
543
|
+
// The replacement candidate is still being created. Noop/update keep
|
|
544
|
+
// driving the same generation; replace means that candidate itself is
|
|
545
|
+
// now obsolete and must be wrapped in a new outer generation.
|
|
546
|
+
if (diff.action === "noop") {
|
|
547
|
+
// this is the stable case - noop means just continue with the replacement
|
|
548
|
+
return Node<Replace>({
|
|
549
|
+
action: "replace",
|
|
550
|
+
deleteFirst: oldState.deleteFirst,
|
|
551
|
+
props: news,
|
|
552
|
+
state: oldState,
|
|
553
|
+
});
|
|
554
|
+
} else if (diff.action === "update") {
|
|
555
|
+
// potential problem here - the props have changed since we tried to replace,
|
|
556
|
+
// but not enough to trigger another replacement. the resource provider should
|
|
557
|
+
// be designed as idempotent to converge to the right state when creating the new resource
|
|
558
|
+
// the newly generated instanceId is intended to assist with this
|
|
559
|
+
return Node<Replace>({
|
|
560
|
+
action: "replace",
|
|
561
|
+
deleteFirst: oldState.deleteFirst,
|
|
562
|
+
props: news,
|
|
563
|
+
state: oldState,
|
|
564
|
+
});
|
|
565
|
+
} else {
|
|
566
|
+
// The in-flight replacement candidate itself now needs replacement.
|
|
567
|
+
// Mark this as a restart so Apply creates a fresh generation instead
|
|
568
|
+
// of resuming the old replacement instance.
|
|
569
|
+
return Node<Replace>({
|
|
570
|
+
restart: true,
|
|
571
|
+
action: "replace",
|
|
572
|
+
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
573
|
+
props: news,
|
|
574
|
+
state: oldState,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
} else if (oldState.status === "replaced") {
|
|
578
|
+
// The new resource already exists. Noop means "just let GC finish",
|
|
579
|
+
// update means "mutate the current replacement before GC finishes",
|
|
580
|
+
// and replace means "the current replacement also became obsolete".
|
|
581
|
+
if (diff.action === "noop") {
|
|
582
|
+
// this is the stable case - noop means just continue cleaning up the replacement
|
|
583
|
+
return Node<Replace>({
|
|
584
|
+
action: "replace",
|
|
585
|
+
deleteFirst: oldState.deleteFirst,
|
|
586
|
+
props: news,
|
|
587
|
+
state: oldState,
|
|
588
|
+
});
|
|
589
|
+
} else if (diff.action === "update") {
|
|
590
|
+
// the replacement has been created but now also needs to be updated
|
|
591
|
+
// the resource provider should:
|
|
592
|
+
// 1. Update the newly created replacement resource
|
|
593
|
+
// 2. Then proceed as normal to delete the replaced resources (after all downstream references are updated)
|
|
594
|
+
return Node<Update>({
|
|
595
|
+
action: "update",
|
|
596
|
+
props: news,
|
|
597
|
+
state: oldState,
|
|
598
|
+
});
|
|
599
|
+
} else {
|
|
600
|
+
// Cleanup is still pending, but the current "new" resource has already
|
|
601
|
+
// become obsolete. Start another replacement generation and preserve
|
|
602
|
+
// the existing replaced node as part of the recursive old chain.
|
|
603
|
+
return Node<Replace>({
|
|
604
|
+
restart: true,
|
|
605
|
+
action: "replace",
|
|
606
|
+
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
607
|
+
props: news,
|
|
608
|
+
state: oldState,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
} else if (oldState.status === "deleting") {
|
|
612
|
+
// we're in a partially deleted state, it is unclear whether it was or was not deleted
|
|
613
|
+
// so continue by re-creating it with the same instanceId and desired props
|
|
488
614
|
return Node<Create>({
|
|
489
615
|
action: "create",
|
|
490
616
|
props: news,
|
|
491
|
-
state:
|
|
617
|
+
state: {
|
|
618
|
+
...oldState,
|
|
619
|
+
status: "creating",
|
|
620
|
+
props: news,
|
|
621
|
+
},
|
|
492
622
|
});
|
|
493
623
|
} else if (diff.action === "update") {
|
|
494
|
-
//
|
|
495
|
-
// again, just continue with the create
|
|
496
|
-
// TODO(sam): should we maybe try an update instead?
|
|
497
|
-
return Node<Create>({
|
|
498
|
-
action: "create",
|
|
499
|
-
props: news,
|
|
500
|
-
state: oldState,
|
|
501
|
-
});
|
|
502
|
-
} else {
|
|
503
|
-
// props have changed in an incompatible way
|
|
504
|
-
// because it's possible that an un-updatable resource has already been created
|
|
505
|
-
// we must use a replace step to create a new one and delete the potential old one
|
|
506
|
-
return Node<Replace>({
|
|
507
|
-
action: "replace",
|
|
508
|
-
props: news,
|
|
509
|
-
deleteFirst: diff.deleteFirst ?? false,
|
|
510
|
-
state: oldState,
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
} else if (oldState.status === "updating") {
|
|
514
|
-
// Updating already targets the live resource, so noop/update both mean
|
|
515
|
-
// "finish the interrupted update". Only a replace diff escalates it
|
|
516
|
-
// into a fresh replacement.
|
|
517
|
-
if (diff.action === "update" || diff.action === "noop") {
|
|
518
|
-
// we can continue where we left off
|
|
624
|
+
// Stable created/updated resources follow the normal CRUD mapping.
|
|
519
625
|
return Node<Update>({
|
|
520
626
|
action: "update",
|
|
521
627
|
props: news,
|
|
522
628
|
state: oldState,
|
|
523
629
|
});
|
|
524
|
-
} else {
|
|
525
|
-
// we started to update a resource but now believe we should replace it
|
|
526
|
-
return Node<Replace>({
|
|
527
|
-
action: "replace",
|
|
528
|
-
deleteFirst: diff.deleteFirst ?? false,
|
|
529
|
-
props: news,
|
|
530
|
-
// TODO(sam): can Apply handle replacements when the oldState is UpdatingResourceState?
|
|
531
|
-
// -> or should we do a provider.read to try and reconcile back to UpdatedResourceState?
|
|
532
|
-
state: oldState,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
} else if (oldState.status === "replacing") {
|
|
536
|
-
// The replacement candidate is still being created. Noop/update keep
|
|
537
|
-
// driving the same generation; replace means that candidate itself is
|
|
538
|
-
// now obsolete and must be wrapped in a new outer generation.
|
|
539
|
-
if (diff.action === "noop") {
|
|
540
|
-
// this is the stable case - noop means just continue with the replacement
|
|
541
|
-
return Node<Replace>({
|
|
542
|
-
action: "replace",
|
|
543
|
-
deleteFirst: oldState.deleteFirst,
|
|
544
|
-
props: news,
|
|
545
|
-
state: oldState,
|
|
546
|
-
});
|
|
547
|
-
} else if (diff.action === "update") {
|
|
548
|
-
// potential problem here - the props have changed since we tried to replace,
|
|
549
|
-
// but not enough to trigger another replacement. the resource provider should
|
|
550
|
-
// be designed as idempotent to converge to the right state when creating the new resource
|
|
551
|
-
// the newly generated instanceId is intended to assist with this
|
|
552
|
-
return Node<Replace>({
|
|
553
|
-
action: "replace",
|
|
554
|
-
deleteFirst: oldState.deleteFirst,
|
|
555
|
-
props: news,
|
|
556
|
-
state: oldState,
|
|
557
|
-
});
|
|
558
|
-
} else {
|
|
559
|
-
// The in-flight replacement candidate itself now needs replacement.
|
|
560
|
-
// Mark this as a restart so Apply creates a fresh generation instead
|
|
561
|
-
// of resuming the old replacement instance.
|
|
562
|
-
return Node<Replace>({
|
|
563
|
-
restart: true,
|
|
564
|
-
action: "replace",
|
|
565
|
-
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
566
|
-
props: news,
|
|
567
|
-
state: oldState,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
} else if (oldState.status === "replaced") {
|
|
571
|
-
// The new resource already exists. Noop means "just let GC finish",
|
|
572
|
-
// update means "mutate the current replacement before GC finishes",
|
|
573
|
-
// and replace means "the current replacement also became obsolete".
|
|
574
|
-
if (diff.action === "noop") {
|
|
575
|
-
// this is the stable case - noop means just continue cleaning up the replacement
|
|
630
|
+
} else if (diff.action === "replace") {
|
|
576
631
|
return Node<Replace>({
|
|
577
632
|
action: "replace",
|
|
578
|
-
deleteFirst: oldState.deleteFirst,
|
|
579
|
-
props: news,
|
|
580
|
-
state: oldState,
|
|
581
|
-
});
|
|
582
|
-
} else if (diff.action === "update") {
|
|
583
|
-
// the replacement has been created but now also needs to be updated
|
|
584
|
-
// the resource provider should:
|
|
585
|
-
// 1. Update the newly created replacement resource
|
|
586
|
-
// 2. Then proceed as normal to delete the replaced resources (after all downstream references are updated)
|
|
587
|
-
return Node<Update>({
|
|
588
|
-
action: "update",
|
|
589
633
|
props: news,
|
|
590
634
|
state: oldState,
|
|
635
|
+
deleteFirst: diff?.deleteFirst ?? false,
|
|
591
636
|
});
|
|
592
637
|
} else {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
// the existing replaced node as part of the recursive old chain.
|
|
596
|
-
return Node<Replace>({
|
|
597
|
-
restart: true,
|
|
598
|
-
action: "replace",
|
|
599
|
-
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
600
|
-
props: news,
|
|
638
|
+
return Node<NoopUpdate>({
|
|
639
|
+
action: "noop",
|
|
601
640
|
state: oldState,
|
|
602
641
|
});
|
|
603
642
|
}
|
|
604
|
-
}
|
|
605
|
-
// we're in a partially deleted state, it is unclear whether it was or was not deleted
|
|
606
|
-
// so continue by re-creating it with the same instanceId and desired props
|
|
607
|
-
return Node<Create>({
|
|
608
|
-
action: "create",
|
|
609
|
-
props: news,
|
|
610
|
-
state: {
|
|
611
|
-
...oldState,
|
|
612
|
-
status: "creating",
|
|
613
|
-
props: news,
|
|
614
|
-
},
|
|
615
|
-
});
|
|
616
|
-
} else if (diff.action === "update") {
|
|
617
|
-
// Stable created/updated resources follow the normal CRUD mapping.
|
|
618
|
-
return Node<Update>({
|
|
619
|
-
action: "update",
|
|
620
|
-
props: news,
|
|
621
|
-
state: oldState,
|
|
622
|
-
});
|
|
623
|
-
} else if (diff.action === "replace") {
|
|
624
|
-
return Node<Replace>({
|
|
625
|
-
action: "replace",
|
|
626
|
-
props: news,
|
|
627
|
-
state: oldState,
|
|
628
|
-
deleteFirst: diff?.deleteFirst ?? false,
|
|
629
|
-
});
|
|
630
|
-
} else {
|
|
631
|
-
return Node<NoopUpdate>({
|
|
632
|
-
action: "noop",
|
|
633
|
-
state: oldState,
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
}),
|
|
637
|
-
),
|
|
638
|
-
{ concurrency: "unbounded" },
|
|
639
|
-
)).map((update) => [update.resource.FQN, update]),
|
|
640
|
-
) as Plan["resources"];
|
|
641
|
-
|
|
642
|
-
// Detect unsatisfiable dependency cycles among create/replace nodes.
|
|
643
|
-
// Update/noop nodes signal their Deferred before waitForDeps so they
|
|
644
|
-
// cannot deadlock. Create/replace nodes only signal early when they
|
|
645
|
-
// have a precreate handler. Simulate the concurrent execution:
|
|
646
|
-
// precreate nodes are immediately "resolved", then iteratively resolve
|
|
647
|
-
// any node whose deps are all resolved. Remaining nodes would deadlock.
|
|
648
|
-
{
|
|
649
|
-
const createReplaceNodes = new Set(
|
|
650
|
-
Object.entries(resourceGraph)
|
|
651
|
-
.filter(
|
|
652
|
-
([_, node]) =>
|
|
653
|
-
node.action === "create" || node.action === "replace",
|
|
654
|
-
)
|
|
655
|
-
.map(([fqn]) => fqn),
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
if (createReplaceNodes.size > 0) {
|
|
659
|
-
const hasPrecreate = new Set(
|
|
660
|
-
[...createReplaceNodes].filter(
|
|
661
|
-
(fqn) => !!resourceGraph[fqn]?.provider?.precreate,
|
|
643
|
+
}),
|
|
662
644
|
),
|
|
645
|
+
{ concurrency: "unbounded" },
|
|
646
|
+
)).map((update) => [update.resource.FQN, update]),
|
|
647
|
+
) as Plan["resources"];
|
|
648
|
+
|
|
649
|
+
// Detect unsatisfiable dependency cycles among create/replace nodes.
|
|
650
|
+
// Update/noop nodes signal their Deferred before waitForDeps so they
|
|
651
|
+
// cannot deadlock. Create/replace nodes only signal early when they
|
|
652
|
+
// have a precreate handler. Simulate the concurrent execution:
|
|
653
|
+
// precreate nodes are immediately "resolved", then iteratively resolve
|
|
654
|
+
// any node whose deps are all resolved. Remaining nodes would deadlock.
|
|
655
|
+
{
|
|
656
|
+
const createReplaceNodes = new Set(
|
|
657
|
+
Object.entries(resourceGraph)
|
|
658
|
+
.filter(
|
|
659
|
+
([_, node]) =>
|
|
660
|
+
node.action === "create" || node.action === "replace",
|
|
661
|
+
)
|
|
662
|
+
.map(([fqn]) => fqn),
|
|
663
663
|
);
|
|
664
664
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
665
|
+
if (createReplaceNodes.size > 0) {
|
|
666
|
+
const hasPrecreate = new Set(
|
|
667
|
+
[...createReplaceNodes].filter(
|
|
668
|
+
(fqn) => !!resourceGraph[fqn]?.provider?.precreate,
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const resolved = new Set(hasPrecreate);
|
|
673
|
+
let changed = true;
|
|
674
|
+
while (changed) {
|
|
675
|
+
changed = false;
|
|
676
|
+
for (const fqn of createReplaceNodes) {
|
|
677
|
+
if (resolved.has(fqn)) continue;
|
|
678
|
+
const deps = (allUpstreamDependencies[fqn] ?? []).filter((dep) =>
|
|
679
|
+
createReplaceNodes.has(dep),
|
|
680
|
+
);
|
|
681
|
+
if (deps.every((dep) => resolved.has(dep))) {
|
|
682
|
+
resolved.add(fqn);
|
|
683
|
+
changed = true;
|
|
684
|
+
}
|
|
677
685
|
}
|
|
678
686
|
}
|
|
679
|
-
}
|
|
680
687
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
);
|
|
684
|
-
if (deadlocked.length > 0) {
|
|
685
|
-
const missingPrecreate = deadlocked.filter(
|
|
686
|
-
(fqn) => !hasPrecreate.has(fqn),
|
|
687
|
-
);
|
|
688
|
-
return yield* Effect.die(
|
|
689
|
-
new UnsatisfiedResourceCycle({
|
|
690
|
-
message:
|
|
691
|
-
`Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. ` +
|
|
692
|
-
`Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. ` +
|
|
693
|
-
`All resources in a dependency cycle must implement precreate to allow early signaling.`,
|
|
694
|
-
cycle: deadlocked,
|
|
695
|
-
missingPrecreate,
|
|
696
|
-
}),
|
|
688
|
+
const deadlocked = [...createReplaceNodes].filter(
|
|
689
|
+
(fqn) => !resolved.has(fqn),
|
|
697
690
|
);
|
|
691
|
+
if (deadlocked.length > 0) {
|
|
692
|
+
const missingPrecreate = deadlocked.filter(
|
|
693
|
+
(fqn) => !hasPrecreate.has(fqn),
|
|
694
|
+
);
|
|
695
|
+
return yield* Effect.die(
|
|
696
|
+
new UnsatisfiedResourceCycle({
|
|
697
|
+
message:
|
|
698
|
+
`Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. ` +
|
|
699
|
+
`Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. ` +
|
|
700
|
+
`All resources in a dependency cycle must implement precreate to allow early signaling.`,
|
|
701
|
+
cycle: deadlocked,
|
|
702
|
+
missingPrecreate,
|
|
703
|
+
}),
|
|
704
|
+
);
|
|
705
|
+
}
|
|
698
706
|
}
|
|
699
707
|
}
|
|
700
|
-
}
|
|
701
708
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
const oldState = yield* state.get({
|
|
711
|
-
stack: stackName,
|
|
712
|
-
stage: stage,
|
|
713
|
-
fqn,
|
|
714
|
-
});
|
|
715
|
-
let attr: any = oldState?.attr;
|
|
716
|
-
if (oldState) {
|
|
717
|
-
const { logicalId } = parseFqn(fqn);
|
|
718
|
-
const resourceType = oldState.resourceType;
|
|
719
|
-
const provider = yield* getProviderByType(resourceType);
|
|
720
|
-
if (oldState.attr === undefined) {
|
|
721
|
-
if (provider.read) {
|
|
722
|
-
attr = yield* provider
|
|
723
|
-
.read({
|
|
724
|
-
id: logicalId,
|
|
725
|
-
instanceId: oldState.instanceId,
|
|
726
|
-
olds: oldState.props as never,
|
|
727
|
-
output: oldState.attr as never,
|
|
728
|
-
})
|
|
729
|
-
.pipe(
|
|
730
|
-
Effect.provideService(InstanceId, oldState.instanceId),
|
|
731
|
-
);
|
|
732
|
-
}
|
|
709
|
+
const deletions = Object.fromEntries(
|
|
710
|
+
(yield* Effect.all(
|
|
711
|
+
(yield* state.list({ stack: stackName, stage: stage })).map(
|
|
712
|
+
Effect.fn(function* (fqn) {
|
|
713
|
+
// Check if this FQN is in the new resources
|
|
714
|
+
if (newResourceFqns.has(fqn)) {
|
|
715
|
+
return;
|
|
733
716
|
}
|
|
734
|
-
|
|
717
|
+
const oldState = yield* state.get({
|
|
718
|
+
stack: stackName,
|
|
719
|
+
stage: stage,
|
|
735
720
|
fqn,
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
721
|
+
});
|
|
722
|
+
let attr: any = oldState?.attr;
|
|
723
|
+
if (oldState) {
|
|
724
|
+
const { logicalId } = parseFqn(fqn);
|
|
725
|
+
const resourceType = oldState.resourceType;
|
|
726
|
+
const provider = yield* getProviderByType(resourceType);
|
|
727
|
+
if (oldState.attr === undefined) {
|
|
728
|
+
if (provider.read) {
|
|
729
|
+
attr = yield* provider
|
|
730
|
+
.read({
|
|
731
|
+
id: logicalId,
|
|
732
|
+
instanceId: oldState.instanceId,
|
|
733
|
+
olds: oldState.props as never,
|
|
734
|
+
output: oldState.attr as never,
|
|
735
|
+
})
|
|
736
|
+
.pipe(providePlanScope(fqn, oldState.instanceId));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return [
|
|
740
|
+
fqn,
|
|
741
|
+
{
|
|
742
|
+
action: "delete",
|
|
743
|
+
state: { ...oldState, attr },
|
|
744
|
+
provider: provider,
|
|
745
|
+
resource: {
|
|
746
|
+
Namespace: oldState.namespace,
|
|
747
|
+
FQN: fqn,
|
|
748
|
+
LogicalId: logicalId,
|
|
749
|
+
Type: oldState.resourceType,
|
|
750
|
+
Attributes: attr,
|
|
751
|
+
Props: oldState.props,
|
|
752
|
+
Binding: undefined!,
|
|
753
|
+
Provider: Provider(resourceType),
|
|
754
|
+
RemovalPolicy: oldState.removalPolicy,
|
|
755
|
+
ExecutionContext: undefined!,
|
|
756
|
+
} as ResourceLike,
|
|
757
|
+
downstream: oldDownstreamDependencies[fqn] ?? [],
|
|
758
|
+
bindings: oldState.bindings.map((binding) => ({
|
|
759
|
+
sid: binding.sid,
|
|
760
|
+
action: "delete" as const,
|
|
761
|
+
data: binding.data,
|
|
762
|
+
})),
|
|
763
|
+
} satisfies Delete,
|
|
764
|
+
] as const;
|
|
765
|
+
}
|
|
766
|
+
}),
|
|
767
|
+
),
|
|
768
|
+
{ concurrency: "unbounded" },
|
|
769
|
+
)).filter((v) => !!v),
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
for (const resourceFqn of Object.keys(deletions)) {
|
|
773
|
+
const dependencies = Object.entries(rawUpstreamDependencies)
|
|
774
|
+
.filter(
|
|
775
|
+
([survivorFqn, upstream]) =>
|
|
776
|
+
survivorFqn in resourceGraph && upstream.includes(resourceFqn),
|
|
777
|
+
)
|
|
778
|
+
.map(([survivorFqn]) => survivorFqn);
|
|
779
|
+
if (dependencies.length > 0) {
|
|
780
|
+
return yield* new DeleteResourceHasDownstreamDependencies({
|
|
781
|
+
message: `Resource ${resourceFqn} has downstream dependencies`,
|
|
782
|
+
resourceId: resourceFqn,
|
|
783
|
+
dependencies,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
780
786
|
}
|
|
781
|
-
}
|
|
782
787
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
788
|
+
return {
|
|
789
|
+
resources: resourceGraph,
|
|
790
|
+
deletions,
|
|
791
|
+
output: stack.output,
|
|
792
|
+
} satisfies Plan<A> as Plan<A>;
|
|
793
|
+
}),
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
const providePlanScope =
|
|
797
|
+
(fqn: string, instanceId: string) =>
|
|
798
|
+
<A, E, R>(
|
|
799
|
+
effect: Effect.Effect<A, E, R>,
|
|
800
|
+
): Effect.Effect<A, E, Exclude<R, InstanceId | Artifacts>> =>
|
|
801
|
+
Effect.serviceOption(ArtifactStore).pipe(
|
|
802
|
+
Effect.map(Option.getOrElse(createArtifactStore)),
|
|
803
|
+
Effect.flatMap((store) =>
|
|
804
|
+
effect.pipe(
|
|
805
|
+
Effect.provideService(Artifacts, makeScopedArtifacts(store, fqn)),
|
|
806
|
+
Effect.provideService(InstanceId, instanceId),
|
|
807
|
+
),
|
|
808
|
+
),
|
|
809
|
+
) as Effect.Effect<A, E, Exclude<R, InstanceId | Artifacts>>;
|
|
789
810
|
|
|
790
811
|
export class DeleteResourceHasDownstreamDependencies extends Data.TaggedError(
|
|
791
812
|
"DeleteResourceHasDownstreamDependencies",
|