import * as quicksight from "@distilled.cloud/aws/quicksight"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Schedule from "effect/Schedule"; import { diffTags } from "../../Tags.ts"; /** * A QuickSight resource whose asynchronous create/update has not converged * to a terminal status yet. */ export class QuickSightNotSettled extends Data.TaggedError( "QuickSightNotSettled", )<{ readonly resourceId: string; readonly status: string | undefined; }> {} /** * A QuickSight resource whose asynchronous create/update converged to a * `*_FAILED` status. */ export class QuickSightOperationFailed extends Data.TaggedError( "QuickSightOperationFailed", )<{ readonly resourceId: string; readonly status: string; readonly reason: string | undefined; }> {} const isInProgress = (status: string | undefined): boolean => status === "CREATION_IN_PROGRESS" || status === "UPDATE_IN_PROGRESS"; const isFailed = (status: string | undefined): boolean => status === "CREATION_FAILED" || status === "UPDATE_FAILED"; /** * Poll a QuickSight resource until its `ResourceStatus` reaches a terminal * value. `*_IN_PROGRESS` repeats (bounded); `*_FAILED` fails immediately. * Creation/update of most QuickSight resources settles within a few seconds * (data-source connectivity validation can take longer) — budget ~2.5 min. * * The explicit `Effect.Effect` return annotation is load-bearing: * inlining retry/repeat combinators in provider lifecycle code lets their * conditional return types survive into declaration emit and widen the * provider layer to `unknown` (see `../VpcLattice/internal.ts`). * * If the bounded schedule is exhausted while the resource is still * `*_IN_PROGRESS`, the last observation is returned as-is so the caller sees * the real (still-converging) resource rather than a spurious failure. */ export const waitForSettled = < A extends { status?: string }, E extends { readonly _tag: string }, R, >( resourceId: string, read: Effect.Effect, ): Effect.Effect => Effect.flatMap( Effect.repeat(read, { schedule: Schedule.max([ Schedule.fixed("5 seconds"), Schedule.recurs(30), ]), until: (observed) => observed === undefined || !isInProgress(observed.status), }), (observed): Effect.Effect => observed !== undefined && isFailed(observed.status) ? Effect.fail( new QuickSightOperationFailed({ resourceId, status: observed.status!, reason: undefined, }), ) : Effect.succeed(observed), ); /** * Convert a QuickSight wire tag list into a plain record. */ export const toTagRecord = ( tags: ReadonlyArray<{ Key?: string; Value?: string }> | undefined, ): Record => Object.fromEntries( (tags ?? []) .filter( (tag): tag is { Key: string; Value: string } => typeof tag.Key === "string" && typeof tag.Value === "string", ) .map((tag) => [tag.Key, tag.Value]), ); /** * Read the observed tags of a QuickSight resource by ARN. Best-effort — a * failure (e.g. a race with deletion) reports no tags. */ export const readQuickSightTags = Effect.fn(function* (arn: string) { const response = yield* quicksight .listTagsForResource({ ResourceArn: arn }) .pipe(Effect.catch(() => Effect.succeed(undefined))); return toTagRecord(response?.Tags); }); /** * Sync tags on a QuickSight resource: diff OBSERVED cloud tags against the * desired set and apply only the delta. */ export const syncQuickSightTags = Effect.fn(function* ( arn: string, desiredTags: Record, ) { const observedTags = yield* readQuickSightTags(arn); const { removed, upsert } = diffTags(observedTags, desiredTags); if (upsert.length > 0) { yield* quicksight.tagResource({ ResourceArn: arn, Tags: upsert }); } if (removed.length > 0) { yield* quicksight.untagResource({ ResourceArn: arn, TagKeys: removed }); } }); /** * Convert a desired tag record into the wire tag list for create calls. * QuickSight requires a non-empty `Tags` list when supplied, so an empty * record maps to `undefined`. */ export const toWireTags = ( tags: Record, ): quicksight.Tag[] | undefined => { const entries = Object.entries(tags); return entries.length > 0 ? entries.map(([Key, Value]) => ({ Key, Value })) : undefined; };