1import * as mediaconvert from "@distilled.cloud/aws/mediaconvert";
2import * as Effect from "effect/Effect";
3import * as Provider from "../../Provider.ts";
4import { Resource } from "../../Resource.ts";
5import { createInternalTags } from "../../Tags.ts";
6import type { Providers } from "../Providers.ts";
7
8export interface JobProps {
9 /**
10 * ARN of the IAM role MediaConvert assumes to read the input from and write
11 * the output to Amazon S3. Required.
12 */
13 role: string;
14 /**
15 * The transcode settings for this job — the input file(s) and the output
16 * groups to produce. Required unless a `jobTemplate` supplies them.
17 */
18 settings?: mediaconvert.JobSettings;
19 /**
20 * Name (or ARN) of a job template to base this job on. When set, the
21 * template's settings are merged with any `settings` overrides.
22 */
23 jobTemplate?: string;
24 /**
25 * Name of the queue to submit the job to. If omitted, the account's
26 * `Default` queue is used.
27 */
28 queue?: string;
29 /**
30 * Relative priority of the job within its queue (-50 to 50).
31 */
32 priority?: number;
33 /**
34 * How often, in seconds, MediaConvert emits `STATUS_UPDATE` events.
35 */
36 statusUpdateInterval?: mediaconvert.StatusUpdateInterval;
37 /**
38 * Accelerated transcoding configuration.
39 */
40 accelerationSettings?: mediaconvert.AccelerationSettings;
41 /**
42 * Arbitrary key/value metadata attached to the job and echoed in events.
43 */
44 userMetadata?: Record<string, string>;
45 /**
46 * User-defined tags for the job.
47 */
48 tags?: Record<string, string>;
49}
50
51export interface Job extends Resource<
52 "AWS.MediaConvert.Job",
53 JobProps,
54 {
55 jobId: string;
56 jobArn: string;
57 status: string | undefined;
58 queue: string | undefined;
59 },
60 never,
61 Providers
62> {}
63
64/**
65 * An AWS Elemental MediaConvert transcode job — a one-shot request to convert
66 * an input in S3 into one or more outputs. Jobs are immutable once submitted:
67 * they run to `COMPLETE`, `ERROR`, or `CANCELED` on their own. Deleting the
68 * resource cancels the job only if it is still `SUBMITTED` or `PROGRESSING`.
69 *
70 * A live job is slow and billable — it requires input/output S3 objects and an
71 * IAM role MediaConvert can assume. Drive it behind an environment gate in
72 * tests rather than on every run.
73 *
74 * ### Submitting a Job
75 * **Example:** File Transcode from a Template
76 * ```typescript
77 * const job = yield* MediaConvert.Job("Transcode", {
78 * role: mediaConvertRole.roleArn,
79 * jobTemplate: template.jobTemplateName,
80 * settings: {
81 * Inputs: [{ FileInput: "s3://my-bucket/input.mp4" }],
82 * OutputGroups: [
83 * {
84 * OutputGroupSettings: {
85 * Type: "FILE_GROUP_SETTINGS",
86 * FileGroupSettings: { Destination: "s3://my-bucket/out/" },
87 * },
88 * Outputs: [{ Preset: preset.presetName }],
89 * },
90 * ],
91 * },
92 * });
93 * ```
94 *
95 * @resource
96 */
97export const Job = Resource<Job>("AWS.MediaConvert.Job");
98
99export const JobProvider = () =>
100 Provider.effect(
101 Job,
102 Effect.gen(function* () {
103 const toAttrs = (job: mediaconvert.Job) => ({
104 jobId: job.Id!,
105 jobArn: job.Arn!,
106 status: job.Status,
107 queue: job.Queue,
108 });
109
110 /** Get a job by id; typed not-found → undefined. */
111 const getJob = Effect.fn(function* (id: string) {
112 const response = yield* mediaconvert
113 .getJob({ Id: id })
114 .pipe(
115 Effect.catchTag("NotFoundException", () =>
116 Effect.succeed(undefined),
117 ),
118 );
119 return response?.Job;
120 });
121
122 return {
123 stables: ["jobId", "jobArn"],
124
125 read: Effect.fn(function* ({ output }) {
126 // Jobs are server-assigned; without an output cache there is no id.
127 if (!output?.jobId) return undefined;
128 const job = yield* getJob(output.jobId);
129 // A job that has aged out of history is still "done" — keep the
130 // cached attributes rather than triggering a resubmit.
131 if (job === undefined) {
132 return {
133 jobId: output.jobId,
134 jobArn: output.jobArn,
135 status: output.status,
136 queue: output.queue,
137 };
138 }
139 return toAttrs(job);
140 }),
141
142 reconcile: Effect.fn(function* ({ id, news, output, session }) {
143 // Jobs are immutable. If one was already submitted, observe it and
144 // return — never resubmit on an update.
145 if (output?.jobId) {
146 const existing = yield* getJob(output.jobId);
147 yield* session.note(output.jobId);
148 return existing
149 ? toAttrs(existing)
150 : {
151 jobId: output.jobId,
152 jobArn: output.jobArn,
153 status: output.status,
154 queue: output.queue,
155 };
156 }
157
158 const internalTags = yield* createInternalTags(id);
159 const created = yield* mediaconvert.createJob({
160 Role: news.role,
161 Settings: news.settings,
162 JobTemplate: news.jobTemplate,
163 Queue: news.queue,
164 Priority: news.priority,
165 StatusUpdateInterval: news.statusUpdateInterval,
166 AccelerationSettings: news.accelerationSettings,
167 UserMetadata: news.userMetadata,
168 Tags: { ...internalTags, ...news.tags },
169 });
170 const job = created.Job!;
171 yield* session.note(job.Id!);
172 return toAttrs(job);
173 }),
174
175 delete: Effect.fn(function* ({ output }) {
176 yield* mediaconvert.cancelJob({ Id: output.jobId }).pipe(
177 // NotFound → already gone; Conflict/BadRequest → the job is already
178 // in a terminal state and cannot be canceled. All are idempotent
179 // no-ops for deletion.
180 Effect.catchTag(
181 ["NotFoundException", "ConflictException", "BadRequestException"],
182 () => Effect.void,
183 ),
184 );
185 }),
186
187 // Jobs are ephemeral, not a managed inventory — enumeration would churn
188 // over transient history, so the engine lists nothing.
189 list: () => Effect.succeed([]),
190 };
191 }),
192 );