1import * as cache from "@distilled.cloud/cloudflare/cache";
2import * as Effect from "effect/Effect";
3import * as Predicate from "effect/Predicate";
4import * as Schedule from "effect/Schedule";
5
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
9import type { Providers } from "../Providers.ts";
10import { listAllZones } from "../Zone/lookup.ts";
11
12const TypeId = "Cloudflare.Cache.Reserve" as const;
13type TypeId = typeof TypeId;
14
15export interface ReserveProps {
16 /**
17 * Zone whose Cache Reserve setting is managed. Stable — changing the
18 * zone triggers a replacement (the old zone's setting is restored to
19 * the value it had before Alchemy managed it).
20 */
21 zoneId: string;
22 /**
23 * Whether Cache Reserve is enabled on the zone (`value: "on"`) or
24 * disabled (`value: "off"`). Mutable — patched in place.
25 * @default true
26 */
27 enabled?: boolean;
28 /**
29 * When true, destroying the resource also clears any data already
30 * stored in Cache Reserve (after restoring the setting), waiting for
31 * the asynchronous clear operation to complete. Disabling Cache
32 * Reserve does NOT purge stored data by itself — storage continues to
33 * bill until it expires or is cleared.
34 * @default false
35 */
36 clearOnDelete?: boolean;
37}
38
39export interface ReserveAttributes {
40 /** Zone the setting belongs to. */
41 zoneId: string;
42 /** Resolved current value of the setting (`"on"` or `"off"`). */
43 value: string;
44 /**
45 * Whether the setting can be modified on the zone's current plan
46 * (`false` means the setting is plan-gated).
47 */
48 editable: boolean;
49 /** When the setting was last modified, if Cloudflare reports it. */
50 modifiedOn: string | undefined;
51 /**
52 * The value the setting had before Alchemy first patched it. Restored
53 * on destroy, so deleting the resource puts the zone back the way it
54 * was found.
55 */
56 initialValue: string;
57}
58
59export type Reserve = Resource<
60 TypeId,
61 ReserveProps,
62 ReserveAttributes,
63 never,
64 Providers
65>;
66
67/**
68 * The Cache Reserve setting of a Cloudflare zone
69 * (`/zones/{zone_id}/cache/cache_reserve`).
70 *
71 * Cache Reserve is a large, persistent data store backed by R2 that serves
72 * as the ultimate upper-tier cache, dramatically reducing origin egress for
73 * cacheable content. The setting is a zone **singleton** — it always exists
74 * on entitled zones (default `off`), so this resource never creates or
75 * deletes anything physical. Reconcile patches the setting when the
76 * observed value differs from the desired one; destroy restores the value
77 * the setting had before Alchemy first managed it (captured as
78 * `initialValue`).
79 *
80 * **Entitlement-gated**: Cache Reserve is a usage-billed add-on that must
81 * be purchased/enabled on the account. On zones without the subscription
82 * both reads and writes fail with the typed `SettingUnavailableForPlan`
83 * error ("this zone setting is not available for your plan type").
84 *
85 * Disabling Cache Reserve does not purge data already in reserve — set
86 * `clearOnDelete: true` to run the asynchronous Cache Reserve Clear
87 * operation on destroy (the provider polls until the clear completes).
88 *
89 * Only one `Reserve` resource per zone makes sense — two instances
90 * managing the same zone would fight over the singleton.
91 * ### Managing Cache Reserve
92 * **Example:** Enable Cache Reserve on a zone
93 * ```typescript
94 * const zone = yield* Cloudflare.Zone.Zone("Site", { name: "example.com" });
95 *
96 * yield* Cloudflare.Cache.Reserve("Reserve", {
97 * zoneId: zone.zoneId,
98 * });
99 * ```
100 *
101 * **Example:** Clear stored data when the resource is destroyed
102 * ```typescript
103 * yield* Cloudflare.Cache.Reserve("Reserve", {
104 * zoneId: zone.zoneId,
105 * clearOnDelete: true,
106 * });
107 * ```
108 *
109 * @see https://developers.cloudflare.com/cache/advanced-configuration/cache-reserve/
110 *
111 * @resource
112 * @product Cache
113 * @category Performance & Reliability
114 */
115export const Reserve = Resource<Reserve>(TypeId, {
116 aliases: ["Cloudflare.Cache.CacheReserve"],
117});
118
119/**
120 * Returns true if the given value is a Reserve resource.
121 */
122export const isReserve = (value: unknown): value is Reserve =>
123 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
124
125const desiredValue = (props: ReserveProps): "on" | "off" =>
126 (props.enabled ?? true) ? "on" : "off";
127
128export const ReserveProvider = () =>
129 Provider.succeed(Reserve, {
130 stables: ["zoneId", "initialValue"],
131
132 list: Effect.fn(function* () {
133 const { accountId } = yield* yield* CloudflareEnvironment;
134 // No account-wide API for this zone singleton — enumerate every
135 // zone in the account and read its setting. Cache Reserve is an
136 // entitlement-gated add-on, so non-subscribed zones reject the
137 // route (`SettingUnavailableForPlan`); skip those.
138 const allZones = yield* listAllZones(accountId);
139 const rows = yield* Effect.forEach(
140 allZones.map((zone) => zone.id),
141 (zoneId) =>
142 cache.getCacheReserve({ zoneId }).pipe(
143 Effect.map((observed) =>
144 toAttributes(zoneId, observed, observed.value),
145 ),
146 // Plan-gated zones (no Cache Reserve subscription) and
147 // partial/deleted zones reject the route; skip them.
148 Effect.catchTag("SettingUnavailableForPlan", () =>
149 Effect.succeed(undefined),
150 ),
151 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
152 ),
153 { concurrency: 10 },
154 );
155 return rows.filter((row): row is ReserveAttributes => row !== undefined);
156 }),
157
158 diff: Effect.fn(function* ({ olds = {}, news, output }) {
159 const o = olds as ReserveProps;
160 const n = news as ReserveProps;
161 // zoneId is Input<string>; compare only once both sides are concrete.
162 const oldZoneId =
163 output?.zoneId ?? (typeof o.zoneId === "string" ? o.zoneId : undefined);
164 if (
165 oldZoneId !== undefined &&
166 typeof n.zoneId === "string" &&
167 oldZoneId !== n.zoneId
168 ) {
169 return { action: "replace" } as const;
170 }
171 return undefined;
172 }),
173
174 read: Effect.fn(function* ({ output, olds }) {
175 const zoneId = output?.zoneId ?? (olds?.zoneId as string | undefined);
176 if (!zoneId) return undefined;
177 const observed = yield* cache.getCacheReserve({ zoneId }).pipe(
178 // Zone deleted out-of-band — the setting is gone with it.
179 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
180 );
181 if (observed === undefined) return undefined;
182 // The setting is a singleton that always exists with a Cloudflare
183 // default — there is nothing to "own", so a cold read adopts
184 // freely (never `Unowned`). The observed value at adoption time
185 // becomes the `initialValue` restored on destroy.
186 const initialValue =
187 output !== undefined ? output.initialValue : observed.value;
188 return toAttributes(zoneId, observed, initialValue);
189 }),
190
191 reconcile: Effect.fn(function* ({ news, output }) {
192 // Inputs have been resolved to concrete strings by Plan.
193 const zoneId = news.zoneId as string;
194
195 // 1. Observe — the setting always exists (on entitled zones); read
196 // its live value. Entitlement-gated zones fail here with the
197 // typed `SettingUnavailableForPlan` error.
198 const observed = yield* cache.getCacheReserve({ zoneId });
199
200 // 2. Capture — the pre-management value, restored on destroy.
201 // `output` (including an adoption read) already carries it;
202 // otherwise this is our first touch and the observed value is
203 // the zone's original.
204 const initialValue =
205 output !== undefined ? output.initialValue : observed.value;
206
207 // 3. Sync — patch only when the observed value differs.
208 const desired = desiredValue(news);
209 if (observed.value === desired) {
210 return toAttributes(zoneId, observed, initialValue);
211 }
212 const patched = yield* cache.patchCacheReserve({
213 zoneId,
214 value: desired,
215 });
216 return toAttributes(zoneId, patched, initialValue);
217 }),
218
219 delete: Effect.fn(function* ({ output, olds }) {
220 const { zoneId, initialValue } = output;
221 // Observe — if the zone itself is gone (or the entitlement was
222 // dropped so the setting no longer exists), nothing to restore.
223 const observed = yield* cache.getCacheReserve({ zoneId }).pipe(
224 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
225 Effect.catchTag("SettingUnavailableForPlan", () =>
226 Effect.succeed(undefined),
227 ),
228 );
229 if (observed === undefined) return;
230 // Restore the pre-management value; skip the call when it already
231 // matches (idempotent re-delete after a crashed run).
232 if (observed.value !== initialValue) {
233 yield* cache
234 .patchCacheReserve({ zoneId, value: initialValue })
235 .pipe(Effect.catchTag("InvalidRoute", () => Effect.void));
236 }
237 // Optionally clear data already stored in reserve — an async
238 // operation that we kick off and poll to completion (bounded).
239 if (olds?.clearOnDelete === true) {
240 yield* cache.clearCacheReserve({ zoneId });
241 yield* cache.statusCacheReserve({ zoneId }).pipe(
242 Effect.repeat({
243 schedule: Schedule.spaced("5 seconds"),
244 until: (status) => status.state === "Completed",
245 times: 60,
246 }),
247 );
248 }
249 }),
250 });
251
252const toAttributes = (
253 zoneId: string,
254 setting: cache.GetCacheReserveResponse | cache.PatchCacheReserveResponse,
255 initialValue: string,
256): ReserveAttributes => ({
257 zoneId,
258 value: setting.value,
259 editable: setting.editable,
260 modifiedOn: setting.modifiedOn ?? undefined,
261 initialValue,
262});