Files
delivery-app-shared/docs/sync-contract.md
T

70 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sync protocol v5
All push/pull requests require the app session cookie and `x-model-user` equal to that session's user ID. POST requests additionally require `x-model-request: 1` and an allowed Origin when present. Missing/expired/mismatched authentication returns 401, pauses synchronization and preserves the entire outbox. Provider tokens never travel in sync requests. Each user has a separate IndexedDB database. See [authentication](authentication.md).
## Local write
1. Generate `entityId` and `operationId` as UUIDs. Load/create the durable device UUID.
2. Validate using the shared schema.
3. In one IndexedDB transaction, save the local record (`version: 0`, `syncState: pending`, deliveryNumber: null) and its outbox entry.
4. Only after commit, report success and attempt background synchronization.
Outbox queue order follows an IndexedDB auto-increment key so same-millisecond writes remain ordered. Create a customer before its address, and the address before a delivery. The UI permits only assigned addresses. Pending and error records survive a reload and are not overwritten by the delta feed.
## Push
`POST /sync/push`, JSON body:
```json
{
"operationId": "e209ee4f-22e8-4f82-a7b6-1ac092d9ec47",
"deviceId": "a8380f2a-478c-4c68-93ac-b2f2e7d22080",
"entityId": "1d9d9740-58bd-4f88-a102-d6d1231e04c5",
"baseVersion": 0,
"kind": "delivery.create",
"data": {
"customerId": "20000000-0000-4000-8000-000000000001",
"addressId": "20000000-0000-4000-8000-000000000002",
"deliveryTypeId": "10000000-0000-4000-8000-000000000003",
"deliveryTypeVersion": 1,
"shipmentUnitHundredths": 150,
"unitPriceAgorot": 1001,
"notes": ""
}
}
```
Other creates: `customer.create` takes `{name,contactName,phone,comments,deliveryWindows}`. `deliveryWindows` contains nullable `morning`, `afternoon` and `lateNight` entries, each `{start,end}` in 24-hour `HH:mm` form with end after start. `address.create` takes `{customerId,label,address,city,street,buildingNumber}`; `address` is the formatted compatibility/display value. Unrecognized fields and nonzero create base versions are rejected. A successful response is `{change: {entityType, record}, replayed: boolean}`. The complete record has its server version, timestamp and (for delivery) number and price total. Returning a receipt does not imply earlier changes have been pulled.
`customer.update` replaces those editable profile fields and requires the current positive `baseVersion`. `customer.remove` takes `{}`, writes `deletedAt`, and preserves the customer's existing addresses and deliveries. Both increment the customer version and set `updatedAt`. Removed customers and their addresses are excluded from new delivery validation. Customer conflicts return the current complete customer record and use the same explicit server/local review flow as deliveries.
`delivery.create` includes nullable `scheduledDate` as an ISO calendar date (`YYYY-MM-DD`); current clients always set it. It also selects `deliverySlot` as `morning`, `afternoon`, `lateNight` or `custom`. Named slots carry a nullable `{start,end}` `deliveryWindow` snapshot copied from the selected customer's current settings and have `specificTime: null`; `custom` requires an exact 24-hour `specificTime` and has `deliveryWindow: null`. `delivery.update` can replace the customer, address, date, timing and notes, while an older update that omits all timing fields preserves the current timing. `delivery.cancel` takes `{reason}`. Both require the current positive `baseVersion`. They preserve the delivery number, type/version, shipment units, unit price, total and creation timestamp. Each successful operation increments the version and sets `updatedAt`; cancellation also sets `status: cancelled`, `cancelledAt` and `cancellationReason`. Cancelled/deleted deliveries reject further changes with `409 DELIVERY_CANCELLED`. Price/type fields in update payloads are rejected rather than silently ignored.
The server hashes the validated, canonical operation object. Replaying an identical operation ID returns its immutable acknowledgment without changing records or numbers. Compatibility checks accept only exact earlier canonical representations for customer/address defaults, a null planned date and operations created before delivery timing; ownership checks remain mandatory. Reusing the ID with different content returns `409 OPERATION_REUSED`; replay from a different authenticated actor returns `409 OPERATION_OWNER_CHANGED`. A different operation creating an existing entity ID or updating a stale version returns `409 VERSION_CONFLICT` with the current server change as `{current: {entityType,record}}`. Neither silently overwrites data.
The local change and its outbox entry commit atomically. Editing requires a synchronized customer or delivery with no other pending operation for that entity. The version captured when the form opens is checked again at save, so a background refresh cannot silently overwrite a newer record. Optimistic edits keep the last acknowledged version until the server accepts them.
On a customer or delivery conflict, preserve both the local draft and server record in IndexedDB. The user explicitly chooses either (a) accept the reviewed server version and remove the queued change, or (b) submit the saved local change using a new operation ID and the reviewed server base version. Choice (b) is prohibited when the server archived/cancelled/deleted the entity. The next push can conflict again. A server version received during review invalidates that review until the new version is considered. Ordinary Retry never bypasses a stored conflict.
`422 ADDRESS_MISMATCH` or `CUSTOMER_MISSING` rejects invalid references. `409 CATALOG_CONFLICT` rejects changed delivery-type versions/multipliers. These remain explicit local errors. Manual retry preserves the exact operation and snapshots; it does not fabricate a new operation ID or silently change prices. Catalog-conflict and invalid-reference draft editing remain deferred.
For transport failures, timeouts, 429 or 5xx, leave the operation pending. While the browser reports online, retry with a 1-second exponential backoff capped at 30 seconds, in addition to the normal sync triggers. An online event may arrive before the server is reachable. If the server committed but its response was lost, retry the same operation. Only delete an outbox entry in the local transaction that also applies a validated acknowledgment. The API retains receipts indefinitely in this slice.
## Pull
`GET /sync/pull?cursor=0&limit=100` returns:
```text
{
changes: [{ sequence: "1", change: { entityType, record } }, ...],
nextCursor: "100",
hasMore: true
}
```
Sequences/cursors are decimal strings, preserving BIGINT precision. Limits are 1–200. The page includes only committed changes in sequence order. Repeat while `hasMore`; atomically apply a page and save its `nextCursor` in IndexedDB. Never derive or advance the pull cursor from a push receipt, or remote records may be skipped. Never replace a newer local acknowledged version with older historical feed entries/receipts. Pending local edits survive pulls; later server versions update their stored conflict details for review.
Records contain `deletedAt: null` while active. Customer archive appends a versioned tombstone through the same commit-ordered write mechanism. Clients retain tombstones locally and hide archived customers from operational selectors while allowing an archive/history view. Delivery numbers on deleted/cancelled records remain reserved.
A cursor ahead of server history returns `409 CURSOR_AHEAD`, preserving local data and requiring reconciliation. This protocol does not yet support server history replacement, epoch changes or log retention/compaction; add a bootstrap protocol before those operations.