posts/0014.md · 2026-04-29
change streams — onSnapshot without polling
Every successful write — insert, update, delete, `commit_tx` — emits a `ChangeEvent` to a process-wide broker. Subscribers receive a stream of events filtered by collection, document id, or a small predicate. No polling, no `LISTEN/NOTIFY` ceremony, no Postgres trigger.
The broker is a single `tokio::sync::broadcast` channel wrapped in a per-collection filter map. New subscriber → `subscribe(filter)` returns a `WatchHandle` that holds an mpsc receiver. Drop the handle, the slot frees. Internal capacity is a ring; if a subscriber falls behind, it gets a `Lagged(n)` event and resubscribes from the new head. No silent missed events.
**Resume tokens.** Each event carries an opaque `resume_id`. Pass it back to `watch(filter, resume_from=...)` to pick up where you left off after a process restart. The token is a monotonically increasing 64-bit counter stamped at WAL append time, so it survives crash recovery.
**Wire surface.** Three transports, same event shape:
* WebSocket — JS SDK consumes this. Subscribe-by-collection, multiplex on one socket.
* Server protocol `watch` op — Python / Go / .NET clients.
* In-process — Swift embedded mode (`addMutationObserver`) and the Python embedded wrapper observe directly via the same broker, no socket required.
Event types: `insert`, `insert_many`, `update`, `delete`, `commit_tx`. Each carries the collection, the doc id, the timestamp, the before/after fragment (for updates), and a metadata bag where the SDK can pin per-watch state.