1/**
2 * @title Acquiring resources with Effect.acquireRelease
3 *
4 * Define a service that uses `Effect.acquireRelease` to manage the lifecycle of
5 * a resource, ensuring that it is properly cleaned up when the service is no
6 * longer needed.
7 */
8import { Config, Context, Effect, Layer, Redacted, Schema } from "effect"
9import * as NodeMailer from "nodemailer"
10
11export class SmtpError extends Schema.Error<SmtpError>("SmtpError")({
12 cause: Schema.Defect()
13}) {}
14
15export class Smtp extends Context.Service<Smtp, {
16 send(message: {
17 readonly to: string
18 readonly subject: string
19 readonly body: string
20 }): Effect.Effect<void, SmtpError>
21}>()("app/Smtp") {
22 static readonly layer = Layer.effect(
23 Smtp,
24 Effect.gen(function*() {
25 const user = yield* Config.String("SMTP_USER")
26 const pass = yield* Config.Redacted("SMTP_PASS")
27
28 // Use `Effect.acquireRelease` to manage the lifecycle of the SMTP
29 // transporter.
30 //
31 // When the Layer is built, the transporter will be created. When the
32 // Layer is torn down, the transporter will be closed, ensuring that
33 // resources are always cleaned up properly.
34 const transporter = yield* Effect.acquireRelease(
35 Effect.sync(() =>
36 NodeMailer.createTransport({
37 host: "smtp.example.com",
38 port: 587,
39 secure: false,
40 auth: { user, pass: Redacted.value(pass) }
41 })
42 ),
43 (transporter) => Effect.sync(() => transporter.close())
44 )
45
46 const send = Effect.fn("Smtp.send")((message: {
47 readonly to: string
48 readonly subject: string
49 readonly body: string
50 }) =>
51 Effect.tryPromise({
52 try: () =>
53 transporter.sendMail({
54 from: "Acme Cloud <cloud@acme.com>",
55 to: message.to,
56 subject: message.subject,
57 text: message.body
58 }),
59 catch: (cause) => new SmtpError({ cause })
60 }).pipe(
61 Effect.asVoid
62 )
63 )
64
65 return Smtp.of({ send })
66 })
67 )
68}
69
70// We can then use the `Smtp` service in another service, and the transporter
71// will be properly managed by the Layer system.
72
73export class MailerError extends Schema.TaggedError<MailerError>()("MailerError", {
74 reason: SmtpError
75}) {}
76
77export class Mailer extends Context.Service<Mailer, {
78 sendWelcomeEmail(to: string): Effect.Effect<void, MailerError>
79}>()("app/Mailer") {
80 static readonly layerNoDeps = Layer.effect(
81 Mailer,
82 Effect.gen(function*() {
83 const smtp = yield* Smtp
84
85 const sendWelcomeEmail = Effect.fn("Mailer.sendWelcomeEmail")(function*(to: string) {
86 yield* smtp.send({
87 to,
88 subject: "Welcome to Acme Cloud!",
89 body: "Thanks for signing up for Acme Cloud. We're glad to have you!"
90 }).pipe(
91 Effect.mapError((reason) => new MailerError({ reason }))
92 )
93 yield* Effect.logInfo(`Sent welcome email to ${to}`)
94 })
95
96 return Mailer.of({ sendWelcomeEmail })
97 })
98 )
99
100 // Locally provide the Smtp layer to the Mailer layer, to eliminate all the
101 // requirements
102 static readonly layer = this.layerNoDeps.pipe(
103 Layer.provide(Smtp.layer)
104 )
105}