1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3
4/**
5 * A former id a resource was previously declared under:
6 *
7 * - a bare `string` resolves against the ambient namespace, exactly like
8 * the resource's own `id` argument does
9 * - `{ fqn: "..." }` is absolute — the full FQN as persisted in state,
10 * ignoring any surrounding namespace. Needed when a resource moved
11 * BETWEEN namespaces (a relative id can only address the current
12 * namespace's subtree).
13 */
14export type FormerId = string | { fqn: string };
15
16/**
17 * RenamePolicy carries the former ids a resource was previously declared
18 * under. It is captured from the ambient context at registration time (like
19 * `AdoptPolicy` / `RemovalPolicy`) and resolved against the same namespace
20 * as the resource's own id — see `ResourceLike.FormerFqns`.
21 */
22export class RenamePolicy extends Context.Service<
23 RenamePolicy,
24 readonly FormerId[]
25>()("RenamePolicy") {}
26
27/**
28 * Declare the logical id(s) this resource was previously registered under,
29 * so the engine migrates its persisted state row instead of planning a
30 * create+delete replacement when the id changes.
31 *
32 * ```ts
33 * // was: Bucket("Bucket") — the row migrates and the deploy plans a noop
34 * const bucket = yield* Bucket("Assets").pipe(renamedFrom("Bucket"));
35 * ```
36 *
37 * A bare string resolves against the ambient namespace, exactly like the
38 * resource's own `id`:
39 *
40 * ```ts
41 * // FQN `Site/Assets`, former FQN `Site/Bucket`
42 * yield* Bucket("Assets").pipe(
43 * renamedFrom("Bucket"),
44 * Namespace.push("Site"),
45 * );
46 * ```
47 *
48 * Moved between namespaces? Pass `{ fqn }` — the absolute FQN exactly as
49 * persisted in state, ignoring the ambient namespace:
50 *
51 * ```ts
52 * // former FQN `LegacySite/Assets` (NOT `NewSite/LegacySite/Assets`)
53 * yield* Bucket("Assets").pipe(
54 * renamedFrom({ fqn: "LegacySite/Assets" }),
55 * Namespace.push("NewSite"),
56 * );
57 * ```
58 *
59 * Renamed more than once? List every former id, most recent first — the
60 * planner checks them in order and migrates from the first matching row:
61 *
62 * ```ts
63 * // rename history: Bucket → StaticAssets → Assets
64 * yield* Bucket("Assets").pipe(renamedFrom("StaticAssets", "Bucket"));
65 * ```
66 *
67 * Migration semantics, by state-row shape (see Plan's rename resolution;
68 * `new` = the row at the resource's FQN, `old` = a row at a former FQN):
69 *
70 * ```text
71 * new old → outcome
72 * ──────────────────────────────────────────────────────────────────────
73 * — row → migrate: the old row
74 * IS the resource's
75 * state; apply moves it
76 * before any lifecycle
77 * op, and ONE update
78 * reconcile re-brands
79 * the physical resource
80 * under the new logical
81 * id (never a create)
82 * row, same instanceId row → interrupted
83 * migration: leftovers
84 * dropped state-only —
85 * ALL of them in one
86 * apply — the physical
87 * resource is never
88 * touched
89 * row, diff instanceId row → someone else's row (a
90 * resource reused the
91 * old name after the
92 * rename shipped):
93 * ignored, normal
94 * orphan handling
95 * row, diff resourceType row → FATAL: migrating over
96 * the foreign-typed row
97 * would silently
98 * abandon its cloud
99 * resource — resolve
100 * the collision first
101 * any row, diff resourceType → never migrated —
102 * cannot be this
103 * resource's row,
104 * whatever its FQN says
105 * ```
106 *
107 * The old id can be REUSED by a new resource in the same deploy — the
108 * rename claim wins the row (it is an explicit statement that the row was
109 * the renamer's), and the reusing resource is created fresh:
110 *
111 * ```ts
112 * // `Assets` keeps the physical resource previously known as `Bucket`;
113 * // this `Bucket` is a brand-new one.
114 * yield* Bucket("Assets").pipe(renamedFrom("Bucket"));
115 * yield* Bucket("Bucket");
116 * ```
117 *
118 * Renames may SHIFT through each other in one deploy — each row follows
119 * its resource (resolved in claim-dependency order):
120 *
121 * ```ts
122 * // the resource at `A` is now `B`; the resource at `B` is now `C`
123 * yield* Bucket("B").pipe(renamedFrom("A"));
124 * yield* Bucket("C").pipe(renamedFrom("B"));
125 * ```
126 *
127 * A SWAP (`A` ⇄ `B`) is a rename cycle and fails the plan loudly — the two
128 * migrations would overwrite and delete each other's rows. Rename through
129 * a temporary id across two deploys instead. Two resources claiming the
130 * same former FQN also fail loudly.
131 */
132export const renamedFrom =
133 (...formerIds: [FormerId, ...FormerId[]]) =>
134 <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
135 Effect.provideService(effect, RenamePolicy, formerIds);