4.9 KiB
Spec Delta: api-plugin-import — add SSE progress push
ADDED Requirements
Requirement: SSE progress push — GET /imports/:id/progress
The package SHALL expose GET /imports/:id/progress returning
Content-Type: text/event-stream; charset=utf-8, mounted under the existing
requireAuth() middleware. Frames SHALL be driven directly off the existing
ImportService.bus events (import.progress, import.completed, import.failed);
NO polling loop over the store is permitted. Every frame SHALL carry the canonical
ImportProgress JSON payload. Three SSE event kinds SHALL be emitted: progress
(every import.progress tick), complete (terminal, on import.completed),
and failed (terminal, on import.failed). The stream SHALL close cleanly after
a terminal event. The handler SHALL respect the request's AbortSignal (client
disconnect) by detaching the bus listener and incrementing
import.sse.disconnected{reason: "client"}. Foreign-owner or missing-job lookups
SHALL return HTTP 404 RFC 7807 problem+json with code === IMPORT.NOT_FOUND
(anti-enumeration parity with GET /:id). Anonymous requests SHALL return HTTP
401 problem+json via the existing auth middleware.
The package SHALL extend getImportInstruments() with three additional counters:
import.sse.connected (Counter), import.sse.events_sent (Counter, labelled by
kind ∈ "progress" | "complete" | "failed"), import.sse.disconnected
(Counter, labelled by reason ∈ "complete" | "failed" | "client"). The counters
SHALL be cached on the same bundle as the existing six instruments and SHALL
rebind through resetImportInstruments().
Scenario: opens a text/event-stream and emits an initial progress frame
- WHEN an authenticated caller
GET /imports/:id/progressfor a job they own - THEN the response is HTTP 200 with
content-type: text/event-stream; charset=utf-8and the first frame isevent: progress\ndata: <ImportProgress JSON>\n\n
Scenario: emits a progress frame for every bus progress event
- WHEN the service emits
import.progresswhile the client is connected - THEN the stream writes a frame whose first non-id line is
event: progressand whosedata:line decodes to anImportProgressobject with the matchingprocessedRows
Scenario: emits a complete frame and closes on import.completed
- WHEN the service emits
import.completed - THEN the stream writes a frame whose first non-id line is
event: completeand then closes; subsequent reads from the stream returndone: true
Scenario: emits a failed frame and closes on import.failed
- WHEN the service emits
import.failed - THEN the stream writes a frame whose first non-id line is
event: failedand then closes
Scenario: returns 404 problem+json for a foreign-owner job
- WHEN alice owns job
j1and bob requestsGET /imports/j1/progress - THEN the response is HTTP 404 with
content-type: application/problem+jsonand a body whosecode === "IMPORT.NOT_FOUND"
Scenario: returns 401 problem+json when the caller is anonymous
- WHEN an anonymous caller requests
GET /imports/:id/progress - THEN the response is HTTP 401 with
content-type: application/problem+json
Scenario: detaches the listener and increments disconnect counter on client abort
- WHEN the client aborts the request before a terminal event
- THEN the handler unsubscribes from the bus and increments
import.sse.disconnected{reason:"client"}
Scenario: increments import.sse counters on connect, send, and disconnect
- WHEN a connection opens, two progress events fire, and a terminal
completeevent closes the stream - THEN
import.sse.connectedincrements by 1,import.sse.events_sent{kind}increments by 3 (2×progress+ 1×complete), andimport.sse.disconnected{reason:"complete"}increments by 1
Verified by:
src/routes/progress-sse/progress-sse.test.ts::"opens a text/event-stream and emits an initial progress frame"
AND src/routes/progress-sse/progress-sse.test.ts::"emits a progress frame for every bus progress event"
AND src/routes/progress-sse/progress-sse.test.ts::"emits a complete frame and closes on import.completed"
AND src/routes/progress-sse/progress-sse.test.ts::"emits a failed frame and closes on import.failed"
AND src/routes/progress-sse/progress-sse.test.ts::"returns 404 problem+json for a foreign-owner job"
AND src/routes/progress-sse/progress-sse.test.ts::"returns 401 problem+json when the caller is anonymous"
AND src/routes/progress-sse/progress-sse.test.ts::"detaches the listener and increments disconnect counter on client abort"
AND src/routes/progress-sse/progress-sse.test.ts::"increments import.sse counters on connect, send, and disconnect"
AND src/otel/instruments.test.ts::"bundle exposes the SSE counters"