Document the four features that landed on main in b44bc8c..85c790e: - AGENTS.md: post-MVP section now covers server-driven auth discovery (GET /api/config, AUTH_PASSWORD_ENABLED 403 enforcement, OIDC_GENERIC_DISPLAY_NAME), the settings screen with GET/PUT /api/me, and the critical Android sync/data-safety fixes (pull-all-lists, client HLC tick per server op, no destructive migration, ProGuard rules). Repo structure updated (httpapi config.go/me.go, ui/settings), roadmap entries added, open-points list extended with test backlog for the new endpoints. - API.md: new sections for GET /api/config (public) and GET/PUT /api/me; 403 responses documented for register/login when password auth is off. - SYNC.md: client pull loop (every tracked list, op_log pruning) and the client HLC discipline (tick per incoming server op) that keeps LWW correct across devices with skewed clocks. - README: highlights for auth discovery/OIDC-only mode and the settings screen.
4.1 KiB
Mitbringsl – Synchronization Protocol & Conflict Resolution
1. Synchronization Model
Mitbringsl uses an Append-Only Operation Log (op_log) with Hybrid Logical Clock (HLC) timestamps and Last-Write-Wins (LWW) projections.
2. Hybrid Logical Clock (HLC)
Every operation carries a 64-bit integer timestamp hlc_ts:
\text{hlc\_ts} = (\text{wall\_ms} \ll 16) \mid \text{counter}
wall_ms(48 bits): Unix epoch milliseconds.counter(16 bits): Logical counter disambiguating events produced within the same millisecond.
Guarantees
- Strict Monotonicity:
t_{n+1} > t_nfor all events generated on the same device. - Causal Ordering: If event
Bwas generated after observing eventA, thenHLC(B) > HLC(A).
3. Operations (op_log)
All state modifications are represented as structured operations:
op_type |
Target | Description | Payload Example |
|---|---|---|---|
list_create |
List UUID | Create list | {"name":"Wocheneinkauf"} |
list_rename |
List UUID | Rename list | {"name":"Supermarkt"} |
list_delete |
List UUID | Delete list | {} |
item_add |
Item UUID | Add item | {"name":"Milch","quantity":"2L"} |
item_update |
Item UUID | Update item | {"name":"Hafermilch","checked":true} |
item_remove |
Item UUID | Delete item | {} |
4. Conflict Resolution (LWW & Tombstones)
- Projection Updates:
Database tables (
items,lists) are projections ofop_log. When an op arrives:UPDATE items SET name = $1, hlc_ts = $2, updated_at = now() WHERE id = $3 AND hlc_ts < $2 AND deleted_at IS NULL; - Tombstones (
deleted_at): When an item or list is deleted (item_remove,list_delete), a tombstone is set (deleted_at = now()). Late-arriving offlineitem_updateoperations with smallerhlc_tsvalues will never revive a tombstoned entity.
5. Idempotent Push & Cursor Pull
- Idempotent Push (
POST /api/lists/{id}/ops): Each operation carries a(client_id, client_seq)tuple enforced by aUNIQUEconstraint in Postgres (op_log). Re-sent requests return previous(seq, hlc_ts)assignments without duplicating side effects. - Cursor Pull (
GET /api/lists/{id}/ops?since={seq}): Clients trackmax(server_seq)locally. Incremental sync fetches ops whereseq > cursor, sorted by monotonic server sequenceseq ASC. - Client pull loop:
The
SyncWorkerpulls for every tracked list on each run (not only lists with pending outbox ops), so remote edits on "quiet" (e.g. shared/joined) lists arrive reliably. Old syncedop_logrows are pruned locally to bound growth.
Client HLC discipline
On every incoming server op the client advances its local HLC with
tick(op.hlc_ts). This is essential for LWW correctness across devices with
skewed clocks: without it, a fast-clock device would permanently win conflicts
while a slow-clock device's own edits would be silently rejected by the
hlc_ts < projection guard. The same rule applies on the server (opstore
ticks the server HLC per incoming op).
6. Shared Lists
Since shared lists were introduced, ops flow between all members of a list, not just its owner:
- Membership: A list has an owner (
lists.owner_id, roleowner) and any number of members (list_members, rolemember). Members join via an 8-character invite code (POST /api/lists/join); owner and members can read the code (POST /api/lists/{id}/invite) to share it. - Sync scope: Push and Pull verify on every request that the caller is owner
or member of the list — non-members receive
404(not403, so membership cannot be probed). Once joined, a member pulls the full op history (?since=0) and applies the same LWW projection locally. - Conflict semantics are unchanged: With multiple writers the LWW register
(hlc_ts, client_id)resolves concurrent edits; the HLC guarantees that ops from different clients that observed each other are still causally ordered. Concurrent edits to the same item converge to the highesthlc_tson every device — last write wins, no data merge.