1/**
2 * @title Using ManagedRuntime with Hono
3 *
4 * Use `ManagedRuntime` to run Effect programs from external frameworks while keeping your domain logic in services and Layers.
5 */
6import { Context, Effect, Layer, ManagedRuntime, Ref, Schema } from "effect"
7import { Hono } from "hono"
8
9class Todo extends Schema.Class<Todo>("Todo")({
10 id: Schema.Int,
11 title: Schema.String,
12 completed: Schema.Boolean
13}) {}
14
15class CreateTodoPayload extends Schema.Class<CreateTodoPayload>("CreateTodoPayload")({
16 title: Schema.String
17}) {}
18
19class TodoNotFound extends Schema.TaggedError<TodoNotFound>()("TodoNotFound", {
20 id: Schema.Int
21}) {}
22
23export class TodoRepo extends Context.Service<TodoRepo, {
24 readonly getAll: Effect.Effect<ReadonlyArray<Todo>>
25 getById(id: number): Effect.Effect<Todo, TodoNotFound>
26 create(payload: CreateTodoPayload): Effect.Effect<Todo>
27}>()("app/TodoRepo") {
28 static readonly layer = Layer.effect(
29 TodoRepo,
30 Effect.gen(function*() {
31 const store = new Map<number, Todo>()
32 const nextId = yield* Ref.make(1)
33
34 const getAll = Effect.gen(function*() {
35 return Array.from(store.values())
36 }).pipe(
37 Effect.withSpan("TodoRepo.getAll")
38 )
39
40 const getById = Effect.fn("TodoRepo.getById")(function*(id: number) {
41 const todo = store.get(id)
42 if (todo === undefined) {
43 return yield* new TodoNotFound({ id })
44 }
45 return todo
46 })
47
48 const create = Effect.fn("TodoRepo.create")(function*(payload: CreateTodoPayload) {
49 const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1)
50 const todo = new Todo({ id, title: payload.title, completed: false })
51 store.set(id, todo)
52 return todo
53 })
54
55 return TodoRepo.of({ getAll, getById, create })
56 })
57 )
58}
59
60// Create a global memo map that can be shared across the app. This is necessary
61// for memoization to work correctly across ManagedRuntime instances.
62export const appMemoMap = Layer.makeMemoMapUnsafe()
63
64// Create a ManagedRuntime for the TodoRepo layer. This runtime can be shared
65// across all handlers in the app, and it will manage the lifecycle of the
66// TodoRepo service and any resources it uses.
67export const runtime = ManagedRuntime.make(TodoRepo.layer, {
68 memoMap: appMemoMap
69})
70
71export const app = new Hono()
72
73app.get("/todos", async (context) => {
74 const todos = await runtime.runPromise(
75 TodoRepo.use((repo) => repo.getAll)
76 )
77 return context.json(todos)
78})
79
80app.get("/todos/:id", async (context) => {
81 const id = Number(context.req.param("id"))
82 if (!Number.isFinite(id)) {
83 return context.json({ message: "Todo id must be a number" }, 400)
84 }
85
86 const todo = await runtime.runPromise(
87 TodoRepo.use((repo) => repo.getById(id)).pipe(
88 Effect.catchTag("TodoNotFound", () => Effect.succeed(null))
89 )
90 )
91
92 if (todo === null) {
93 return context.json({ message: "Todo not found" }, 404)
94 }
95
96 return context.json(todo)
97})
98
99const decodeCreateTodoPayload = Schema.decodeUnknownSync(CreateTodoPayload)
100
101app.post("/todos", async (context) => {
102 const body = await context.req.json()
103
104 let payload: CreateTodoPayload
105 try {
106 payload = decodeCreateTodoPayload(body)
107 } catch {
108 return context.json({ message: "Invalid request body" }, 400)
109 }
110
111 const todo = await runtime.runPromise(
112 TodoRepo.use((repo) => repo.create(payload))
113 )
114
115 return context.json(todo, 201)
116})
117
118// The same bridge pattern works for Express, Fastify, Koa, and other frameworks.
119// Use `runtime.runSync` for synchronous edges or `runtime.runCallback` for
120// callback-only APIs.
121
122// When the process receives a shutdown signal, dispose the runtime to clean up
123// any resources used by the TodoRepo service and its dependencies.
124const shutdown = () => {
125 void runtime.dispose()
126}
127
128process.once("SIGINT", shutdown)
129process.once("SIGTERM", shutdown)