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.

Emacs 29.3 (Org mode 9.6.15)