merge

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: Apache-2.0 Imports: 4 Imported by: 0

Documentation

Overview

Package merge is the data structure under multiplayer editing: a last-writer-wins map over a manifest's leaf paths, which is a CRDT (Shapiro et al., "A comprehensive study of Convergent and Commutative Replicated Data Types", 2011, section 3.2, LWW-Register and LWW-Map). Any set of operations, applied in any order, materialises the same document. That is the property the two interfaces (and anyone's third) rely on to converge without a coordinator.

Why this and not a general JSON CRDT. A manifest is a form, not a prose document: fields are short, most are scalars or references, and the lists that matter (key results, deliverables, stakeholders) are lists of identified things. Last writer wins per field, with lists merged by key, is what Kubernetes server-side apply does for the same shape of document, and it is what a person expects: the field shows what was last saved, and a conflict is a note, never a lost edit. A character level text CRDT (RGA, Peritext) can back one control later if a long text field ever needs co-typing; nothing here prevents it.

Paths are JSON pointers (RFC 6901) with one extension: an element of a keyed list is addressed by its key, not its index, written {key}. So /spec/keyResults/{kr-2}/target is the target of the key result whose key is kr-2, wherever it sits. Which lists are keyed, and by what, is the schema's business (x-cartograph-list-key); this package only needs the paths it is given to be stable, which keys are and indices are not.

Deprecated: removed in 2.0.0. Automerge replaces these types in the engine and in every interface; docs/adr/0007 says why, including the defects found in this package.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Between

func Between(a, b string) string

Between returns a rank that sorts between a and b (either may be "").

func Escape

func Escape(seg string) string

Escape escapes one segment per RFC 6901 (and the braces this package reserves for keys).

func Rank

func Rank(i, n int) string

Rank is a lexically sortable position for element i of n, with room to insert between neighbours without renumbering them (fractional indexing, the way collaborative editors order lists). Five digits per level is plenty for a form's lists.

func Split

func Split(path string) []string

Split splits a pointer into segments, unescaping RFC 6901's ~0 and ~1.

Types

type Clock

type Clock struct {
	Wall    int64  `json:"wall"`
	Logical uint32 `json:"logical"`
	Actor   string `json:"actor"`
}

Clock is a hybrid logical clock stamp (Kulkarni et al., 2014): wall time in milliseconds, a logical counter for events within one millisecond, and the actor as the final tiebreak. Total order, so two replicas always agree on which write wins.

func (Clock) IsZero

func (c Clock) IsZero() bool

IsZero reports an unset clock.

func (Clock) Less

func (c Clock) Less(o Clock) bool

Less orders clocks: wall, then logical, then actor.

type Conflict

type Conflict struct {
	Path   string
	Winner Op
	Loser  Op
}

Conflict is a write that landed on a leaf another actor had changed since the writer last saw it. The state is the same whichever order the writes arrived in (that is the CRDT); the conflict is a note for the people, so neither value is silently lost. Winner is what the leaf now holds; Loser is the write that did not take.

type HLC

type HLC struct {
	Actor string
	// contains filtered or unexported fields
}

HLC issues clocks for one actor. Now never returns a clock lower than one it has seen (Observe), so a replica that received a newer write stamps its next write after it, whatever its wall clock says.

func (*HLC) Now

func (h *HLC) Now(ms int64) Clock

Now stamps a new event at wall time ms.

func (*HLC) Observe

func (h *HLC) Observe(c Clock)

Observe advances the clock past a stamp received from elsewhere.

type Keyer

type Keyer func(listPath string, elem map[string]any) (key string, ok bool)

Keyer says whether a list at listPath is keyed and, for one element, which key names it. Return ok=false for a list that is not keyed (it is then stored as one leaf, last writer wins on the whole list).

type Op

type Op struct {
	Path   string `json:"path"`
	Value  any    `json:"value,omitempty"`
	Delete bool   `json:"delete,omitempty"`
	Clock  Clock  `json:"clock"`
	Base   Clock  `json:"base"`
	// Text, when set, is a character edit to a text leaf rather than a
	// write of the whole value; Value and Delete are then ignored.
	Text *TextOp `json:"text,omitempty"`
}

Op is one write to one leaf: set Value at Path, or delete it. Base is the clock of the value the writer saw there, so a concurrent overwrite can be noticed (see Conflict); zero when the writer saw nothing.

func Decompose

func Decompose(doc map[string]any, clock Clock, keys Keyer) []Op

Decompose turns a whole document into leaf ops stamped with clock, the way a client that edits a form rather than single fields hands its draft back, and the way a committed version becomes the base state.

type State

type State struct {
	// contains filtered or unexported fields
}

State is the replicated map. The zero value is empty and usable.

func New

func New() *State

New returns an empty State.

func (*State) Apply

func (s *State) Apply(op Op) (applied bool, conflict *Conflict)

Apply merges one op into the state and reports whether it changed the leaf, and a Conflict when another actor's write sits between the op's Base and now.

func (*State) ClockAt

func (s *State) ClockAt(path string) (Clock, bool)

ClockAt returns the clock of a leaf, for a writer to carry as Base.

func (*State) Clone

func (s *State) Clone() *State

Clone copies the state.

func (*State) Document

func (s *State) Document() map[string]any

Document materialises the state into the nested document a schema validates. Keyed lists come back as lists ordered by each element's "@rank" leaf when present, then by key; a scalar set (leaves of the form /list/{value} holding true) comes back as a sorted list of values.

func (*State) Merge

func (s *State) Merge(o *State)

Merge folds another state in: per leaf, the higher clock wins. Commutative, associative and idempotent.

func (*State) Ops

func (s *State) Ops() []Op

Ops returns every live leaf and tombstone as ops, sorted by path, so a state can be shipped or replayed.

func (*State) TextEdit

func (s *State) TextEdit(path, want string, h *HLC) []Op

TextEdit returns the ops that change the text leaf at path to want, the way a client with a plain input turns its new value into character edits. A leaf that is not yet a text is treated as one seeded from its current string.

type Text

type Text struct {
	// contains filtered or unexported fields
}

Text is a replicated sequence of characters: the Replicated Growable Array (Roh, Jeon, Kim, Lee, "Replicated abstract data types: Building blocks for collaborative applications", 2011). Every character has a unique id (the clock that inserted it) and names the character it was typed after; a deleted character stays as a tombstone so a concurrent insert after it still has its anchor. Two people typing in the same sentence at once both keep what they typed, in a deterministic order, on every replica.

A text leaf in a State holds one of these instead of a scalar. The field is still one leaf: a plain Set on it (from a client that does not speak text ops, or an import) replaces the whole text with a new one seeded from the string, under the usual last-writer rule, and text ops from then on edit that one.

func NewText

func NewText() *Text

NewText returns an empty Text.

func TextFrom

func TextFrom(s string, clock Clock) *Text

TextFrom seeds a Text with s, every character stamped from clock onwards (logical counter advancing per character) by the given actor.

func (*Text) Apply

func (t *Text) Apply(op TextOp) bool

Apply merges one op. An insert whose anchor is unknown waits until the anchor arrives; a duplicate is ignored. Returns whether the op took effect now.

func (*Text) Clone

func (t *Text) Clone() *Text

Clone copies the text.

func (*Text) Edit

func (t *Text) Edit(want string, next func() Clock) []TextOp

Edit computes the text ops that turn the current string into want, as one person typing would: the common prefix and suffix are kept, the middle is deleted and retyped. Each new character gets its own clock from next. This is how a client that holds a plain input box (the web, a terminal) turns a keystroke into ops without tracking cursors.

func (*Text) Merge

func (t *Text) Merge(o *Text)

Merge folds another Text in by replaying its characters as inserts and its tombstones as deletes. Commutative, associative, idempotent.

func (*Text) Ops

func (t *Text) Ops() []TextOp

Ops returns the inserts (in document order, so anchors precede what follows them) and then the deletes that rebuild this text elsewhere.

func (*Text) String

func (t *Text) String() string

String materialises the text.

type TextOp

type TextOp struct {
	Insert bool  `json:"insert,omitempty"`
	ID     Clock `json:"id"`
	After  Clock `json:"after,omitempty"`
	R      rune  `json:"r,omitempty"`
}

TextOp is one edit to a text leaf: insert rune R with id ID after the character After (zero Clock means at the start), or delete the character ID.

func (TextOp) Less

func (t TextOp) Less(c Clock) bool

Less orders a text op's own stamp against a leaf clock.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL