Workflow status is tracked in GitHub. This local document is retained as an engineering spec/evidence record.
RUST-FEAT-007 - REST push: Server-Sent Events stream for live transfer updates¶
Summary¶
Add a push path to /api/v1 so a controller (TrackMuleBB) gets transfer state
changes without polling. Expose GET /api/v1/events as a text/event-stream
(Server-Sent Events) that emits transfer change events (add / update / remove),
and advertise the transfers.sse capability via GET /capabilities. The existing
GET /api/v1/transfers full-poll stays as the baseline + fallback.
SSE is chosen over WebSocket because the dashboard need is unidirectional
server -> client: commands (pause/resume/add) already flow through normal REST
POSTs, so a bidirectional socket would be pure overhead. SSE is plain HTTP,
reuses the X-API-Key header, and supports resume via Last-Event-ID.
Why This Matters¶
/transfers re-serializes the whole list every poll; push delivers only changes,
near-instantly, and scales with churn rather than library size. emulebb-rust is
already event-driven internally (it is the source of truth), so the events exist —
this just surfaces them.
Intended Shape¶
GET /api/v1/events->text/event-stream; each event carries a monotonically increasing id and a JSON payload of the changed transfer(s) (same field shape as theTransferDTO) plus aremovedform keyed by hash.- Internal Tokio
broadcastbus: the transfer manager publishes change events; the SSE handler fans them out to subscribers. Periodic heartbeat/comment line to keep the connection alive through proxies. - Resume: honour
Last-Event-ID(replay-from or signal "re-baseline via/transfers"). Auth unchanged (X-API-Key). GET /capabilitiesadvertisestransfers.sseso a controller gates on it and falls back to poll when absent.- (Considered, not chosen) a delta-pull
GET /api/v1/sync?rid=Nmirroring qBittorrent's/sync/maindata; recorded as the lower-effort alternative if a long-lived stream is undesirable.
Acceptance Criteria¶
- [ ]
GET /api/v1/eventsstreams add/update/remove transfer events as SSE with incrementing event ids; survives a quiet period via heartbeats. - [ ]
Last-Event-IDeither replays missed events or instructs a/transfersre-baseline; no silent gaps. - [ ]
GET /capabilitiesliststransfers.sse;/transfersis unchanged. - [ ] Auth via
X-API-Key; contract version (x-contract-version) bumped.
Notes¶
- RC2 soft-freeze: this is a forward-lineage
/api/v1contract addition, not a parity/release-blocking fix. Design now, schedule after the freeze (the backlog explicitly stages design work; emulebb-rust is out of RC2 ship scope). - Controller side: trackmulebb TMBB-FEAT-014 (capability-gated push adapter,
poll fallback). qBittorrentBB has no SSE; its push-ish path is the
/sync/maindatadelta (trackmulebb TMBB-FEAT-005) — both unify behind one adapter abstraction.
Progress¶
- 2026-07-18: Made the SSE
TransferEventcontract mechanically variant-safe. Rust now models transfer add/update/remove/reset events as explicit tagged enum variants instead of one optional-field DTO, WebUI types use the matching discriminated union, serialization tests lock the wire shape, and OpenAPI documents the stream payload asoneOfevent variants. - 2026-07-18: Added
X-Contract-Version: 1.2.0to/api/v1responses through the Rust REST router layer, including normal JSON responses, SSE streams, and REST error envelopes. OpenAPI response components and the persisted SSE conformance probe now assert the header. - 2026-07-18: Extended the persisted REST smoke harness to assert
X-Contract-Version: 1.2.0on native JSON success and error envelopes whenever live transport headers are present. This closes the gap between the documented all-/api/v1header contract and the non-SSE smoke path. - 2026-07-18: Added Rust route-validation coverage for router-generated
NOT_FOUNDandMETHOD_NOT_ALLOWEDresponses so fallback errors are locked to the sameX-Contract-Version: 1.2.0contract header as handler responses. - 2026-07-18: Extended the persisted Rust REST response-conformance probe to
assert the
/api/v1/eventsSSE response headers promised by the contract:Cache-Control: no-cache, no-transformandX-Accel-Buffering: no. The conformance report now lists missing stream headers asgetEventsfailures. - 2026-07-18: Hardened the embedded WebUI SSE client error path. Non-OK
/api/v1/eventsresponses now parse canonical REST JSON error envelopes and surface their messages instead of a generic stream failure; unit coverage locks the auth-error path. - 2026-07-18: Hardened the SSE transport contract for adapters and reverse
proxies.
GET /api/v1/eventsnow explicitly returnsCache-Control: no-cache, no-transformandX-Accel-Buffering: no; REST tests assert both headers and the OpenAPI event-stream response documents them. - 2026-07-18: Implemented the first Rust-native SSE slice.
GET /api/v1/eventsnow requires normal REST auth and servestext/event-stream; transfer add/update/remove changes publish through a Tokio broadcast bus with monotonically increasing event ids; lagged subscribers receivesync.resetwith an event id and instructions to re-baseline viaGET /transfers.GET /capabilitiesnow advertisestransfers.sse. The OpenAPI route/schema, route metadata, REST tests, core lifecycle test, WebUI API type, and smoke-test operation filtering were updated together. Per current Rust forward-lineage policy, the contract version was not bumped in this slice. - 2026-07-18: Honored
Last-Event-IDexplicitly. Reconnected streams now begin with a fresh-idsync.resetevent containingreason: "last-event-id"and the suppliedlastEventId, making the no-replay-cache behavior mechanical and adapter-visible instead of silently dropping missed history. The OpenAPI header parameter/schema, Rust DTO, WebUI type, and REST route test were aligned. - 2026-07-18: Added a dedicated live conformance probe for the long-lived SSE
route. The persisted Rust REST response-conformance command now opens
/api/v1/eventswithLast-Event-ID, verifiestext/event-stream, reads the first frame, and failsgetEventsif the expected resumesync.resetpayload is missing. - 2026-07-18: Cleaned the Rust DTO shape behind the same wire contract.
TransferEvent.typeand resetreasonnow use explicit serde-backed Rust enums instead of ad-hoc strings, while preserving the OpenAPI/WebUI enum values and SSE event names. - 2026-07-18: Added route-level REST proof that
/api/v1/eventsemits a realtransfer.addedSSE frame after a transfer is created, covering the handler, Tokio broadcast subscription, SSE formatting, and transfer DTO payload together. - 2026-07-18: Wired the embedded WebUI to consume
transfers.ssewith a fetch-based event stream so it can sendX-API-KeyandLast-Event-ID. Transfer events andsync.resetnow trigger immediate dashboard refreshes while the existing poll loop remains as fallback. - 2026-07-18: Surfaced the embedded WebUI event-stream health in Diagnostics.
Power users can now see whether the transfer stream is polling, connecting,
streaming, or reconnecting, plus the last event type/id, reconnect count, poll
interval, and last stream error. The browser smoke fixture asserts the
diagnostic values from a mocked
sync.resetframe. - 2026-07-18: Added daemon-side transfer event bus metrics to
/api/v1/diagnostics. The Rust runtime now reports SSE enablement, channel capacity, queued event count, subscriber count, latest/next event ids, and reset resume behavior through a closed OpenAPI schema, and the WebUI shows the values alongside its client-side stream state. - 2026-07-18: Restored the full WebUI unit gate after the SSE consumer work by
updating stale component expectations to the current Tabler badge classes;
npm run test:unit, e2e smoke, typecheck, and production build now pass. - 2026-07-18: Added static OpenAPI drift coverage for the SSE response component
so
EventStreamResponsemust keep documentingCache-ControlandX-Accel-Bufferingalongside the contract-version header.