import * as agw2 from "@distilled.cloud/aws/apigatewayv2"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import { pipe } from "effect/Function"; import * as Schedule from "effect/Schedule"; import { diffTags, normalizeTags } from "../../Tags.ts"; /** * API Gateway v2 ARN helpers. Unlike most services the resource ARNs used * for tagging omit the account id (`arn:aws:apigateway:{region}::/apis/{id}`). */ export const apiArn = (region: string, apiId: string) => `arn:aws:apigateway:${region}::/apis/${apiId}`; export const stageArn = (region: string, apiId: string, stageName: string) => `arn:aws:apigateway:${region}::/apis/${apiId}/stages/${stageName}`; export const domainNameArn = (region: string, domainName: string) => `arn:aws:apigateway:${region}::/domainnames/${domainName}`; export const vpcLinkArn = (region: string, vpcLinkId: string) => `arn:aws:apigateway:${region}::/vpclinks/${vpcLinkId}`; /** * The `execute-api` ARN that IAM policies (Lambda resource policies, * `execute-api:Invoke`/`ManageConnections` statements) use to scope access * to an API. Wildcards match across path segments. */ export const executeApiArn = ( region: string, accountId: string, apiId: string, suffix = "/*", ) => `arn:aws:execute-api:${region}:${accountId}:${apiId}${suffix}`; /** * `Create/Update/Delete` operations across API Gateway v2 share an * account-wide throttle; parallel test suites and deploys routinely see * `TooManyRequestsException` outlast the blanket SDK retry budget. The * schedule below (exponential base 1s capped at 20s, 10 attempts, ~60s * total) rides out the throttle window without hiding real failures. * * The helper carries an EXPLICIT return annotation so the conditional type * of `Effect.retry` never leaks into declaration emit (which would widen the * provider layer to `unknown` for every consumer of `AWS.providers()`). */ export const retryOnTooManyRequests = ( effect: Effect.Effect, ): Effect.Effect => Effect.retry(effect, { while: (error: E) => error._tag === "TooManyRequestsException" || error._tag === "ConflictException", schedule: Schedule.max([ pipe( Schedule.exponential(Duration.seconds(1), 2), Schedule.modifyDelay(({ duration }) => Effect.succeed( Duration.isGreaterThan(duration, Duration.seconds(20)) ? Duration.seconds(20) : duration, ), ), ), Schedule.recurs(10), ]), }) as Effect.Effect; /** * Normalize the wire tag map (values may be `undefined`) to a plain record. */ export const tagRecord = ( tags: { [key: string]: string | undefined } | undefined, ): Record => Object.fromEntries( Object.entries(tags ?? {}).filter( (entry): entry is [string, string] => entry[1] !== undefined, ), ); /** * API Gateway v2 collection operations (`getApis`, `getRoutes`, `getStages`, * …) return `{ Items, NextToken }` pages without a Smithy pagination trait, * so distilled exposes no `.pages` stream for them. Collect every page * manually, bounded at 100 pages so a misbehaving token can never hang the * engine. */ export const collectAllPages = Effect.fn(function* ( fetchPage: ( nextToken: string | undefined, ) => Effect.Effect<{ Items?: readonly A[] | A[]; NextToken?: string }, E, R>, ) { const items: A[] = []; let nextToken: string | undefined = undefined; for (let page = 0; page < 100; page++) { const result: { Items?: readonly A[] | A[]; NextToken?: string } = yield* fetchPage(nextToken); items.push(...(result.Items ?? [])); nextToken = result.NextToken; if (!nextToken) break; } return items; }); /** * Diff observed tags against desired tags and apply only the delta via the * v2 `tagResource`/`untagResource` operations. */ export const syncTags = Effect.fn(function* ({ resourceArn, oldTags, newTags, }: { resourceArn: string; oldTags: Record; newTags: Record; }) { const { removed, upsert } = diffTags(oldTags, newTags); if (removed.length > 0) { yield* agw2 .untagResource({ ResourceArn: resourceArn, TagKeys: removed }) .pipe(Effect.catchTag("NotFoundException", () => Effect.void)); } if (upsert.length > 0) { yield* agw2.tagResource({ ResourceArn: resourceArn, Tags: normalizeTags(upsert), }); } });