fix(dsh): synchronize observation state and manage thumbnail capture

This commit is contained in:
drakezhang
2026-09-07 16:24:04 +08:00
parent 5ebe609c40
commit e961395be9
10 changed files with 800 additions and 122 deletions
@@ -6,7 +6,8 @@
* observation channel through the documented `webServer` route seam instead:
*
* GET /bsk-observation/state → { sessions, available }
* GET /bsk-observation/events → SSE stream of ObservationEvent
* GET /bsk-observation/events → SSE snapshot, then ObservationEvent increments
* (?thumbnails=0 for state-only subscribers)
* POST /bsk-observation/interrupt → body {sessionId?} → {interrupted: boolean}
* POST /bsk-observation/stop → body {sessionId} → {stopped: boolean}
* GET /bsk-observation/thumbnail/<attachmentId> → image bytes
@@ -99,6 +100,7 @@ export function registerObservationRoutes(
if (webServer === undefined) {
return () => {};
}
const streams = new Set<() => void>();
const disposers = [
webServer.register({
kind: "exact",
@@ -129,14 +131,42 @@ export function registerObservationRoutes(
"cache-control": "no-cache",
connection: "keep-alive",
});
const unsubscribe = observation.subscribe((event) => {
res.write(`data: ${JSON.stringify(event)}\n\n`);
});
const heartbeat = setInterval(() => res.write(": heartbeat\n\n"), SSE_HEARTBEAT_MS);
res.on("close", () => {
const thumbnails =
new URL(req.url ?? "/", "http://localhost").searchParams.get("thumbnails") !== "0";
let unsubscribe: (() => void) | undefined;
let heartbeat: ReturnType<typeof setInterval> | undefined;
let closed = false;
const close = () => {
if (closed) return;
closed = true;
clearInterval(heartbeat);
unsubscribe();
});
unsubscribe?.();
streams.delete(close);
res.end();
};
const write = (data: string) => {
if (closed) return;
try {
res.write(data);
} catch {
close();
}
};
streams.add(close);
res.on("close", close);
res.on("error", close);
try {
unsubscribe = observation.subscribe(
(event) => {
write(`data: ${JSON.stringify(event)}\n\n`);
},
{ thumbnails },
);
if (closed) unsubscribe();
else heartbeat = setInterval(() => write(": heartbeat\n\n"), SSE_HEARTBEAT_MS);
} catch {
close();
}
},
}),
webServer.register({
@@ -228,6 +258,7 @@ export function registerObservationRoutes(
}),
];
return () => {
for (const close of streams) close();
for (const dispose of disposers) dispose();
};
}