1import * as medialive from "@distilled.cloud/aws/medialive";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { diffTags } from "../../Tags.ts";
6
7/**
8 * Raised while a MediaLive resource is still transitioning (e.g. a channel
9 * in `CREATING`); used as a typed retry signal, never surfaced on success.
10 */
11export class MediaLiveResourcePending extends Data.TaggedError(
12 "MediaLiveResourcePending",
13)<{ message: string }> {}
14
15// Explicitly-typed pipeable retry helpers. Inlining `Effect.retry` in a
16// provider lifecycle op leaks `Retry.Return`'s conditional into declaration
17// emit and widens the provider layer to `unknown` R for every consumer of
18// `AWS.providers()`.
19
20/** Bounded wait (3s x 30 = 90s) while a resource is still transitioning. */
21export const retryWhilePending = <A, E extends { readonly _tag: string }, R>(
22 self: Effect.Effect<A, E, R>,
23): Effect.Effect<A, E, R> =>
24 Effect.retry(self, {
25 while: (e) => e._tag === "MediaLiveResourcePending",
26 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(30)]),
27 });
28
29/**
30 * Bounded retry (3s x 10) on `ConflictException` — MediaLive rejects
31 * deletes/updates while a dependent resource is still detaching.
32 */
33export const retryWhileConflict = <A, E extends { readonly _tag: string }, R>(
34 self: Effect.Effect<A, E, R>,
35): Effect.Effect<A, E, R> =>
36 Effect.retry(self, {
37 while: (e) => e._tag === "ConflictException",
38 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(10)]),
39 });
40
41/**
42 * Raised when a MediaLive API response omits a field the provider requires
43 * (e.g. a create response without the resource body or its Id/Arn).
44 */
45export class MediaLiveIncompleteResponse extends Data.TaggedError(
46 "MediaLiveIncompleteResponse",
47)<{ message: string }> {}
48
49/** Narrow an optional wire field to defined with a typed failure. */
50export const ensurePresent = <T>(
51 value: T | undefined,
52 what: string,
53): Effect.Effect<T, MediaLiveIncompleteResponse> =>
54 value === undefined
55 ? Effect.fail(
56 new MediaLiveIncompleteResponse({
57 message: `MediaLive response is missing ${what}`,
58 }),
59 )
60 : Effect.succeed(value);
61
62/**
63 * Narrow a MediaLive resource body to one whose server-assigned `Id`/`Arn`
64 * are present, failing with a typed error otherwise.
65 */
66export const ensureIdentified = <T extends { Id?: string; Arn?: string }>(
67 value: T | undefined,
68 what: string,
69): Effect.Effect<
70 T & { Id: string; Arn: string },
71 MediaLiveIncompleteResponse
72> =>
73 value === undefined || value.Id === undefined || value.Arn === undefined
74 ? Effect.fail(
75 new MediaLiveIncompleteResponse({
76 message: `MediaLive response is missing ${what}`,
77 }),
78 )
79 : Effect.succeed(value as T & { Id: string; Arn: string });
80
81/**
82 * Coerce a MediaLive wire tag map (values are `string | undefined`) into a
83 * plain `Record<string, string>`, dropping any undefined values.
84 */
85export const toTagRecord = (
86 tags: { [key: string]: string | undefined } | undefined,
87): Record<string, string> =>
88 Object.fromEntries(
89 Object.entries(tags ?? {}).filter(
90 (entry): entry is [string, string] => typeof entry[1] === "string",
91 ),
92 );
93
94/**
95 * Read the observed tags of a MediaLive resource by ARN. Tag reads are
96 * best-effort — a failure (e.g. a race with deletion) reports no tags.
97 */
98export const readMlTags = Effect.fn(function* (arn: string) {
99 const response = yield* medialive
100 .listTagsForResource({ ResourceArn: arn })
101 .pipe(Effect.catch(() => Effect.succeed(undefined)));
102 return toTagRecord(response?.Tags);
103});
104
105/**
106 * Sync tags on a MediaLive resource: diff the OBSERVED cloud tags against
107 * the desired set and apply only the delta. MediaLive's `createTags` upserts
108 * a tag map; `deleteTags` removes keys.
109 */
110export const syncMlTags = Effect.fn(function* (
111 arn: string,
112 desiredTags: Record<string, string>,
113) {
114 const observedTags = yield* readMlTags(arn);
115 const { removed, upsert } = diffTags(observedTags, desiredTags);
116 if (upsert.length > 0) {
117 yield* medialive.createTags({
118 ResourceArn: arn,
119 Tags: Object.fromEntries(upsert.map((t) => [t.Key, t.Value])),
120 });
121 }
122 if (removed.length > 0) {
123 yield* medialive.deleteTags({ ResourceArn: arn, TagKeys: removed });
124 }
125});