Skip to content

Concurrency and guarantees

Colla guarantees pairwise convergence for applicable concurrent Changes when every participant uses the same transform order.

TP1: pairwise convergence

Let left and right be Changes built from one base v. Transform returns adjusted Changes and requires:

text
(leftPrime, rightPrime) = transformPair(left, right, tieBreak)
apply(apply(v, left), rightPrime)
  ==
apply(apply(v, right), leftPrime)

This is TP1. It says that applying the two operations in either order reaches the same Value. It does not mean that the operations are valid for every Value, that a server accepted either operation, or that a three-way history is path-independent.

Deterministic tie-breaks

Content alone cannot order every conflict. Two inserts at one Text position, two replacements, or two inserts for one Map key need a shared convention:

rust
use colla::{transform_pair, TieBreak};

let (left_prime, right_prime) =
    transform_pair(&left, &right, TieBreak::LeftFirst)?;
# Ok::<(), Box<dyn std::error::Error>>(())

LeftFirst gives the left argument precedence; RightFirst gives the right argument precedence. The JavaScript Document rebase path always uses { order: 'left-first' }. A controller must preserve the same left/right convention at every hop; replacing it with local timestamps changes the contract.

Conflict behavior

Concurrent ChangesResult
Replace vs child editReplace wins at that node; the child side becomes Noop.
Replace vs ReplaceThe tie-break selects the winning replacement.
Map delete vs modifyDelete wins; the modification is discarded.
Map modify vs modifyChild Changes are transformed recursively.
Map insert vs insert for one keyThe tie-break selects one inserted value.
List delete vs element modifyDelete wins for the removed element.
List/Text inserts at one positionBoth survive in the tie-break order.
Overlapping sequence deletesThe shared base range is deleted once.
RichText patches on different keysPatches merge and both keys survive.
RichText patches on one keyThe tie-break selects the resulting value.
RichText delete vs formatDelete wins for deleted content.
Integer add vs integer addBoth deltas remain checked additions; overflow fails.

Changes with incompatible root kinds (for example, Text versus List) cannot describe concurrent edits to one valid base and are rejected by transform.

Rebase order matters

When several local Changes are pending, transform each one in queue order over the current remote remainder. Keep the same confirmed revision and tie-break through the complete transition, then commit state atomically. See Transform and rebasing for the state equations.

TP2 is outside the guarantee

TP2 asks whether a third concurrent operation reaches an equivalent transformed form regardless of which earlier transformation path ran first. Colla does not claim TP2. Core Changes intentionally omit stable operation identity, context vectors, versions, and timestamps, so those facts cannot be reconstructed from the Value/Change bytes.

Applications that need peer-to-peer history or a path-independent three-way controller must add identity and context in their own protocol and choose an algorithm whose assumptions they can enforce. Do not infer global convergence from one successful TP1 example or from equal-looking encoded Changes.

Controller checklist

  • Establish one canonical base and server revision before transforming.
  • Use one documented tie-break and left/right convention everywhere.
  • Reject revision gaps, malformed Changes, and incompatible roots.
  • Transform pending locals in FIFO order and commit atomically.
  • Keep request identity, retries, deduplication, and history outside Core.
  • Test TP1 properties for each Value kind and boundary error.

Next: Protocol boundaries and production testing.

Colla v0.3.0 · Released under the MIT License.