Commit Graph

4 Commits

Author SHA1 Message Date
Tyler da29188dd0 feat(api): GET /api/{claims,remittances,activity}/stream + unsubscribe
Adds three live-tail streaming endpoints that emit an NDJSON snapshot
then forward new event-bus events as they arrive, with a 15s idle
heartbeat (overridable via CYCLONE_TAIL_HEARTBEAT_S for tests).

Each endpoint:
  1. yields a snapshot of existing rows as {"type":"item","data":<row>}
  2. terminates the snapshot with {"type":"snapshot_end","data":{"count":N}}
  3. subscribes to its event kind and forwards each new event as an
     {"type":"item","data":<event>} line
  4. emits a {"type":"heartbeat","data":{"ts":<iso>}} line every
     CYCLONE_TAIL_HEARTBEAT_S seconds when idle
  5. checks request.is_disconnected() before each yield and unsubscribes
     from the bus on cleanup so a closed stream releases its queue

The shared tail loop lives in api._tail_events, which polls
bus.subscribe_raw()'s queue directly instead of using the bus's
async-iterator wrapper — wait_for on an async generator cancels the
inner future on timeout, which poisons subsequent __anext__ calls with
StopAsyncIteration. Queue.get() is idempotent under cancellation, so
heartbeats don't break the subscription.

EventBus gains an unsubscribe(queue, kinds) method (idempotent) so
the tail loop can release its queue in a try/finally. The disconnect
test asserts the subscriber list is empty after the body iterator is
closed, validating no queue leak per open stream.

Tests in test_api_stream_live.py: 8 tests covering snapshot shape,
post-snapshot publish, heartbeat timing, multi-item snapshots, and
client disconnect cleanup. Plus 2 tests in test_pubsub.py for the
new unsubscribe method.
2026-06-20 16:57:30 -06:00
Tyler 8bcb23c78d feat(api): parse endpoints pass EventBus into store writes
- api.parse_837 / parse_835: pass request.app.state.event_bus into store.add()
- conftest: autouse fixture wires a fresh EventBus onto app.state for every
  test, since TestClient does not invoke the FastAPI lifespan handler
  unless used as a context manager
- test_pubsub: split get_event_bus coverage into a raises-when-missing test
  and a returns-attached-bus test, both save/restore app.state.event_bus
  around the assertion so the autouse fixture's bus is preserved

Phase 2 complete: db init moved to lifespan, EventBus is the process-wide
publish point, and the two ingest endpoints publish claim_written /
remittance_written / activity_recorded events on every store.add().
2026-06-20 15:51:18 -06:00
Tyler 25a76a515d feat(pubsub): get_event_bus() late-import accessor 2026-06-20 15:35:02 -06:00
Tyler b2f5a16541 feat(pubsub): EventBus with drop-oldest overflow, per-kind fan-out 2026-06-20 15:28:13 -06:00