| 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 | */ |
| 6 | import { Context, Effect, Layer, ManagedRuntime, Ref, Schema } from "effect" |
| 7 | import { Hono } from "hono" |
| 8 | |
| 9 | class Todo extends Schema.Class<Todo>("Todo")({ |
| 10 | id: Schema.Int, |
| 11 | title: Schema.String, |
| 12 | completed: Schema.Boolean |
| 13 | }) {} |
| 14 | |
| 15 | class CreateTodoPayload extends Schema.Class<CreateTodoPayload>("CreateTodoPayload")({ |
| 16 | title: Schema.String |
| 17 | }) {} |
| 18 | |
| 19 | class TodoNotFound extends Schema.TaggedError<TodoNotFound>()("TodoNotFound", { |
| 20 | id: Schema.Int |
| 21 | }) {} |
| 22 | |
| 23 | export 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. |
| 62 | export 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. |
| 67 | export const runtime = ManagedRuntime.make(TodoRepo.layer, { |
| 68 | memoMap: appMemoMap |
| 69 | }) |
| 70 | |
| 71 | export const app = new Hono() |
| 72 | |
| 73 | app.get("/todos", async (context) => { |
| 74 | const todos = await runtime.runPromise( |
| 75 | TodoRepo.use((repo) => repo.getAll) |
| 76 | ) |
| 77 | return context.json(todos) |
| 78 | }) |
| 79 | |
| 80 | app.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 | |
| 99 | const decodeCreateTodoPayload = Schema.decodeUnknownSync(CreateTodoPayload) |
| 100 | |
| 101 | app.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. |
| 124 | const shutdown = () => { |
| 125 | void runtime.dispose() |
| 126 | } |
| 127 | |
| 128 | process.once("SIGINT", shutdown) |
| 129 | process.once("SIGTERM", shutdown) |