Skip to content

Merge concurrent writes instead of refusing them with 412 #92

Description

@tophcodes

Two people edit the same resource from clients that were offline. Today the second write arrives against a stale ETag, the pod answers 412, and the client either discards its work or overwrites the other person's. There is no third option, and for a pod meant to hold shared data that is the wrong number of options.

The local-first answer exists but the shipped ones give up the server to get it. That trade is refused: the server is where WAC lives, where a consumer reads without holding a replica, and what makes a browser client with no local state a working client. What is wanted is convergence and a pod.

Design doc: docs/superpowers/specs/2026-08-08-crdt-merge-design.md, currently on an
unmerged line (f898f4a), not on main. ADR-11 already depends on it: "the per-container
opt-in that convergence already needs" is this.

What is settled

  • The server merges. A client holding no replica and speaking only PUT stays a valid client. Braid's simpleton merge type is the same shape: the server rebases every client's edits.
  • GET is unchanged. User triples only: no tombstones, no reified causality. Mandatory CRDT metadata in the representation breaks every generic Solid client, the same argument that makes shape validation opt-in.
  • Opt-in per container, bound the way a shape is bound. Everywhere else keeps today's semantics down to the 412.
  • Versioning borrowed, delivery is Solid's. Version/Parents from Braid-HTTP; live delivery is a Solid notification channel, because two vocabularies for one event is how a protocol rots. Braid's subscription half is rejected for that reason.
  • Three surfaces, three mechanisms. Writing on the resource URL; catching up at /.aux/{subject}.crdt (full causal history); being told through a notification channel. A Solid notification carries a validator, not a payload, so the catch-up URL is load-bearing rather than a convenience.
  • Provenance is compacted and separate from history, and the projection must run before any compaction of the log: trim the log first and the provenance it would have produced is unrecoverable.
  • Native Rust behind a trait, cut so it could become a WASM ABI without redesign. ADR-10 rejected WASM for extraction because that workload was heavy and needed the network; the objection here is different and does not generalize from it: merging is microseconds and happens inside the write transaction.
  • A stale PUT is a lossy write and the doc says so. Diffing full state against the projection turns a concurrent addition into an intentional deletion. Convergence survives, the other person's work does not. If-Match keeps its meaning so the safe behaviour stays available; PATCH is the honest recommendation.

Correction to the design doc

The doc claims the pod's provenance partition being read-only is "a new rule". It is not.
.meta already answers 405 to PUT/DELETE, PATCH reaches only the default graph
(#64), and named graphs in a client body on an auxiliary are refused, unwritable by
construction, not by promise. #75 has already settled where provenance lives: one dedicated
named graph per resource inside .meta, in PROV-O terms. Merge provenance is another
producer's claims about a resource, which is what that graph is for. The section should be
rewritten to reuse that decision rather than announce one.

What it reuses rather than re-decides

How we know it converges

The oracle already exists and it is the validator: the ETag hashes sorted quads before de-skolemization, so blank nodes are ground terms at the point of hashing and isomorphism never enters. RDFC-1.0 is the right answer to a different question.

Acceptance test: given a common ancestor and two divergent operation sets, any interleaving
yields the same validator, as a property test over randomly generated and randomly
interleaved sets, because the failures that matter are the interleavings nobody thought of.

Open, and staying open for now

  • Tombstones grow forever; compaction needs a causal stability watermark, which needs a known replica set. Splitting compacted provenance from full history makes provenance cheap to trim; it does not make the store stop growing.
  • Authority does not survive replay by itself. Same unresolved shape as the extraction trust gate, settle once for both.
  • Where the merge type is declared, and in what vocabulary. Depends on Settle the quadpod.toph.so/ns# namespace before anything else mints into it #73.
  • One resource now has two validators and only the version answers "did my write land": a notification's state is the ETag, which structurally cannot. Relates to Send an ETag on successful writes #28.
  • A patch type for RDF has to be registered; Braid's range patches address positions and a graph has none.
  • Clock: (timestamp, actor) as the functional-predicate tiebreak depends on client wall clocks, which lie.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions