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.AccountSetting" as const;
11type TypeId = typeof TypeId;
12
13export type AccountSettingProps = {
14 /**
15 * Default usage model applied to new Workers in this account (e.g.
16 * `"standard"`). Mostly legacy since Workers Standard pricing — new
17 * accounts only support `"standard"`.
18 *
19 * Mutable — updated in place. When omitted, the account's current value
20 * is left untouched.
21 * @default keep the account's current value
22 */
23 defaultUsageModel?: string;
24 /**
25 * Whether Green Compute is enabled for the account. When enabled,
26 * scheduled (cron) Workers run only on hardware powered by renewable
27 * energy.
28 *
29 * Mutable — updated in place. When omitted, the account's current value
30 * is left untouched.
31 * @default keep the account's current value
32 */
33 greenCompute?: boolean;
34};
35
36export type AccountSettingAttributes = {
37 /** The Cloudflare account these settings belong to. */
38 accountId: string;
39 /** Resolved default usage model for the account. */
40 defaultUsageModel: string | undefined;
41 /** Resolved Green Compute flag for the account. */
42 greenCompute: boolean | undefined;
43 /**
44 * The `defaultUsageModel` the account had before Alchemy first managed
45 * this singleton. Restored on destroy.
46 */
47 initialDefaultUsageModel: string | undefined;
48 /**
49 * The `greenCompute` flag the account had before Alchemy first managed
50 * this singleton. Restored on destroy.
51 */
52 initialGreenCompute: boolean | undefined;
53};
54
55export type AccountSetting = Resource<
56 TypeId,
57 AccountSettingProps,
58 AccountSettingAttributes,
59 never,
60 Providers
61>;
62
63/**
64 * The account-wide Workers settings singleton
65 * (`/accounts/{account_id}/workers/account-settings`): the default usage
66 * model for new Workers and the Green Compute flag for scheduled Workers.
67 *
68 * This is a singleton — it always exists on every account with Cloudflare
69 * defaults, so this resource never creates or deletes anything physical.
70 * Reconcile PUTs the settings when the observed values differ from the
71 * desired ones; destroy restores the values the account had before Alchemy
72 * first managed it (captured as `initialDefaultUsageModel` /
73 * `initialGreenCompute`).
74 * ### Managing account settings
75 * **Example:** Enable Green Compute for scheduled Workers
76 * ```typescript
77 * yield* Cloudflare.Workers.AccountSetting("GreenCompute", {
78 * greenCompute: true,
79 * });
80 * ```
81 *
82 * **Example:** Pin the default usage model
83 * ```typescript
84 * yield* Cloudflare.Workers.AccountSetting("UsageModel", {
85 * defaultUsageModel: "standard",
86 * greenCompute: false,
87 * });
88 * ```
89 *
90 * @see https://developers.cloudflare.com/api/resources/workers/subresources/account_settings/
91 *
92 * @resource
93 * @product Workers
94 * @category Workers & Compute
95 */
96export const AccountSetting = Resource<AccountSetting>(TypeId);
97
98/**
99 * Returns true if the given value is a AccountSetting resource.
100 */
101export const isAccountSetting = (value: unknown): value is AccountSetting =>
102 Predicate.hasProperty(value, "Type") && value.Type === TypeId;
103
104export const AccountSettingProvider = () =>
105 Provider.succeed(AccountSetting, {
106 nuke: { singleton: true },
107 stables: ["accountId", "initialDefaultUsageModel", "initialGreenCompute"],
108
109 diff: Effect.fn(function* ({ output }) {
110 const { accountId } = yield* yield* CloudflareEnvironment;
111 // The singleton's identity is the account it lives on.
112 if (output !== undefined && output.accountId !== accountId) {
113 return { action: "replace" } as const;
114 }
115 return undefined;
116 }),
117
118 read: Effect.fn(function* ({ output }) {
119 const { accountId } = yield* yield* CloudflareEnvironment;
120 const acct = output?.accountId ?? accountId;
121 const observed = yield* workers.getAccountSetting({ accountId: acct });
122 // The settings singleton always exists with Cloudflare defaults —
123 // there is nothing to "own", so a cold read adopts freely (never
124 // `Unowned`). The observed values at adoption time become the
125 // initial values restored on destroy.
126 return toAttributes(
127 acct,
128 observed,
129 output !== undefined
130 ? output.initialDefaultUsageModel
131 : (observed.defaultUsageModel ?? undefined),
132 output !== undefined
133 ? output.initialGreenCompute
134 : (observed.greenCompute ?? undefined),
135 );
136 }),
137
138 // Account-scoped singleton: there is no collection API — the settings
139 // object always exists once per account. Read the single live object and
140 // return it as a one-element array, exactly mirroring `read` (a cold
141 // observation where the observed values are the initial values).
142 list: Effect.fn(function* () {
143 const { accountId } = yield* yield* CloudflareEnvironment;
144 const observed = yield* workers.getAccountSetting({ accountId });
145 return [
146 toAttributes(
147 accountId,
148 observed,
149 observed.defaultUsageModel ?? undefined,
150 observed.greenCompute ?? undefined,
151 ),
152 ];
153 }),
154
155 reconcile: Effect.fn(function* ({ news, output }) {
156 const { accountId } = yield* yield* CloudflareEnvironment;
157
158 // 1. Observe — the singleton always exists; read its live values.
159 const observed = yield* workers.getAccountSetting({ accountId });
160
161 // 2. Capture — the pre-management values, restored on destroy.
162 // `output` (including an adoption read) already carries them;
163 // otherwise this is our first touch and the observed values are
164 // the account's originals.
165 const initialDefaultUsageModel =
166 output !== undefined
167 ? output.initialDefaultUsageModel
168 : (observed.defaultUsageModel ?? undefined);
169 const initialGreenCompute =
170 output !== undefined
171 ? output.initialGreenCompute
172 : (observed.greenCompute ?? undefined);
173
174 // 3. Sync — desired = news merged over observed (unspecified props
175 // keep the account's current value); PUT only when dirty.
176 const desired = {
177 defaultUsageModel:
178 news.defaultUsageModel ?? observed.defaultUsageModel ?? undefined,
179 greenCompute: news.greenCompute ?? observed.greenCompute ?? undefined,
180 };
181 const dirty =
182 (news.defaultUsageModel !== undefined &&
183 (observed.defaultUsageModel ?? undefined) !==
184 news.defaultUsageModel) ||
185 (news.greenCompute !== undefined &&
186 (observed.greenCompute ?? undefined) !== news.greenCompute);
187 if (!dirty) {
188 return toAttributes(
189 accountId,
190 observed,
191 initialDefaultUsageModel,
192 initialGreenCompute,
193 );
194 }
195
196 yield* workers.putAccountSetting({ accountId, ...desired });
197 // 4. Return — the PUT is authoritative for what we just wrote. We do
198 // NOT re-read here: `getAccountSetting` is eventually consistent and
199 // can briefly echo the pre-write values right after a PUT (observed
200 // under concurrent load), which would surface a stale `greenCompute`
201 // in the reconcile output. The values we sent are the new state.
202 return {
203 accountId,
204 defaultUsageModel: desired.defaultUsageModel,
205 greenCompute: desired.greenCompute,
206 initialDefaultUsageModel,
207 initialGreenCompute,
208 };
209 }),
210
211 delete: Effect.fn(function* ({ output }) {
212 const { accountId, initialDefaultUsageModel, initialGreenCompute } =
213 output;
214 // There is no DELETE API — destroy restores the pre-management
215 // values. Skip the call when the live values already match
216 // (idempotent re-delete after a crashed run).
217 const observed = yield* workers.getAccountSetting({ accountId });
218 const dirty =
219 (initialDefaultUsageModel !== undefined &&
220 (observed.defaultUsageModel ?? undefined) !==
221 initialDefaultUsageModel) ||
222 (initialGreenCompute !== undefined &&
223 (observed.greenCompute ?? undefined) !== initialGreenCompute);
224 if (!dirty) return;
225 yield* workers.putAccountSetting({
226 accountId,
227 defaultUsageModel:
228 initialDefaultUsageModel ?? observed.defaultUsageModel ?? undefined,
229 greenCompute: initialGreenCompute ?? observed.greenCompute ?? undefined,
230 });
231 }),
232 });
233
234const toAttributes = (
235 accountId: string,
236 observed: workers.GetAccountSettingResponse,
237 initialDefaultUsageModel: string | undefined,
238 initialGreenCompute: boolean | undefined,
239): AccountSettingAttributes => ({
240 accountId,
241 defaultUsageModel: observed.defaultUsageModel ?? undefined,
242 greenCompute: observed.greenCompute ?? undefined,
243 initialDefaultUsageModel,
244 initialGreenCompute,
245});