1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3
4/**
5 * What the engine does with a resource's **physical** cloud object when the
6 * resource is deleted — orphaned (its declaration was removed or renamed
7 * away), destroyed (`alchemy destroy`), or superseded by a replacement's old
8 * generation.
9 *
10 * - `"destroy"` (the default for most resource types) — call the provider's
11 * `delete`.
12 * - `"retain"` — skip `provider.delete` entirely. The state row is still
13 * dropped: alchemy forgets the resource, the cloud object lives on.
14 *
15 * The policy is a *decoration* on the declaration, applied with
16 * {@link retain} / {@link destroy}, and it is persisted on the resource's
17 * state row. Because it isn't a prop, changing it produces no diff — the
18 * resource plans as a `noop` and the deploy re-commits the row's policy in
19 * place (see the noop branch of `Apply.ts`). So a policy change does take
20 * effect from the very next deploy, even though the plan shows no changes.
21 *
22 * A few resource types default to `"retain"` because their contents are
23 * irreplaceable (e.g. `GitHub.Repository`, `Cloudflare.Zone`); those opt into
24 * deletion with `destroy()`.
25 */
26export class RemovalPolicy extends Context.Service<
27 RemovalPolicy,
28 "retain" | "destroy"
29>()("RemovalPolicy") {}
30
31/**
32 * Retain the physical cloud resource when the resource is deleted: the
33 * provider's `delete` is never called, and the state row is dropped so
34 * alchemy stops tracking it.
35 *
36 * Applies to every resource declared inside the piped effect, so it can
37 * decorate a single resource or a whole scope.
38 *
39 * @example Retain one resource
40 * ```typescript
41 * const bucket = yield* R2.Bucket("Uploads").pipe(RemovalPolicy.retain());
42 * ```
43 *
44 * @example Retain only in production
45 * ```typescript
46 * const stack = yield* Stack;
47 * const bucket = yield* R2.Bucket("Uploads").pipe(
48 * RemovalPolicy.retain(stack.stage === "prod"),
49 * );
50 * ```
51 *
52 * @param enabled `true` (the default) retains; `false` selects `destroy`.
53 * May also be an `Effect` producing the boolean.
54 */
55export const retain: {
56 (
57 enabled?: boolean,
58 ): <R, Req = never>(
59 enabled: Effect.Effect<R, never, Req>,
60 ) => Effect.Effect<R, never, Req>;
61 <Req = never>(
62 enabled: Effect.Effect<boolean, never, Req>,
63 ): <R, Req2 = never>(
64 a: Effect.Effect<R, never, Req2>,
65 ) => Effect.Effect<R, never, Req | Req2>;
66} = ((enabled: boolean | Effect.Effect<boolean, never, any> = true) =>
67 (eff: Effect.Effect<any, never, any>) =>
68 eff.pipe(
69 typeof enabled === "boolean"
70 ? Effect.provideService(RemovalPolicy, enabled ? "retain" : "destroy")
71 : Effect.provideServiceEffect(
72 RemovalPolicy,
73 enabled.pipe(Effect.map((a) => (a ? "retain" : "destroy"))),
74 ),
75 )) as any;
76
77/**
78 * Destroy the physical cloud resource when the resource is deleted — the
79 * default for most resource types, and the way to opt out for the ones that
80 * default to `retain` (e.g. `GitHub.Repository`, `Cloudflare.Zone`).
81 *
82 * @example Opt a retain-by-default resource into deletion
83 * ```typescript
84 * yield* GitHub.Repository("Preview", {
85 * owner: "my-org",
86 * name: "ephemeral-preview",
87 * }).pipe(RemovalPolicy.destroy());
88 * ```
89 *
90 * @param enabled `true` (the default) destroys; `false` selects `retain`.
91 * May also be an `Effect` producing the boolean.
92 */
93export const destroy: {
94 (
95 enabled?: boolean,
96 ): <R, Req = never>(
97 enabled: Effect.Effect<R, never, Req>,
98 ) => Effect.Effect<R, never, Req>;
99 <Req = never>(
100 enabled: Effect.Effect<boolean, never, Req>,
101 ): <R, Req2 = never>(
102 a: Effect.Effect<R, never, Req2>,
103 ) => Effect.Effect<R, never, Req | Req2>;
104} = ((enabled: boolean | Effect.Effect<boolean, never, any> = true) =>
105 (eff: Effect.Effect<any, never, any>) =>
106 eff.pipe(
107 typeof enabled === "boolean"
108 ? Effect.provideService(RemovalPolicy, enabled ? "destroy" : "retain")
109 : Effect.provideServiceEffect(
110 RemovalPolicy,
111 enabled.pipe(Effect.map((a) => (a ? "destroy" : "retain"))),
112 ),
113 )) as any;