1import * as argo from "@distilled.cloud/cloudflare/argo";
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";
9import { listAllZones } from "../Zone/lookup.ts";
10
11const TypeId = "Cloudflare.Argo.TieredCaching" as const;
12type TypeId = typeof TypeId;
13
14export type TieredCachingProps = {
15 /**
16 * Zone the Tiered Caching setting belongs to. Stable — changing the
17 * zone triggers a replacement (the old zone's setting is restored to
18 * the value it had before Alchemy managed it).
19 */
20 zoneId: string;
21 /**
22 * Whether Tiered Caching is enabled on the zone. Mutable — patched in
23 * place.
24 *
25 * @default true
26 */
27 enabled?: boolean;
28};
29
30export type TieredCachingAttributes = {
31 /** Zone the Tiered Caching setting belongs to. */
32 zoneId: string;
33 /** Resolved current value of the setting (`"on"` or `"off"`). */
34 value: "on" | "off";
35 /**
36 * Whether the setting can be modified on the zone's current plan.
37 */
38 editable: boolean;
39 /** When the setting was last modified, if Cloudflare reports it. */
40 modifiedOn: string | undefined;
41 /**
42 * The value the setting had before Alchemy first patched it. Restored
43 * on destroy, so deleting the resource puts the zone back the way it
44 * was found.
45 */
46 initialValue: "on" | "off";
47};
48
49export type TieredCaching = Resource<
50 TypeId,
51 TieredCachingProps,
52 TieredCachingAttributes,
53 never,
54 Providers
55>;
56
57/**
58 * Tiered Caching for a Cloudflare zone
59 * (`/zones/{zone_id}/argo/tiered_caching`).
60 *
61 * Tiered Caching routes cache misses through upper-tier Cloudflare data
62 * centers instead of every edge location contacting the origin directly,
63 * reducing origin load and improving cache hit ratios. It is available on
64 * all plans, free included.
65 *
66 * The setting is a singleton — it always exists on every zone with a
67 * Cloudflare default, so this resource never creates or deletes anything
68 * physical. Reconcile patches the setting when the observed value differs
69 * from the desired one; destroy restores the value the setting had before
70 * Alchemy first managed it (captured as `initialValue`).
71 *
72 * This is the generic Tiered Cache toggle (the dashboard "Tiered Cache"
73 * switch). Smart Tiered Cache (the smart-topology variant managed under
74 * `/cache/tiered_cache_smart_topology_enable`) requires Tiered Caching to
75 * be enabled — deploy this resource first when combining the two.
76 * ### Enabling Tiered Caching
77 * **Example:** Enable Tiered Caching on a zone
78 * ```typescript
79 * const zone = yield* Cloudflare.Zone.Zone("Site", { name: "example.com" });
80 *
81 * yield* Cloudflare.Argo.TieredCaching("TieredCaching", {
82 * zoneId: zone.zoneId,
83 * });
84 * ```
85 *
86 * **Example:** Explicitly disable Tiered Caching
87 * ```typescript
88 * yield* Cloudflare.Argo.TieredCaching("TieredCaching", {
89 * zoneId: zone.zoneId,
90 * enabled: false,
91 * });
92 * ```
93 *
94 * @see https://developers.cloudflare.com/cache/how-to/tiered-cache/
95 *
96 * @resource
97 * @product Argo
98 * @category Performance & Reliability
99 */
100export const TieredCaching = Resource<TieredCaching>(TypeId);
101
102/**
103 * Returns true if the given value is a TieredCaching resource.
104 */
105export const isTieredCaching = (value: unknown): value is TieredCaching =>
106 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
107
108export const TieredCachingProvider = () =>
109 Provider.succeed(TieredCaching, {
110 nuke: { singleton: true },
111 stables: ["zoneId", "initialValue"],
112
113 list: Effect.fn(function* () {
114 const { accountId } = yield* yield* CloudflareEnvironment;
115 // No account-wide API for this zone singleton — enumerate every
116 // zone in the account and read its setting (every zone has one,
117 // tiered caching is available on all plans).
118 const allZones = yield* listAllZones(accountId);
119 const rows = yield* Effect.forEach(
120 allZones.map((zone) => zone.id),
121 (zoneId) =>
122 argo.getTieredCaching({ zoneId }).pipe(
123 Effect.map((observed) =>
124 toAttributes(zoneId, observed, toValue(observed.value)),
125 ),
126 // Zone deleted out-of-band between enumeration and read —
127 // the setting is gone with it; skip it. A concurrently-purged
128 // zone surfaces as `ZoneNotFound` (404 "Invalid or missing
129 // zone") rather than the 7003 object-identifier code.
130 Effect.catchTag(["InvalidObjectIdentifier", "ZoneNotFound"], () =>
131 Effect.succeed(undefined),
132 ),
133 ),
134 { concurrency: 10 },
135 );
136 return rows.filter(
137 (row): row is TieredCachingAttributes => row !== undefined,
138 );
139 }),
140
141 diff: Effect.fn(function* ({ olds = {}, news, output }) {
142 const o = olds as TieredCachingProps;
143 const n = news as TieredCachingProps;
144 // zoneId is Input<string>; compare only once both sides are concrete.
145 const oldZoneId =
146 output?.zoneId ?? (typeof o.zoneId === "string" ? o.zoneId : undefined);
147 if (
148 oldZoneId !== undefined &&
149 typeof n.zoneId === "string" &&
150 oldZoneId !== n.zoneId
151 ) {
152 return { action: "replace" } as const;
153 }
154 return undefined;
155 }),
156
157 read: Effect.fn(function* ({ output, olds }) {
158 const zoneId = output?.zoneId ?? (olds?.zoneId as string | undefined);
159 if (!zoneId) return undefined;
160 const observed = yield* argo.getTieredCaching({ zoneId }).pipe(
161 // Zone deleted out-of-band — the setting is gone with it.
162 Effect.catchTag("InvalidObjectIdentifier", () =>
163 Effect.succeed(undefined),
164 ),
165 );
166 if (observed === undefined) return undefined;
167 // The setting is a singleton that always exists with a Cloudflare
168 // default — there is nothing to "own", so a cold read adopts freely
169 // (never `Unowned`). The observed value at adoption time becomes the
170 // `initialValue` restored on destroy.
171 const initialValue =
172 output !== undefined ? output.initialValue : toValue(observed.value);
173 return toAttributes(zoneId, observed, initialValue);
174 }),
175
176 reconcile: Effect.fn(function* ({ news, output }) {
177 // Inputs have been resolved to concrete strings by Plan.
178 const zoneId = news.zoneId as string;
179 const desired: "on" | "off" = (news.enabled ?? true) ? "on" : "off";
180
181 // 1. Observe — the setting always exists; read its live value.
182 const observed = yield* argo.getTieredCaching({ zoneId });
183
184 // 2. Capture — the pre-management value, restored on destroy.
185 // `output` (including an adoption read) already carries it;
186 // otherwise this is our first touch and the observed value is
187 // the zone's original.
188 const initialValue =
189 output !== undefined ? output.initialValue : toValue(observed.value);
190
191 // 3. Sync — patch only when the observed value differs.
192 if (toValue(observed.value) === desired) {
193 return toAttributes(zoneId, observed, initialValue);
194 }
195 const patched = yield* argo.patchTieredCaching({
196 zoneId,
197 value: desired,
198 });
199 return toAttributes(zoneId, patched, initialValue);
200 }),
201
202 delete: Effect.fn(function* ({ output }) {
203 const { zoneId, initialValue } = output;
204 // Observe — if the zone itself is gone, so is the setting.
205 const observed = yield* argo
206 .getTieredCaching({ zoneId })
207 .pipe(
208 Effect.catchTag("InvalidObjectIdentifier", () =>
209 Effect.succeed(undefined),
210 ),
211 );
212 if (observed === undefined) return;
213 // Restore the pre-management value; skip the call when it already
214 // matches (idempotent re-delete after a crashed run).
215 if (toValue(observed.value) === initialValue) return;
216 yield* argo
217 .patchTieredCaching({ zoneId, value: initialValue })
218 .pipe(Effect.catchTag("InvalidObjectIdentifier", () => Effect.void));
219 }),
220 });
221
222/**
223 * Narrow the distilled response's open `"on" | "off" | (string & {})`
224 * value to the closed pair — Cloudflare only ever returns the two
225 * literals for this setting.
226 */
227const toValue = (value: string): "on" | "off" =>
228 value === "on" ? "on" : "off";
229
230const toAttributes = (
231 zoneId: string,
232 setting: argo.GetTieredCachingResponse | argo.PatchTieredCachingResponse,
233 initialValue: "on" | "off",
234): TieredCachingAttributes => ({
235 zoneId,
236 value: toValue(setting.value),
237 editable: setting.editable,
238 modifiedOn: setting.modifiedOn ?? undefined,
239 initialValue,
240});