fix(server): keep external annotation SSE streams alive (#439)

This commit is contained in:
foxytanuki
2026-03-31 00:49:34 +09:00
committed by GitHub
parent a88da925de
commit 16756e082b
5 changed files with 42 additions and 8 deletions
+4 -2
View File
@@ -127,7 +127,7 @@ export async function startAnnotateServer(
server = Bun.serve({
port: configuredPort,
async fetch(req) {
async fetch(req, server) {
const url = new URL(req.url);
// API: Get plan content (reuse /api/plan so the plan editor UI works)
@@ -195,7 +195,9 @@ export async function startAnnotateServer(
}
// API: External annotations (SSE-based, for any external tool)
const externalResponse = await externalAnnotations.handle(req, url);
const externalResponse = await externalAnnotations.handle(req, url, {
disableIdleTimeout: () => server.timeout(req, 0),
});
if (externalResponse) return externalResponse;
// API: Submit annotation feedback
@@ -0,0 +1,18 @@
import { describe, expect, test, mock } from "bun:test";
import { createExternalAnnotationHandler } from "./external-annotations";
describe("external annotations SSE", () => {
test("disables idle timeout for stream requests", async () => {
const handler = createExternalAnnotationHandler("plan");
const disableIdleTimeout = mock(() => {});
const res = await handler.handle(
new Request("http://localhost/api/external-annotations/stream"),
new URL("http://localhost/api/external-annotations/stream"),
{ disableIdleTimeout },
);
expect(disableIdleTimeout).toHaveBeenCalledTimes(1);
expect(res?.headers.get("content-type")).toBe("text/event-stream");
});
});
+12 -2
View File
@@ -28,7 +28,11 @@ export type { ExternalAnnotationEvent } from "@plannotator/shared/external-annot
// ---------------------------------------------------------------------------
export interface ExternalAnnotationHandler {
handle: (req: Request, url: URL) => Promise<Response | null>;
handle: (
req: Request,
url: URL,
options?: { disableIdleTimeout?: () => void },
) => Promise<Response | null>;
}
// ---------------------------------------------------------------------------
@@ -64,9 +68,15 @@ export function createExternalAnnotationHandler(
});
return {
async handle(req: Request, url: URL): Promise<Response | null> {
async handle(
req: Request,
url: URL,
options?: { disableIdleTimeout?: () => void },
): Promise<Response | null> {
// --- SSE stream ---
if (url.pathname === STREAM && req.method === "GET") {
options?.disableIdleTimeout?.();
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
let ctrl: ReadableStreamDefaultController;
+4 -2
View File
@@ -202,7 +202,7 @@ export async function startPlannotatorServer(
server = Bun.serve({
port: configuredPort,
async fetch(req) {
async fetch(req, server) {
const url = new URL(req.url);
// API: Get a specific plan version from history
@@ -367,7 +367,9 @@ export async function startPlannotatorServer(
if (editorResponse) return editorResponse;
// API: External annotations (SSE-based, for any external tool)
const externalResponse = await externalAnnotations?.handle(req, url);
const externalResponse = await externalAnnotations?.handle(req, url, {
disableIdleTimeout: () => server.timeout(req, 0),
});
if (externalResponse) return externalResponse;
// API: Save to notes (decoupled from approve/deny)
+4 -2
View File
@@ -248,7 +248,7 @@ export async function startReviewServer(
server = Bun.serve({
port: configuredPort,
async fetch(req) {
async fetch(req, server) {
const url = new URL(req.url);
// API: Get diff content
@@ -447,7 +447,9 @@ export async function startReviewServer(
if (editorResponse) return editorResponse;
// API: External annotations (SSE-based, for any external tool)
const externalResponse = await externalAnnotations.handle(req, url);
const externalResponse = await externalAnnotations.handle(req, url, {
disableIdleTimeout: () => server.timeout(req, 0),
});
if (externalResponse) return externalResponse;
// API: Submit review feedback