import * as medialive from "@distilled.cloud/aws/medialive";
import * as Data from "effect/Data";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
import { diffTags } from "../../Tags.ts";
/**
* Raised while a MediaLive resource is still transitioning (e.g. a channel
* in `CREATING`); used as a typed retry signal, never surfaced on success.
*/
export class MediaLiveResourcePending extends Data.TaggedError(
"MediaLiveResourcePending",
)<{ message: string }> {}
// Explicitly-typed pipeable retry helpers. Inlining `Effect.retry` in a
// provider lifecycle op leaks `Retry.Return`'s conditional into declaration
// emit and widens the provider layer to `unknown` R for every consumer of
// `AWS.providers()`.
/** Bounded wait (3s x 30 = 90s) while a resource is still transitioning. */
export const retryWhilePending = (
self: Effect.Effect,
): Effect.Effect =>
Effect.retry(self, {
while: (e) => e._tag === "MediaLiveResourcePending",
schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(30)]),
});
/**
* Bounded retry (3s x 10) on `ConflictException` — MediaLive rejects
* deletes/updates while a dependent resource is still detaching.
*/
export const retryWhileConflict = (
self: Effect.Effect,
): Effect.Effect =>
Effect.retry(self, {
while: (e) => e._tag === "ConflictException",
schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(10)]),
});
/**
* Raised when a MediaLive API response omits a field the provider requires
* (e.g. a create response without the resource body or its Id/Arn).
*/
export class MediaLiveIncompleteResponse extends Data.TaggedError(
"MediaLiveIncompleteResponse",
)<{ message: string }> {}
/** Narrow an optional wire field to defined with a typed failure. */
export const ensurePresent = (
value: T | undefined,
what: string,
): Effect.Effect =>
value === undefined
? Effect.fail(
new MediaLiveIncompleteResponse({
message: `MediaLive response is missing ${what}`,
}),
)
: Effect.succeed(value);
/**
* Narrow a MediaLive resource body to one whose server-assigned `Id`/`Arn`
* are present, failing with a typed error otherwise.
*/
export const ensureIdentified = (
value: T | undefined,
what: string,
): Effect.Effect<
T & { Id: string; Arn: string },
MediaLiveIncompleteResponse
> =>
value === undefined || value.Id === undefined || value.Arn === undefined
? Effect.fail(
new MediaLiveIncompleteResponse({
message: `MediaLive response is missing ${what}`,
}),
)
: Effect.succeed(value as T & { Id: string; Arn: string });
/**
* Coerce a MediaLive wire tag map (values are `string | undefined`) into a
* plain `Record`, dropping any undefined values.
*/
export const toTagRecord = (
tags: { [key: string]: string | undefined } | undefined,
): Record =>
Object.fromEntries(
Object.entries(tags ?? {}).filter(
(entry): entry is [string, string] => typeof entry[1] === "string",
),
);
/**
* Read the observed tags of a MediaLive resource by ARN. Tag reads are
* best-effort — a failure (e.g. a race with deletion) reports no tags.
*/
export const readMlTags = Effect.fn(function* (arn: string) {
const response = yield* medialive
.listTagsForResource({ ResourceArn: arn })
.pipe(Effect.catch(() => Effect.succeed(undefined)));
return toTagRecord(response?.Tags);
});
/**
* Sync tags on a MediaLive resource: diff the OBSERVED cloud tags against
* the desired set and apply only the delta. MediaLive's `createTags` upserts
* a tag map; `deleteTags` removes keys.
*/
export const syncMlTags = Effect.fn(function* (
arn: string,
desiredTags: Record,
) {
const observedTags = yield* readMlTags(arn);
const { removed, upsert } = diffTags(observedTags, desiredTags);
if (upsert.length > 0) {
yield* medialive.createTags({
ResourceArn: arn,
Tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
});
}
if (removed.length > 0) {
yield* medialive.deleteTags({ ResourceArn: arn, TagKeys: removed });
}
});