1import * as smsvoice from "@distilled.cloud/aws/pinpoint-sms-voice-v2";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { diffTags } from "../../Tags.ts";
5
6/**
7 * Flatten the wire `Tag[]` list into a plain string record.
8 */
9export const toTagRecord = (
10 tags: readonly smsvoice.Tag[] | undefined,
11): Record<string, string> =>
12 Object.fromEntries((tags ?? []).map((t) => [t.Key, t.Value]));
13
14/**
15 * Convert a plain string record into the wire `Tag[]` list.
16 */
17export const toTagList = (tags: Record<string, string>): smsvoice.Tag[] =>
18 Object.entries(tags).map(([Key, Value]) => ({ Key, Value }));
19
20/**
21 * Read the observed tags of an End User Messaging SMS resource.
22 * Best-effort — a failure (e.g. a race with deletion) reports no tags.
23 */
24export const readSmsVoiceTags = Effect.fn(function* (arn: string) {
25 const response = yield* smsvoice
26 .listTagsForResource({ ResourceArn: arn })
27 .pipe(Effect.catch(() => Effect.succeed(undefined)));
28 return toTagRecord(response?.Tags);
29});
30
31/**
32 * Sync tags on an End User Messaging SMS resource: diff the OBSERVED
33 * cloud tags against the desired set and apply only the delta.
34 */
35export const syncSmsVoiceTags = Effect.fn(function* (
36 arn: string,
37 desiredTags: Record<string, string>,
38) {
39 const observedTags = yield* readSmsVoiceTags(arn);
40 const { removed, upsert } = diffTags(observedTags, desiredTags);
41 if (upsert.length > 0) {
42 yield* smsvoice
43 .tagResource({ ResourceArn: arn, Tags: upsert })
44 .pipe(retrySmsVoiceThrottled);
45 }
46 if (removed.length > 0) {
47 yield* smsvoice
48 .untagResource({ ResourceArn: arn, TagKeys: removed })
49 .pipe(retrySmsVoiceThrottled);
50 }
51});
52
53/**
54 * Explicitly-typed pipeable retry helper (inlining `Effect.retry` in a
55 * provider op widens the layer type in declaration emit). Retries
56 * `ThrottlingException` on a bounded exponential schedule — the End User
57 * Messaging SMS control plane has low per-account TPS limits.
58 */
59export const retrySmsVoiceThrottled = <
60 A,
61 E extends { readonly _tag: string },
62 R,
63>(
64 self: Effect.Effect<A, E, R>,
65): Effect.Effect<A, E, R> =>
66 Effect.retry(self, {
67 while: (e) => e._tag === "ThrottlingException",
68 schedule: Schedule.max([
69 Schedule.exponential("1 second"),
70 Schedule.recurs(6),
71 ]),
72 });