| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Data from "effect/Data"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | |
| 5 | /** |
| 6 | * AdoptPolicy controls whether the engine should "adopt" pre-existing cloud |
| 7 | * resources that conflict with this stack instead of failing. |
| 8 | * |
| 9 | * The engine always calls a provider's `read` lifecycle operation when there |
| 10 | * is no prior state and `read` is implemented. The provider's job is to |
| 11 | * report two things: |
| 12 | * |
| 13 | * 1. Whether the resource exists (returns attributes vs. `undefined`). |
| 14 | * 2. Whether *we* own it (plain attributes vs. {@link Unowned}-branded). |
| 15 | * |
| 16 | * The engine then routes: |
| 17 | * |
| 18 | * | `read` outcome | `adopt: false` | `adopt: true` | |
| 19 | * | ------------------- | -------------- | ----------------- | |
| 20 | * | undefined | create | create | |
| 21 | * | owned (plain attrs) | silent adopt | silent adopt | |
| 22 | * | {@link Unowned} | fail | takeover (adopt) | |
| 23 | * |
| 24 | * "Owned" means a provider has affirmatively determined the resource was |
| 25 | * created by this stack/stage/logical-id (typically by inspecting tags or |
| 26 | * naming conventions). Resources without ownership semantics simply always |
| 27 | * return plain attrs — they are treated as owned by default, so silent |
| 28 | * adoption is the norm and `--adopt` is unnecessary. |
| 29 | * |
| 30 | * The policy can be overridden per-effect via {@link adopt} — most commonly |
| 31 | * applied at the resource or stack scope. |
| 32 | */ |
| 33 | export class AdoptPolicy extends Context.Service<AdoptPolicy, boolean>()( |
| 34 | "AdoptPolicy", |
| 35 | ) {} |
| 36 | |
| 37 | /** |
| 38 | * Engine-raised failure produced when `read` reports an existing resource |
| 39 | * marked as {@link Unowned} and `adopt` is `false`. Surface this to the user |
| 40 | * to indicate they should either re-tag the cloud resource, pick a different |
| 41 | * physical name, or re-run with `--adopt` (or `adopt(true)`) to force a |
| 42 | * takeover. |
| 43 | */ |
| 44 | export class OwnedBySomeoneElse extends Data.TaggedError("OwnedBySomeoneElse")<{ |
| 45 | /** A human-readable description of why the resource cannot be adopted. */ |
| 46 | message: string; |
| 47 | /** The Resource Type (e.g. `AWS.S3.Bucket`). */ |
| 48 | resourceType?: string; |
| 49 | /** The logical ID of the conflicting resource. */ |
| 50 | logicalId?: string; |
| 51 | /** The physical name/identifier of the conflicting resource. */ |
| 52 | physicalName?: string; |
| 53 | }> {} |
| 54 | |
| 55 | /** |
| 56 | * Private symbol used to brand attribute objects returned by `read` as |
| 57 | * "exists in cloud, but not owned by this stack/stage/logical-id". |
| 58 | * |
| 59 | * Deliberately *not* registered via `Symbol.for` — the brand is an |
| 60 | * engine-internal routing hint and must not be observable or |
| 61 | * forgeable from outside this module. The engine strips it before any |
| 62 | * state persistence (see {@link stripUnowned}) so it never leaks into |
| 63 | * the state store, JSON serialization, or downstream `Output`s. |
| 64 | */ |
| 65 | const UnownedTag: unique symbol = Symbol("alchemy/Unowned"); |
| 66 | |
| 67 | /** |
| 68 | * Brand a `read` return value as belonging to a different owner. |
| 69 | * |
| 70 | * Use this in a provider's `read` implementation when the resource exists in |
| 71 | * the cloud but a positive ownership check (e.g. tag inspection) shows it |
| 72 | * was *not* created by this stack/stage/logical-id: |
| 73 | * |
| 74 | * ```ts |
| 75 | * read: Effect.fn(function* ({ id, output, olds }) { |
| 76 | * const settings = yield* getScriptSettings({ ... }).pipe( |
| 77 | * Effect.catchTag("WorkerNotFound", () => Effect.succeed(undefined)), |
| 78 | * ); |
| 79 | * if (!settings) return undefined; |
| 80 | * |
| 81 | * const attrs = { workerName, ...buildAttrs(settings) }; |
| 82 | * return hasAlchemyWorkerTags(id, settings.tags) ? attrs : Unowned(attrs); |
| 83 | * }), |
| 84 | * ``` |
| 85 | * |
| 86 | * The returned value is structurally still `T` — there is no wrapper to |
| 87 | * unwrap. The brand is a non-enumerable symbol that the engine inspects |
| 88 | * during planning to decide whether to take over (`adopt: true`) or fail |
| 89 | * loudly (`adopt: false`). |
| 90 | * |
| 91 | * Resources without ownership semantics should simply return plain attrs — |
| 92 | * the engine treats them as owned and silent adoption is the default. |
| 93 | */ |
| 94 | export const Unowned: { |
| 95 | <T extends object>(attrs: T): T; |
| 96 | is: (value: unknown) => boolean; |
| 97 | } = Object.assign( |
| 98 | <T extends object>(attrs: T): T => { |
| 99 | const cloned = { ...attrs } as T; |
| 100 | Object.defineProperty(cloned, UnownedTag, { |
| 101 | value: true, |
| 102 | enumerable: false, |
| 103 | writable: false, |
| 104 | configurable: false, |
| 105 | }); |
| 106 | return cloned; |
| 107 | }, |
| 108 | { |
| 109 | is: (value: unknown): boolean => |
| 110 | typeof value === "object" && |
| 111 | value !== null && |
| 112 | (value as any)[UnownedTag] === true, |
| 113 | }, |
| 114 | ); |
| 115 | |
| 116 | /** |
| 117 | * Strip the {@link Unowned} brand from an attributes object before persisting |
| 118 | * it — state should not carry per-deploy ownership-routing metadata. |
| 119 | * |
| 120 | * The brand is intentionally non-enumerable, so a plain spread already |
| 121 | * drops it; the additional `delete` is defense-in-depth in case a future |
| 122 | * change makes the descriptor enumerable. Always returns a fresh object |
| 123 | * (never mutates the input). |
| 124 | */ |
| 125 | export const stripUnowned = <T extends object>(attrs: T): T => { |
| 126 | const out: any = { ...attrs }; |
| 127 | if (UnownedTag in out) delete out[UnownedTag]; |
| 128 | return out; |
| 129 | }; |
| 130 | |
| 131 | export const adopt: { |
| 132 | ( |
| 133 | enabled?: boolean, |
| 134 | ): <A, E, R = never>( |
| 135 | effect: Effect.Effect<A, E, R>, |
| 136 | ) => Effect.Effect<A, E, R>; |
| 137 | <R1 = never>( |
| 138 | enabled: Effect.Effect<boolean, never, R1>, |
| 139 | ): <A, E, R2 = never>( |
| 140 | effect: Effect.Effect<A, E, R2>, |
| 141 | ) => Effect.Effect<A, E, R1 | R2>; |
| 142 | } = ((enabled: boolean | Effect.Effect<boolean, never, any> = true) => |
| 143 | (eff: Effect.Effect<any, any, any>) => |
| 144 | eff.pipe( |
| 145 | typeof enabled === "boolean" |
| 146 | ? Effect.provideService(AdoptPolicy, enabled ?? true) |
| 147 | : Effect.provideServiceEffect(AdoptPolicy, enabled), |
| 148 | )) as any; |