1import type lambda from "aws-lambda";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Stream from "effect/Stream";
5
6import * as Namespace from "../../Namespace.ts";
7import type { Queue } from "../SQS/Queue.ts";
8import {
9 QueueEventSource as SQSQueueEventSource,
10 type QueueEventSourceProps,
11 type QueueEventSourceService,
12 type SQSRecord,
13} from "../SQS/QueueEventSource.ts";
14import { EventSourceMapping } from "./EventSourceMapping.ts";
15import * as Lambda from "./Function.ts";
16
17export const isSQSEvent = (event: any): event is lambda.SQSEvent =>
18 Array.isArray(event?.Records) &&
19 event.Records.length > 0 &&
20 event.Records[0].eventSource === "aws:sqs";
21
22/** @binding */
23export const QueueEventSource = Layer.effect(
24 SQSQueueEventSource,
25 Effect.gen(function* () {
26 const host = yield* Lambda.Function;
27 const Mapping = yield* EventSourceMapping;
28
29 return Effect.fn(function* <StreamReq = never, Req = never>(
30 queue: Queue,
31 props: QueueEventSourceProps,
32 process: (
33 stream: Stream.Stream<SQSRecord, never, StreamReq>,
34 ) => Effect.Effect<void, never, Req | StreamReq>,
35 ) {
36 // Deploy-time: grant IAM and create the event-source mapping. Skipped once
37 // running inside the deployed Function (the global guard), where the only
38 // work is registering the runtime handler below. Namespaced under the host
39 // so the mapping's logical identity matches the previous Binding.Policy.
40 if (!globalThis.__ALCHEMY_RUNTIME__) {
41 yield* Namespace.push(
42 host.LogicalId,
43 Effect.gen(function* () {
44 yield* host.bind`Allow(${host}, AWS.Lambda.QueueEventSource(${queue}))`(
45 {
46 policyStatements: [
47 {
48 Effect: "Allow",
49 Action: [
50 "sqs:ReceiveMessage",
51 "sqs:DeleteMessage",
52 "sqs:GetQueueAttributes",
53 ],
54 Resource: [queue.queueArn],
55 },
56 ],
57 },
58 );
59
60 yield* Mapping(`${queue.LogicalId}-EventSource`, {
61 functionName: host.functionName,
62 eventSourceArn: queue.queueArn,
63 batchSize: props.batchSize,
64 maximumBatchingWindow: props.maximumBatchingWindow,
65 enabled: true,
66 });
67 }),
68 );
69 }
70
71 yield* host.listen(
72 Effect.gen(function* () {
73 return (event: any) => {
74 if (isSQSEvent(event)) {
75 return process(Stream.fromArray(event.Records)).pipe(
76 Effect.orDie,
77 );
78 }
79 };
80 }),
81 );
82 }) as QueueEventSourceService;
83 }),
84);