Skip to content

feat: opt-in per-sender message ordering (sid/sn/gt) - #122

Merged
M0LTE merged 1 commit into
masterfrom
feat/opt-in-message-ordering
May 5, 2026
Merged

feat: opt-in per-sender message ordering (sid/sn/gt)#122
M0LTE merged 1 commit into
masterfrom
feat/opt-in-message-ordering

Conversation

@M0LTE

@M0LTE M0LTE commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds opt-in per-sender ordered delivery: apps set streamId on submit, the daemon mints monotonic sn= values, and the receiving daemon delivers in sn order via a per-(originator, stream) reorder buffer.
  • Two policies via gt=: 0 (strict, stall forever) and >0 (timeout, skip the gap after N seconds via StreamGapSweeperService).
  • Wire-compatible end-to-end: three short keys (sid/sn/gt) on the ihave envelope plus a flag bit on the binary datagram codec (version 6 -> 7). Intermediate hops re-emit verbatim.
  • Two new SQLite tables (streamsendstate, streamrecvstate) and four new columns on messages (sid/sn/gt + PendingInOrder).
  • New /Streams dashboard page lists both directions with pending-row counts so stalled streams are visible at a glance.

Test plan

  • dotnet build clean across dapps.client, dapps.core, dapps.core.tests, harness, uitests, FakeBpqAndDappsClient
  • Full dotnet test: 565 / 565 passing (551 prior + 14 new in StreamOrderingTests)
  • Codec round-trip preserves sid=c1, sn=42, gt=600
  • Codec round-trip distinguishes gt=0 (strict marker) from absent
  • Codec rejects partial stream trio
  • IHaveValidator parses + emits all three keys; rejects partial sets and non-numeric sn
  • Inbox: out-of-order sn=3, sn=2, sn=1 drains as sn=1, 2, 3 to MQTT
  • Strict (gt=0) parked rows never set GapDeadline; sweeper finds nothing stale
  • Timeout (gt=600) parked rows: sweeper skips gap after deadline, advances cursor, drains pending
  • Stale sn after cursor drops with reason stream-stale; no double-MQTT-delivery
  • Sender counter persists across submissions; independent counters per stream id
  • No counter mutation when no streamId is supplied

🤖 Generated with Claude Code

Apps that care about ordered delivery (chat, telemetry, change-log
streams) can now opt in by setting a stream id on submit. The daemon
mints a monotonic per-(local, remote, stream) seq, the receiving daemon
gates local MQTT delivery on a per-(originator, stream) cursor, and a
configurable gap timeout chooses between strict (stall forever) and
timeout (skip past the gap after N seconds) semantics.

Wire form: three short keys on the ihave envelope (sid=, sn=, gt=) and
a new flag bit on the binary datagram codec; gt=0 is the strict marker.
All three travel together end-to-end, intermediate hops re-emit
verbatim. Codec version bumped 6 -> 7.

State: two new SQLite tables (streamsendstate, streamrecvstate); four
new columns on messages; a StreamGapSweeperService advances cursors
past elapsed deadlines on a 1-minute cadence. Strict-mode parked rows
sit until the missing prior arrives or the regular TTL sweeper drops
them.

Dashboard: new /Streams page surfaces both sender-side counters and
receive-side cursors with parked-row counts so a stalled stream shows
up at a glance.

Tests: 14 new in StreamOrderingTests covering codec round-trip,
ihave parser, in-order drain on out-of-order arrival, strict-mode
indefinite stall, timeout-mode sweeper-driven skip, stale-seq drop
with stream-stale reason, and sender-counter persistence.

Docs: extended app-developers/reference.md with the new wire fields
and a Message ordering section (REST + MQTT examples, tradeoffs,
sender-reset mitigation via stream-id rotation).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@M0LTE
M0LTE merged commit acc3236 into master May 5, 2026
6 checks passed
@M0LTE
M0LTE deleted the feat/opt-in-message-ordering branch May 5, 2026 18:54
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.

1 participant