1import type lambda from "aws-lambda";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Stream from "effect/Stream";
5import * as Namespace from "../../Namespace.ts";
6import { Subscription as SNSSubscription } from "../SNS/Subscription.ts";
7import type { Topic } from "../SNS/Topic.ts";
8import {
9 TopicEventSource as SNSTopicEventSource,
10 type TopicEventSourceProps,
11 type TopicEventSourceService,
12 type TopicNotification,
13} from "../SNS/TopicEventSource.ts";
14import * as Lambda from "./Function.ts";
15import { Permission as LambdaPermission } from "./Permission.ts";
16
17export const isSNSEvent = (event: any): event is lambda.SNSEvent =>
18 Array.isArray(event?.Records) &&
19 event.Records.some((record: any) => record.EventSource === "aws:sns");
20
21/** @binding */
22export const TopicEventSource = Layer.effect(
23 SNSTopicEventSource,
24 Effect.gen(function* () {
25 const host = yield* Lambda.Function;
26 const Permission = yield* LambdaPermission;
27 const Subscription = yield* SNSSubscription;
28
29 return Effect.fn(function* <StreamReq = never, Req = never>(
30 topic: Topic,
31 props: TopicEventSourceProps,
32 process: (
33 stream: Stream.Stream<TopicNotification, never, StreamReq>,
34 ) => Effect.Effect<void, never, Req>,
35 ) {
36 const TopicArn = yield* topic.topicArn;
37
38 // Deploy-time: grant invoke permission and create the SNS subscription.
39 // Skipped once running inside the deployed Function (the global guard),
40 // where the only work is registering the runtime handler below.
41 // Namespaced under the host so the sub-resources' logical identity matches
42 // the previous Binding.Policy.
43 if (!globalThis.__ALCHEMY_RUNTIME__) {
44 yield* Namespace.push(
45 host.LogicalId,
46 Effect.gen(function* () {
47 yield* Permission(`AWS.Lambda.InvokeFunction(${topic.LogicalId})`, {
48 action: "lambda:InvokeFunction",
49 functionName: host.functionName,
50 principal: "sns.amazonaws.com",
51 sourceArn: topic.topicArn,
52 });
53
54 yield* Subscription(
55 `AWS.SNS.Subscription(${topic.LogicalId}, ${host.LogicalId})`,
56 {
57 topicArn: topic.topicArn,
58 protocol: "lambda",
59 endpoint: host.functionArn,
60 attributes: props.attributes,
61 returnSubscriptionArn: true,
62 },
63 );
64 }),
65 );
66 }
67
68 yield* host.listen(
69 Effect.gen(function* () {
70 const topicArn = yield* TopicArn;
71
72 return (event: any) => {
73 if (isSNSEvent(event)) {
74 const records = event.Records.filter(
75 (record) => record.Sns?.TopicArn === topicArn,
76 );
77
78 if (records.length > 0) {
79 return process(
80 Stream.fromArray(
81 records.map((record) => record.Sns as TopicNotification),
82 ),
83 ).pipe(Effect.orDie);
84 }
85 }
86 };
87 }),
88 );
89 }) as TopicEventSourceService;
90 }),
91);