1import type * as Effect from "effect/Effect";
2import type * as Stream from "effect/Stream";
3import {
4 consumeBusEvents,
5 type EventRecord,
6} from "../EventBridge/EventSource.ts";
7import type { Instance } from "./Instance.ts";
8
9/**
10 * Lifecycle states EC2 reports through the
11 * `EC2 Instance State-change Notification` EventBridge event.
12 */
13export type InstanceState =
14 | "pending"
15 | "running"
16 | "shutting-down"
17 | "stopping"
18 | "stopped"
19 | "terminated";
20
21/**
22 * The `detail` payload EC2 delivers to EventBridge when an instance changes
23 * state.
24 */
25export interface InstanceStateChangeDetail {
26 /**
27 * ID of the instance that changed state.
28 */
29 "instance-id": string;
30 /**
31 * The state the instance transitioned into.
32 */
33 state: InstanceState;
34}
35
36/** An instance state-change EventBridge event delivered to the handler. */
37export type InstanceStateChangeEvent = EventRecord<InstanceStateChangeDetail>;
38
39export interface InstanceStateEventSourceProps {
40 /**
41 * Logical id prefix for the EventBridge rule. Defaults to the instance's
42 * logical id.
43 */
44 id?: string;
45 /**
46 * Which instance states to deliver.
47 * @default all six lifecycle states
48 */
49 states?: InstanceState[];
50}
51
52/**
53 * Deliver EC2 instance state-change events to the host Function via
54 * EventBridge — e.g. to deregister a host from an external system when it
55 * stops, or alert when a critical box is terminated.
56 *
57 * The EventBridge pattern matches every instance state-change in the account
58 * (narrowed to `states` when given); inspect `event.detail["instance-id"]` in
59 * the handler if the Function should only react to specific instances — the
60 * bound instance's id is an Output and cannot appear in the deploy-time rule
61 * pattern.
62 *
63 * ### Observing Instance State
64 * **Example:** React to an instance stopping or terminating
65 * ```typescript
66 * yield* consumeInstanceStateEvents(
67 * instance,
68 * { states: ["stopped", "terminated"] },
69 * (events) =>
70 * Stream.runForEach(events, (event) =>
71 * Effect.log(
72 * `${event.detail["instance-id"]} is now ${event.detail.state}`,
73 * ),
74 * ),
75 * );
76 * ```
77 */
78export const consumeInstanceStateEvents = <StreamReq = never, Req = never>(
79 instance: Instance,
80 props: InstanceStateEventSourceProps,
81 process: (
82 events: Stream.Stream<InstanceStateChangeEvent, never, StreamReq>,
83 ) => Effect.Effect<void, never, Req>,
84) =>
85 // The pattern uses only literal strings so it round-trips through both the
86 // deploy-time rule and the runtime matcher (Output values cannot appear in
87 // the pattern — they don't resolve inside the deployed bundle).
88 consumeBusEvents(
89 `${props.id ?? instance.LogicalId}-InstanceState`,
90 {
91 source: ["aws.ec2"],
92 "detail-type": ["EC2 Instance State-change Notification"],
93 ...(props.states ? { detail: { state: props.states } } : {}),
94 },
95 process,
96 );