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>
54 lines
1.5 KiB
TypeScript
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;
|
|
}
|