JSON Patch RFC issues
datadata uses JSON Patch (RFC 6902) as its structured change format. The RFC was designed for HTTP PATCH — one writer, one round-trip — and applying it in a concurrent, multi-writer engine surfaces real problems. This page collects the ones we’ve hit.
Array operations address positions, not items
Section titled “Array operations address positions, not items”The issue. RFC 6902 paths address array elements by index (/items/3).
An index is only meaningful against the exact array the patch was computed
from: any concurrent insert or remove earlier in the array silently retargets
every following operation. The patch still applies — to the wrong elements.
The - append token has the mirror problem: two concurrent appends both
“succeed” with no way to express ordering intent between them.
A sibling problem hits objects too: add and replace of a container
replace the whole container — JSON Patch does not deep-merge. Two clients
concurrently mutating entries of the same nested object produce patches that
target the same container path, and the later write clobbers the earlier one
even though the edits were logically independent.
datadata’s workaround. Collections that matter are neither arrays nor nested trees. They’re flat records keyed by stable ids with fractional-index ordering — so “insert between A and B” and “update item X” are single-key operations at disjoint leaf paths, which concurrent edits can’t retarget or clobber. This works well, but it’s a modeling convention the RFC forces on us, not something the patch format helps with — and it’s load-bearing enough that every mutable collection the engine itself maintains (the index, schema migration logs, session changesets) follows it.
add has no precondition
Section titled “add has no precondition”The issue. The auto-generated test guards cover replace and remove
— operations that have a prior value to assert. An add of a
previously-absent member has nothing to test: the RFC offers no way to say
“this key must not exist yet.” So two clients concurrently adding the same
new key both pass a patch guard, and the later write silently overwrites the
earlier one. Guarded writes are a complete same-path conflict detector for
changes and deletions, but not for additions — and every layer built on
guards (including
session conflict detection)
inherits that hole.
datadata’s position. Open. Candidate fixes: extend the patch profile with an explicit absence test, hash-based value preconditions, or telling strict callers to use the coarse sequence guard (which does catch it). For id-keyed records with generated ids the collision is improbable by construction, which is why this is a sharp edge rather than a daily wound.
test is the only concurrency tool, and it’s blunt
Section titled “test is the only concurrency tool, and it’s blunt”The issue. The RFC’s only precondition mechanism is the test op:
assert a value, fail the whole patch otherwise. There is no way to express
intent (“increment”, “insert after X”, “set if unset”), so any concurrent
touch of a tested value rejects the entire patch — even when the writes were
trivially compatible.
datadata’s workaround. Guard modes:
auto-generated test ops scoped to the values actually changed (tolerating
disjoint edits), or whole-document sequence guards when strictness is wanted.
Rejection is treated as a benign, expected signal with a retry path — and the
session client turns it
into reviewable three-way conflicts. But the underlying expressiveness gap is
the RFC’s.
Diff-generated patches lose intent
Section titled “Diff-generated patches lose intent”The issue. Patches produced by diffing two states (the common way to get
them) are not unique — a move is indistinguishable from a remove+add, a
small edit inside a string is a whole-value replace. Whatever the differ
guesses becomes the recorded “change”, which degrades both conflict detection
(coarser test ops than the actual edit warranted) and the
event log’s value as history.
datadata’s position. Live where it’s tolerable (structured fields are small), escape where it isn’t (Yjs for text, where intent-per-keystroke is exactly what the CRDT captures).
Further issues — including ones around move/copy semantics and patch
composability — are queued to be written up. If you’ve fought this RFC in
your own engine, compare notes with us.