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.provideMerge(
28 Layer.effect(
29 JobNotifications,
30 Effect.gen(function* () {
31 const topic = yield* AWS.SNS.Topic("JobNotificationsTopic", {
32 attributes: {
33 DisplayName: "job-notifications",
34 },
35 });
36
37 const publish = yield* AWS.SNS.Publish(topic);
38
39 yield* AWS.SNS.consumeTopicNotifications(topic, (stream) =>
40 stream.pipe(
41 Stream.mapEffect((notification) =>
42 Effect.try({
43 try: () => JSON.parse(notification.Message) as JobNotification,
44 catch: (cause) =>
45 new NotifyJobError({
46 message: "Failed to parse SNS job notification",
47 cause,
48 }),
49 }).pipe(
50 Effect.flatMap((payload) =>
51 Effect.logInfo(
52 `Job notification received: ${payload.type} (${payload.job.id})`,
53 ),
54 ),
55 // Keep the example resilient to malformed demo messages.
56 Effect.catchTag("NotifyJobError", (error) =>
57 Effect.logWarning(error.message),
58 ),
59 ),
60 ),
61 Stream.runDrain,
62 ),
63 );
64
65 const notifyJobCreated = (job: Job) =>
66 publish({
67 Subject: "JobCreated",
68 Message: JSON.stringify({
69 type: "job.created",
70 job,
71 } satisfies JobNotification),
72 }).pipe(
73 Effect.asVoid,
74 Effect.mapError(
75 (cause) =>
76 new NotifyJobError({
77 message: `Failed to publish job notification for "${job.id}"`,
78 cause,
79 }),
80 ),
81 );
82
83 return JobNotifications.of({
84 notifyJobCreated,
85 });
86 }),
87 ),
88 Layer.mergeAll(AWS.Lambda.TopicEventSource, AWS.SNS.PublishHttp),
89);