sync: shared notes in the feed, revocations, and text pushes from an edit share
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 9s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 54s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 9s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 54s
The change feed answers `?shares=1` with every note the caller can see, each saying how it is held, plus a `revoked` list of notes that left them. Granting a share moves the note past the recipient's cursor; ending one, or the owner deleting the note, leaves a revocation on the same cursor. A recipient at edit may push the note's text, and nothing else. Protocol 6, feature `shares`; opt-in, so the floor stays at 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,9 @@ syncs everything else.
|
||||
- v5 (#5168): attachments became a sync entity — an upload route keyed by the
|
||||
client's id, and `attachment`/`preview` deletes in push. Additive, so it is
|
||||
the `attachment_sync` feature and the floor stays.
|
||||
- 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.
|
||||
- **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
|
||||
@@ -196,6 +199,37 @@ boundaries, so nothing between `cursor` and the next pull is skipped. Loop while
|
||||
|
||||
Note attachment metadata carries `{id, url, mime, size, sha256}`.
|
||||
|
||||
### Shared notes (`shares`, v6)
|
||||
|
||||
With `?shares=1` the feed carries every note the caller can **see**: their own, plus
|
||||
those shared with them directly or through a group. Each note then also says how
|
||||
it is held:
|
||||
|
||||
```json
|
||||
{ "permission": "owner" | "edit" | "view",
|
||||
"shared": true,
|
||||
"shared_by": { "id": "...", "display_name": "..." } }
|
||||
```
|
||||
|
||||
`shared_by` is null on the caller's own notes, and `shared` says whether they have
|
||||
shared it with anyone. A note shared with the caller comes with `labels: []`:
|
||||
labels are personal, and a recipient files nothing under the owner's tags.
|
||||
|
||||
Granting or changing a share moves the note to a new revision (without touching
|
||||
`updated_at`, so last-write-wins is unaffected), which is how it passes a
|
||||
recipient's cursor. Ending a share — or the owner purging the note — adds the note
|
||||
to the recipient's `revoked` list:
|
||||
|
||||
```json
|
||||
{ "notes": [ ... ], "labels": [ ... ], "revoked": ["<note id>", ...],
|
||||
"cursor": 51, "has_more": false }
|
||||
```
|
||||
|
||||
A client deletes its copy of each revoked note. Revocations draw from the same
|
||||
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.
|
||||
|
||||
## Push — `POST /api/sync/push`
|
||||
|
||||
Body: `{ "changes": [ ... ] }` (max 1000 per batch). Each change:
|
||||
@@ -233,6 +267,12 @@ 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".
|
||||
|
||||
### Conflict resolution — last-write-wins + history
|
||||
|
||||
|
||||
Reference in New Issue
Block a user