Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -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 -->