Skip to content

Commit ca5f086

Browse files
authored
refactor(server): simplify router middleware with next() (#21720)
1 parent 57c40eb commit ca5f086

32 files changed

+767
-467
lines changed

packages/opencode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
"bun": "./src/pty/pty.bun.ts",
4040
"node": "./src/pty/pty.node.ts",
4141
"default": "./src/pty/pty.bun.ts"
42+
},
43+
"#hono": {
44+
"bun": "./src/server/adapter.bun.ts",
45+
"node": "./src/server/adapter.node.ts",
46+
"default": "./src/server/adapter.bun.ts"
4247
}
4348
},
4449
"devDependencies": {

packages/opencode/src/agent/agent.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -398,13 +398,11 @@ export namespace Agent {
398398
}),
399399
)
400400

401-
export const defaultLayer = Layer.suspend(() =>
402-
layer.pipe(
403-
Layer.provide(Provider.defaultLayer),
404-
Layer.provide(Auth.defaultLayer),
405-
Layer.provide(Config.defaultLayer),
406-
Layer.provide(Skill.defaultLayer),
407-
),
401+
export const defaultLayer = layer.pipe(
402+
Layer.provide(Provider.defaultLayer),
403+
Layer.provide(Auth.defaultLayer),
404+
Layer.provide(Config.defaultLayer),
405+
Layer.provide(Skill.defaultLayer),
408406
)
409407

410408
const { runPromise } = makeRuntime(Service, defaultLayer)

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,18 @@ export const TuiThreadCommand = cmd({
137137
),
138138
})
139139
worker.onerror = (e) => {
140-
Log.Default.error(e)
140+
Log.Default.error("thread error", {
141+
message: e.message,
142+
filename: e.filename,
143+
lineno: e.lineno,
144+
colno: e.colno,
145+
error: e.error,
146+
})
141147
}
142148

143149
const client = Rpc.client<typeof rpc>(worker)
144150
const error = (e: unknown) => {
145-
Log.Default.error(e)
151+
Log.Default.error("process error", { error: errorMessage(e) })
146152
}
147153
const reload = () => {
148154
client.call("reload", undefined).catch((err) => {

packages/opencode/src/config/config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { pathToFileURL } from "url"
44
import os from "os"
55
import { Process } from "../util/process"
66
import z from "zod"
7-
import { ModelsDev } from "../provider/models"
87
import { mergeDeep, pipe, unique } from "remeda"
98
import { Global } from "../global"
109
import fsNode from "fs/promises"

packages/opencode/src/plugin/index.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export namespace Plugin {
124124
Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
125125
}
126126
: undefined,
127-
fetch: async (...args) => Server.Default().app.fetch(...args),
127+
fetch: async (...args) => (await Server.Default()).app.fetch(...args),
128128
})
129129
const cfg = yield* config.get()
130130
const input: PluginInput = {
@@ -210,13 +210,15 @@ export namespace Plugin {
210210
return message
211211
},
212212
}).pipe(
213-
Effect.catch((message) =>
214-
bus.publish(Session.Event.Error, {
215-
error: new NamedError.Unknown({
216-
message: `Failed to load plugin ${load.spec}: ${message}`,
217-
}).toObject(),
218-
}),
219-
),
213+
Effect.catch(() => {
214+
// TODO: make proper events for this
215+
// bus.publish(Session.Event.Error, {
216+
// error: new NamedError.Unknown({
217+
// message: `Failed to load plugin ${load.spec}: ${message}`,
218+
// }).toObject(),
219+
// })
220+
return Effect.void
221+
}),
220222
)
221223
}
222224

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Hono } from "hono"
2+
import { createBunWebSocket } from "hono/bun"
3+
import type { Adapter } from "./adapter"
4+
5+
export const adapter: Adapter = {
6+
create(app: Hono) {
7+
const ws = createBunWebSocket()
8+
return {
9+
upgradeWebSocket: ws.upgradeWebSocket,
10+
async listen(opts) {
11+
const args = {
12+
fetch: app.fetch,
13+
hostname: opts.hostname,
14+
idleTimeout: 0,
15+
websocket: ws.websocket,
16+
} as const
17+
const start = (port: number) => {
18+
try {
19+
return Bun.serve({ ...args, port })
20+
} catch {
21+
return
22+
}
23+
}
24+
const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
25+
if (!server) {
26+
throw new Error(`Failed to start server on port ${opts.port}`)
27+
}
28+
if (!server.port) {
29+
throw new Error(`Failed to resolve server address for port ${opts.port}`)
30+
}
31+
return {
32+
port: server.port,
33+
stop(close?: boolean) {
34+
return Promise.resolve(server.stop(close))
35+
},
36+
}
37+
},
38+
}
39+
},
40+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { createAdaptorServer, type ServerType } from "@hono/node-server"
2+
import { createNodeWebSocket } from "@hono/node-ws"
3+
import type { Hono } from "hono"
4+
import type { Adapter } from "./adapter"
5+
6+
export const adapter: Adapter = {
7+
create(app: Hono) {
8+
const ws = createNodeWebSocket({ app })
9+
return {
10+
upgradeWebSocket: ws.upgradeWebSocket,
11+
async listen(opts) {
12+
const start = (port: number) =>
13+
new Promise<ServerType>((resolve, reject) => {
14+
const server = createAdaptorServer({ fetch: app.fetch })
15+
ws.injectWebSocket(server)
16+
const fail = (err: Error) => {
17+
cleanup()
18+
reject(err)
19+
}
20+
const ready = () => {
21+
cleanup()
22+
resolve(server)
23+
}
24+
const cleanup = () => {
25+
server.off("error", fail)
26+
server.off("listening", ready)
27+
}
28+
server.once("error", fail)
29+
server.once("listening", ready)
30+
server.listen(port, opts.hostname)
31+
})
32+
33+
const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
34+
const addr = server.address()
35+
if (!addr || typeof addr === "string") {
36+
throw new Error(`Failed to resolve server address for port ${opts.port}`)
37+
}
38+
39+
let closing: Promise<void> | undefined
40+
return {
41+
port: addr.port,
42+
stop(close?: boolean) {
43+
closing ??= new Promise((resolve, reject) => {
44+
server.close((err) => {
45+
if (err) {
46+
reject(err)
47+
return
48+
}
49+
resolve()
50+
})
51+
if (close) {
52+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
53+
server.closeAllConnections()
54+
}
55+
if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
56+
server.closeIdleConnections()
57+
}
58+
}
59+
})
60+
return closing
61+
},
62+
}
63+
},
64+
}
65+
},
66+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { Hono } from "hono"
2+
import type { UpgradeWebSocket } from "hono/ws"
3+
4+
export type Opts = {
5+
port: number
6+
hostname: string
7+
}
8+
9+
export type Listener = {
10+
port: number
11+
stop: (close?: boolean) => Promise<void>
12+
}
13+
14+
export interface Runtime {
15+
upgradeWebSocket: UpgradeWebSocket
16+
listen(opts: Opts): Promise<Listener>
17+
}
18+
19+
export interface Adapter {
20+
create(app: Hono): Runtime
21+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { Auth } from "@/auth"
2+
import { Log } from "@/util/log"
3+
import { ProviderID } from "@/provider/schema"
4+
import { Hono } from "hono"
5+
import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
6+
import z from "zod"
7+
import { errors } from "../error"
8+
import { GlobalRoutes } from "../instance/global"
9+
10+
export function ControlPlaneRoutes(): Hono {
11+
const app = new Hono()
12+
return app
13+
.route("/global", GlobalRoutes())
14+
.put(
15+
"/auth/:providerID",
16+
describeRoute({
17+
summary: "Set auth credentials",
18+
description: "Set authentication credentials",
19+
operationId: "auth.set",
20+
responses: {
21+
200: {
22+
description: "Successfully set authentication credentials",
23+
content: {
24+
"application/json": {
25+
schema: resolver(z.boolean()),
26+
},
27+
},
28+
},
29+
...errors(400),
30+
},
31+
}),
32+
validator(
33+
"param",
34+
z.object({
35+
providerID: ProviderID.zod,
36+
}),
37+
),
38+
validator("json", Auth.Info.zod),
39+
async (c) => {
40+
const providerID = c.req.valid("param").providerID
41+
const info = c.req.valid("json")
42+
await Auth.set(providerID, info)
43+
return c.json(true)
44+
},
45+
)
46+
.delete(
47+
"/auth/:providerID",
48+
describeRoute({
49+
summary: "Remove auth credentials",
50+
description: "Remove authentication credentials",
51+
operationId: "auth.remove",
52+
responses: {
53+
200: {
54+
description: "Successfully removed authentication credentials",
55+
content: {
56+
"application/json": {
57+
schema: resolver(z.boolean()),
58+
},
59+
},
60+
},
61+
...errors(400),
62+
},
63+
}),
64+
validator(
65+
"param",
66+
z.object({
67+
providerID: ProviderID.zod,
68+
}),
69+
),
70+
async (c) => {
71+
const providerID = c.req.valid("param").providerID
72+
await Auth.remove(providerID)
73+
return c.json(true)
74+
},
75+
)
76+
.get(
77+
"/doc",
78+
openAPIRouteHandler(app, {
79+
documentation: {
80+
info: {
81+
title: "opencode",
82+
version: "0.0.3",
83+
description: "opencode api",
84+
},
85+
openapi: "3.1.1",
86+
},
87+
}),
88+
)
89+
.use(
90+
validator(
91+
"query",
92+
z.object({
93+
directory: z.string().optional(),
94+
workspace: z.string().optional(),
95+
}),
96+
),
97+
)
98+
.post(
99+
"/log",
100+
describeRoute({
101+
summary: "Write log",
102+
description: "Write a log entry to the server logs with specified level and metadata.",
103+
operationId: "app.log",
104+
responses: {
105+
200: {
106+
description: "Log entry written successfully",
107+
content: {
108+
"application/json": {
109+
schema: resolver(z.boolean()),
110+
},
111+
},
112+
},
113+
...errors(400),
114+
},
115+
}),
116+
validator(
117+
"json",
118+
z.object({
119+
service: z.string().meta({ description: "Service name for the log entry" }),
120+
level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
121+
message: z.string().meta({ description: "Log message" }),
122+
extra: z
123+
.record(z.string(), z.any())
124+
.optional()
125+
.meta({ description: "Additional metadata for the log entry" }),
126+
}),
127+
),
128+
async (c) => {
129+
const { service, level, message, extra } = c.req.valid("json")
130+
const logger = Log.create({ service })
131+
132+
switch (level) {
133+
case "debug":
134+
logger.debug(message, extra)
135+
break
136+
case "info":
137+
logger.info(message, extra)
138+
break
139+
case "error":
140+
logger.error(message, extra)
141+
break
142+
case "warn":
143+
logger.warn(message, extra)
144+
break
145+
}
146+
147+
return c.json(true)
148+
},
149+
)
150+
}
File renamed without changes.

0 commit comments

Comments
 (0)