1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Stream from "effect/Stream";
4import * as Namespace from "../../Namespace.ts";
5import type { Broker } from "../MQ/Broker.ts";
6import {
7 BrokerEventSource as MQBrokerEventSource,
8 type BrokerEventSourceProps,
9 type MQEvent,
10 type MQMessage,
11} from "../MQ/BrokerEventSource.ts";
12import { EventSourceMapping } from "./EventSourceMapping.ts";
13import * as Lambda from "./Function.ts";
14
15/** Narrow an incoming Lambda event to an Amazon MQ (ActiveMQ/RabbitMQ) event. */
16export const isMQEvent = (event: any): event is MQEvent =>
17 event?.eventSource === "aws:mq" || event?.eventSource === "aws:rmq";
18
19/** Flatten an MQ event into a flat list of messages across all queues. */
20const messagesOf = (event: MQEvent): MQMessage[] =>
21 event.eventSource === "aws:mq"
22 ? (event.messages ?? [])
23 : Object.values(event.rmqMessagesByQueue ?? {}).flat();
24
25/** @binding */
26export const BrokerEventSource = Layer.effect(
27 MQBrokerEventSource,
28 // @ts-expect-error - the impl resolves plan-time services (EventSourceMapping)
29 // whereas BrokerEventSourceService erases the requirement channel to `never`.
30 // @effect-diagnostics-next-line missingEffectContext:off
31 Effect.gen(function* () {
32 const host = yield* Lambda.Function;
33 const Mapping = yield* EventSourceMapping;
34
35 return Effect.fn(function* <Req = never>(
36 broker: Broker,
37 props: BrokerEventSourceProps,
38 process: (
39 stream: Stream.Stream<MQMessage>,
40 ) => Effect.Effect<void, never, Req>,
41 ) {
42 // Deploy-time: grant IAM and create the event-source mapping. Skipped
43 // once running inside the deployed Function (the global guard).
44 // Namespaced under the host so the mapping's logical identity is stable.
45 if (!globalThis.__ALCHEMY_RUNTIME__) {
46 yield* Namespace.push(
47 host.LogicalId,
48 Effect.gen(function* () {
49 yield* host.bind`Allow(${host}, AWS.MQ.BrokerEventSource(${broker}))`(
50 {
51 policyStatements: [
52 {
53 Effect: "Allow",
54 Action: ["mq:DescribeBroker"],
55 Resource: [broker.brokerArn],
56 },
57 {
58 Effect: "Allow",
59 Action: ["secretsmanager:GetSecretValue"],
60 Resource: [props.credentialsSecretArn],
61 },
62 {
63 // MQ event sources run in the broker's VPC; the poller
64 // manages elastic network interfaces on the function's
65 // behalf. These EC2 actions have no resource-level scoping.
66 Effect: "Allow",
67 Action: [
68 "ec2:CreateNetworkInterface",
69 "ec2:DeleteNetworkInterface",
70 "ec2:DescribeNetworkInterfaces",
71 "ec2:DescribeSecurityGroups",
72 "ec2:DescribeSubnets",
73 "ec2:DescribeVpcs",
74 ],
75 Resource: ["*"],
76 },
77 ],
78 },
79 );
80
81 yield* Mapping(`${broker.LogicalId}-EventSource`, {
82 functionName: host.functionName,
83 eventSourceArn: broker.brokerArn,
84 queues: props.queues,
85 sourceAccessConfigurations: [
86 { Type: "BASIC_AUTH", URI: props.credentialsSecretArn },
87 ],
88 batchSize: props.batchSize ?? 100,
89 maximumBatchingWindow: props.maximumBatchingWindow,
90 enabled: props.enabled ?? true,
91 });
92 }),
93 );
94 }
95
96 yield* host.listen(
97 Effect.gen(function* () {
98 return (event: any) => {
99 if (isMQEvent(event)) {
100 return process(Stream.fromArray(messagesOf(event))).pipe(
101 Effect.orDie,
102 );
103 }
104 };
105 }),
106 );
107 });
108 }),
109);