Files
suaveplan/openspec/archive/2026-05-20-add-import-sse-progress/proposal.md
2026-05-27 23:29:53 +01:00

61 lines
3.6 KiB
Markdown

# Change: add-import-sse-progress
## Why
`@suaveplan/api-plugin-import` shipped (archive `2026-05-19-add-api-plugin-import`) with the
**`## Excluded from this migration`** clause explicitly deferring SSE progress push:
> Foreign-key resolution, parallel batch workers, resumable checkpoints, S3/URL source adapters, **SSE progress push**
This change lifts that single deferral. Consumers can now subscribe to a job's progress
via `GET /imports/:id/progress` and receive `text/event-stream` frames driven directly off
the existing `ImportService.bus` events — no polling. The other deferred items
(foreign-key resolution, parallel workers, checkpoints, S3 sources) remain deferred.
The endpoint reuses:
- the canonical `ImportProgress` payload type,
- the existing `import.progress` / `import.completed` / `import.failed` events,
- the existing `IMPORT.*` error registry,
- the owner-scoping invariants of `ImportService.getJob(jobId, ownerId)`.
The endpoint introduces three new OTel counters scoped to the new feature module:
`import.sse.connected`, `import.sse.events_sent{kind}`, `import.sse.disconnected{reason}`.
## Affected requirements
ADDED requirement: **SSE progress push** (one new requirement; no MODIFIED / REMOVED entries).
## Source mapping
| Spec scenario | Implementation | Test |
| --- | --- | --- |
| GET `/imports/:id/progress` opens an event-stream | `src/routes/progress-sse/progress-sse.ts` | `src/routes/progress-sse/progress-sse.test.ts::"opens a text/event-stream and emits an initial progress frame"` |
| Each `import.progress` bus emit produces an `event: progress` frame | `src/routes/progress-sse/progress-sse.ts` | `src/routes/progress-sse/progress-sse.test.ts::"emits a progress frame for every bus progress event"` |
| Terminal `import.completed` emits `event: complete` then closes | `src/routes/progress-sse/progress-sse.ts` | `src/routes/progress-sse/progress-sse.test.ts::"emits a complete frame and closes on import.completed"` |
| Terminal `import.failed` emits `event: failed` then closes | `src/routes/progress-sse/progress-sse.ts` | `src/routes/progress-sse/progress-sse.test.ts::"emits a failed frame and closes on import.failed"` |
| Foreign-owner request returns 404 problem+json | `src/routes/progress-sse/progress-sse.ts` | `src/routes/progress-sse/progress-sse.test.ts::"returns 404 problem+json for a foreign-owner job"` |
| Anonymous request returns 401 problem+json | route is mounted under `requireAuth()` in `build-routes.ts` | `src/routes/progress-sse/progress-sse.test.ts::"returns 401 problem+json when the caller is anonymous"` |
| Client disconnect cleans up the bus listener | `AbortSignal` wired into the `ReadableStream.cancel` path | `src/routes/progress-sse/progress-sse.test.ts::"detaches the listener and increments disconnect counter on client abort"` |
| OTel counters increment on connect / event / disconnect | `src/routes/progress-sse/progress-sse.ts` (new instruments wired through `getImportInstruments()`) | `src/routes/progress-sse/progress-sse.test.ts::"increments import.sse counters on connect, send, and disconnect"` |
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/api-plugin-import` to enumerate.
- `@suaveplan/api` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason:
additive route under an existing prefix; no signature changes to the consumed surface)
Foundation package? No. This is a feature-additive change on a server-tier plugin.
## Out of scope (still deferred from the original `Excluded` list)
- Foreign-key resolution
- Parallel batch workers
- Resumable checkpoints
- S3 / URL source adapters
- Streaming input parsing
- Benchmarks