3.3 KiB
3.3 KiB
Route handler effectification
Practical reference for converting server route handlers in packages/opencode to a single AppRuntime.runPromise(Effect.gen(...)) body.
Goal
Route handlers should wrap their entire body in a single AppRuntime.runPromise(Effect.gen(...)) call, yielding services from context rather than calling facades one-by-one.
This eliminates multiple runPromise round-trips and lets handlers compose naturally.
// Before - one facade call per service
;async (c) => {
await SessionRunState.assertNotBusy(id)
await Session.removeMessage({ sessionID: id, messageID })
return c.json(true)
}
// After - one Effect.gen, yield services from context
;async (c) => {
await AppRuntime.runPromise(
Effect.gen(function* () {
const state = yield* SessionRunState.Service
const session = yield* Session.Service
yield* state.assertNotBusy(id)
yield* session.removeMessage({ sessionID: id, messageID })
}),
)
return c.json(true)
}
Rules
- Wrap the whole handler body in one
AppRuntime.runPromise(Effect.gen(...))call when the handler is service-heavy. - Yield services from context instead of calling async facades repeatedly.
- When independent service calls can run in parallel, use
Effect.all(..., { concurrency: "unbounded" }). - Prefer one composed Effect body over multiple separate
runPromise(...)calls in the same handler.
Current route files
Current instance route files live under src/server/routes/instance.
Files that are already mostly on the intended service-yielding shape:
server/routes/instance/question.ts— handlers yieldQuestion.Serviceserver/routes/instance/provider.ts— handlers yieldProvider.Service,ProviderAuth.Service, andConfig.Serviceserver/routes/instance/permission.ts— handlers yieldPermission.Serviceserver/routes/instance/mcp.ts— handlers mostly yieldMCP.Serviceserver/routes/instance/pty.ts— handlers yieldPty.Service
Files still worth tracking here:
server/routes/instance/session.ts— still the heaviest mixed file; many handlers are composed, but the file still mixes patterns and has directBus.publish(...)/Session.list(...)usageserver/routes/instance/index.ts— mostly converted, but still has directInstance.dispose()/Instance.*reads for/instance/disposeand/pathserver/routes/instance/file.ts— most handlers yield services, but/findstill passesInstance.directorydirectly into ripgrep and/find/symbolis still stubbedserver/routes/instance/experimental.ts— mixed state; many handlers are composed, but some still rely onrunRequest(...)or directInstance.projectreadsserver/routes/instance/middleware.ts— still enters the instance viaInstance.provide(...)server/routes/global.ts— still usesInstance.disposeAll()and remains partly outside the fully-composed style
Notes
- Route conversion is now less about facade removal and more about removing the remaining direct
Instance.*reads,Instance.provide(...)boundaries, and small Promise-style bridges inside route files. jsonRequest(...)/runRequest(...)already provide a good intermediate shape for many handlers. The remaining cleanup is mostly consistency work in the heavier files.