Skip to content

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.

GET /lve/v1/sync/pull?cursor=18734&limit=500 HTTP/1.1
Authorization: Bearer 7|kM2pQ9xV3rT8wZ1yB6nH4jL0sD5fG7aC2eR9tY1u3f9a1c07
Accept-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.
{
"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 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.

  • 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 400 with invalid_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).

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.

cursor = stored cursor, or none
repeat:
page = GET /sync/pull?cursor=<cursor>&limit=500
merge page.changes
cursor = page.cursor
persist cursor
until 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_more is true exactly when the server held back changes because of limit. A page with has_more: false may still contain changes.
  • An empty page with has_more: false means the client is up to date.
  • The reference app pulls with limit=500 and persists the cursor after each page in its preferences. It forgets the cursor when it disconnects or signs in again.
  • 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).
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.