|
|
@@ -2,8 +2,8 @@ import { Bus } from "@/bus"
|
|
|
import * as Log from "@opencode-ai/core/util/log"
|
|
|
import { Effect, Schema } from "effect"
|
|
|
import * as Stream from "effect/Stream"
|
|
|
-import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
|
|
-import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
|
+import { HttpServerResponse } from "effect/unstable/http"
|
|
|
+import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
|
|
import * as Sse from "effect/unstable/encoding/Sse"
|
|
|
|
|
|
const log = Log.create({ service: "server" })
|
|
|
@@ -16,7 +16,7 @@ export const EventApi = HttpApi.make("event").add(
|
|
|
HttpApiGroup.make("event")
|
|
|
.add(
|
|
|
HttpApiEndpoint.get("subscribe", EventPaths.event, {
|
|
|
- success: Schema.Unknown,
|
|
|
+ success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
|
|
|
}).annotateMerge(
|
|
|
OpenApi.annotations({
|
|
|
identifier: "event.subscribe",
|
|
|
@@ -37,38 +37,38 @@ function eventData(data: unknown): Sse.Event {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export const eventRoute = HttpRouter.use((router) =>
|
|
|
+function eventResponse(bus: Bus.Interface) {
|
|
|
+ const events = bus.subscribeAll().pipe(Stream.takeUntil((event) => event.type === Bus.InstanceDisposed.type))
|
|
|
+ const heartbeat = Stream.tick("10 seconds").pipe(
|
|
|
+ Stream.drop(1),
|
|
|
+ Stream.map(() => ({ type: "server.heartbeat", properties: {} })),
|
|
|
+ )
|
|
|
+
|
|
|
+ log.info("event connected")
|
|
|
+ return HttpServerResponse.stream(
|
|
|
+ Stream.make({ type: "server.connected", properties: {} }).pipe(
|
|
|
+ Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
|
|
+ Stream.map(eventData),
|
|
|
+ Stream.pipeThroughChannel(Sse.encode()),
|
|
|
+ Stream.encodeText,
|
|
|
+ Stream.ensuring(Effect.sync(() => log.info("event disconnected"))),
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ contentType: "text/event-stream",
|
|
|
+ headers: {
|
|
|
+ "Cache-Control": "no-cache, no-transform",
|
|
|
+ "X-Accel-Buffering": "no",
|
|
|
+ "X-Content-Type-Options": "nosniff",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export const eventHandlers = HttpApiBuilder.group(EventApi, "event", (handlers) =>
|
|
|
Effect.gen(function* () {
|
|
|
const bus = yield* Bus.Service
|
|
|
- yield* router.add(
|
|
|
- "GET",
|
|
|
- EventPaths.event,
|
|
|
- Effect.gen(function* () {
|
|
|
- const events = bus.subscribeAll().pipe(Stream.takeUntil((event) => event.type === Bus.InstanceDisposed.type))
|
|
|
- const heartbeat = Stream.tick("10 seconds").pipe(
|
|
|
- Stream.drop(1),
|
|
|
- Stream.map(() => ({ type: "server.heartbeat", properties: {} })),
|
|
|
- )
|
|
|
-
|
|
|
- log.info("event connected")
|
|
|
- return HttpServerResponse.stream(
|
|
|
- Stream.make({ type: "server.connected", properties: {} }).pipe(
|
|
|
- Stream.concat(events.pipe(Stream.merge(heartbeat, { haltStrategy: "left" }))),
|
|
|
- Stream.map(eventData),
|
|
|
- Stream.pipeThroughChannel(Sse.encode()),
|
|
|
- Stream.encodeText,
|
|
|
- Stream.ensuring(Effect.sync(() => log.info("event disconnected"))),
|
|
|
- ),
|
|
|
- {
|
|
|
- contentType: "text/event-stream",
|
|
|
- headers: {
|
|
|
- "Cache-Control": "no-cache, no-transform",
|
|
|
- "X-Accel-Buffering": "no",
|
|
|
- "X-Content-Type-Options": "nosniff",
|
|
|
- },
|
|
|
- },
|
|
|
- )
|
|
|
- }),
|
|
|
- )
|
|
|
+ return handlers.handleRaw("subscribe", Effect.fn("EventHttpApi.subscribe")(function* () {
|
|
|
+ return eventResponse(bus)
|
|
|
+ }))
|
|
|
}),
|
|
|
)
|