1import * as AWS from "alchemy/AWS";
2import * as Context from "effect/Context";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Stream from "effect/Stream";
7
8import type { Job } from "./Job.ts";
9
10export class NotifyJobError extends Data.TaggedError("NotifyJobError")<{
11 readonly message: string;
12 readonly cause?: unknown;
13}> {}
14
15type JobNotification = {
16 type: "job.created";
17 job: Job;
18};
19
20export class JobNotifications extends Context.Service<
21 JobNotifications,
22 {
23 notifyJobCreated(job: Job): Effect.Effect<void, NotifyJobError>;
24 }
25>()("JobNotifications") {}
26
27export const JobNotificationsSNS = Layer.effect(
28 JobNotifications,
29 Effect.gen(function* () {
30 const topic = yield* AWS.SNS.Topic("JobNotificationsTopic", {
31 attributes: {
32 DisplayName: "job-notifications",
33 },
34 });
35
36 const publish = yield* AWS.SNS.Publish(topic);
37
38 yield* AWS.SNS.consumeTopicNotifications(topic, (stream) =>
39 stream.pipe(
40 Stream.mapEffect((notification) =>
41 Effect.try({
42 try: () => JSON.parse(notification.Message) as JobNotification,
43 catch: (cause) =>
44 new NotifyJobError({
45 message: "Failed to parse SNS job notification",
46 cause,
47 }),
48 }).pipe(
49 Effect.flatMap((payload) =>
50 Effect.logInfo(
51 `Job notification received: ${payload.type} (${payload.job.id})`,
52 ),
53 ),
54 // Keep the example resilient to malformed demo messages.
55 Effect.catchTag("NotifyJobError", (error) =>
56 Effect.logWarning(error.message),
57 ),
58 ),
59 ),
60 Stream.runDrain,
61 ),
62 );
63
64 const notifyJobCreated = (job: Job) =>
65 publish({
66 Subject: "JobCreated",
67 Message: JSON.stringify({
68 type: "job.created",
69 job,
70 } satisfies JobNotification),
71 }).pipe(
72 Effect.asVoid,
73 Effect.mapError(
74 (cause) =>
75 new NotifyJobError({
76 message: `Failed to publish job notification for "${job.id}"`,
77 cause,
78 }),
79 ),
80 );
81
82 return JobNotifications.of({
83 notifyJobCreated,
84 });
85 }),
86).pipe(
87 Layer.provideMerge(
88 Layer.mergeAll(AWS.Lambda.TopicEventSource, AWS.SNS.PublishHttp),
89 ),
90);