1import { Region as AwsRegion } from "@distilled.cloud/aws/Region";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4
5// Global Accelerator is a global service, but its control-plane API only
6// exists in us-west-2 ("you must specify the US West (Oregon) Region to
7// create, update, or otherwise work with accelerators"). Pin every call so
8// the resources work regardless of the ambient deployment region.
9export const GA_REGION = "us-west-2" as const;
10
11// NOTE: the Region service's value is an Effect<RegionName>, not a string.
12export const withGaRegion = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
13 effect.pipe(Effect.provideService(AwsRegion, Effect.succeed(GA_REGION)));
14
15/**
16 * Global Accelerator serializes configuration changes per accelerator as
17 * "transactions"; a second mutation while one is propagating is rejected with
18 * `TransactionInProgressException` (or `ConflictException`). Retry on a
19 * bounded schedule (~60s) until the in-flight transaction lands.
20 *
21 * NOTE: explicit return annotation is load-bearing — an inline `Effect.retry`
22 * in provider lifecycle code leaks `Retry.Return` conditionals into
23 * declaration emit and widens the provider layer to `unknown` R.
24 */
25export const retryGaTransaction = <A, E extends { _tag: string }, R>(
26 self: Effect.Effect<A, E, R>,
27): Effect.Effect<A, E, R> =>
28 Effect.retry(self, {
29 while: (e) =>
30 e._tag === "TransactionInProgressException" ||
31 e._tag === "ConflictException",
32 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(20)]),
33 });
34
35/**
36 * Deletion is subject to both GA's serialized transactions and occasional
37 * transient internal-service failures. Keep the retry bounded to about one
38 * minute so teardown converges without hanging indefinitely.
39 */
40export const retryGaDeletion = <A, E extends { _tag: string }, R>(
41 self: Effect.Effect<A, E, R>,
42): Effect.Effect<A, E, R> =>
43 Effect.retry(self, {
44 while: (e) =>
45 e._tag === "TransactionInProgressException" ||
46 e._tag === "ConflictException" ||
47 e._tag === "InternalServiceErrorException",
48 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(20)]),
49 });
50
51/**
52 * `deleteAccelerator` requires the accelerator to be fully disabled and all
53 * listeners removed. Both conditions clear asynchronously after the disabling
54 * `updateAccelerator` / dependent deletes return, so retry the delete on a
55 * bounded schedule (~3 minutes) until the disable transaction propagates.
56 */
57export const retryUntilAcceleratorDeletable = <
58 A,
59 E extends { _tag: string },
60 R,
61>(
62 self: Effect.Effect<A, E, R>,
63): Effect.Effect<A, E, R> =>
64 Effect.retry(self, {
65 while: (e) =>
66 e._tag === "AcceleratorNotDisabledException" ||
67 e._tag === "AssociatedListenerFoundException" ||
68 e._tag === "TransactionInProgressException" ||
69 e._tag === "InternalServiceErrorException",
70 schedule: Schedule.max([Schedule.fixed("5 seconds"), Schedule.recurs(36)]),
71 });
72
73/**
74 * `deleteListener` is rejected with `AssociatedEndpointGroupFoundException`
75 * while a just-deleted endpoint group is still detaching. Retry briefly.
76 */
77export const retryUntilListenerDeletable = <A, E extends { _tag: string }, R>(
78 self: Effect.Effect<A, E, R>,
79): Effect.Effect<A, E, R> =>
80 Effect.retry(self, {
81 while: (e) =>
82 e._tag === "AssociatedEndpointGroupFoundException" ||
83 e._tag === "TransactionInProgressException" ||
84 e._tag === "InternalServiceErrorException",
85 schedule: Schedule.max([Schedule.fixed("5 seconds"), Schedule.recurs(24)]),
86 });