sync: recipients keep their own pin, archive and place on shared notes
A note_user_state row per (note, recipient) holds what used to be the owner's columns as far as anyone else could tell. The board filters and orders through the viewer's own state; PATCH and reorder write it for a note shared at any level; the feed's revision for a shared note is the later of the note's and the caller's row, so a recipient's pin reaches their devices and no one else's. Push takes the three with their own `state_at` stamp (protocol 7, `shared_state`), so pinning a copy whose text is behind never makes that text win over the owner's edit. A body is only stamped as an edit when it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+26
-6
@@ -67,6 +67,10 @@ syncs everything else.
|
||||
- v6 (#5175): shared notes — see "Shared notes" below. Opt-in per request
|
||||
(`changes?shares=1`), so it is the `shares` feature and the floor stays: a v5
|
||||
client never asks and gets exactly its own notes.
|
||||
- v7 (#5176): a recipient's own pin, archive and position on a shared note —
|
||||
see "Shared notes". Additive, so it is the `shared_state` feature and the
|
||||
floor stays: a v6 client pushes only text, and reads its own state as if the
|
||||
note were simply unpinned.
|
||||
- **Additive change** (a new field, a new capability) → add a `sync_features`
|
||||
name. Do **not** raise a minimum. Old clients keep working.
|
||||
- **Breaking change only** → raise `MIN_CLIENT_PROTOCOL_VERSION` (or the client's
|
||||
@@ -230,6 +234,13 @@ sequence and page on the same cursor as notes and labels (three streams now; the
|
||||
smallest full boundary wins). Sharing with the person again deletes their
|
||||
revocation, so a device that never saw it is never told to drop a note it has.
|
||||
|
||||
**Pin, archive and position are personal** (`shared_state`, v7). On a note shared
|
||||
with the caller, the feed's `pinned`, `archived` and `position` are the caller's
|
||||
own: unpinned and unarchived until they change them, and the owner's `position`
|
||||
until they move it. They live in a per-person row with a revision of its own, so
|
||||
the note's `sync_revision` in the feed is the later of the note's and that row's —
|
||||
a recipient pinning a note reaches their devices and no one else's.
|
||||
|
||||
## Push — `POST /api/sync/push`
|
||||
|
||||
Body: `{ "changes": [ ... ] }` (max 1000 per batch). Each change:
|
||||
@@ -267,12 +278,21 @@ Body: `{ "changes": [ ... ] }` (max 1000 per batch). Each change:
|
||||
the same as one that never existed. Removals are explicit rather than "the
|
||||
note's current attachment set" on purpose: push runs before pull, so a set
|
||||
would delete an attachment another device added that this one has not seen.
|
||||
- **A note someone else owns** (`shares`): a recipient at `edit` may push an upsert
|
||||
and only its `body` is applied, under the same last-write-wins — every other
|
||||
field is ignored, since pinning, archiving, reminders, labels and deleting stay
|
||||
the owner's. A `view` recipient's change, or any delete, is `rejected` ("only its
|
||||
owner can change that"); a note the caller can't see at all answers the generic
|
||||
"cannot apply".
|
||||
- **A note someone else owns** (`shares`): an upsert may carry two things, each
|
||||
under its own last-write-wins.
|
||||
- Its `body`, from a recipient at `edit`, compared on `edited_at` against the
|
||||
note. From a `view` recipient a body is `rejected` ("only its owner can change
|
||||
that").
|
||||
- Their own `pinned`, `archived` and `position` (`shared_state`), from anyone it
|
||||
is shared with, compared on **`state_at`** against what they last set. A
|
||||
client stamps `state_at` apart from the text's `edited_at` so that pinning a
|
||||
copy whose text is out of date never makes that text look newer than the
|
||||
owner's edit. Without `state_at` the three are ignored.
|
||||
|
||||
Trashing, reminders, labels and deleting stay the owner's: those fields are
|
||||
ignored and a delete is `rejected`. The answer is `applied` if either part
|
||||
applied, else `kept`, with the revision as that caller's feed would show it. A
|
||||
note the caller can't see at all answers the generic "cannot apply".
|
||||
|
||||
### Conflict resolution — last-write-wins + history
|
||||
|
||||
|
||||
Reference in New Issue
Block a user