Task: Open the change stream
Table of Contents
This page documents a task in the Stream the changes as they happen story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
A client can follow the changes over one long response, and a quiet market does not look like a dead connection.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Stream the changes as they happen |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-31 |
3. Acceptance
[X]A change published to the hub reaches every open stream.[X]A stream that is not read drops its oldest frames instead of growing without limit.[X]A quiet stream writes a keep-alive comment and does not spin on the queue.[X]A closed subscription ends the stream.
4. Plan
The hub is the concurrent state, so it is the part that had to be written first. A subscription owns a bounded queue and a waker; a publisher takes the subscription list under one lock and pushes outside it, so a slow stream never blocks the publisher. The source reads its own queue on the connection's executor and waits on a timer that the waker cancels, so a frame arrives as soon as it is published and a quiet stream costs one timer per interval rather than a spin.
The route is thin on purpose: it registers GET /api/v1/changes, refuses without a session,
sets text/event-stream with the two headers a proxy needs, and hands the source to the
response. Everything the stream does is in the source, where a case can reach it without a
socket.
5. Measurements
| What | Value | Command | Commit |
|---|---|---|---|
| Change stream cases | 7 cases, 18 assertions, all passing | ./ores.http.core.tests "[change_stream]" |
this branch |
| Routes in the inventory | 409, one more than before | python3 build/scripts/route_inventory.py --check |
this branch |
| Server builds with the route | 402 targets, exit 0 | cmake --build … --target ores.http.server |
this branch |
6. Notes
- The keep-alive test asserts the elapsed time as well as the bytes: a source that spun on the queue would also write a comment eventually, and the case that only checked for the text would pass on a busy loop.
- A frame published while the source is between waits costs up to one keep-alive interval, because the waker can only cancel a wait that has begun. That bound is stated rather than hidden: with a 15 s keep-alive the worst case is 15 s, which is the interval the design chose for a quiet market and not for a busy one.
- Nothing fills the hub yet. The next task subscribes it to the change events and applies the party filter; this one proves the path from a publish to a client.
7. Result
The stream exists end to end: a publish reaches every open subscription, a slow reader loses its oldest frames rather than the server's memory, a quiet stream is kept alive without spinning, and a closed subscription ends the body. The route serves it as an event stream.