mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-03-30 05:43:55 +00:00
fix(core): use a queue to process events in event routes (#18259)
This commit is contained in:
parent
baa204193c
commit
0540751897
@ -51,8 +51,8 @@ export namespace Bus {
|
|||||||
})
|
})
|
||||||
const pending = []
|
const pending = []
|
||||||
for (const key of [def.type, "*"]) {
|
for (const key of [def.type, "*"]) {
|
||||||
const match = state().subscriptions.get(key)
|
const match = [...(state().subscriptions.get(key) ?? [])]
|
||||||
for (const sub of match ?? []) {
|
for (const sub of match) {
|
||||||
pending.push(sub(payload))
|
pending.push(sub(payload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
85
packages/opencode/src/server/routes/event.ts
Normal file
85
packages/opencode/src/server/routes/event.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Hono } from "hono"
|
||||||
|
import { describeRoute, resolver } from "hono-openapi"
|
||||||
|
import { streamSSE } from "hono/streaming"
|
||||||
|
import { Log } from "@/util/log"
|
||||||
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
|
import { Bus } from "@/bus"
|
||||||
|
import { lazy } from "../../util/lazy"
|
||||||
|
import { AsyncQueue } from "../../util/queue"
|
||||||
|
import { Instance } from "@/project/instance"
|
||||||
|
|
||||||
|
const log = Log.create({ service: "server" })
|
||||||
|
|
||||||
|
export const EventRoutes = lazy(() =>
|
||||||
|
new Hono().get(
|
||||||
|
"/event",
|
||||||
|
describeRoute({
|
||||||
|
summary: "Subscribe to events",
|
||||||
|
description: "Get events",
|
||||||
|
operationId: "event.subscribe",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Event stream",
|
||||||
|
content: {
|
||||||
|
"text/event-stream": {
|
||||||
|
schema: resolver(BusEvent.payloads()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
log.info("event connected")
|
||||||
|
c.header("X-Accel-Buffering", "no")
|
||||||
|
c.header("X-Content-Type-Options", "nosniff")
|
||||||
|
return streamSSE(c, async (stream) => {
|
||||||
|
const q = new AsyncQueue<string | null>()
|
||||||
|
let done = false
|
||||||
|
|
||||||
|
q.push(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "server.connected",
|
||||||
|
properties: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send heartbeat every 10s to prevent stalled proxy streams.
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
q.push(
|
||||||
|
JSON.stringify({
|
||||||
|
type: "server.heartbeat",
|
||||||
|
properties: {},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, 10_000)
|
||||||
|
|
||||||
|
const unsub = Bus.subscribeAll((event) => {
|
||||||
|
q.push(JSON.stringify(event))
|
||||||
|
if (event.type === Bus.InstanceDisposed.type) {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
|
clearInterval(heartbeat)
|
||||||
|
unsub()
|
||||||
|
q.push(null)
|
||||||
|
log.info("event disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.onAbort(stop)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const data of q) {
|
||||||
|
if (data === null) return
|
||||||
|
await stream.writeSSE({ data })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
@ -4,6 +4,7 @@ import { streamSSE } from "hono/streaming"
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { BusEvent } from "@/bus/bus-event"
|
import { BusEvent } from "@/bus/bus-event"
|
||||||
import { GlobalBus } from "@/bus/global"
|
import { GlobalBus } from "@/bus/global"
|
||||||
|
import { AsyncQueue } from "@/util/queue"
|
||||||
import { Instance } from "../../project/instance"
|
import { Instance } from "../../project/instance"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
import { Log } from "../../util/log"
|
import { Log } from "../../util/log"
|
||||||
@ -69,41 +70,54 @@ export const GlobalRoutes = lazy(() =>
|
|||||||
c.header("X-Accel-Buffering", "no")
|
c.header("X-Accel-Buffering", "no")
|
||||||
c.header("X-Content-Type-Options", "nosniff")
|
c.header("X-Content-Type-Options", "nosniff")
|
||||||
return streamSSE(c, async (stream) => {
|
return streamSSE(c, async (stream) => {
|
||||||
stream.writeSSE({
|
const q = new AsyncQueue<string | null>()
|
||||||
data: JSON.stringify({
|
let done = false
|
||||||
|
|
||||||
|
q.push(
|
||||||
|
JSON.stringify({
|
||||||
payload: {
|
payload: {
|
||||||
type: "server.connected",
|
type: "server.connected",
|
||||||
properties: {},
|
properties: {},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
)
|
||||||
async function handler(event: any) {
|
|
||||||
await stream.writeSSE({
|
|
||||||
data: JSON.stringify(event),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
GlobalBus.on("event", handler)
|
|
||||||
|
|
||||||
// Send heartbeat every 10s to prevent stalled proxy streams.
|
// Send heartbeat every 10s to prevent stalled proxy streams.
|
||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
stream.writeSSE({
|
q.push(
|
||||||
data: JSON.stringify({
|
JSON.stringify({
|
||||||
payload: {
|
payload: {
|
||||||
type: "server.heartbeat",
|
type: "server.heartbeat",
|
||||||
properties: {},
|
properties: {},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
})
|
)
|
||||||
}, 10_000)
|
}, 10_000)
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
async function handler(event: any) {
|
||||||
stream.onAbort(() => {
|
q.push(JSON.stringify(event))
|
||||||
|
}
|
||||||
|
GlobalBus.on("event", handler)
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
clearInterval(heartbeat)
|
clearInterval(heartbeat)
|
||||||
GlobalBus.off("event", handler)
|
GlobalBus.off("event", handler)
|
||||||
resolve()
|
q.push(null)
|
||||||
log.info("global event disconnected")
|
log.info("event disconnected")
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
stream.onAbort(stop)
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const data of q) {
|
||||||
|
if (data === null) return
|
||||||
|
await stream.writeSSE({ data })
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import { BusEvent } from "@/bus/bus-event"
|
|
||||||
import { Bus } from "@/bus"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
|
||||||
import { Hono } from "hono"
|
import { Hono } from "hono"
|
||||||
import { cors } from "hono/cors"
|
import { cors } from "hono/cors"
|
||||||
import { streamSSE } from "hono/streaming"
|
|
||||||
import { proxy } from "hono/proxy"
|
import { proxy } from "hono/proxy"
|
||||||
import { basicAuth } from "hono/basic-auth"
|
import { basicAuth } from "hono/basic-auth"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
@ -34,6 +31,7 @@ import { FileRoutes } from "./routes/file"
|
|||||||
import { ConfigRoutes } from "./routes/config"
|
import { ConfigRoutes } from "./routes/config"
|
||||||
import { ExperimentalRoutes } from "./routes/experimental"
|
import { ExperimentalRoutes } from "./routes/experimental"
|
||||||
import { ProviderRoutes } from "./routes/provider"
|
import { ProviderRoutes } from "./routes/provider"
|
||||||
|
import { EventRoutes } from "./routes/event"
|
||||||
import { InstanceBootstrap } from "../project/bootstrap"
|
import { InstanceBootstrap } from "../project/bootstrap"
|
||||||
import { NotFoundError } from "../storage/db"
|
import { NotFoundError } from "../storage/db"
|
||||||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||||
@ -251,6 +249,7 @@ export namespace Server {
|
|||||||
.route("/question", QuestionRoutes())
|
.route("/question", QuestionRoutes())
|
||||||
.route("/provider", ProviderRoutes())
|
.route("/provider", ProviderRoutes())
|
||||||
.route("/", FileRoutes())
|
.route("/", FileRoutes())
|
||||||
|
.route("/", EventRoutes())
|
||||||
.route("/mcp", McpRoutes())
|
.route("/mcp", McpRoutes())
|
||||||
.route("/tui", TuiRoutes())
|
.route("/tui", TuiRoutes())
|
||||||
.post(
|
.post(
|
||||||
@ -498,64 +497,6 @@ export namespace Server {
|
|||||||
return c.json(await Format.status())
|
return c.json(await Format.status())
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
|
||||||
"/event",
|
|
||||||
describeRoute({
|
|
||||||
summary: "Subscribe to events",
|
|
||||||
description: "Get events",
|
|
||||||
operationId: "event.subscribe",
|
|
||||||
responses: {
|
|
||||||
200: {
|
|
||||||
description: "Event stream",
|
|
||||||
content: {
|
|
||||||
"text/event-stream": {
|
|
||||||
schema: resolver(BusEvent.payloads()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
async (c) => {
|
|
||||||
log.info("event connected")
|
|
||||||
c.header("X-Accel-Buffering", "no")
|
|
||||||
c.header("X-Content-Type-Options", "nosniff")
|
|
||||||
return streamSSE(c, async (stream) => {
|
|
||||||
stream.writeSSE({
|
|
||||||
data: JSON.stringify({
|
|
||||||
type: "server.connected",
|
|
||||||
properties: {},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
const unsub = Bus.subscribeAll(async (event) => {
|
|
||||||
await stream.writeSSE({
|
|
||||||
data: JSON.stringify(event),
|
|
||||||
})
|
|
||||||
if (event.type === Bus.InstanceDisposed.type) {
|
|
||||||
stream.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Send heartbeat every 10s to prevent stalled proxy streams.
|
|
||||||
const heartbeat = setInterval(() => {
|
|
||||||
stream.writeSSE({
|
|
||||||
data: JSON.stringify({
|
|
||||||
type: "server.heartbeat",
|
|
||||||
properties: {},
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}, 10_000)
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
stream.onAbort(() => {
|
|
||||||
clearInterval(heartbeat)
|
|
||||||
unsub()
|
|
||||||
resolve()
|
|
||||||
log.info("event disconnected")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.all("/*", async (c) => {
|
.all("/*", async (c) => {
|
||||||
const path = c.req.path
|
const path = c.req.path
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user