JsonCrdt

A CRDT-backed JSON document: a recursive, convergent, arbitrary-depth JSON value that merges correctly under concurrent edits from multiple replicas.

The root is always a JSON object keyed by String. Values at each key can be nested objects, arrays, or scalars:

JsonNode = JsonObject(ORMap<String, JsonNode>)
| JsonArray(Rga<JsonNode>)
| JsonLeaf(MVRegister<JsonValue>)

Conflict resolution. Merge is structural and recursive:

  • Key presence — add-wins: a concurrent put of the same key survives a remove.

  • Nested values — recursed via JsonNode.piece: objects merge their maps, arrays merge their op-logs, leaves merge their multi-value registers.

  • Concurrent scalar writes — the JsonNode.Leaf's MVRegister retains all concurrent values; the caller resolves by calling set again once they read the multi-value state.

  • Cross-type conflicts (e.g. one replica replaces an object with a scalar concurrently with the other replica adding a key to the object) — the richer structural type wins: Object > Array > Leaf. This is a data-loss decision, not a data-preservation one. The losing node's entire subtree is silently discarded. The scalar equivalent (JsonNode.Leaf vs JsonNode.Leaf) surfaces both values via MVRegister, but a Leaf-vs-Object cross-type conflict does not. This is a deliberate v1 simplification; a future version may model cross-type conflicts as a multi-valued register at the type level.

Known limitations (v1):

  • Move / subtree-reattachment — not supported.

  • Nested Rga GC — arrays embedded inside a JSON document do not participate in the Rga.compact / us.tractat.kuilt.quilter.Quilter GC path. Tombstones inside array elements accumulate without bound until an explicit compact is triggered by the caller.

  • Conflict-free re-typing — concurrent changes of a key's type are resolved by the precedence rule above, not by surfacing a conflict.

Serialization. Use JsonCrdt.serializer to obtain a KSerializer. The replica id is not included in the wire format — it is a local identity. After deserializing, call withReplica to restore the local replica id before performing mutations.

Caution — mutate after withReplica. The deserialized document defaults to ReplicaId(""), which collides with RgaId.HEAD's sentinel replica and may corrupt Dot uniqueness if used to mint new operations. Always call withReplica before invoking set or remove on a deserialized document.

See also

the node algebra this document is built over.

the scalar type for JsonNode.Leaf registers.

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard

The top-level keys currently present in this document.

Functions

Link copied to clipboard
open override fun causalDots(): Set<Dot>

Unions the Rga.causalDots of every JsonNode.Array reachable from the root, recursing through JsonNode.Object values. This feeds the causal-stability GC barrier in us.tractat.kuilt.quilter.Quilter: without this override, embedded Rga tombstones in nested arrays would never be considered for compaction because the delivered frontier would always be empty.

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean
Link copied to clipboard
operator fun get(key: String): JsonNode?

Returns the JsonNode for key, or null if absent.

Link copied to clipboard
open override fun hashCode(): Int
Link copied to clipboard
open override fun piece(other: JsonCrdt): JsonCrdt

The join: the least-upper-bound of this and other.

Link copied to clipboard

Remove key from this document, returning the updated document. Concurrent adds of the same key on another replica will survive the merge (add-wins).

Link copied to clipboard
fun set(key: String, node: JsonNode): JsonCrdt

Set key to node, returning the updated document.

Link copied to clipboard
open override fun toString(): String
Link copied to clipboard

Returns a copy of this document configured to issue mutations on behalf of replica. Call this after deserialization to restore the local replica id.