1import type * as Effect from "effect/Effect";
2import type * as Stream from "effect/Stream";
3import {
4 consumeBusEvents,
5 type EventRecord,
6 type EventRouteProps,
7} from "../EventBridge/EventSource.ts";
8
9/**
10 * The `detail` payload Amazon ECS delivers to EventBridge. Task state-change
11 * events carry the task's shape (`lastStatus`, `stoppedReason`, `containers`,
12 * …); container-instance state-change events carry the instance's shape;
13 * deployment and service-action events carry `eventName`/`reason`. Fields not
14 * shared by every event kind are optional (the schema grows over time).
15 */
16export interface ClusterEventDetail {
17 /** ARN of the cluster the event's resource belongs to. */
18 clusterArn?: string;
19 /** Task events: the state the task transitioned to (e.g. `STOPPED`). */
20 lastStatus?: string;
21 /** Task events: desired state of the task. */
22 desiredStatus?: string;
23 /** Task events: why the task stopped. */
24 stoppedReason?: string;
25 /** Task events: ARN of the task. */
26 taskArn?: string;
27 /** Task events: what started the task (e.g. `ecs-svc/…` for services). */
28 startedBy?: string;
29 /** Deployment / service-action events: the event name (e.g. `SERVICE_DEPLOYMENT_COMPLETED`). */
30 eventName?: string;
31 /** Deployment / service-action events: human-readable reason. */
32 reason?: string;
33 /** Additional event fields (the schema grows over time). */
34 [key: string]: unknown;
35}
36
37/** An ECS EventBridge event delivered to the handler. */
38export type ClusterEvent = EventRecord<ClusterEventDetail>;
39
40/** Which ECS notifications to subscribe to. */
41export type ClusterEventKind =
42 | "task-state-change"
43 | "container-instance-state-change"
44 | "deployment-state-change"
45 | "service-action";
46
47const DETAIL_TYPES: Record<ClusterEventKind, string> = {
48 "task-state-change": "ECS Task State Change",
49 "container-instance-state-change": "ECS Container Instance State Change",
50 "deployment-state-change": "ECS Deployment State Change",
51 "service-action": "ECS Service Action",
52};
53
54export interface ClusterEventSourceProps extends EventRouteProps {
55 /**
56 * Logical id for the backing EventBridge rule.
57 * @default "EcsClusterEvents"
58 */
59 id?: string;
60 /**
61 * Which notifications to subscribe to: task state changes, container
62 * instance state changes, deployment state changes, and/or service
63 * actions.
64 * @default ["task-state-change"]
65 */
66 kinds?: readonly ClusterEventKind[];
67 /**
68 * Restrict to events about specific clusters, matched against the event
69 * detail's `clusterArn`.
70 */
71 clusterArns?: readonly string[];
72}
73
74/**
75 * Event source connecting Amazon ECS notifications to the hosting compute.
76 * ECS publishes task state changes, container-instance state changes,
77 * deployment state changes, and service actions to the account's default
78 * EventBridge bus (source `aws.ecs`); this subscribes the host Function to
79 * those events so it can react when a task stops, a deployment completes or
80 * rolls back, or an instance drops out.
81 *
82 * ECS publishes to EventBridge automatically — no additional resource is
83 * created besides the EventBridge rule targeting the host. Provide the
84 * host-specific implementation layer (e.g. `AWS.Lambda.EventSource`) on the
85 * Function effect.
86 *
87 * ### Consuming Cluster Events
88 * **Example:** React When A Task Stops
89 * ```typescript
90 * import * as AWS from "alchemy/AWS";
91 *
92 * export default AlertFunction.make(
93 * { main: import.meta.url },
94 * Effect.gen(function* () {
95 * yield* AWS.ECS.consumeClusterEvents(
96 * { kinds: ["task-state-change"], clusterArns: [cluster.clusterArn] },
97 * (events) =>
98 * Stream.runForEach(events, (event) =>
99 * event.detail.lastStatus === "STOPPED"
100 * ? Effect.logError(`task stopped: ${event.detail.stoppedReason}`)
101 * : Effect.log(`task ${event.detail.lastStatus}`),
102 * ),
103 * );
104 * return {};
105 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
106 * );
107 * ```
108 *
109 * **Example:** Alert On Failed Deployments
110 * ```typescript
111 * yield* AWS.ECS.consumeClusterEvents(
112 * { kinds: ["deployment-state-change"] },
113 * (events) =>
114 * Stream.runForEach(events, (event) =>
115 * event.detail.eventName === "SERVICE_DEPLOYMENT_FAILED"
116 * ? Effect.logError(`deployment failed: ${event.detail.reason}`)
117 * : Effect.void,
118 * ),
119 * );
120 * ```
121 */
122export const consumeClusterEvents = <StreamReq = never, Req = never>(
123 props: ClusterEventSourceProps,
124 process: (
125 events: Stream.Stream<ClusterEvent, never, StreamReq>,
126 ) => Effect.Effect<void, never, Req>,
127) =>
128 consumeBusEvents(
129 props.id ?? "EcsClusterEvents",
130 {
131 source: ["aws.ecs"],
132 "detail-type": (props.kinds ?? (["task-state-change"] as const)).map(
133 (kind) => DETAIL_TYPES[kind],
134 ),
135 ...(props.clusterArns !== undefined
136 ? { detail: { clusterArn: [...props.clusterArns] } }
137 : {}),
138 },
139 { description: props.description, state: props.state },
140 process,
141 );