4. State and changes¶
Every JMAP data type has a per-account state string that bumps on every mutation. Clients use it for two things:
ifInStateon<Type>/set— optimistic concurrency. If the server's state doesn't match, the call fails withstateMismatchand the client re-fetches before retrying.<Type>/changes— incremental sync. Pass the last state you saw, get back the lists of created/updated/destroyed ids since then.
Look at the state¶
After every successful mutation, the server bumps the counter for that
(account_id, data_type) pair. The state string is just the counter cast to
string.
curl -s -X POST http://localhost:8000/jmap -d '{
"using": ["urn:ietf:params:jmap:core", "urn:example:todos"],
"methodCalls": [["Todo/get", {"accountId": "ACC"}, "c0"]]
}' | jq '.methodResponses[0][1].state'
Returns something like "3". After you Todo/set to create a new one, it
becomes "4".
Sync incrementally¶
A client that last synced at state "3" asks:
The response tells it what changed:
{
"accountId": "ACC",
"oldState": "3",
"newState": "7",
"hasMoreChanges": false,
"created": ["<id>"],
"updated": [],
"destroyed": []
}
If hasMoreChanges is true, the client passes newState back to fetch the
next page.
Optimistic concurrency¶
If the server is at state "3", the update applies. If it's at "4" (someone
else mutated meanwhile), you get back:
Refetch the object, re-apply your change against the new state, retry.
Behind the scenes¶
The framework writes one row to a shared data_changes table for each
create / update / destroy. <Type>/changes queries
data_changes WHERE data_type = '<Type>' to return only your capability's
events. You don't write or maintain this table — it's framework-owned.