Skip to content

feat(ws)!: late-join snapshots from the record's current value (#434) - #439

Merged
lxsaah merged 2 commits into
mainfrom
feat/ws-snapshots-from-latest
Nov 7, 2026
Merged

lxsaah merged 2 commits into
mainfrom
feat/ws-snapshots-from-latest

Conversation

@lxsaah

@lxsaah lxsaah commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Closes #434. Related to #422 and #438.

Problem

AimX builds late-join snapshots from each record's current value when a client subscribes. WebSocket instead cached the last bytes it sent per (record, topic) in a Mutex<HashMap> on the broadcast path and served snapshots from that. It was a second copy of state the database already holds. With a topic writer, it gained one entry for every distinct topic ever published and never shrank. And it gave WebSocket and AimX clients different initial state.

Change

aimdb-core

  • New public OutboundLatest (outbound/latest.rs), built per scheme like OutboundRoutes and using the same route ids. render(id) reads the record's current value (TypedRecord::latest, i.e. buffer peek) and runs it through the link's topic writer and serializer, returning LatestMessage { record_index, topic, payload, metadata }. It keeps no state and needs no with_remote_access(). render_if(id, accept) resolves the topic first and serializes only if accept takes it; a link with a fixed topic is checked before its value is even read.
  • ConnectorLink carries the renderer (latest_fn) next to its route factory, built in typed_api.rs from the same serializer and topic writer the live route uses.

aimdb-websocket-connector

  • WsSession::snapshots calls render_if with the subscribe pattern for each route the client may read, so non-matching links are never serialized.
  • Removed: the snapshot cache (SnapshotCache, DynMapSnapshot), the crate-private SnapshotProvider / NoSnapshot / Snapshot, and server/session.rs. run_broadcast only fans out.
  • with_late_join docs describe the new behaviour.

Tests (tests/e2e.rs)

  • The fixture gains SingleLatest records (sensors.temp, sensors.humidity, t) and a SingleLatest record with a topic writer (moving). The existing snapshot and golden-frame tests use them and no longer need sleep(100ms), since snapshots read the record directly.
  • New ring_records_send_no_snapshot: a ring record's subscriber gets the next live event first, with no snap.
  • New topic_writer_snapshot_is_on_the_current_topic: after writing on m.a then m.b, m.# gets one snapshot, on m.b.
  • New snapshots_serialize_only_matching_links: a counting serializer shows a non-matching subscribe serializes nothing and a matching one serializes once.
  • The snapshot tests in decouple_record_keys_topics.rs, including the permission-filtered one, pass unchanged.

CHANGELOG entries for aimdb-core (Added) and aimdb-websocket-connector (Changed, breaking, wire).

Behaviour changes

  • SpmcRing records send no snapshot, as on AimX. A ring has no current value. Records whose subscribers need the current value should use SingleLatest.
  • Topic writers: a snapshot is the current value on the topic that value produces now. Topics the record published on earlier get none.
  • Exact-topic subscribes still get snapshots on WebSocket. Doing the same on AimX is AimX exact-topic subscribes send a late-join snapshot, like wildcards and WebSocket #438.
  • Frame format unchanged.

Not changed

  • No public API is removed: the snapshot types were crate-private.
  • aimdb-weather-mesh's hub uses rings, but only with link_from, not WebSocket link_to, so it is unaffected.

Merging alongside #437

#437 touches record_query in server/dispatch.rs and the aimdb-core CHANGELOG. A trial merge of both branches applied cleanly.

Tested locally

  • cargo test -p aimdb-websocket-connector --all-features (e2e run 3×)
  • cargo test -p aimdb-core with the Makefile's four feature sets (alloc, std,tracing, std,tracing,observability, alloc,observability)
  • cargo clippy -p aimdb-core -p aimdb-websocket-connector -p aimdb-mqtt-connector --all-targets --all-features -- -D warnings
  • cargo clippy -p embedded-check-fixture --target thumbv7em-none-eabihf -- -D warnings
  • cargo doc -p aimdb-core -p aimdb-websocket-connector --no-deps --all-features
  • cargo fmt --check

The full suite runs in CI.

🤖 Generated with Claude Code

lxsaah and others added 2 commits November 7, 2026 20:28
WebSocket kept a per-(record, topic) cache of the last bytes it sent and
served late-join snapshots from it. Snapshots now come from the record's
current value, rendered through the link's topic writer and serializer
when a client subscribes, as AimX does.

- aimdb-core: new `OutboundLatest`, each outbound link's current value
  rendered on demand, with the same route ids as `OutboundRoutes`.
  Each `ConnectorLink` carries the renderer next to its route factory.
- aimdb-websocket-connector: the snapshot cache, `SnapshotProvider`,
  `NoSnapshot` and `server/session.rs` are gone; `run_broadcast` only
  fans out.

Behaviour changes: `SpmcRing` records send no snapshot, and a record
with a topic writer sends one only on its current topic.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`OutboundLatest::render_if(id, accept)` runs the link's topic writer,
checks the topic with `accept`, and serializes only on a match. A link
with a fixed topic is checked before its value is read. Replaces the
separate `fixed_topic` pre-filter, so a WebSocket subscribe never
serializes links its pattern does not match.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@lxsaah
lxsaah merged commit 492cb94 into main Nov 7, 2026
10 checks passed
@lxsaah
lxsaah deleted the feat/ws-snapshots-from-latest branch November 7, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WebSocket late-join snapshots come from the record's latest value, not a per-topic cache

1 participant