import { Effect, Layer, ManagedRuntime, Ref, Schema, ServiceMap } from "effect"
import { Hono } from "hono"
class Todo extends Schema.Class<Todo>("Todo")({
id: Schema.Number,
title: Schema.String,
completed: Schema.Boolean
}) {}
class CreateTodoPayload extends Schema.Class<CreateTodoPayload>("CreateTodoPayload")({
title: Schema.String
}) {}
class TodoNotFound extends Schema.TaggedErrorClass<TodoNotFound>()("TodoNotFound", {
id: Schema.Number
}) {}
export class TodoRepo extends ServiceMap.Service<TodoRepo, {
readonly getAll: Effect.Effect<ReadonlyArray<Todo>>
getById(id: number): Effect.Effect<Todo, TodoNotFound>
create(payload: CreateTodoPayload): Effect.Effect<Todo>
}>()("app/TodoRepo") {
static readonly layer = Layer.effect(
TodoRepo,
Effect.gen(function*() {
const store = new Map<number, Todo>()
const nextId = yield* Ref.make(1)
const getAll = Effect.gen(function*() {
return Array.from(store.values())
}).pipe(
Effect.withSpan("TodoRepo.getAll")
)
const getById = Effect.fn("TodoRepo.getById")(function*(id: number) {
const todo = store.get(id)
if (todo === undefined) {
return yield* new TodoNotFound({ id })
}
return todo
})
const create = Effect.fn("TodoRepo.create")(function*(payload: CreateTodoPayload) {
const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1)
const todo = new Todo({ id, title: payload.title, completed: false })
store.set(id, todo)
return todo
})
return TodoRepo.of({ getAll, getById, create })
})
)
}
// Create a shared runtime for all handlers
export const appMemoMap = Layer.makeMemoMapUnsafe()
export const runtime = ManagedRuntime.make(TodoRepo.layer, {
memoMap: appMemoMap
})
export const app = new Hono()
// GET /todos - List all todos
app.get("/todos", async (context) => {
const todos = await runtime.runPromise(
TodoRepo.use((repo) => repo.getAll)
)
return context.json(todos)
})
// GET /todos/:id - Get a single todo
app.get("/todos/:id", async (context) => {
const id = Number(context.req.param("id"))
if (!Number.isFinite(id)) {
return context.json({ message: "Todo id must be a number" }, 400)
}
const todo = await runtime.runPromise(
TodoRepo.use((repo) => repo.getById(id)).pipe(
Effect.catchTag("TodoNotFound", () => Effect.succeed(null))
)
)
if (todo === null) {
return context.json({ message: "Todo not found" }, 404)
}
return context.json(todo)
})
// POST /todos - Create a new todo
const decodeCreateTodoPayload = Schema.decodeUnknownSync(CreateTodoPayload)
app.post("/todos", async (context) => {
const body = await context.req.json()
let payload: CreateTodoPayload
try {
payload = decodeCreateTodoPayload(body)
} catch {
return context.json({ message: "Invalid request body" }, 400)
}
const todo = await runtime.runPromise(
TodoRepo.use((repo) => repo.create(payload))
)
return context.json(todo, 201)
})