Subject: [Jmap] Minimal updates

From: Neil Jenkins <[email-masked]>
Date: Wed, 11 Oct 2017 14:43:55 +1100

Updates in JMAP are already patches, so you can omit any top-level
properties that you don't want to change. However, at the moment you
have to set the whole property if you make a change to any part of it. I
think we should allow a more fine-grained patching approach, to minimise
data transfer and to allow you to do things like add a keyword without
caring what other keywords are currently on the object.
My proposed change is in a pull request on GitHub, but I'll also
summarise it below for discussion:
In setFoos:

---

From: Neil Jenkins <[email-masked]>
Date: Wed, 11 Oct 2017 14:53:16 +1100

*Ah, undo send, *sigh*. Anyway, let's try that again:*

Updates in JMAP are already patches, so you can omit any top-level
properties that you don't want to change. However, at the moment you
have to set the whole property if you make a change to any part of it.
I think we should allow a more fine-grained patching approach, to
minimise data transfer and to allow you to do things like add/remove a
keyword without caring what other keywords are currently on the object
(at the moment you have to specify the whole set if you want to change
them in any way).
My proposed change is in a pull request on GitHub[1], but I'll also
summarise it below for discussion:
In *setFoos, *this is the new description for the *update* argument
(type: String[PatchObject]|null):
A map of id to a Patch object to apply to the current Foo object with
that id, or null if no objects are to be updated.A *PatchObject* is of type String[*], and represents an unordered set of
patches.  The keys are a path in [@!RFC6901] JSON pointer format, with
an implicit leading “/” (i.e. prefix each key with “/” before applying
the JSON pointer evaluation algorithm).All paths MUST also conform to the following restrictions; if there is
any violation, the update MUST be rejected with an invalidPatch error:

 * The pointer MUST NOT reference inside an array (i.e. you MUST NOT
   insert/delete from an array; the array MUST be replaced in its
   entirety instead).
 * All parts prior to the last (i.e. the value after the final slash)
   MUST already exist on the object being patched.
 * There MUST NOT be two patches in the PatchObject where the pointer of
   one is the prefix of the pointer of the other, e.g. “alerts/1/offset”
   and “alerts”.The value associated with each pointer determines how to apply
that patch:

 * If null, set to the default value if specified for this property,
   otherwise remove the property from the patched object. If the key is
   not present in the parent, this a no-op.
 * Anything else: The value to set for this property (this may be a
   replacement or addition to the object being patched).Any server-set properties MAY be included in the patch if their value is
identical to the current server value (before applying the patches to
the object). Otherwise, the update MUST be rejected with an
*invalidProperties* SetError.This patch definition is designed such that an entire Foo object is also
a valid PatchObject. The client MAY choose to optimise network usage by
just sending the diff, or MAY just send the whole object; the server
processes it the same either way.Example

Suppose we have a type *Todo* with the following properties:

 * *id*: String (immutable; server-set) The id of the object.

 * *title*: String The “From” *name* the client SHOULD use when creating
   a new message from this identity.

 * *keywords*: String[Boolean] (mutable; default: {}) A set of keywords
   that apply to the message. The set is represented as an object, with
   the keys being the *keywords*. The value for each key in the object
   MUST be true.

 * *neuralNetworkTimeEstimation*: Number (server-set) The title and
   keywords are fed into the server’s state-of-the-art neural network to
   get an estimation of how long this todo will take, in seconds.Now we fetched a Todo of id “a” (let’s presume we already knew a Todo
with this id existed):
[["getTodos", { "ids": ["a"] }, "0"]]
and got back:

[["todos", { "accountId": "x", "state": "10324", "list": [{ "id": "a",
"title": "Practice Piano", "keywords": { "beethoven": true, "mozart":
true, "liszt": true, "rachmaninov": true },
"neuralNetworkTimeEstimation": 3600 }] }, "0"]]
Now the user adds a keyword “chopin” and removes the keyword “mozart”.
The client may send the whole object to the server, as this is a valid
PatchObject:
[["setTodos", { "ifInState": "10324", "update": { "a": { "id": "a",
"title": "Practice Piano", "keywords": { "beethoven": true, "chopin":
true, "liszt": true, "rachmaninov": true, }
"neuralNetworkTimeEstimation": 360 } } }, "0"]]
or it may send a minimal patch:

[["setTodos", { "ifInState": "10324", "update": { "a": {
"keywords/chopin": true, "keywords/mozart": null } } }, "0"]]
The effect is exactly the same on the server in either case, and
presuming the server is still in state “10324” it will probably
return success:

[["todosSet", { "accountId": "x", "oldState": "10324", "newState":
"10329", "updated": { "a": { "neuralNetworkTimeEstimation": 5400 }
} }, "0"]]The server changed the “neuralNetworkTimeEstimation” property on the
object as part of this change; as this changed in a way *not* explicitly
requested by the PatchObject sent to the server, it is returned with the
“updated” confirmation.-------

Any comments, queries?

Neil.

Links:

  1. https://github.com/jmapio/jmap/pull/148/commits/da93735ecca6c63e1c366180c442656a340e1d9e

---

From: Neil Jenkins <[email-masked]>
Date: Mon, 16 Oct 2017 16:08:57 +1100

With IETF Singapore coming up, I would like to merge this pull request
by next week so they are included when I upload a new draft spec to the
IETF data tracker. Any comments on:
 * patch syntax for updates
 * using the result of one method as a subsequent method argument… please send them through this week!

Cheers,
Neil.