import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; /** * A former id a resource was previously declared under: * * - a bare `string` resolves against the ambient namespace, exactly like * the resource's own `id` argument does * - `{ fqn: "..." }` is absolute — the full FQN as persisted in state, * ignoring any surrounding namespace. Needed when a resource moved * BETWEEN namespaces (a relative id can only address the current * namespace's subtree). */ export type FormerId = string | { fqn: string }; /** * RenamePolicy carries the former ids a resource was previously declared * under. It is captured from the ambient context at registration time (like * `AdoptPolicy` / `RemovalPolicy`) and resolved against the same namespace * as the resource's own id — see `ResourceLike.FormerFqns`. */ export class RenamePolicy extends Context.Service< RenamePolicy, readonly FormerId[] >()("RenamePolicy") {} /** * Declare the logical id(s) this resource was previously registered under, * so the engine migrates its persisted state row instead of planning a * create+delete replacement when the id changes. * * ```ts * // was: Bucket("Bucket") — the row migrates and the deploy plans a noop * const bucket = yield* Bucket("Assets").pipe(renamedFrom("Bucket")); * ``` * * A bare string resolves against the ambient namespace, exactly like the * resource's own `id`: * * ```ts * // FQN `Site/Assets`, former FQN `Site/Bucket` * yield* Bucket("Assets").pipe( * renamedFrom("Bucket"), * Namespace.push("Site"), * ); * ``` * * Moved between namespaces? Pass `{ fqn }` — the absolute FQN exactly as * persisted in state, ignoring the ambient namespace: * * ```ts * // former FQN `LegacySite/Assets` (NOT `NewSite/LegacySite/Assets`) * yield* Bucket("Assets").pipe( * renamedFrom({ fqn: "LegacySite/Assets" }), * Namespace.push("NewSite"), * ); * ``` * * Renamed more than once? List every former id, most recent first — the * planner checks them in order and migrates from the first matching row: * * ```ts * // rename history: Bucket → StaticAssets → Assets * yield* Bucket("Assets").pipe(renamedFrom("StaticAssets", "Bucket")); * ``` * * Migration semantics, by state-row shape (see Plan's rename resolution; * `new` = the row at the resource's FQN, `old` = a row at a former FQN): * * ```text * new old → outcome * ────────────────────────────────────────────────────────────────────── * — row → migrate: the old row * IS the resource's * state; apply moves it * before any lifecycle * op, and ONE update * reconcile re-brands * the physical resource * under the new logical * id (never a create) * row, same instanceId row → interrupted * migration: leftovers * dropped state-only — * ALL of them in one * apply — the physical * resource is never * touched * row, diff instanceId row → someone else's row (a * resource reused the * old name after the * rename shipped): * ignored, normal * orphan handling * row, diff resourceType row → FATAL: migrating over * the foreign-typed row * would silently * abandon its cloud * resource — resolve * the collision first * any row, diff resourceType → never migrated — * cannot be this * resource's row, * whatever its FQN says * ``` * * The old id can be REUSED by a new resource in the same deploy — the * rename claim wins the row (it is an explicit statement that the row was * the renamer's), and the reusing resource is created fresh: * * ```ts * // `Assets` keeps the physical resource previously known as `Bucket`; * // this `Bucket` is a brand-new one. * yield* Bucket("Assets").pipe(renamedFrom("Bucket")); * yield* Bucket("Bucket"); * ``` * * Renames may SHIFT through each other in one deploy — each row follows * its resource (resolved in claim-dependency order): * * ```ts * // the resource at `A` is now `B`; the resource at `B` is now `C` * yield* Bucket("B").pipe(renamedFrom("A")); * yield* Bucket("C").pipe(renamedFrom("B")); * ``` * * A SWAP (`A` ⇄ `B`) is a rename cycle and fails the plan loudly — the two * migrations would overwrite and delete each other's rows. Rename through * a temporary id across two deploys instead. Two resources claiming the * same former FQN also fail loudly. */ export const renamedFrom = (...formerIds: [FormerId, ...FormerId[]]) => (effect: Effect.Effect): Effect.Effect => Effect.provideService(effect, RenamePolicy, formerIds);