1import * as Lambda from "@/AWS/Lambda";
2import * as MediaLive from "@/AWS/MediaLive";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Stream from "effect/Stream";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12export class MediaLiveTestFunction extends Lambda.Function<Lambda.Function>()(
13 "MediaLiveTestFunction",
14) {}
15
16export default MediaLiveTestFunction.make(
17 {
18 main,
19 functionUrl: true,
20 },
21 Effect.gen(function* () {
22 // The input the input-scoped binding is bound to. A URL_PULL input is
23 // free, provisions in seconds, and needs no security group.
24 const input = yield* MediaLive.Input("BindingInput", {
25 type: "URL_PULL",
26 sources: [{ Url: "https://example.com/stream/index.m3u8" }],
27 tags: { fixture: "medialive-bindings" },
28 });
29
30 // Event source: subscribe the host to MediaLive state-change/alert
31 // events. The deploy proves the EventBridge rule + invoke permission
32 // wiring.
33 yield* MediaLive.consumeChannelEvents(
34 { kinds: ["state-change", "alert"] },
35 (events) =>
36 Stream.runForEach(events, (event) =>
37 Effect.log(
38 `medialive event: ${event["detail-type"]} ${event.detail.state ?? event.detail.message ?? ""}`,
39 ),
40 ),
41 );
42
43 const describeInput = yield* MediaLive.DescribeInput(input);
44 const listChannels = yield* MediaLive.ListChannels();
45 const listInputs = yield* MediaLive.ListInputs();
46
47 const bound = {
48 describeInput,
49 listChannels,
50 listInputs,
51 };
52
53 return {
54 fetch: Effect.gen(function* () {
55 const request = yield* HttpServerRequest;
56 const url = new URL(request.originalUrl);
57 const pathname = url.pathname;
58
59 if (request.method === "GET" && pathname === "/bindings") {
60 return yield* HttpServerResponse.json({
61 bound: Object.keys(bound),
62 });
63 }
64
65 // Read the input's live state — the fixture input idles DETACHED.
66 if (request.method === "GET" && pathname === "/input") {
67 const current = yield* describeInput();
68 return yield* HttpServerResponse.json({
69 state: current.State,
70 type: current.Type,
71 sourceUrls: (current.Sources ?? []).map((s) => s.Url),
72 });
73 }
74
75 // Account-level enumerations.
76 if (request.method === "GET" && pathname === "/channels") {
77 const { Channels } = yield* listChannels({ MaxResults: 20 });
78 return yield* HttpServerResponse.json({
79 count: (Channels ?? []).length,
80 });
81 }
82 if (request.method === "GET" && pathname === "/inputs") {
83 const { Inputs } = yield* listInputs({ MaxResults: 20 });
84 return yield* HttpServerResponse.json({
85 count: (Inputs ?? []).length,
86 });
87 }
88
89 return yield* HttpServerResponse.json(
90 { error: "Not found", method: request.method, pathname },
91 { status: 404 },
92 );
93 }).pipe(Effect.orDie),
94 };
95 }).pipe(
96 Effect.provide(
97 Layer.mergeAll(
98 Lambda.EventSource,
99 MediaLive.DescribeInputHttp,
100 MediaLive.ListChannelsHttp,
101 MediaLive.ListInputsHttp,
102 ),
103 ),
104 ),
105);