1import * as Effect from "effect/Effect";
2import * as Schedule from "effect/Schedule";
3
4/**
5 * Derives a deterministic Service Catalog idempotency token from the
6 * resource's instance ID so retried creates never double-provision.
7 * Tokens must match `[a-zA-Z0-9][a-zA-Z0-9_-]*` and be ≤ 128 chars.
8 */
9export const idempotencyToken = (instanceId: string): string =>
10 instanceId.replaceAll(/[^a-zA-Z0-9]/g, "").slice(0, 64) || "alchemy";
11
12/**
13 * Retries an effect while Service Catalog reports the resource as in use
14 * (`ResourceInUseException`) on a bounded schedule (~30s). Deleting a
15 * portfolio or product right after its associations were removed can
16 * transiently hit this due to eventual consistency.
17 *
18 * NOTE: extracted to module scope with an explicit return annotation —
19 * inlining `Effect.retry` in a provider lifecycle op leaves `Retry.Return`'s
20 * conditional type unresolved in declaration emit, which widens the
21 * provider's layer to an `unknown` R and poisons `AWS.providers()` for
22 * every downstream consumer.
23 */
24export const retryWhileResourceInUse = <A, E extends { _tag: string }, R>(
25 self: Effect.Effect<A, E, R>,
26): Effect.Effect<A, E, R> =>
27 Effect.retry(self, {
28 while: (e) => e._tag === "ResourceInUseException",
29 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(10)]),
30 });
31
32/**
33 * Polls a boolean check on a bounded fixed schedule until it observes
34 * `true` (or gives up silently after `attempts`). Service Catalog
35 * association lists are eventually consistent, so a freshly-created
36 * association may take a few seconds to appear.
37 */
38export const awaitVisible = <E, R>(
39 check: Effect.Effect<boolean, E, R>,
40 attempts = 10,
41): Effect.Effect<void, E, R> =>
42 Effect.gen(function* () {
43 for (let i = 0; i < attempts; i++) {
44 if (yield* check) return;
45 yield* Effect.sleep("2 seconds");
46 }
47 });