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 ¶
- func Between(a, b string) string
- func Escape(seg string) string
- func Rank(i, n int) string
- func Split(path string) []string
- type Clock
- type Conflict
- type HLC
- type Keyer
- type Op
- type State
- func (s *State) Apply(op Op) (applied bool, conflict *Conflict)
- func (s *State) ClockAt(path string) (Clock, bool)
- func (s *State) Clone() *State
- func (s *State) Document() map[string]any
- func (s *State) Merge(o *State)
- func (s *State) Ops() []Op
- func (s *State) TextEdit(path, want string, h *HLC) []Op
- type Text
- type TextOp
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Escape ¶
Escape escapes one segment per RFC 6901 (and the braces this package reserves for keys).
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.
type Conflict ¶
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.
type Keyer ¶
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.
type State ¶
type State struct {
// contains filtered or unexported fields
}
State is the replicated map. The zero value is empty and usable.
func (*State) Apply ¶
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) Document ¶
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 ¶
Merge folds another state in: per leaf, the higher clock wins. Commutative, associative and idempotent.
func (*State) Ops ¶
Ops returns every live leaf and tombstone as ops, sorted by path, so a state can be shipped or replayed.
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 TextFrom ¶
TextFrom seeds a Text with s, every character stamped from clock onwards (logical counter advancing per character) by the given actor.
func (*Text) Apply ¶
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) Edit ¶
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 ¶
Merge folds another Text in by replaying its characters as inserts and its tombstones as deletes. Commutative, associative, idempotent.
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.