alchemy 2.0.0-beta.64 → 2.0.0-beta.65
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/lib/AWS/EC2/InternetGateway.d.ts.map +1 -1
- package/lib/AWS/EC2/InternetGateway.js +8 -3
- package/lib/AWS/EC2/InternetGateway.js.map +1 -1
- package/lib/AWS/EC2/LingeringEnis.d.ts +26 -0
- package/lib/AWS/EC2/LingeringEnis.d.ts.map +1 -0
- package/lib/AWS/EC2/LingeringEnis.js +128 -0
- package/lib/AWS/EC2/LingeringEnis.js.map +1 -0
- package/lib/AWS/EC2/SecurityGroup.d.ts.map +1 -1
- package/lib/AWS/EC2/SecurityGroup.js +14 -14
- package/lib/AWS/EC2/SecurityGroup.js.map +1 -1
- package/lib/AWS/EC2/Subnet.d.ts.map +1 -1
- package/lib/AWS/EC2/Subnet.js +13 -21
- package/lib/AWS/EC2/Subnet.js.map +1 -1
- package/lib/AWS/ECR/Image.d.ts +1 -0
- package/lib/AWS/ECR/Image.d.ts.map +1 -1
- package/lib/AWS/EKS/Addon.d.ts.map +1 -1
- package/lib/AWS/EKS/Addon.js +15 -3
- package/lib/AWS/EKS/Addon.js.map +1 -1
- package/lib/AWS/EKS/Deployment.d.ts +10 -1
- package/lib/AWS/EKS/Deployment.d.ts.map +1 -1
- package/lib/AWS/EKS/Deployment.js +10 -4
- package/lib/AWS/EKS/Deployment.js.map +1 -1
- package/lib/AWS/EKS/HyperPod.d.ts +57 -0
- package/lib/AWS/EKS/HyperPod.d.ts.map +1 -0
- package/lib/AWS/EKS/HyperPod.js +46 -0
- package/lib/AWS/EKS/HyperPod.js.map +1 -0
- package/lib/AWS/EKS/Job.d.ts +10 -1
- package/lib/AWS/EKS/Job.d.ts.map +1 -1
- package/lib/AWS/EKS/Job.js +16 -6
- package/lib/AWS/EKS/Job.js.map +1 -1
- package/lib/AWS/EKS/internal/client.d.ts +4 -3
- package/lib/AWS/EKS/internal/client.d.ts.map +1 -1
- package/lib/AWS/EKS/internal/client.js +33 -2
- package/lib/AWS/EKS/internal/client.js.map +1 -1
- package/lib/AWS/Lambda/Function.d.ts.map +1 -1
- package/lib/AWS/Lambda/Function.js +1 -1
- package/lib/AWS/Lambda/Function.js.map +1 -1
- package/lib/AWS/Providers.d.ts.map +1 -1
- package/lib/AWS/Providers.js +4 -1
- package/lib/AWS/Providers.js.map +1 -1
- package/lib/AWS/SageMaker/Cluster.d.ts +327 -0
- package/lib/AWS/SageMaker/Cluster.d.ts.map +1 -0
- package/lib/AWS/SageMaker/Cluster.js +469 -0
- package/lib/AWS/SageMaker/Cluster.js.map +1 -0
- package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts +96 -0
- package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts.map +1 -0
- package/lib/AWS/SageMaker/ClusterSchedulerConfig.js +270 -0
- package/lib/AWS/SageMaker/ClusterSchedulerConfig.js.map +1 -0
- package/lib/AWS/SageMaker/ComputeQuota.d.ts +106 -0
- package/lib/AWS/SageMaker/ComputeQuota.d.ts.map +1 -0
- package/lib/AWS/SageMaker/ComputeQuota.js +268 -0
- package/lib/AWS/SageMaker/ComputeQuota.js.map +1 -0
- package/lib/AWS/SageMaker/index.d.ts +3 -0
- package/lib/AWS/SageMaker/index.d.ts.map +1 -1
- package/lib/AWS/SageMaker/index.js +3 -0
- package/lib/AWS/SageMaker/index.js.map +1 -1
- package/lib/Bundle/PurePlugin.d.ts +29 -7
- package/lib/Bundle/PurePlugin.d.ts.map +1 -1
- package/lib/Bundle/PurePlugin.js +56 -33
- package/lib/Bundle/PurePlugin.js.map +1 -1
- package/lib/Cli/commands/nuke.d.ts.map +1 -1
- package/lib/Cli/commands/nuke.js +231 -37
- package/lib/Cli/commands/nuke.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +26 -8
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Assets.js +46 -1
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +2 -2
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +39 -6
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +7 -5
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +0 -1
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -2
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts +20 -1
- package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerBundle.js +11 -0
- package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/WorkerProvider.js +10 -4
- package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts +2 -1
- package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts.map +1 -1
- package/lib/Cloudflare/Workflows/WorkflowBridge.js +5 -5
- package/lib/Cloudflare/Workflows/WorkflowBridge.js.map +1 -1
- package/lib/Cloudflare/Workflows/index.d.ts +1 -1
- package/lib/Cloudflare/Workflows/index.d.ts.map +1 -1
- package/lib/Cloudflare/Workflows/index.js +1 -1
- package/lib/Cloudflare/Workflows/index.js.map +1 -1
- package/lib/Docker/Container.d.ts +26 -0
- package/lib/Docker/Container.d.ts.map +1 -1
- package/lib/Docker/Container.js +20 -1
- package/lib/Docker/Container.js.map +1 -1
- package/lib/Docker/Docker.d.ts +2 -0
- package/lib/Docker/Docker.d.ts.map +1 -1
- package/lib/Docker/Docker.js.map +1 -1
- package/lib/Output.d.ts +4 -1
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js.map +1 -1
- package/lib/Provider.d.ts +20 -0
- package/lib/Provider.d.ts.map +1 -1
- package/lib/Provider.js.map +1 -1
- package/lib/Random.d.ts +1 -1
- package/lib/Random.d.ts.map +1 -1
- package/lib/Resource.d.ts +14 -1
- package/lib/Resource.d.ts.map +1 -1
- package/lib/Resource.js.map +1 -1
- package/lib/SQL/D1.d.ts +1 -1
- package/lib/SQL/D1.js +1 -1
- package/lib/SQL/Postgres.d.ts +1 -1
- package/lib/SQL/Postgres.js +1 -1
- package/lib/State/HttpStateApi.d.ts +11 -11
- package/lib/Test/Alchemy.d.ts +1 -1
- package/lib/Test/Alchemy.js +1 -1
- package/lib/Test/Vitest.d.ts +58 -0
- package/lib/Test/Vitest.d.ts.map +1 -0
- package/lib/Test/Vitest.js +114 -0
- package/lib/Test/Vitest.js.map +1 -0
- package/lib/Util/proxy-chain.d.ts +5 -0
- package/lib/Util/proxy-chain.d.ts.map +1 -1
- package/lib/Util/proxy-chain.js +52 -4
- package/lib/Util/proxy-chain.js.map +1 -1
- package/package.json +23 -15
- package/src/AWS/EC2/InternetGateway.ts +8 -3
- package/src/AWS/EC2/LingeringEnis.ts +169 -0
- package/src/AWS/EC2/SecurityGroup.ts +24 -28
- package/src/AWS/EC2/Subnet.ts +23 -34
- package/src/AWS/EKS/Addon.ts +17 -4
- package/src/AWS/EKS/Deployment.ts +26 -5
- package/src/AWS/EKS/HyperPod.ts +97 -0
- package/src/AWS/EKS/Job.ts +32 -7
- package/src/AWS/EKS/internal/client.ts +40 -3
- package/src/AWS/Lambda/Function.ts +5 -3
- package/src/AWS/Providers.ts +6 -0
- package/src/AWS/SageMaker/Cluster.ts +775 -0
- package/src/AWS/SageMaker/ClusterSchedulerConfig.ts +430 -0
- package/src/AWS/SageMaker/ComputeQuota.ts +426 -0
- package/src/AWS/SageMaker/index.ts +3 -0
- package/src/Bundle/PurePlugin.ts +92 -39
- package/src/Cli/commands/nuke.ts +295 -56
- package/src/Cloudflare/Workers/Assets.ts +53 -1
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +46 -7
- package/src/Cloudflare/Workers/Worker.ts +7 -5
- package/src/Cloudflare/Workers/WorkerBundle.ts +31 -1
- package/src/Cloudflare/Workers/WorkerProvider.ts +18 -4
- package/src/Cloudflare/Workflows/WorkflowBridge.ts +5 -5
- package/src/Cloudflare/Workflows/index.ts +1 -1
- package/src/Docker/Container.ts +30 -1
- package/src/Docker/Docker.ts +2 -0
- package/src/Output.ts +19 -11
- package/src/Provider.ts +20 -0
- package/src/Resource.ts +18 -1
- package/src/SQL/D1.ts +1 -1
- package/src/SQL/Postgres.ts +1 -1
- package/src/Test/Alchemy.ts +1 -1
- package/src/Test/Vitest.ts +244 -0
- package/src/Util/proxy-chain.ts +73 -4
- package/lib/SQL/index.d.ts +0 -4
- package/lib/SQL/index.d.ts.map +0 -1
- package/lib/SQL/index.js +0 -4
- package/lib/SQL/index.js.map +0 -1
- package/src/SQL/index.ts +0 -3
package/src/Cli/commands/nuke.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as Effect from "effect/Effect";
|
|
|
4
4
|
import * as Layer from "effect/Layer";
|
|
5
5
|
import * as Logger from "effect/Logger";
|
|
6
6
|
import { MinimumLogLevel } from "effect/References";
|
|
7
|
+
import * as Schedule from "effect/Schedule";
|
|
7
8
|
import { Command, Flag } from "effect/unstable/cli";
|
|
8
9
|
import picomatch from "picomatch";
|
|
9
10
|
|
|
@@ -75,6 +76,26 @@ const timeoutFlag = Flag.integer("timeout").pipe(
|
|
|
75
76
|
Flag.withDefault(120),
|
|
76
77
|
);
|
|
77
78
|
|
|
79
|
+
const independentFlag = Flag.boolean("independent").pipe(
|
|
80
|
+
Flag.withDescription(
|
|
81
|
+
"Delete every resource independently instead of in coordinated passes. " +
|
|
82
|
+
"In pass mode a single slow or hanging delete delays the next pass for " +
|
|
83
|
+
"everything; with --independent each resource retries its own delete " +
|
|
84
|
+
"with backoff, in parallel, until it succeeds or --retries is " +
|
|
85
|
+
"exhausted. Dependency violations resolve naturally as the blocking " +
|
|
86
|
+
"resources are deleted concurrently.",
|
|
87
|
+
),
|
|
88
|
+
Flag.withDefault(false),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const retriesFlag = Flag.integer("retries").pipe(
|
|
92
|
+
Flag.withDescription(
|
|
93
|
+
"With --independent: retries per resource after the initial delete " +
|
|
94
|
+
"attempt (each attempt still bounded by --timeout). Default: 10.",
|
|
95
|
+
),
|
|
96
|
+
Flag.withDefault(10),
|
|
97
|
+
);
|
|
98
|
+
|
|
78
99
|
interface DiscoveredProvider {
|
|
79
100
|
id: string;
|
|
80
101
|
provider: ProviderService;
|
|
@@ -262,6 +283,59 @@ const groupBy = <T>(items: T[], key: (item: T) => string): Map<string, T[]> => {
|
|
|
262
283
|
return out;
|
|
263
284
|
};
|
|
264
285
|
|
|
286
|
+
const addEdge = (map: Map<string, Set<string>>, from: string, to: string) => {
|
|
287
|
+
const set = map.get(from) ?? new Set<string>();
|
|
288
|
+
set.add(to);
|
|
289
|
+
map.set(from, set);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Strongly-connected components of the type-level teardown-dependency graph
|
|
294
|
+
* (Tarjan). Components are emitted in reverse topological order of the
|
|
295
|
+
* condensation — every component's successors are emitted before it — so
|
|
296
|
+
* callers can compute layers by iterating the result backwards.
|
|
297
|
+
*/
|
|
298
|
+
const stronglyConnectedComponents = (
|
|
299
|
+
nodes: readonly string[],
|
|
300
|
+
successors: Map<string, Set<string>>,
|
|
301
|
+
): string[][] => {
|
|
302
|
+
const index = new Map<string, number>();
|
|
303
|
+
const low = new Map<string, number>();
|
|
304
|
+
const onStack = new Set<string>();
|
|
305
|
+
const stack: string[] = [];
|
|
306
|
+
const components: string[][] = [];
|
|
307
|
+
let counter = 0;
|
|
308
|
+
const strongConnect = (v: string): void => {
|
|
309
|
+
index.set(v, counter);
|
|
310
|
+
low.set(v, counter);
|
|
311
|
+
counter += 1;
|
|
312
|
+
stack.push(v);
|
|
313
|
+
onStack.add(v);
|
|
314
|
+
for (const w of successors.get(v) ?? []) {
|
|
315
|
+
if (!index.has(w)) {
|
|
316
|
+
strongConnect(w);
|
|
317
|
+
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
|
318
|
+
} else if (onStack.has(w)) {
|
|
319
|
+
low.set(v, Math.min(low.get(v)!, index.get(w)!));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (low.get(v) === index.get(v)) {
|
|
323
|
+
const component: string[] = [];
|
|
324
|
+
for (;;) {
|
|
325
|
+
const w = stack.pop()!;
|
|
326
|
+
onStack.delete(w);
|
|
327
|
+
component.push(w);
|
|
328
|
+
if (w === v) break;
|
|
329
|
+
}
|
|
330
|
+
components.push(component);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
for (const v of nodes) {
|
|
334
|
+
if (!index.has(v)) strongConnect(v);
|
|
335
|
+
}
|
|
336
|
+
return components;
|
|
337
|
+
};
|
|
338
|
+
|
|
265
339
|
const nukeSession: ScopedPlanStatusSession = {
|
|
266
340
|
emit: () => Effect.void,
|
|
267
341
|
done: () => Effect.void,
|
|
@@ -278,6 +352,8 @@ const nukeCommand = Command.make(
|
|
|
278
352
|
verbose: verboseFlag,
|
|
279
353
|
concurrency: concurrencyFlag,
|
|
280
354
|
timeout: timeoutFlag,
|
|
355
|
+
independent: independentFlag,
|
|
356
|
+
retries: retriesFlag,
|
|
281
357
|
include: includeFlag,
|
|
282
358
|
exclude: excludeFlag,
|
|
283
359
|
filter: filterFlag,
|
|
@@ -295,6 +371,8 @@ const nukeCommand = Command.make(
|
|
|
295
371
|
verbose,
|
|
296
372
|
concurrency,
|
|
297
373
|
timeout,
|
|
374
|
+
independent,
|
|
375
|
+
retries,
|
|
298
376
|
include,
|
|
299
377
|
exclude,
|
|
300
378
|
filter,
|
|
@@ -476,69 +554,223 @@ const nukeCommand = Command.make(
|
|
|
476
554
|
const emitDelete = (event: NukeUI.DeleteEvent) =>
|
|
477
555
|
deleteUI ? Effect.sync(() => deleteUI.emit(event)) : Effect.void;
|
|
478
556
|
|
|
479
|
-
|
|
557
|
+
// One delete attempt for a single resource, bounded by --timeout, with
|
|
558
|
+
// the failure cause logged to the stack's file logger. Failure stays on
|
|
559
|
+
// the error channel so callers can retry or fold it as they see fit.
|
|
560
|
+
const attemptDelete = (item: (typeof targets)[number]) =>
|
|
561
|
+
item.provider
|
|
562
|
+
.delete({
|
|
563
|
+
id: displayName(item.attr),
|
|
564
|
+
// Enumerated straight from the cloud, so there is no
|
|
565
|
+
// Alchemy namespace — an un-namespaced fqn is just the id.
|
|
566
|
+
fqn: displayName(item.attr),
|
|
567
|
+
instanceId: "",
|
|
568
|
+
olds: item.attr as never,
|
|
569
|
+
output: item.attr as never,
|
|
570
|
+
session: nukeSession,
|
|
571
|
+
bindings: [],
|
|
572
|
+
// Nuke is an explicit, operator-confirmed account
|
|
573
|
+
// teardown: allow destructive prerequisites (e.g.
|
|
574
|
+
// emptying a bucket) that normal destroys gate behind
|
|
575
|
+
// props such as `forceDestroy` — `olds` here is cloud
|
|
576
|
+
// Attributes, not the originally-deployed Props.
|
|
577
|
+
force: true,
|
|
578
|
+
})
|
|
579
|
+
.pipe(
|
|
580
|
+
Effect.timeout(`${timeout} seconds`),
|
|
581
|
+
Effect.tapCause((cause) =>
|
|
582
|
+
Effect.logWarning(
|
|
583
|
+
`nuke: delete failed for ${item.id} ${displayName(item.attr)}`,
|
|
584
|
+
cause,
|
|
585
|
+
),
|
|
586
|
+
),
|
|
587
|
+
Effect.provide(context),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const retrySchedule = Schedule.min([
|
|
591
|
+
Schedule.exponential("1 second"),
|
|
592
|
+
Schedule.spaced("15 seconds"),
|
|
593
|
+
]);
|
|
594
|
+
|
|
480
595
|
let pass = 0;
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
596
|
+
|
|
597
|
+
// Delete one tier of resources to completion, returning the items
|
|
598
|
+
// that could not be deleted.
|
|
599
|
+
const runTier = (tier: typeof targets) =>
|
|
600
|
+
Effect.gen(function* () {
|
|
601
|
+
if (tier.length === 0) return [] as typeof targets;
|
|
602
|
+
if (independent) {
|
|
603
|
+
// Independent mode: no pass barrier. Every resource retries its
|
|
604
|
+
// own delete with capped exponential backoff, all in parallel
|
|
605
|
+
// (provider groups bounded by --concurrency, resources within a
|
|
606
|
+
// provider unbounded, matching pass mode). A dependency
|
|
607
|
+
// violation resolves on a later attempt once the blocking
|
|
608
|
+
// resource's concurrent delete lands — a hanging delete only
|
|
609
|
+
// ever stalls itself.
|
|
610
|
+
pass += 1;
|
|
611
|
+
yield* emitDelete({ kind: "pass", pass });
|
|
612
|
+
const results = yield* Effect.all(
|
|
613
|
+
[...groupBy(tier, (t) => t.id).values()].map((items) =>
|
|
614
|
+
Effect.all(
|
|
615
|
+
items.map((item) =>
|
|
616
|
+
attemptDelete(item).pipe(
|
|
617
|
+
Effect.retry({ schedule: retrySchedule, times: retries }),
|
|
618
|
+
Effect.matchCause({
|
|
619
|
+
onSuccess: () => ({ item, ok: true as const }),
|
|
620
|
+
onFailure: () => ({ item, ok: false as const }),
|
|
621
|
+
}),
|
|
622
|
+
// Only the final outcome is emitted, so the UI's
|
|
623
|
+
// failure count reflects exhausted resources, not
|
|
624
|
+
// transient attempts.
|
|
625
|
+
Effect.tap((r) =>
|
|
626
|
+
r.ok
|
|
627
|
+
? emitDelete({ kind: "deleted", id: item.id })
|
|
628
|
+
: emitDelete({ kind: "failed", id: item.id }),
|
|
514
629
|
),
|
|
515
630
|
),
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
631
|
+
),
|
|
632
|
+
{ concurrency: "unbounded" },
|
|
633
|
+
),
|
|
634
|
+
),
|
|
635
|
+
{ concurrency },
|
|
636
|
+
);
|
|
637
|
+
return results.flat().flatMap((r) => (r.ok ? [] : [r.item]));
|
|
638
|
+
}
|
|
639
|
+
let remaining = tier;
|
|
640
|
+
while (remaining.length > 0) {
|
|
641
|
+
pass += 1;
|
|
642
|
+
yield* emitDelete({ kind: "pass", pass });
|
|
643
|
+
|
|
644
|
+
const byType = groupBy(remaining, (t) => t.id);
|
|
645
|
+
const results = yield* Effect.all(
|
|
646
|
+
[...byType.values()].map((items) =>
|
|
647
|
+
Effect.all(
|
|
648
|
+
items.map((item) =>
|
|
649
|
+
attemptDelete(item).pipe(
|
|
650
|
+
Effect.matchCause({
|
|
651
|
+
onSuccess: () => ({ item, ok: true as const }),
|
|
652
|
+
onFailure: () => ({ item, ok: false as const }),
|
|
653
|
+
}),
|
|
654
|
+
Effect.tap((r) =>
|
|
655
|
+
r.ok
|
|
656
|
+
? emitDelete({ kind: "deleted", id: item.id })
|
|
657
|
+
: emitDelete({ kind: "failed", id: item.id }),
|
|
658
|
+
),
|
|
525
659
|
),
|
|
526
660
|
),
|
|
661
|
+
{ concurrency: "unbounded" },
|
|
662
|
+
),
|
|
527
663
|
),
|
|
528
|
-
{ concurrency
|
|
529
|
-
)
|
|
530
|
-
),
|
|
531
|
-
{ concurrency },
|
|
532
|
-
);
|
|
664
|
+
{ concurrency },
|
|
665
|
+
);
|
|
533
666
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
667
|
+
const failed = results
|
|
668
|
+
.flat()
|
|
669
|
+
.flatMap((r) => (r.ok ? [] : [r.item]));
|
|
670
|
+
// No resource deleted this pass: dependencies can't resolve
|
|
671
|
+
// further, so stop instead of looping forever.
|
|
672
|
+
if (failed.length === remaining.length) {
|
|
673
|
+
return failed;
|
|
674
|
+
}
|
|
675
|
+
remaining = failed;
|
|
676
|
+
}
|
|
677
|
+
return remaining;
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// ---- Teardown-dependency ordering ---------------------------------
|
|
681
|
+
// Providers declare `nuke.dependsOn: [globs]` when their cloud-side
|
|
682
|
+
// teardown CONSUMES another type (e.g. SageMaker HyperPod deletes
|
|
683
|
+
// node ENIs by assuming the instance group's execution role inside
|
|
684
|
+
// the cluster's VPC — deleting the role or network mid-teardown
|
|
685
|
+
// wedges the cluster in `Deleting` forever). Edge A→B means every A
|
|
686
|
+
// must be fully GONE before any B is deleted. Only types present in
|
|
687
|
+
// the target set participate, so the constraint costs nothing when no
|
|
688
|
+
// dependent resources exist; declaration cycles collapse into one
|
|
689
|
+
// concurrent wave (handled by the intra-wave retry machinery) with a
|
|
690
|
+
// logged warning instead of failing.
|
|
691
|
+
const typeIds = [...new Set(targets.map((t) => t.id))];
|
|
692
|
+
const providerOfType = new Map(targets.map((t) => [t.id, t.provider]));
|
|
693
|
+
const successors = new Map<string, Set<string>>();
|
|
694
|
+
const predecessors = new Map<string, Set<string>>();
|
|
695
|
+
for (const id of typeIds) {
|
|
696
|
+
const globs = providerOfType.get(id)?.nuke?.dependsOn;
|
|
697
|
+
if (!globs || globs.length === 0) continue;
|
|
698
|
+
const matches = picomatch([...globs]);
|
|
699
|
+
for (const other of typeIds) {
|
|
700
|
+
if (other === id || !matches(other)) continue;
|
|
701
|
+
addEdge(successors, id, other);
|
|
702
|
+
addEdge(predecessors, other, id);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const components = stronglyConnectedComponents(typeIds, successors);
|
|
707
|
+
for (const component of components) {
|
|
708
|
+
if (component.length > 1) {
|
|
709
|
+
yield* Effect.logWarning(
|
|
710
|
+
`nuke: teardown-dependency cycle between ${component.join(", ")} — deleting them concurrently`,
|
|
711
|
+
).pipe(Effect.provide(context));
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const compOf = new Map<string, number>();
|
|
715
|
+
components.forEach((component, i) => {
|
|
716
|
+
for (const node of component) compOf.set(node, i);
|
|
717
|
+
});
|
|
718
|
+
// Layer each component: 0 for sources, else 1 + max predecessor
|
|
719
|
+
// layer. Tarjan emits components in reverse topological order, so
|
|
720
|
+
// iterating backwards visits predecessors before dependents.
|
|
721
|
+
const layerOfComp: number[] = new Array(components.length).fill(0);
|
|
722
|
+
for (let i = components.length - 1; i >= 0; i--) {
|
|
723
|
+
let layer = 0;
|
|
724
|
+
for (const node of components[i]!) {
|
|
725
|
+
for (const pred of predecessors.get(node) ?? []) {
|
|
726
|
+
const predComp = compOf.get(pred)!;
|
|
727
|
+
if (predComp !== i) {
|
|
728
|
+
layer = Math.max(layer, layerOfComp[predComp]! + 1);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
layerOfComp[i] = layer;
|
|
733
|
+
}
|
|
734
|
+
const waves: string[][] = [];
|
|
735
|
+
components.forEach((component, i) => {
|
|
736
|
+
(waves[layerOfComp[i]!] ??= []).push(...component);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// ---- Execute waves -------------------------------------------------
|
|
740
|
+
const targetsOfType = groupBy(targets, (t) => t.id);
|
|
741
|
+
const remainingCount = new Map(
|
|
742
|
+
[...targetsOfType.entries()].map(([id, items]) => [id, items.length]),
|
|
743
|
+
);
|
|
744
|
+
const remaining: typeof targets = [];
|
|
745
|
+
const held: string[] = [];
|
|
746
|
+
for (const wave of waves) {
|
|
747
|
+
const runnable: typeof targets = [];
|
|
748
|
+
for (const typeId of wave) {
|
|
749
|
+
const items = targetsOfType.get(typeId) ?? [];
|
|
750
|
+
// A dependent type still has undeleted resources — deleting this
|
|
751
|
+
// type now could wedge their in-flight teardown, which is exactly
|
|
752
|
+
// what the ordering exists to prevent. Hold it back and report.
|
|
753
|
+
const blockers = [...(predecessors.get(typeId) ?? [])].filter(
|
|
754
|
+
(pred) =>
|
|
755
|
+
compOf.get(pred) !== compOf.get(typeId) &&
|
|
756
|
+
(remainingCount.get(pred) ?? 0) > 0,
|
|
757
|
+
);
|
|
758
|
+
if (blockers.length > 0) {
|
|
759
|
+
held.push(`${typeId} (blocked by ${blockers.join(", ")})`);
|
|
760
|
+
remaining.push(...items);
|
|
761
|
+
for (const item of items) {
|
|
762
|
+
yield* emitDelete({ kind: "failed", id: item.id });
|
|
763
|
+
}
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
runnable.push(...items);
|
|
767
|
+
}
|
|
768
|
+
const failed = yield* runTier(runnable);
|
|
769
|
+
remaining.push(...failed);
|
|
770
|
+
const failedOfType = groupBy(failed, (t) => t.id);
|
|
771
|
+
for (const typeId of new Set(runnable.map((t) => t.id))) {
|
|
772
|
+
remainingCount.set(typeId, failedOfType.get(typeId)?.length ?? 0);
|
|
540
773
|
}
|
|
541
|
-
remaining = failed;
|
|
542
774
|
}
|
|
543
775
|
|
|
544
776
|
if (deleteUI) {
|
|
@@ -549,8 +781,15 @@ const nukeCommand = Command.make(
|
|
|
549
781
|
const deleted = targets.length - remaining.length;
|
|
550
782
|
yield* Console.log("");
|
|
551
783
|
yield* Console.log(
|
|
552
|
-
|
|
784
|
+
independent
|
|
785
|
+
? `Deleted ${deleted} resource(s).`
|
|
786
|
+
: `Deleted ${deleted} resource(s) over ${pass} pass(es).`,
|
|
553
787
|
);
|
|
788
|
+
if (held.length > 0) {
|
|
789
|
+
yield* Console.log(
|
|
790
|
+
`Held back (their dependent types could not be fully deleted): ${held.join("; ")}`,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
554
793
|
if (remaining.length > 0) {
|
|
555
794
|
yield* Console.log(
|
|
556
795
|
`${remaining.length} resource(s) could not be deleted.`,
|
|
@@ -110,6 +110,54 @@ const createIgnoreMatcher = (patterns: string[]) => {
|
|
|
110
110
|
return (file: string) => matcher.ignores(file);
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Read the special `_headers` / `_redirects` files from an assets
|
|
115
|
+
* directory. They are excluded from the upload manifest, but their raw
|
|
116
|
+
* contents must be sent to Cloudflare in the script metadata's asset
|
|
117
|
+
* config (`config._headers` / `config._redirects`) for the rules to
|
|
118
|
+
* apply.
|
|
119
|
+
*
|
|
120
|
+
* The directory is optional: a config-only assets object (e.g. Vite's
|
|
121
|
+
* `assets: { runWorkerFirst: true }`, whose directory is supplied by the
|
|
122
|
+
* build) has no directory to read, and in `dev` there is no build output at
|
|
123
|
+
* all. That yields no rules rather than an error.
|
|
124
|
+
*/
|
|
125
|
+
export const readAssetsConfigFiles = Effect.fn(function* (
|
|
126
|
+
directory: string | undefined,
|
|
127
|
+
) {
|
|
128
|
+
if (directory === undefined) {
|
|
129
|
+
return { _headers: undefined, _redirects: undefined };
|
|
130
|
+
}
|
|
131
|
+
const path = yield* Path.Path;
|
|
132
|
+
const resolvedDirectory = path.resolve(directory);
|
|
133
|
+
const [_headers, _redirects] = yield* Effect.all([
|
|
134
|
+
maybeReadString(path.join(resolvedDirectory, "_headers")),
|
|
135
|
+
maybeReadString(path.join(resolvedDirectory, "_redirects")),
|
|
136
|
+
]);
|
|
137
|
+
return { _headers, _redirects };
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Merge `_headers` / `_redirects` file contents into an asset config,
|
|
142
|
+
* producing the config to send in the script-upload metadata. Explicit
|
|
143
|
+
* `headers` / `redirects` props win over the files.
|
|
144
|
+
*/
|
|
145
|
+
export const mergeAssetsConfigFiles = (
|
|
146
|
+
config: AssetsConfig | undefined,
|
|
147
|
+
files: { _headers: string | undefined; _redirects: string | undefined },
|
|
148
|
+
): AssetsConfig | undefined => {
|
|
149
|
+
const headers = config?.headers ?? files._headers;
|
|
150
|
+
const redirects = config?.redirects ?? files._redirects;
|
|
151
|
+
if (headers === undefined && redirects === undefined) {
|
|
152
|
+
return config;
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...config,
|
|
156
|
+
...(headers !== undefined ? { headers } : undefined),
|
|
157
|
+
...(redirects !== undefined ? { redirects } : undefined),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
|
|
113
161
|
export const readAssets = Effect.fn(function* ({
|
|
114
162
|
directory,
|
|
115
163
|
...config
|
|
@@ -193,7 +241,11 @@ export const readAssets = Effect.fn(function* ({
|
|
|
193
241
|
});
|
|
194
242
|
return {
|
|
195
243
|
directory,
|
|
196
|
-
config
|
|
244
|
+
// Fold the `_headers` / `_redirects` file contents into the config
|
|
245
|
+
// that gets sent to Cloudflare (`metadata.assets.config`). Merged
|
|
246
|
+
// *after* hashing so the hash input shape stays stable for
|
|
247
|
+
// already-deployed workers.
|
|
248
|
+
config: mergeAssetsConfigFiles(config, { _headers, _redirects }),
|
|
197
249
|
manifest: sortedManifest,
|
|
198
250
|
_headers,
|
|
199
251
|
_redirects,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type Assets as RuntimeAssets,
|
|
9
9
|
type DurableObjectNamespace as RuntimeDurableObject,
|
|
10
10
|
type QueueConsumer as RuntimeQueueConsumer,
|
|
11
|
+
type Workflow as RuntimeWorkflow,
|
|
11
12
|
type RuntimeServices,
|
|
12
13
|
} from "@distilled.cloud/cloudflare-runtime";
|
|
13
14
|
import {
|
|
@@ -65,6 +66,7 @@ import { sha256, unwrapRedacted } from "../../Util/index.ts";
|
|
|
65
66
|
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
66
67
|
import { LOCAL_ENTRY_URL, LocalRuntimeState } from "../LocalRuntime.ts";
|
|
67
68
|
import type { WorkerAssetsConfig, WorkerProps } from "../Workers/Worker.ts";
|
|
69
|
+
import { readAssetsConfigFiles } from "./Assets.ts";
|
|
68
70
|
import { getCompatibility } from "./Compatibility.ts";
|
|
69
71
|
import { isPythonMain, watchPythonWorkerBundle } from "./PythonWorkerBundle.ts";
|
|
70
72
|
import { Worker } from "./Worker.ts";
|
|
@@ -293,9 +295,10 @@ export const LocalWorkerProvider = () =>
|
|
|
293
295
|
bindings: worker.workerBindings as never,
|
|
294
296
|
hyperdrives: worker.hyperdrives,
|
|
295
297
|
durableObjectNamespaces: worker.durableObjectNamespaces,
|
|
298
|
+
workflows: worker.workflows,
|
|
296
299
|
queueConsumers,
|
|
297
300
|
modules: yield* toRuntimeModules(bundle),
|
|
298
|
-
assets: toRuntimeAssets(worker.assets),
|
|
301
|
+
assets: yield* toRuntimeAssets(worker.assets),
|
|
299
302
|
})
|
|
300
303
|
.pipe(Scope.provide(scope));
|
|
301
304
|
workerdScopes.set(worker.id, scope);
|
|
@@ -408,6 +411,7 @@ export const LocalWorkerProvider = () =>
|
|
|
408
411
|
string,
|
|
409
412
|
RuntimeDurableObject & { uniqueKey: string }
|
|
410
413
|
> = {};
|
|
414
|
+
const workflows: Record<string, RuntimeWorkflow> = {};
|
|
411
415
|
const hyperdrives: Record<string, Required<HyperdriveOrigin>> = {};
|
|
412
416
|
const containers: Record<string, ContainerImage> = {};
|
|
413
417
|
for (const { data } of bindings) {
|
|
@@ -435,6 +439,18 @@ export const LocalWorkerProvider = () =>
|
|
|
435
439
|
}),
|
|
436
440
|
);
|
|
437
441
|
} else {
|
|
442
|
+
if (
|
|
443
|
+
binding.type === "workflow" &&
|
|
444
|
+
// Same ownership rule as DOs: only declare workflows hosted by
|
|
445
|
+
// this worker. Cross-script workflow bindings are routed via
|
|
446
|
+
// the registry proxy.
|
|
447
|
+
(!binding.scriptName || binding.scriptName === name)
|
|
448
|
+
) {
|
|
449
|
+
workflows[binding.workflowName] = {
|
|
450
|
+
workflowName: binding.workflowName,
|
|
451
|
+
className: binding.className,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
438
454
|
workerBindings.push(yield* toRuntimeBinding(binding));
|
|
439
455
|
}
|
|
440
456
|
}
|
|
@@ -479,6 +495,7 @@ export const LocalWorkerProvider = () =>
|
|
|
479
495
|
compatibility,
|
|
480
496
|
workerBindings,
|
|
481
497
|
durableObjectNamespaces: Object.values(durableObjectNamespaces),
|
|
498
|
+
workflows: Object.values(workflows),
|
|
482
499
|
viteMain: props.vite?.main,
|
|
483
500
|
viteEnvironments: props.vite?.viteEnvironments,
|
|
484
501
|
hyperdrives,
|
|
@@ -586,9 +603,10 @@ export const LocalWorkerProvider = () =>
|
|
|
586
603
|
name: worker.name,
|
|
587
604
|
bindings: worker.workerBindings,
|
|
588
605
|
durableObjectNamespaces: worker.durableObjectNamespaces,
|
|
606
|
+
workflows: worker.workflows,
|
|
589
607
|
hyperdrives: worker.hyperdrives,
|
|
590
608
|
queueConsumers: yield* getQueueConsumers(worker.name),
|
|
591
|
-
assets: toRuntimeAssets(worker.assets),
|
|
609
|
+
assets: yield* toRuntimeAssets(worker.assets),
|
|
592
610
|
},
|
|
593
611
|
context,
|
|
594
612
|
},
|
|
@@ -949,19 +967,40 @@ const structuralSignature = (value: unknown): Effect.Effect<string> => {
|
|
|
949
967
|
return sha256(JSON.stringify(normalize(value)));
|
|
950
968
|
};
|
|
951
969
|
|
|
952
|
-
const toRuntimeAssets = (
|
|
970
|
+
const toRuntimeAssets = Effect.fn(function* (
|
|
953
971
|
assets: WorkerAssetsConfig | undefined,
|
|
954
|
-
)
|
|
972
|
+
) {
|
|
955
973
|
if (!assets) return undefined;
|
|
974
|
+
// Mirror the deploy path: the special `_headers` / `_redirects` files
|
|
975
|
+
// in the assets directory carry the rules unless overridden by
|
|
976
|
+
// explicit `headers` / `redirects` props. The local runtime parses
|
|
977
|
+
// the raw string contents just like Cloudflare does.
|
|
978
|
+
//
|
|
979
|
+
// A Vite website's `assets` is config-only (`{ runWorkerFirst: true }`) —
|
|
980
|
+
// the client output directory is the build's business, and in `dev` the
|
|
981
|
+
// vite plugin serves assets from the dev server, so there is no directory
|
|
982
|
+
// to read here.
|
|
983
|
+
const directory: string | undefined =
|
|
984
|
+
typeof assets === "string" ? assets : assets.directory;
|
|
985
|
+
// An unreadable file just means no rules here — the assets plugin
|
|
986
|
+
// reports directory problems itself.
|
|
987
|
+
const files = yield* readAssetsConfigFiles(directory).pipe(
|
|
988
|
+
Effect.orElseSucceed(() => ({
|
|
989
|
+
_headers: undefined,
|
|
990
|
+
_redirects: undefined,
|
|
991
|
+
})),
|
|
992
|
+
);
|
|
956
993
|
if (typeof assets === "string") {
|
|
957
994
|
return {
|
|
958
995
|
directory: assets,
|
|
996
|
+
headers: files._headers,
|
|
997
|
+
redirects: files._redirects,
|
|
959
998
|
};
|
|
960
999
|
}
|
|
961
1000
|
return {
|
|
962
1001
|
directory: assets.directory,
|
|
963
|
-
headers: assets.headers,
|
|
964
|
-
redirects: assets.redirects,
|
|
1002
|
+
headers: assets.headers ?? files._headers,
|
|
1003
|
+
redirects: assets.redirects ?? files._redirects,
|
|
965
1004
|
// Distilled widened generated string enums to open unions (`string & {}`);
|
|
966
1005
|
// the API only ever returns the known variants here.
|
|
967
1006
|
htmlHandling: assets.htmlHandling as
|
|
@@ -978,7 +1017,7 @@ const toRuntimeAssets = (
|
|
|
978
1017
|
runWorkerFirst: assets.runWorkerFirst,
|
|
979
1018
|
serveDirectly: assets.serveDirectly,
|
|
980
1019
|
};
|
|
981
|
-
};
|
|
1020
|
+
});
|
|
982
1021
|
|
|
983
1022
|
const moduleTypeFromExtension = (ext: string): Module["type"] | "SourceMap" => {
|
|
984
1023
|
switch (ext) {
|
|
@@ -7,7 +7,6 @@ import * as Data from "effect/Data";
|
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import type * as Layer from "effect/Layer";
|
|
9
9
|
import * as Redacted from "effect/Redacted";
|
|
10
|
-
import * as Bundle from "../../Bundle/Bundle.ts";
|
|
11
10
|
import { type MemoOptions } from "../../Command/Memo.ts";
|
|
12
11
|
import type { Dependencies } from "../../Dependencies.ts";
|
|
13
12
|
import type { InputProps } from "../../Input.ts";
|
|
@@ -45,7 +44,7 @@ import type {
|
|
|
45
44
|
WorkerBindingResource,
|
|
46
45
|
WorkerBindings,
|
|
47
46
|
} from "./WorkerBinding.ts";
|
|
48
|
-
import { type ModuleRule } from "./WorkerBundle.ts";
|
|
47
|
+
import { type ModuleRule, type WorkerBuildOptions } from "./WorkerBundle.ts";
|
|
49
48
|
import {
|
|
50
49
|
makeWorkerRuntimeContext,
|
|
51
50
|
type WorkerRuntimeContext,
|
|
@@ -478,10 +477,13 @@ export interface WorkerProps<
|
|
|
478
477
|
*/
|
|
479
478
|
routes?: WorkerRouteConfig[];
|
|
480
479
|
/**
|
|
481
|
-
* Extra bundler options applied on top of the standard rolldown
|
|
482
|
-
* options used to build this Worker.
|
|
480
|
+
* Extra bundler options applied on top of the standard rolldown
|
|
481
|
+
* input/output options used to build this Worker. Includes the generic
|
|
482
|
+
* bundle extras (pure-annotation packages, bundle analyzer) plus an
|
|
483
|
+
* `output` field of rolldown output overrides (e.g. `codeSplitting`
|
|
484
|
+
* groups) merged over Alchemy's defaults. See {@link WorkerBuildOptions}.
|
|
483
485
|
*/
|
|
484
|
-
build?:
|
|
486
|
+
build?: WorkerBuildOptions;
|
|
485
487
|
/**
|
|
486
488
|
* Whether to bundle {@link main} with rolldown before upload.
|
|
487
489
|
*
|