Files
legal-ai/web-ui/src/lib/sse.ts
Chaim 10540a38b4 Phase 4c: bulk document upload with live SSE progress
New UploadSheet on the case detail page wraps react-dropzone + a
selector for doc_type. Files post to
POST /api/cases/{n}/documents/upload-tagged as multipart form-data;
the returned task_id is streamed via GET /api/progress/{task_id}
through the new lib/sse.ts EventSource wrapper.

Each upload row shows a per-file progress bar that transitions to
success/error on the terminal SSE payload. Closing the stream inside
the message handler avoids EventSource's auto-reconnect after EOF.

Phase 4 (task 86) is now complete end-to-end: create, upload, edit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:25:44 +00:00

54 lines
1.5 KiB
TypeScript

/**
* Minimal SSE helper — wraps `EventSource` so consumers get typed event
* callbacks and a single `close()` for cleanup.
*
* Used by the upload flow to stream /api/progress/{task_id} events.
* Kept framework-agnostic so any component can drive it; a thin React
* hook layer sits on top in lib/api/documents.ts.
*/
export type SSEHandlers<T> = {
onMessage: (data: T) => void;
onError?: (err: Event) => void;
/* Called when the server closes the stream cleanly. EventSource has
* no native "closed" event, so the backend signals completion via a
* terminal payload and we close from the onMessage handler — callers
* can return `true` to trigger this path. */
onClose?: () => void;
};
export function openSSE<T>(
url: string,
{ onMessage, onError, onClose }: SSEHandlers<T>,
): () => void {
const es = new EventSource(url);
let closed = false;
const close = () => {
if (closed) return;
closed = true;
es.close();
onClose?.();
};
es.addEventListener("message", (ev) => {
if (closed) return;
try {
const payload = JSON.parse(ev.data) as T;
onMessage(payload);
} catch {
/* backend sends heartbeats as comments — EventSource filters them,
* so any non-JSON message here is a protocol bug worth ignoring */
}
});
es.addEventListener("error", (ev) => {
if (closed) return;
onError?.(ev);
/* EventSource auto-reconnects; we only close on an explicit terminal
* payload from the server, not on transient network errors */
});
return close;
}