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 AWS FIS delivers to EventBridge when an experiment
11 * changes state. Delivery is best-effort and near real time; fields not
12 * shared by every event are optional (the schema grows over time).
13 */
14export interface ExperimentEventDetail {
15 /** The id of the experiment the event is about, e.g. `EXP1a2b3c4d`. */
16 "experiment-id"?: string;
17 /** The id of the template the experiment was started from. */
18 "experiment-template-id"?: string;
19 /** The state the experiment transitioned to. */
20 "new-state"?: {
21 /** e.g. `initiating`, `running`, `completed`, `stopped`, `failed`. */
22 status?: string;
23 /** Why the experiment entered this state. */
24 reason?: string;
25 };
26 /** Additional event fields (the schema grows over time). */
27 [key: string]: unknown;
28}
29
30/** An AWS FIS EventBridge event delivered to the handler. */
31export type ExperimentEvent = EventRecord<ExperimentEventDetail>;
32
33export interface ExperimentEventSourceProps extends EventRouteProps {
34 /**
35 * Logical id for the backing EventBridge rule.
36 * @default "FISExperimentEvents"
37 */
38 id?: string;
39 /**
40 * Restrict to events about experiments started from specific templates
41 * (matched against the event detail's `experiment-template-id`).
42 */
43 experimentTemplateIds?: readonly string[];
44 /**
45 * Restrict to specific experiment state transitions (matched against the
46 * event detail's `new-state.status`), e.g. `["completed", "failed"]`.
47 */
48 statuses?: readonly string[];
49}
50
51/**
52 * Event source connecting AWS Fault Injection Service experiment state
53 * changes to the hosting compute. FIS publishes an event to the account's
54 * default EventBridge bus (source `aws.fis`, detail-type
55 * `FIS Experiment State Change`) whenever an experiment transitions state —
56 * starts running, completes, stops, or fails — so a function can post
57 * chaos-run reports or trigger follow-up verification the moment an
58 * experiment finishes.
59 *
60 * FIS publishes to EventBridge automatically — no additional resource is
61 * created besides the EventBridge rule targeting the host. Provide the
62 * host-specific implementation layer (e.g. `AWS.Lambda.EventSource`) on the
63 * Function effect.
64 *
65 * ### Consuming Experiment Events
66 * **Example:** Report When an Experiment Finishes
67 * ```typescript
68 * import * as AWS from "alchemy/AWS";
69 *
70 * export default ReportFunction.make(
71 * { main: import.meta.url },
72 * Effect.gen(function* () {
73 * yield* AWS.FIS.consumeExperimentEvents(
74 * { statuses: ["completed", "stopped", "failed"] },
75 * (events) =>
76 * Stream.runForEach(events, (event) =>
77 * Effect.log(
78 * `experiment ${event.detail["experiment-id"]} -> ` +
79 * `${event.detail["new-state"]?.status}`,
80 * ),
81 * ),
82 * );
83 * return {};
84 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
85 * );
86 * ```
87 */
88export const consumeExperimentEvents = <StreamReq = never, Req = never>(
89 props: ExperimentEventSourceProps,
90 process: (
91 events: Stream.Stream<ExperimentEvent, never, StreamReq>,
92 ) => Effect.Effect<void, never, Req>,
93) =>
94 consumeBusEvents(
95 props.id ?? "FISExperimentEvents",
96 {
97 source: ["aws.fis"],
98 "detail-type": ["FIS Experiment State Change"],
99 ...(props.experimentTemplateIds !== undefined ||
100 props.statuses !== undefined
101 ? {
102 detail: {
103 ...(props.experimentTemplateIds !== undefined
104 ? { "experiment-template-id": [...props.experimentTemplateIds] }
105 : {}),
106 ...(props.statuses !== undefined
107 ? { "new-state": { status: [...props.statuses] } }
108 : {}),
109 },
110 }
111 : {}),
112 },
113 { description: props.description, state: props.state },
114 process,
115 );