Initial Commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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/progress` for a job they own
|
||||
- **THEN** the response is HTTP 200 with `content-type: text/event-stream; charset=utf-8`
|
||||
and the first frame is `event: progress\ndata: <ImportProgress JSON>\n\n`
|
||||
|
||||
#### Scenario: emits a progress frame for every bus progress event
|
||||
|
||||
- **WHEN** the service emits `import.progress` while the client is connected
|
||||
- **THEN** the stream writes a frame whose first non-id line is `event: progress`
|
||||
and whose `data:` line decodes to an `ImportProgress` object with the matching
|
||||
`processedRows`
|
||||
|
||||
#### 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: complete`
|
||||
and then closes; subsequent reads from the stream return `done: 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: failed`
|
||||
and then closes
|
||||
|
||||
#### Scenario: returns 404 problem+json for a foreign-owner job
|
||||
|
||||
- **WHEN** alice owns job `j1` and bob requests `GET /imports/j1/progress`
|
||||
- **THEN** the response is HTTP 404 with `content-type: application/problem+json`
|
||||
and a body whose `code === "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 `complete`
|
||||
event closes the stream
|
||||
- **THEN** `import.sse.connected` increments by 1, `import.sse.events_sent{kind}`
|
||||
increments by 3 (2× `progress` + 1× `complete`), and
|
||||
`import.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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user