Pull
A client receives everyone's changes with GET /lve/v1/sync/pull. The server hands out its change log in the order it applied the changes, and a cursor that marks how far the client has read.
Request
Section titled “Request”GET /lve/v1/sync/pull?cursor=18734&limit=500 HTTP/1.1Authorization: Bearer 7|kM2pQ9xV3rT8wZ1yB6nH4jL0sD5fG7aC2eR9tY1u3f9a1c07Accept-Language: en| Parameter | Rules |
|---|---|
cursor |
Optional, at most 64 characters. Missing or empty starts the initial pull. Otherwise exactly the value from the previous response. |
limit |
Optional integer from 1 to 1000, default 500. Maximum number of changes in the response. |
Response
Section titled “Response”{ "changes": [ { "entity": "article", "id": "8C1F3D2E-5B7A-4A61-9F0E-2B4C6D8E0A13", "op": "upsert", "hlc": "001791792000123-0001-3F2A9C1B", "data": { "sku": "100101", "name": "Wireless access point AP-7", "unit": "Stk" } }, { "entity": "location", "id": "6F3D9A20-1C5B-4E77-8A2D-3B9C0E4F5A61", "op": "delete", "hlc": "001791792060000-0002-9D04E6A2", "data": null } ], "cursor": "18736", "has_more": false}| Field | Meaning |
|---|---|
changes |
Up to limit change envelopes. A delete has data: null. |
cursor |
The position after this page. Always present, also when changes is empty. |
has_more |
true when more changes are waiting. The client pulls again right away with the new cursor. |
The change log
Section titled “The change log”The server keeps every applied change in a log, numbered in the order it applied them. Ignored and rejected changes are not in the log. A record changed five times appears five times; a client that reads the whole log ends up with the last version of every record.
The log includes the changes of the pulling client itself. They come back with the same stamp as the stored version and are ignored by the client's merge. Servers do not filter them, because the same user may have signed in on several devices with different device ids, and because a change may have been rewritten in between.
Cursor semantics
Section titled “Cursor semantics”- The cursor is opaque. Clients MUST NOT parse, compare, increment or build cursors; they store the value they received and send it back unchanged.
- A cursor is valid for the server that issued it and for as long as that server keeps its log. Servers keep the log forever in version 1; there is no compaction.
- Pulling twice with the same cursor returns the same changes (plus any applied since).
- When a server does not recognise a cursor it answers
400withinvalid_cursor. The client then discards its cursor and starts again with an initial pull. Merging the full state again is safe; the merge rules ignore what the client already has.
The reference server uses the log sequence number as cursor ("18736"), and snapshot.<seq>.<record> while an initial pull is paging. Any string of 1 to 18 digits is accepted, including a number beyond the end of the log (the client then receives nothing until the log grows past it).
Initial pull
Section titled “Initial pull”Without cursor the server returns its current state: the current version of every record, tombstones included, one entry per record. This is much smaller than the whole log and is how a new device gets its data.
sequenceDiagram autonumber participant C as Client participant S as Server Note over S: log ends at seq 18734 C->>S: GET /sync/pull (no cursor) Note over S: remember snapshot position 18734 S-->>C: records 1-500, cursor "snapshot.18734.500", has_more true Note over C: merge, store cursor Note over S: device B pushes, log grows to 18736 C->>S: GET /sync/pull?cursor=snapshot.18734.500 S-->>C: records 501-812, cursor "18734", has_more false Note over C: merge, store cursor "18734" C->>S: GET /sync/pull?cursor=18734 S-->>C: changes 18735-18736, cursor "18736", has_more false
The snapshot is read page by page while other devices keep pushing. To lose nothing, the server remembers the end of its log when the first snapshot page is read and, on the last snapshot page, returns a cursor that points there. Every change applied during the snapshot is then delivered by the next incremental pull. A record changed during the snapshot may appear twice: once in a snapshot page (in its old or new version) and again in the incremental pull. Clients handle this like any repeated change.
Snapshot pages of the reference server are ordered by the internal row id of the record table, which is the order in which records were first stored. The order carries no meaning for clients.
Paging
Section titled “Paging”cursor = stored cursor, or nonerepeat: page = GET /sync/pull?cursor=<cursor>&limit=500 merge page.changes cursor = page.cursor persist cursoruntil page.has_more is false- A client MUST merge a page before it persists the page's cursor. If the client stops between the two steps, it pulls the same page again next time, which is harmless. Persisting first would skip changes forever.
has_moreistrueexactly when the server held back changes because oflimit. A page withhas_more: falsemay still contain changes.- An empty page with
has_more: falsemeans the client is up to date. - The reference app pulls with
limit=500and persists the cursor after each page in its preferences. It forgets the cursor when it disconnects or signs in again.
Ordering guarantees
Section titled “Ordering guarantees”- Changes in the log are in the order the server applied them. For any two changes of the same record, the log order equals their stamp order, because a change is applied only when its stamp is greater than the stored one.
- A client that reads the log from a cursor never misses a change applied after that cursor. A server MUST ensure that log positions become visible in order: a reader must not see position 11 before position 10 is committed, or it would move its cursor past 10. The reference server takes one row lock for every write to its log, so positions are assigned and committed in the same order on SQLite, PostgreSQL and SQL Server.
- There is no order between different records beyond the log order. A movement may arrive before the article it books; clients keep it and show it once the article arrives (see Derived state).
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
400 |
invalid_cursor |
The cursor was not issued by this server |
401 |
unauthenticated |
Token missing or revoked |
422 |
validation_failed |
limit outside 1 to 1000 or not an integer, cursor longer than 64 characters |
5xx |
server_error |
Retry later with the same cursor |
The reference app does not reset its cursor by itself on invalid_cursor; it shows the server's message ("Please synchronise again from the start"), and the user reconnects. Other clients SHOULD restart with an initial pull automatically.