136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import { Hono } from "hono"
|
|
import { describeRoute, resolver } from "hono-openapi"
|
|
import { streamSSE } from "hono/streaming"
|
|
import z from "zod"
|
|
import { BusEvent } from "@/bus/bus-event"
|
|
import { GlobalBus } from "@/bus/global"
|
|
import { Instance } from "../../project/instance"
|
|
import { Installation } from "@/installation"
|
|
import { Log } from "../../util/log"
|
|
import { lazy } from "../../util/lazy"
|
|
|
|
const log = Log.create({ service: "server" })
|
|
|
|
export const GlobalDisposedEvent = BusEvent.define("global.disposed", z.object({}))
|
|
|
|
export const GlobalRoutes = lazy(() =>
|
|
new Hono()
|
|
.get(
|
|
"/health",
|
|
describeRoute({
|
|
summary: "Get health",
|
|
description: "Get health information about the OpenCode server.",
|
|
operationId: "global.health",
|
|
responses: {
|
|
200: {
|
|
description: "Health information",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(z.object({ healthy: z.literal(true), version: z.string() })),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
async (c) => {
|
|
return c.json({ healthy: true, version: Installation.VERSION })
|
|
},
|
|
)
|
|
.get(
|
|
"/event",
|
|
describeRoute({
|
|
summary: "Get global events",
|
|
description: "Subscribe to global events from the OpenCode system using server-sent events.",
|
|
operationId: "global.event",
|
|
responses: {
|
|
200: {
|
|
description: "Event stream",
|
|
content: {
|
|
"text/event-stream": {
|
|
schema: resolver(
|
|
z
|
|
.object({
|
|
directory: z.string(),
|
|
payload: BusEvent.payloads(),
|
|
})
|
|
.meta({
|
|
ref: "GlobalEvent",
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
async (c) => {
|
|
log.info("global event connected")
|
|
return streamSSE(c, async (stream) => {
|
|
stream.writeSSE({
|
|
data: JSON.stringify({
|
|
payload: {
|
|
type: "server.connected",
|
|
properties: {},
|
|
},
|
|
}),
|
|
})
|
|
async function handler(event: any) {
|
|
await stream.writeSSE({
|
|
data: JSON.stringify(event),
|
|
})
|
|
}
|
|
GlobalBus.on("event", handler)
|
|
|
|
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
|
|
const heartbeat = setInterval(() => {
|
|
stream.writeSSE({
|
|
data: JSON.stringify({
|
|
payload: {
|
|
type: "server.heartbeat",
|
|
properties: {},
|
|
},
|
|
}),
|
|
})
|
|
}, 30000)
|
|
|
|
await new Promise<void>((resolve) => {
|
|
stream.onAbort(() => {
|
|
clearInterval(heartbeat)
|
|
GlobalBus.off("event", handler)
|
|
resolve()
|
|
log.info("global event disconnected")
|
|
})
|
|
})
|
|
})
|
|
},
|
|
)
|
|
.post(
|
|
"/dispose",
|
|
describeRoute({
|
|
summary: "Dispose instance",
|
|
description: "Clean up and dispose all OpenCode instances, releasing all resources.",
|
|
operationId: "global.dispose",
|
|
responses: {
|
|
200: {
|
|
description: "Global disposed",
|
|
content: {
|
|
"application/json": {
|
|
schema: resolver(z.boolean()),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
async (c) => {
|
|
await Instance.disposeAll()
|
|
GlobalBus.emit("event", {
|
|
directory: "global",
|
|
payload: {
|
|
type: GlobalDisposedEvent.type,
|
|
properties: {},
|
|
},
|
|
})
|
|
return c.json(true)
|
|
},
|
|
),
|
|
)
|