Per-peer read watermark for cross-device read state #9
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
posta/server!9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "read-watermark"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds the smallest viable read-state mechanism so iOS, TUI, and any future client can sync "what have I seen?" via the server. Watermark is per-peer + per-identity, stored on
contacts.ALTER TABLE contacts ADD COLUMN last_read_row_id INTEGER NOT NULL DEFAULT 0. Additive, idempotent, same shape as the legacy schema.POST /api/v1/contacts/read {peer, rowId}: advances the watermark.200with{peer, rowId}on advance,204on no-op (request at-or-behind current value — monotonic floor),404if the peer isn't in contacts. Publishes a newread-watermark-changedSSE event on advance only, so sibling devices update without polling.GET /api/v1/contacts: every row now carrieslastReadRowId. Unread = inbound messages in this thread withrowId > lastReadRowId(client-side derivation; the index onmessages.idmakes that trivial).The store-level
MarkPeerReadusesUPDATE … WHERE last_read_row_id < ?so the SQL itself arbitrates concurrent advances — the lower of two racing devices sees 0 rows affected and returnsadvanced=falsewithout an explicit retry loop.Intentionally not included: per-message read state (e.g. mark-an-individual-message-unread). The watermark is monotonic; if explicit unread surfaces later it should be a separate
POST /contacts/unreadso the read path stays accident-proof.Test plan
go vet ./...cleango test ./...cleanMarkPeerRead: advance from 0, no-op on equal, no-op on lower (monotonic floor), advance to higher,ErrPeerNotFoundfor unknown peerPOST /contacts/read: 200 on advance, 204 on equal, 204 on lower, 404 on unknown peer; exactly oneread-watermark-changedSSE event across that sequence (publish fires on advance only)GET /contactssurfaceslastReadRowIdafter advancearne, observe the SSE event arrives at a second connected client🤖 Generated with Claude Code
Migration v5 adds a single last_read_row_id column to contacts (default 0), the natural sync point for "device A read up to here" across iOS, TUI, and future clients. New API surface: - POST /api/v1/contacts/read {peer, rowId}: 200 + body on advance, 204 on no-op (request was at-or-behind the current watermark — the watermark is monotonic, so concurrent advances from two devices race safely; the lower loses), 404 if peer not in contacts. Publishes a new read-watermark-changed SSE event on advance only. - GET /api/v1/contacts now includes lastReadRowId per row. Clients derive unread as inbound messages with rowId > lastReadRowId. The store-level MarkPeerRead uses `UPDATE … SET last_read_row_id = ? WHERE url = ? AND last_read_row_id < ?`, so the UPDATE itself is the arbiter on concurrent advances — the loser sees 0 rows affected and returns advanced=false without a read-the-DB retry loop. CLIENT_API.md updated for the endpoint, the new contact field, and the new SSE event type. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>