1import * as workers from "@distilled.cloud/cloudflare/workers";
2import * as Effect from "effect/Effect";
3import * as Predicate from "effect/Predicate";
4
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
8import type { Providers } from "../Providers.ts";
9
10const TypeId = "Cloudflare.Workers.Subdomain" as const;
11type TypeId = typeof TypeId;
12
13export type SubdomainProps = {
14 /**
15 * The account's `workers.dev` subdomain name (the `<subdomain>` in
16 * `https://<script>.<subdomain>.workers.dev`). Must contain only
17 * lowercase ASCII letters, digits, and hyphens.
18 *
19 * Mutable — renamed in place via PUT. **Renaming changes the URL of
20 * every deployed Worker on the account that uses a `workers.dev`
21 * subdomain**, so treat changes with extreme care.
22 */
23 subdomain: string;
24};
25
26export type SubdomainAttributes = {
27 /** The Cloudflare account the subdomain belongs to. */
28 accountId: string;
29 /** The account's current `workers.dev` subdomain name. */
30 subdomain: string;
31 /**
32 * The subdomain name the account had before Alchemy first managed this
33 * singleton, or `undefined` if the account had no subdomain registered.
34 * Restored on destroy (or the subdomain is removed entirely when there
35 * was none).
36 */
37 initialSubdomain: string | undefined;
38};
39
40export type Subdomain = Resource<
41 TypeId,
42 SubdomainProps,
43 SubdomainAttributes,
44 never,
45 Providers
46>;
47
48/**
49 * The account-wide `workers.dev` subdomain singleton
50 * (`/accounts/{account_id}/workers/subdomain`). Every Worker with
51 * `workers.dev` enabled is served at
52 * `https://<script>.<subdomain>.workers.dev`.
53 *
54 * Each account has at most one subdomain — "creating" this resource claims
55 * (or renames to) the requested name. Subdomain names are globally unique
56 * across all Cloudflare accounts; claiming a taken name fails with the
57 * typed `SubdomainAlreadyExists` error.
58 *
59 * Destroy is capture-and-restore: the subdomain is renamed back to the
60 * value it had before Alchemy first managed it. If the account had no
61 * subdomain at first touch, destroy removes it entirely.
62 *
63 * **Warning:** renaming or removing the subdomain immediately changes the
64 * URL of every deployed Worker on the account that relies on
65 * `workers.dev`. Only manage this resource on accounts where that is
66 * acceptable.
67 * ### Managing the subdomain
68 * **Example:** Pin the account's workers.dev subdomain
69 * ```typescript
70 * const sub = yield* Cloudflare.Workers.Subdomain("Subdomain", {
71 * subdomain: "my-team",
72 * });
73 * // Workers are now served from https://<script>.my-team.workers.dev
74 * ```
75 *
76 * @see https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
77 *
78 * @resource
79 * @product Workers
80 * @category Workers & Compute
81 */
82export const Subdomain = Resource<Subdomain>(TypeId);
83
84/**
85 * Returns true if the given value is a Subdomain resource.
86 */
87export const isSubdomain = (value: unknown): value is Subdomain =>
88 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
89
90export const SubdomainProvider = () =>
91 Provider.succeed(Subdomain, {
92 nuke: { singleton: true },
93 stables: ["accountId", "initialSubdomain"],
94
95 diff: Effect.fn(function* ({ output }) {
96 const { accountId } = yield* yield* CloudflareEnvironment;
97 // The singleton's identity is the account it lives on; the name
98 // itself is renamed in place via PUT.
99 if (output !== undefined && output.accountId !== accountId) {
100 return { action: "replace" } as const;
101 }
102 return undefined;
103 }),
104
105 read: Effect.fn(function* ({ output }) {
106 const { accountId } = yield* yield* CloudflareEnvironment;
107 const acct = output?.accountId ?? accountId;
108 const observed = yield* getSubdomain(acct);
109 if (observed === undefined) return undefined;
110 // The subdomain is an account singleton — there is nothing to
111 // "own", so a cold read adopts freely (never `Unowned`). The
112 // observed name at adoption time becomes the `initialSubdomain`
113 // restored on destroy.
114 return {
115 accountId: acct,
116 subdomain: observed,
117 initialSubdomain:
118 output !== undefined ? output.initialSubdomain : observed,
119 };
120 }),
121
122 list: Effect.fn(function* () {
123 const { accountId } = yield* yield* CloudflareEnvironment;
124 // Account singleton — at most one workers.dev subdomain per account.
125 // Mirror `read` with no prior output: the observed name is also the
126 // `initialSubdomain`. Return a one-element array when registered, `[]`
127 // when the account has never claimed a subdomain.
128 const observed = yield* getSubdomain(accountId);
129 if (observed === undefined) return [];
130 return [
131 {
132 accountId,
133 subdomain: observed,
134 initialSubdomain: observed,
135 },
136 ];
137 }),
138
139 reconcile: Effect.fn(function* ({ news, output }) {
140 const { accountId } = yield* yield* CloudflareEnvironment;
141
142 // 1. Observe — read the account's live subdomain (undefined when
143 // the account has never registered one).
144 const observed = yield* getSubdomain(accountId);
145
146 // 2. Capture — the pre-management name, restored on destroy.
147 // `output` (including an adoption read) already carries it;
148 // otherwise this is our first touch and the observed name is
149 // the account's original (undefined = none existed).
150 const initialSubdomain =
151 output !== undefined ? output.initialSubdomain : observed;
152
153 // 3. Sync — claim/rename only when the observed name differs.
154 if (observed === news.subdomain) {
155 return { accountId, subdomain: observed, initialSubdomain };
156 }
157 const updated = yield* workers.putSubdomain({
158 accountId,
159 subdomain: news.subdomain,
160 });
161 return { accountId, subdomain: updated.subdomain, initialSubdomain };
162 }),
163
164 delete: Effect.fn(function* ({ output }) {
165 const { accountId, initialSubdomain } = output;
166 // Observe — already gone (or never existed) means nothing to do.
167 const observed = yield* getSubdomain(accountId);
168 // Restore the pre-management name; skip the call when it already
169 // matches (idempotent re-delete after a crashed run).
170 if (observed === initialSubdomain) return;
171 if (initialSubdomain === undefined) {
172 // The account had no subdomain before we claimed one — remove it.
173 yield* workers
174 .deleteSubdomain({ accountId })
175 .pipe(Effect.catchTag("SubdomainNotFound", () => Effect.void));
176 return;
177 }
178 if (observed === undefined) return;
179 yield* workers.putSubdomain({
180 accountId,
181 subdomain: initialSubdomain,
182 });
183 }),
184 });
185
186/**
187 * Read the account's workers.dev subdomain, mapping "no subdomain
188 * registered" (`SubdomainNotFound`, HTTP 404) to `undefined`.
189 */
190const getSubdomain = (accountId: string) =>
191 workers.getSubdomain({ accountId }).pipe(
192 Effect.map((r) => r.subdomain),
193 Effect.catchTag("SubdomainNotFound", () => Effect.succeed(undefined)),
194 );