From 16756e082b49d67d8ef16fc7837e56748e07de4a Mon Sep 17 00:00:00 2001 From: foxytanuki <45069709+foxytanuki@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:49:34 +0900 Subject: [PATCH] fix(server): keep external annotation SSE streams alive (#439) --- packages/server/annotate.ts | 6 ++++-- packages/server/external-annotations.test.ts | 18 ++++++++++++++++++ packages/server/external-annotations.ts | 14 ++++++++++++-- packages/server/index.ts | 6 ++++-- packages/server/review.ts | 6 ++++-- 5 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 packages/server/external-annotations.test.ts diff --git a/packages/server/annotate.ts b/packages/server/annotate.ts index e2f701a3..078ce789 100644 --- a/packages/server/annotate.ts +++ b/packages/server/annotate.ts @@ -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 diff --git a/packages/server/external-annotations.test.ts b/packages/server/external-annotations.test.ts new file mode 100644 index 00000000..a4bda3cf --- /dev/null +++ b/packages/server/external-annotations.test.ts @@ -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"); + }); +}); diff --git a/packages/server/external-annotations.ts b/packages/server/external-annotations.ts index cd77a5ac..d1562165 100644 --- a/packages/server/external-annotations.ts +++ b/packages/server/external-annotations.ts @@ -28,7 +28,11 @@ export type { ExternalAnnotationEvent } from "@plannotator/shared/external-annot // --------------------------------------------------------------------------- export interface ExternalAnnotationHandler { - handle: (req: Request, url: URL) => Promise; + handle: ( + req: Request, + url: URL, + options?: { disableIdleTimeout?: () => void }, + ) => Promise; } // --------------------------------------------------------------------------- @@ -64,9 +68,15 @@ export function createExternalAnnotationHandler( }); return { - async handle(req: Request, url: URL): Promise { + async handle( + req: Request, + url: URL, + options?: { disableIdleTimeout?: () => void }, + ): Promise { // --- SSE stream --- if (url.pathname === STREAM && req.method === "GET") { + options?.disableIdleTimeout?.(); + let heartbeatTimer: ReturnType | null = null; let ctrl: ReadableStreamDefaultController; diff --git a/packages/server/index.ts b/packages/server/index.ts index 8f783dc6..38e7d5a6 100644 --- a/packages/server/index.ts +++ b/packages/server/index.ts @@ -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) diff --git a/packages/server/review.ts b/packages/server/review.ts index 7a25dfc3..4673bac9 100644 --- a/packages/server/review.ts +++ b/packages/server/review.ts @@ -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