1import { Region as AwsRegion } from "@distilled.cloud/aws/Region";
2import * as kvs from "@distilled.cloud/aws/cloudfront-keyvaluestore";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Redacted from "effect/Redacted";
6import * as Schedule from "effect/Schedule";
7
8// CloudFront KeyValueStore APIs are only available from us-east-1, even when
9// the distribution using the store lives elsewhere.
10export const KVS_REGION = "us-east-1" as const;
11
12export const extractValue = (v: string | Redacted.Redacted<string>): string =>
13 typeof v === "string" ? v : Redacted.value(v);
14
15export const withKvsRegion = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
16 // The distilled Region service value is `Effect<RegionName>`, not a raw
17 // string — providing a bare string makes the client `yield*` a string and
18 // crash. Wrap in Effect.succeed (same as ACM/ECRPublic/WAFv2/GlobalAccelerator).
19 effect.pipe(Effect.provideService(AwsRegion, Effect.succeed(KVS_REGION)));
20
21export const withKvsRegionFn =
22 <Args extends any[], A, E, R>(
23 fn: (...args: Args) => Effect.Effect<A, E, R>,
24 ) =>
25 (...args: Args) =>
26 withKvsRegion(fn(...args));
27
28const isKvsNotReady = (error: unknown) => {
29 const tag = (error as { _tag?: string })._tag;
30 return tag === "ResourceNotFoundException" || tag === "ConflictException";
31};
32
33/**
34 * Bounded KVS retry: exponential backoff with the per-attempt delay capped
35 * at 2s and at most 24 recurrences (~45s worst case). NEVER use a bare
36 * `Schedule.max([Schedule.exponential(...), Schedule.recurs(n)])` for KVS
37 * retries — `recurs` bounds the COUNT, not the TIME, and an uncapped
38 * exponential makes the tail attempts wait minutes-to-hours (24 attempts
39 * at 100ms doubling total ~19 days), which reads as a silent hang.
40 */
41export const cappedKvsRetrySchedule = Schedule.max([
42 Schedule.exponential("100 millis"),
43 Schedule.recurs(24),
44]).pipe(
45 Schedule.modifyDelay(({ duration }) =>
46 Effect.succeed(
47 Duration.isGreaterThan(duration, Duration.seconds(2))
48 ? Duration.seconds(2)
49 : duration,
50 ),
51 ),
52);
53
54export const retryForKvsReadiness = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
55 effect.pipe(
56 Effect.retry({
57 while: isKvsNotReady,
58 schedule: cappedKvsRetrySchedule,
59 }),
60 );
61
62export const getKvsEtag = Effect.fn(function* (store: string) {
63 const response = yield* kvs.describeKeyValueStore({ KvsARN: store });
64 return response.ETag;
65});
66
67export const isKvsPreconditionFailed = (err: kvs.ValidationException) =>
68 typeof err.message === "string" &&
69 err.message.includes("Pre-Condition failed");