1import * as dax from "@distilled.cloud/aws/dax";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { isResolved } from "../../Diff.ts";
5import { createPhysicalName } from "../../PhysicalName.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9
10export interface ParameterGroupProps {
11 /**
12 * Name of the parameter group. If omitted, a deterministic physical name
13 * is generated. Changing the name replaces the parameter group.
14 */
15 parameterGroupName?: string;
16 /**
17 * Human-readable description of the parameter group.
18 */
19 description?: string;
20 /**
21 * Parameter overrides, e.g. `{ "query-ttl-millis": "60000" }`. DAX exposes
22 * two tunable parameters: `query-ttl-millis` and `record-ttl-millis`.
23 * DAX has no reset-to-default API, so a key removed from this map keeps
24 * its last applied value.
25 */
26 parameters?: Record<string, string>;
27}
28
29export interface ParameterGroup extends Resource<
30 "AWS.DAX.ParameterGroup",
31 ParameterGroupProps,
32 {
33 /** Name of the parameter group. */
34 parameterGroupName: string;
35 /** Description of the parameter group. */
36 description: string | undefined;
37 /** Current non-default parameter values, keyed by parameter name. */
38 parameters: Record<string, string>;
39 },
40 never,
41 Providers
42> {}
43
44/**
45 * A DAX parameter group — a named set of DAX engine parameters (item and
46 * query cache TTLs) that can be attached to one or more DAX
47 * {@link Cluster}s.
48 *
49 * Parameter groups are free and provision instantly. DAX does not support
50 * tags on parameter groups.
51 * ### Creating a Parameter Group
52 * **Example:** Parameter Group with Custom Cache TTLs
53 * ```typescript
54 * const params = yield* ParameterGroup("DaxParams", {
55 * description: "5 minute item and query TTLs",
56 * parameters: {
57 * "query-ttl-millis": "300000",
58 * "record-ttl-millis": "300000",
59 * },
60 * });
61 * ```
62 *
63 * ### Attaching to a Cluster
64 * **Example:** Cluster Using the Parameter Group
65 * ```typescript
66 * const cluster = yield* Cluster("Cache", {
67 * nodeType: "dax.t3.small",
68 * replicationFactor: 1,
69 * iamRoleArn: role.roleArn,
70 * parameterGroupName: params.parameterGroupName,
71 * });
72 * ```
73 *
74 * @resource
75 */
76export const ParameterGroup = Resource<ParameterGroup>(
77 "AWS.DAX.ParameterGroup",
78);
79
80export const ParameterGroupProvider = () =>
81 Provider.effect(
82 ParameterGroup,
83 Effect.gen(function* () {
84 const toName = (id: string, props: ParameterGroupProps) =>
85 props.parameterGroupName
86 ? Effect.succeed(props.parameterGroupName)
87 : createPhysicalName({ id, maxLength: 255, lowercase: true });
88
89 const readGroup = Effect.fn(function* (name: string) {
90 const response = yield* dax
91 .describeParameterGroups({ ParameterGroupNames: [name] })
92 .pipe(
93 Effect.catchTag("ParameterGroupNotFoundFault", () =>
94 Effect.succeed(undefined),
95 ),
96 );
97 return response?.ParameterGroups?.[0];
98 });
99
100 // Observed parameter values for the group (bounded pagination — DAX
101 // has no paginated distilled ops).
102 const readParameters = Effect.fn(function* (name: string) {
103 const values: Record<string, string> = {};
104 let nextToken: string | undefined;
105 for (let page = 0; page < 20; page++) {
106 const response = yield* dax.describeParameters({
107 ParameterGroupName: name,
108 NextToken: nextToken,
109 });
110 for (const parameter of response.Parameters ?? []) {
111 if (
112 parameter.ParameterName !== undefined &&
113 parameter.ParameterValue !== undefined
114 ) {
115 values[parameter.ParameterName] = parameter.ParameterValue;
116 }
117 }
118 nextToken = response.NextToken;
119 if (!nextToken) break;
120 }
121 return values;
122 });
123
124 const toAttrs = (
125 group: dax.ParameterGroup,
126 parameters: Record<string, string>,
127 overrides: Record<string, string> | undefined,
128 ) => ({
129 parameterGroupName: group.ParameterGroupName ?? "",
130 description: group.Description,
131 // Report only the user's overridden keys with their observed values,
132 // not the entire engine parameter list.
133 parameters: Object.fromEntries(
134 Object.keys(overrides ?? {}).map((key) => [
135 key,
136 parameters[key] ?? "",
137 ]),
138 ),
139 });
140
141 return {
142 stables: ["parameterGroupName"],
143
144 diff: Effect.fn(function* ({ id, olds, news }) {
145 if (!isResolved(news)) return undefined;
146 if (
147 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
148 ) {
149 return { action: "replace" } as const;
150 }
151 }),
152
153 read: Effect.fn(function* ({ id, olds, output }) {
154 const name =
155 output?.parameterGroupName ?? (yield* toName(id, olds ?? {}));
156 const group = yield* readGroup(name);
157 if (group?.ParameterGroupName === undefined) return undefined;
158 const parameters = yield* readParameters(name);
159 // DAX parameter groups do not support tags, so ownership cannot be
160 // verified — return the attributes directly.
161 return toAttrs(group, parameters, olds?.parameters);
162 }),
163
164 reconcile: Effect.fn(function* ({ id, news, output, session }) {
165 const props = news ?? {};
166 const name = output?.parameterGroupName ?? (yield* toName(id, props));
167
168 // 1. Observe — cloud state is authoritative.
169 let observed = yield* readGroup(name);
170
171 // 2. Ensure — create if missing; tolerate AlreadyExists as a race.
172 if (observed === undefined) {
173 yield* dax
174 .createParameterGroup({
175 ParameterGroupName: name,
176 Description: props.description,
177 })
178 .pipe(
179 Effect.catchTag(
180 "ParameterGroupAlreadyExistsFault",
181 () => Effect.void,
182 ),
183 );
184 observed = yield* readGroup(name);
185 }
186 if (observed === undefined) {
187 return yield* Effect.fail(
188 new Error(`DAX parameter group '${name}' not found after create`),
189 );
190 }
191
192 // 3. Sync parameters — diff desired overrides against OBSERVED
193 // values and apply only the delta. (Description is create-only in
194 // the DAX API — UpdateParameterGroup accepts only parameter
195 // values.)
196 const observedParameters = yield* readParameters(name);
197 const delta = Object.entries(props.parameters ?? {}).filter(
198 ([key, value]) => observedParameters[key] !== value,
199 );
200 if (delta.length > 0) {
201 yield* dax.updateParameterGroup({
202 ParameterGroupName: name,
203 ParameterNameValues: delta.map(
204 ([ParameterName, ParameterValue]) => ({
205 ParameterName,
206 ParameterValue,
207 }),
208 ),
209 });
210 for (const [key, value] of delta) {
211 observedParameters[key] = value;
212 }
213 }
214
215 yield* session.note(name);
216 return toAttrs(observed, observedParameters, props.parameters);
217 }),
218
219 delete: Effect.fn(function* ({ output }) {
220 // A parameter group still attached to a cluster rejects deletion
221 // with InvalidParameterGroupStateFault — retry (bounded) while the
222 // cluster releases it. NotFound is success (idempotent delete).
223 yield* dax
224 .deleteParameterGroup({
225 ParameterGroupName: output.parameterGroupName,
226 })
227 .pipe(
228 Effect.catchTag("ParameterGroupNotFoundFault", () => Effect.void),
229 Effect.retry({
230 while: (e) => e._tag === "InvalidParameterGroupStateFault",
231 schedule: Schedule.max([
232 Schedule.fixed("5 seconds"),
233 Schedule.recurs(10),
234 ]),
235 }),
236 );
237 }),
238
239 list: () =>
240 Effect.gen(function* () {
241 // Bounded hand-rolled pagination (no distilled paginator).
242 const groups: dax.ParameterGroup[] = [];
243 let nextToken: string | undefined;
244 for (let page = 0; page < 20; page++) {
245 const response = yield* dax.describeParameterGroups({
246 NextToken: nextToken,
247 });
248 groups.push(...(response.ParameterGroups ?? []));
249 nextToken = response.NextToken;
250 if (!nextToken) break;
251 }
252 return groups
253 .filter(
254 (group) =>
255 group.ParameterGroupName !== undefined &&
256 // AWS-managed default parameter groups (`default.dax1.0`,
257 // …) always exist and can never be deleted — keep them out
258 // of enumeration for account-wide teardown (nuke).
259 !group.ParameterGroupName.startsWith("default."),
260 )
261 .map((group) => toAttrs(group, {}, undefined));
262 }),
263 };
264 }),
265 );