Documentation
¶
Overview ¶
Package structured turns the replicated primitives of the crdt package into a shared substrate for co-editing structured documents. One core expresses both a spreadsheet and an isometric diagram — and any other structured document — rather than each growing a merge engine of its own.
One core, many document types ¶
Nothing here re-implements a CRDT. The merge logic lives entirely in the crdt package, and this layer only composes three of its structures:
- crdt.List is an RGA: an ordered sequence whose every element carries a stable identity that survives concurrent insertion and deletion elsewhere. With its order ignored it is a reload-safe source of stable identities and an existence set, which is what a diagram's nodes and connectors are. It is not what an axis of a spreadsheet is, because an RGA has no operation for moving something already in it; see Sequence.
- crdt.Map is a last-writer-wins key-value map. Each key is, on its own, an LWW-register: a single value merged by a (Lamport clock, site) total order with no tie left to chance and no wall clock read. Register names that degenerate case, and RecordMap composes many such registers into records whose fields merge independently.
- crdt.Composite holds those parts under one name, one snapshot and one version, so a whole document is one thing to persist and to authorise.
Two things a map and a list do not express are built here rather than added to the crdt package, because neither needs a new merge rule — only a way of using the ones that exist:
- Counter is a number several replicas add to at once. A register cannot be one, because "add one" is not a value and writing a value cannot say it. Keying the map by site, so that a replica writes only its own total, makes concurrent additions concurrent writes to different keys.
- Tree is a tree whose nodes move. A parent is a single value and merges on its own; what does not is the shape two legal moves make between them, which is a ring. Tree resolves that when the tree is read, by rules that are a function of the state alone.
- RichText is text that carries formatting. Written into the sequence — a bold-on character, a bold-off character — two replicas bolding overlapping stretches produce interleaved markers and read differently; written per character it converges and costs a write per letter. A mark is one operation naming two boundaries instead, and the formatting is worked out when the text is read.
- Undo puts back what this replica did, and only that. It is not a stack of states — restoring one would throw away what everybody else has done since, and travel to them as an instruction to do the same. An undo here is a new edit, made now, that has the effect of the old one not having happened, and it reaches a peer as an ordinary edit.
- Ink is what is drawn by hand. A stroke is a path that arrives a point at a time, so held as one value every point sent rewrites the whole path and the person watching sees the line redrawn rather than extended. The points are a sequence of their own here, appended to, each saying which stroke it belongs to — one stream rather than one per stroke, because a part cannot be taken out of a composite and a whiteboard would otherwise spend more on saying what it has than on what was drawn.
- Blobs holds the files a document refers to but is not made of. A file as one map value is one operation the size of the file: it cannot be sent as it is read, resumed if the connection drops, or recognised as one a peer already has. Cut into chunks stored under the hash of their own bytes, it is as many operations as it has chunks, and the same chunk written by two replicas is the same key and the same value — so there is nothing to merge and nothing is stored twice.
- Proposals are changes to a document that are not part of it yet: a suggested edit, a change put up for review. Held as a second copy of the document and reconciled later, accepting one means applying a difference between two texts, and applying a difference mints new characters — so every comment, mark and cursor anchored to the characters it replaced would be left pointing at nothing. A proposal is a replica that has not synced instead: its operations are the document's own, against the document's own identities, written down where reviewers can read them. Accepting is applying them, which merges with whatever happened meanwhile because that is what a replica coming back from offline does; turning one down costs nothing, because they were never applied.
- Set is a collection of names replicas add to and take from at once — the labels on a card, the people in a conversation, the layers that are showing. A map keyed by the name converges on the case that happens badly: one replica adds a label while another, which has never seen it, takes it away, and one of the two writes wins by an order that has nothing to do with what either knew. Every addition mints a tag of its own instead, and a removal takes away the tags it can see — so an addition nobody had seen is untouched by it, not as a policy but for want of anything to base one on.
- MultiRegister is a value two replicas are allowed to disagree about. A Register settles every concurrent write by the (clock, site) order, which is right when the losing write is of no interest and wrong when it is: two people rename the same thing at once and one of the names is gone, with nothing anywhere recording that there was a second. This keeps a version vector beside each replica's own value, so a value written without seeing another is not superseded by it and both are read. Choosing between them is writing the one chosen — a write dominates everything its writer could see — so there is no operation for settling and none is needed.
- Blocks is a document made of blocks — paragraphs, headings, list items, quotes, code — each holding formatted text and nested to a depth. Written as a rich text per block it converges and does not scale: a part cannot be taken out of a composite and a version carries one entry per part, so a thousand blocks is a thousand entries exchanged on every sync whether the document still holds them or not. The blocks are markers in one text instead, keyed to one map, which is three parts however many blocks there are. The marker is a character rather than a boundary between two, because "the end of this paragraph" and "the start of the next" are the same offset and are not the same place — a character has two sides and a boundary has one.
- Sequence is an ordered collection whose items move. crdt.List is an RGA and has no operation for moving something already in it; written as a delete and an insert, a move is two operations that a concurrent move splits, leaving the item in both places or in neither. An item carries where it sits as a rank instead, so a move is one field write. It is what the rows and columns of a Sheet are, which is what makes a row something a person can drag.
Sheet and Diagram are thin wrappers over the same crdt.Composite. That they share it is the point: the convergence, commutativity, idempotence and associativity the crdt package proves for its parts are inherited by every document type built here, because there is only ever one set of parts doing the merging.
Determinism ¶
Like the crdt package, this one never reads the wall clock and never draws a random number, so a document compiled to js/wasm behaves exactly as it does on a server. Replica identity is the caller's crdt.SiteID; stable element identities are minted by the RGA, which is why they are reload-safe and never reused, even across a deletion.
What is replicated and what is not ¶
The raw content of a cell — a literal or a formula's source text together with the stable identities it references — is replicated. A formula's computed value is not: it is derived locally from content every replica already agrees on, so replicating it would be replicating a function of the state rather than the state. This package is the data layer; it holds no formula engine and no widget.
Index ¶
- Constants
- Variables
- func CellSelectionOf(p awareness.Peer) (RowID, ColID, bool)
- func DecodeBool(data []byte) (bool, bool)
- func DecodeInt(data []byte) (int32, bool)
- func EncodeBool(v bool) []byte
- func EncodeInt(v int32) []byte
- func PublishCellSelection(reg *awareness.Registry, site crdt.SiteID, row RowID, col ColID, ...) awareness.Update
- func PublishNodeSelection(reg *awareness.Registry, site crdt.SiteID, node NodeID, meta map[string]string) awareness.Update
- type Blobs
- func (b *Blobs) Apply(batches ...crdt.PartOps) error
- func (b *Blobs) Composite() *crdt.Composite
- func (b *Blobs) Get(name string) ([]byte, bool)
- func (b *Blobs) Missing(name string) int
- func (b *Blobs) Names() []string
- func (b *Blobs) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (b *Blobs) Pending() int
- func (b *Blobs) Put(name string, data []byte) ([]crdt.PartOps, error)
- func (b *Blobs) PutWith(name string, data []byte, cut Chunker) ([]crdt.PartOps, error)
- func (b *Blobs) Remove(name string) (crdt.PartOps, error)
- func (b *Blobs) Site() crdt.SiteID
- func (b *Blobs) Size(name string) (int, bool)
- func (b *Blobs) Snapshot() []byte
- func (b *Blobs) Stored() int
- func (b *Blobs) Sweep() ([]crdt.PartOps, error)
- func (b *Blobs) Version() crdt.CompositeVersion
- type Block
- type BlockID
- type Blocks
- func (b *Blocks) Apply(batches ...crdt.PartOps) error
- func (b *Blocks) At(id BlockID, offset int) (int, error)
- func (b *Blocks) Block(id BlockID) (Block, bool)
- func (b *Blocks) Children(parent BlockID) []BlockID
- func (b *Blocks) Composite() *crdt.Composite
- func (b *Blocks) DeleteText(id BlockID, offset, count int) (crdt.PartOps, error)
- func (b *Blocks) Field(id BlockID, field string) ([]byte, bool)
- func (b *Blocks) IDs() []BlockID
- func (b *Blocks) Insert(after BlockID, typ string) (BlockID, []crdt.PartOps, error)
- func (b *Blocks) InsertText(id BlockID, offset int, s string) (crdt.PartOps, error)
- func (b *Blocks) Len() int
- func (b *Blocks) List() []Block
- func (b *Blocks) Mark(from BlockID, fromOff int, to BlockID, toOff int, name string, value []byte, ...) (crdt.PartOps, error)
- func (b *Blocks) MarksAt(id BlockID, offset int) map[string][]byte
- func (b *Blocks) Merge(id BlockID) ([]crdt.PartOps, error)
- func (b *Blocks) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (b *Blocks) Outline() []Outlined
- func (b *Blocks) Pending() int
- func (b *Blocks) Plain(sep string) string
- func (b *Blocks) Preamble() string
- func (b *Blocks) Records() *RecordMap
- func (b *Blocks) Remove(id BlockID) ([]crdt.PartOps, error)
- func (b *Blocks) RichText() *RichText
- func (b *Blocks) SetDepth(id BlockID, depth int) (crdt.PartOps, error)
- func (b *Blocks) SetField(id BlockID, field string, value []byte) (crdt.PartOps, error)
- func (b *Blocks) SetType(id BlockID, typ string) (crdt.PartOps, error)
- func (b *Blocks) Site() crdt.SiteID
- func (b *Blocks) Snapshot() []byte
- func (b *Blocks) Spans(id BlockID) []Span
- func (b *Blocks) Split(id BlockID, offset int, typ string) (BlockID, []crdt.PartOps, error)
- func (b *Blocks) Text(id BlockID) (string, bool)
- func (b *Blocks) Unmark(from BlockID, fromOff int, to BlockID, toOff int, name string) (crdt.PartOps, error)
- func (b *Blocks) Version() crdt.CompositeVersion
- type Cell
- type CellKind
- type CellRef
- type Chunker
- type ColID
- type ConnID
- type Counter
- func (c *Counter) Add(delta int64) (crdt.MapOp, error)
- func (c *Counter) Added() int64
- func (c *Counter) Apply(ops ...crdt.MapOp) error
- func (c *Counter) Map() *crdt.Map
- func (c *Counter) OpsSince(vv crdt.VersionVector) []crdt.MapOp
- func (c *Counter) Removed() int64
- func (c *Counter) Site() crdt.SiteID
- func (c *Counter) Snapshot() []byte
- func (c *Counter) Value() int64
- func (c *Counter) Version() crdt.VersionVector
- type Diagram
- func (d *Diagram) AddConn(from, to NodeID) (ConnID, []crdt.PartOps, error)
- func (d *Diagram) AddNode() (NodeID, crdt.PartOps, error)
- func (d *Diagram) Apply(batches ...crdt.PartOps) error
- func (d *Diagram) Collect(stable crdt.CompositeVersion) int
- func (d *Diagram) Composite() *crdt.Composite
- func (d *Diagram) ConnEndpoints(c ConnID) (from, to NodeID, ok bool)
- func (d *Diagram) ConnField(c ConnID, field string) ([]byte, bool)
- func (d *Diagram) Conns() []ConnID
- func (d *Diagram) HasConn(c ConnID) bool
- func (d *Diagram) HasNode(n NodeID) bool
- func (d *Diagram) NodeColour(n NodeID) (string, bool)
- func (d *Diagram) NodeField(n NodeID, field string) ([]byte, bool)
- func (d *Diagram) NodeLabel(n NodeID) (string, bool)
- func (d *Diagram) NodePosition(n NodeID) (x, y int32, ok bool)
- func (d *Diagram) Nodes() []NodeID
- func (d *Diagram) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (d *Diagram) Pending() int
- func (d *Diagram) RemoveConn(c ConnID) (crdt.PartOps, error)
- func (d *Diagram) RemoveNode(n NodeID) (crdt.PartOps, error)
- func (d *Diagram) SetConnEndpoints(c ConnID, from, to NodeID) (crdt.PartOps, error)
- func (d *Diagram) SetConnField(c ConnID, field string, value []byte) (crdt.PartOps, error)
- func (d *Diagram) SetNodeColour(n NodeID, colour string) (crdt.PartOps, error)
- func (d *Diagram) SetNodeField(n NodeID, field string, value []byte) (crdt.PartOps, error)
- func (d *Diagram) SetNodeLabel(n NodeID, label string) (crdt.PartOps, error)
- func (d *Diagram) SetNodePosition(n NodeID, x, y int32) (crdt.PartOps, error)
- func (d *Diagram) Site() crdt.SiteID
- func (d *Diagram) Snapshot() []byte
- func (d *Diagram) Sweep() ([]crdt.PartOps, error)
- func (d *Diagram) Version() crdt.CompositeVersion
- type Document
- func (d *Document) Add(fam Family, id string) (crdt.PartOps, error)
- func (d *Document) Apply(batches ...crdt.PartOps) error
- func (d *Document) DeleteField(fam Family, id, field string) (crdt.PartOps, error)
- func (d *Document) Field(fam Family, id, field string) ([]byte, bool)
- func (d *Document) Fields(fam Family, id string) []string
- func (d *Document) Has(fam Family, id string) bool
- func (d *Document) IDs(fam Family) []string
- func (d *Document) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (d *Document) Pending() int
- func (d *Document) Remove(fam Family, id string) (crdt.PartOps, error)
- func (d *Document) Set(fam Family, id, field string, value []byte) (crdt.PartOps, error)
- func (d *Document) Site() crdt.SiteID
- func (d *Document) Snapshot() []byte
- func (d *Document) Version() crdt.CompositeVersion
- type Draft
- type Expand
- type Family
- type Ink
- func (i *Ink) Apply(batches ...crdt.PartOps) error
- func (i *Ink) Begin() (StrokeID, []crdt.PartOps, error)
- func (i *Ink) Composite() *crdt.Composite
- func (i *Ink) Erase(stroke StrokeID) (crdt.PartOps, error)
- func (i *Ink) Extend(stroke StrokeID, pts ...Point) (crdt.PartOps, error)
- func (i *Ink) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (i *Ink) Paths() [][]Point
- func (i *Ink) Pending() int
- func (i *Ink) Points(stroke StrokeID) []Point
- func (i *Ink) Site() crdt.SiteID
- func (i *Ink) Snapshot() []byte
- func (i *Ink) Strokes() *Sequence
- func (i *Ink) Sweep() (crdt.PartOps, error)
- func (i *Ink) Version() crdt.CompositeVersion
- type ItemID
- type MultiRegister
- func (r *MultiRegister) Apply(ops ...crdt.MapOp) error
- func (r *MultiRegister) Clear() (crdt.MapOp, error)
- func (r *MultiRegister) Conflicted() bool
- func (r *MultiRegister) Map() *crdt.Map
- func (r *MultiRegister) OpsSince(vv crdt.VersionVector) []crdt.MapOp
- func (r *MultiRegister) Readings() []Reading
- func (r *MultiRegister) Set(value []byte) (crdt.MapOp, error)
- func (r *MultiRegister) Site() crdt.SiteID
- func (r *MultiRegister) Snapshot() []byte
- func (r *MultiRegister) Value() ([]byte, bool)
- func (r *MultiRegister) Values() [][]byte
- func (r *MultiRegister) Version() crdt.VersionVector
- type NodeID
- type Outlined
- type Point
- type Proposal
- type ProposalID
- type Proposals
- func (p *Proposals) Accept(id ProposalID) ([]crdt.PartOps, error)
- func (p *Proposals) Apply(batches ...crdt.PartOps) error
- func (p *Proposals) Composite() *crdt.Composite
- func (p *Proposals) Draft(site crdt.SiteID) (*Draft, error)
- func (p *Proposals) Forget(id ProposalID) ([]crdt.PartOps, error)
- func (p *Proposals) Get(id ProposalID) (Proposal, bool)
- func (p *Proposals) List() []Proposal
- func (p *Proposals) Open() []Proposal
- func (p *Proposals) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (p *Proposals) Pending() int
- func (p *Proposals) Preview(id ProposalID, site crdt.SiteID) (*crdt.Composite, error)
- func (p *Proposals) Put(title string, d *Draft) (ProposalID, []crdt.PartOps, error)
- func (p *Proposals) Records() *RecordMap
- func (p *Proposals) Site() crdt.SiteID
- func (p *Proposals) Snapshot() []byte
- func (p *Proposals) Version() crdt.CompositeVersion
- func (p *Proposals) Withdraw(id ProposalID) (crdt.PartOps, error)
- type Reading
- type RecordMap
- func (r *RecordMap) DeleteField(rec, field string) (crdt.MapOp, error)
- func (r *RecordMap) DeleteRecord(rec string) ([]crdt.MapOp, error)
- func (r *RecordMap) Fields(rec string) []string
- func (r *RecordMap) GetField(rec, field string) ([]byte, bool)
- func (r *RecordMap) HasRecord(rec string) bool
- func (r *RecordMap) Map() *crdt.Map
- func (r *RecordMap) Records() []string
- func (r *RecordMap) SetField(rec, field string, value []byte) (crdt.MapOp, error)
- type Register
- func (r *Register) Apply(ops ...crdt.MapOp) error
- func (r *Register) Clear() (crdt.MapOp, error)
- func (r *Register) Get() ([]byte, bool)
- func (r *Register) OpsSince(vv crdt.VersionVector) []crdt.MapOp
- func (r *Register) Set(value []byte) (crdt.MapOp, error)
- func (r *Register) Snapshot() []byte
- func (r *Register) Version() crdt.VersionVector
- type RichText
- func (r *RichText) Apply(batches ...crdt.PartOps) error
- func (r *RichText) Composite() *crdt.Composite
- func (r *RichText) Delete(pos, count int) (crdt.PartOps, error)
- func (r *RichText) Doc() *crdt.Doc
- func (r *RichText) Insert(pos int, s string) (crdt.PartOps, error)
- func (r *RichText) Len() int
- func (r *RichText) Mark(from, to int, name string, value []byte, expand Expand) (crdt.PartOps, error)
- func (r *RichText) MarksAt(pos int) map[string][]byte
- func (r *RichText) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (r *RichText) Pending() int
- func (r *RichText) Site() crdt.SiteID
- func (r *RichText) Snapshot() []byte
- func (r *RichText) Spans() []Span
- func (r *RichText) Text() string
- func (r *RichText) Unmark(from, to int, name string) (crdt.PartOps, error)
- func (r *RichText) Version() crdt.CompositeVersion
- type RowID
- type Sequence
- func (s *Sequence) Apply(ops ...crdt.MapOp) error
- func (s *Sequence) At(pos int) (ItemID, bool)
- func (s *Sequence) GetField(item ItemID, field string) ([]byte, bool)
- func (s *Sequence) IndexOf(item ItemID) int
- func (s *Sequence) Insert(after ItemID, value []byte) (ItemID, []crdt.MapOp, error)
- func (s *Sequence) Items() []ItemID
- func (s *Sequence) Len() int
- func (s *Sequence) Map() *crdt.Map
- func (s *Sequence) Move(item, after ItemID) (crdt.MapOp, error)
- func (s *Sequence) OpsSince(vv crdt.VersionVector) []crdt.MapOp
- func (s *Sequence) Records() *RecordMap
- func (s *Sequence) Remove(item ItemID) ([]crdt.MapOp, error)
- func (s *Sequence) Set(item ItemID, value []byte) (crdt.MapOp, error)
- func (s *Sequence) SetField(item ItemID, field string, value []byte) (crdt.MapOp, error)
- func (s *Sequence) Snapshot() []byte
- func (s *Sequence) Value(item ItemID) ([]byte, bool)
- func (s *Sequence) Values() [][]byte
- func (s *Sequence) Version() crdt.VersionVector
- type Set
- func (s *Set) Add(name string) ([]crdt.MapOp, error)
- func (s *Set) Adders(name string) []crdt.SiteID
- func (s *Set) Apply(ops ...crdt.MapOp) error
- func (s *Set) Contains(name string) bool
- func (s *Set) Len() int
- func (s *Set) Map() *crdt.Map
- func (s *Set) Names() []string
- func (s *Set) OpsSince(vv crdt.VersionVector) []crdt.MapOp
- func (s *Set) Records() *RecordMap
- func (s *Set) Remove(name string) ([]crdt.MapOp, error)
- func (s *Set) Site() crdt.SiteID
- func (s *Set) Snapshot() []byte
- func (s *Set) Tags(name string) int
- func (s *Set) Version() crdt.VersionVector
- type Sheet
- func (s *Sheet) AppendCol() (ColID, crdt.PartOps, error)
- func (s *Sheet) AppendRow() (RowID, crdt.PartOps, error)
- func (s *Sheet) Apply(batches ...crdt.PartOps) error
- func (s *Sheet) ClearCell(row RowID, col ColID) (crdt.PartOps, error)
- func (s *Sheet) ColCount() int
- func (s *Sheet) Cols() []ColID
- func (s *Sheet) DeleteCol(pos int) (crdt.PartOps, error)
- func (s *Sheet) DeleteRow(pos int) (crdt.PartOps, error)
- func (s *Sheet) GetCell(row RowID, col ColID) (Cell, bool)
- func (s *Sheet) InsertCol(pos int) (ColID, crdt.PartOps, error)
- func (s *Sheet) InsertRow(pos int) (RowID, crdt.PartOps, error)
- func (s *Sheet) MoveCol(from, to int) (crdt.PartOps, error)
- func (s *Sheet) MoveRow(from, to int) (crdt.PartOps, error)
- func (s *Sheet) OpsSince(v crdt.CompositeVersion) ([]crdt.PartOps, error)
- func (s *Sheet) Pending() int
- func (s *Sheet) RowCount() int
- func (s *Sheet) Rows() []RowID
- func (s *Sheet) SetCell(row RowID, col ColID, cell Cell) (crdt.PartOps, error)
- func (s *Sheet) Site() crdt.SiteID
- func (s *Sheet) Snapshot() []byte
- func (s *Sheet) Version() crdt.CompositeVersion
- type Span
- type State
- type StrokeID
- type Tree
- func (t *Tree) Apply(ops ...crdt.MapOp) error
- func (t *Tree) Children(parent TreeID) []TreeID
- func (t *Tree) Depth(node TreeID) (int, bool)
- func (t *Tree) GetField(node TreeID, field string) ([]byte, bool)
- func (t *Tree) Insert(parent, after TreeID) (TreeID, []crdt.MapOp, error)
- func (t *Tree) Map() *crdt.Map
- func (t *Tree) Move(node, parent, after TreeID) ([]crdt.MapOp, error)
- func (t *Tree) Nodes() []TreeID
- func (t *Tree) OpsSince(vv crdt.VersionVector) []crdt.MapOp
- func (t *Tree) Parent(node TreeID) (TreeID, bool)
- func (t *Tree) Records() *RecordMap
- func (t *Tree) Remove(node TreeID) ([]crdt.MapOp, error)
- func (t *Tree) RemoveSubtree(node TreeID) ([]crdt.MapOp, error)
- func (t *Tree) SetField(node TreeID, field string, value []byte) (crdt.MapOp, error)
- func (t *Tree) Snapshot() []byte
- func (t *Tree) Version() crdt.VersionVector
- type TreeID
- type Undo
- func (u *Undo) Begin()
- func (u *Undo) CanRedo() bool
- func (u *Undo) CanUndo() bool
- func (u *Undo) Commit()
- func (u *Undo) Delete(pos, count int) ([]crdt.Op, error)
- func (u *Undo) Doc() *crdt.Doc
- func (u *Undo) Insert(pos int, text string) ([]crdt.Op, error)
- func (u *Undo) Redo() ([]crdt.Op, error)
- func (u *Undo) Undo() ([]crdt.Op, error)
Examples ¶
Constants ¶
const BlockMark = '\uFFFF'
BlockMark is the character a block begins at. It is U+FFFF, a Unicode noncharacter: permanently reserved and never legal in interchange, so it is not something a person types.
It is exported so that anything reading the text part directly — an anchor, a cursor, an authorship pass — can tell a marker from a character somebody wrote, rather than having to know the number.
const DefaultChunkSize = 64 << 10
DefaultChunkSize is what Blobs.Put cuts at. Sixty-four kilobytes makes a two-megabyte figure thirty-two operations rather than one, which is small enough to send between two keystrokes and large enough that the hash beside each is a rounding error against it.
const SelectionMetaKey = "sel"
SelectionMetaKey is the metadata key a structured selection travels under.
Variables ¶
var DocStart = BlockID{}
DocStart is the place before the first block: passing it to Blocks.Insert puts a block at the top of the document.
var ErrInvalidID = errors.New("structured: invalid entity id")
ErrInvalidID reports an entity identity that cannot key a record: the empty string, or bytes that are not valid UTF-8. An id crosses into JavaScript, where a string is UTF-16, so it is held to the same rule a crdt.Map key is.
var ErrNoChange = errors.New("structured: the operation would change nothing")
ErrNoChange is returned by an operation that would change nothing, so that a caller never has to decide whether a zero-valued operation is safe to send.
var ErrReservedRune = errors.New("structured: text contains the block marker")
ErrReservedRune reports text offered to a Blocks that contains BlockMark. The marker is what tells one block from the next, so a caller that could write one could write a block boundary into the middle of a word, and no reader could tell that apart from a block somebody meant.
var ErrUnknownConn = errors.New("structured: unknown connector")
ErrUnknownConn reports an operation naming a connector this replica does not hold as present.
var ErrUnknownEntity = errors.New("structured: unknown entity")
ErrUnknownEntity reports an operation naming an entity this replica does not hold as present — one never added, or already removed.
var ErrUnknownFamily = errors.New("structured: unknown family")
ErrUnknownFamily reports an operation naming a family a Document does not hold. The five valid families are the constants above.
var ErrUnknownNode = errors.New("structured: unknown node")
ErrUnknownNode reports an operation naming a node this replica does not hold as present — one never added, or already removed.
var SeqStart = ItemID{}
SeqStart is the place before the first item: passing it to Sequence.Insert or Sequence.Move puts an item at the front.
var TreeRoot = TreeID{}
TreeRoot is the parent of a node at the top of the tree. It is not a node and has no fields; it is the name of the place a top-level node hangs from.
Functions ¶
func CellSelectionOf ¶
CellSelectionOf returns the cell a peer has selected, and whether it has a valid one. A peer publishing a selection this package did not encode — or none — reports not ok.
func DecodeBool ¶
DecodeBool reverses EncodeBool, reporting failure for anything but a single 0 or 1 byte — a peer may send other bytes, and a boolean has exactly two values.
func DecodeInt ¶
DecodeInt reverses EncodeInt, reporting failure for bytes it did not produce: an empty slice, a truncated or trailing varint, or a value outside int32, since a peer's operation carries the bytes and a wider value would store differently on a 32-bit replica.
func EncodeBool ¶
EncodeBool renders a boolean as a single byte, 0 or 1.
func EncodeInt ¶
EncodeInt renders a signed integer as one varint, so that every replica, on any architecture, stores and compares an integer field identically. It is int32 for the same reason a Diagram's position is: a 32-bit target — js/wasm — and a 64-bit one must agree byte for byte.
func PublishCellSelection ¶
func PublishCellSelection(reg *awareness.Registry, site crdt.SiteID, row RowID, col ColID, meta map[string]string) awareness.Update
PublishCellSelection records that this replica has the cell (row, col) selected and returns the awareness update to broadcast. meta carries any presentation details — a display name, a colour — alongside the selection.
Types ¶
type Blobs ¶ added in v0.24.0
type Blobs struct {
// contains filtered or unexported fields
}
Blobs holds the files a document refers to but is not made of: the figures of a paper, an image pasted into a page, a font, a recorded clip.
Why a map value is not one ¶
A file could be one value in a crdt.Map, and for a small one that is the right answer. For a two-megabyte figure it is not, because the operation that writes it is two megabytes: it cannot be sent as it is read, it cannot be resumed if the connection drops halfway, a peer that already has the same figure under another name receives it again, and changing one corner of the image sends the whole thing a second time.
How this one works ¶
A file is cut into chunks, and each chunk is stored under the hash of its own bytes. What a name refers to is a manifest: the length of the file and the hashes of its chunks, in order.
Every property this type has follows from that one decision:
- A file is as many operations as it has chunks, so it is sent as it is read and a peer that stops halfway has a prefix rather than nothing.
- Two replicas storing the same chunk write the same key and the same bytes, so there is no conflict to resolve, ever. The same figure under two names, in two documents, or added twice by two people is stored once.
- Changing part of a file rewrites only the chunks that changed, if the chunker cuts on the content rather than on a count; see Chunker.
- A chunk arrives verified or not at all. The key says what the bytes must hash to, so a peer cannot quietly put something else there.
What a name points at is one value, so two replicas replacing the same file at once is a conflict crdt.Map settles: one of the two whole files wins, which is what replacing a file means. Neither replica ends up with a mixture.
func BlobsOf ¶ added in v0.24.0
BlobsOf reads a composite as a blob store, for a document that holds one among other parts — the usual case, since the files belong to the document that refers to them.
func LoadBlobs ¶ added in v0.24.0
LoadBlobs rebuilds a store from a snapshot, to be written as site.
func (*Blobs) Composite ¶ added in v0.24.0
Composite returns the document underneath, which is what is snapshotted and what operations are applied to.
func (*Blobs) Get ¶ added in v0.24.0
Get returns the file stored under name.
It returns false while any of the file's chunks has not arrived, rather than a hole where one should be: half a figure is not a figure. Use Blobs.Missing to tell "not here" from "not here yet".
A chunk whose bytes do not hash to the key they are under is treated as not having arrived. A peer can write anything into the map; it cannot make this hand back bytes nobody asked for.
func (*Blobs) Missing ¶ added in v0.24.0
Missing returns how many of a file's chunks have not arrived, or have arrived as something other than what their key says they must be.
func (*Blobs) Pending ¶ added in v0.24.0
Pending reports how many received operations are still waiting.
func (*Blobs) Put ¶ added in v0.24.0
Put stores data under name, cut at DefaultChunkSize.
func (*Blobs) PutWith ¶ added in v0.24.0
PutWith stores data under name, cut by the given chunker.
The batches come back in the order they should be sent: every chunk, and the manifest last. A peer that receives a prefix of them holds chunks it cannot yet assemble, which is what makes the transfer resumable — nothing refers to them until the manifest arrives, and Blobs.Missing says what is still to come.
A chunk already stored is not written again, so putting a file twice costs one operation and putting a file that shares chunks with one already there costs only what is new.
func (*Blobs) Remove ¶ added in v0.24.0
Remove takes a name away. The chunks stay, because another name may share them; see Blobs.Sweep.
func (*Blobs) Size ¶ added in v0.24.0
Size returns how long the file under name is, and whether there is one. It answers before the chunks have arrived, because the manifest carries the length — which is what lets a caller show a figure's dimensions, or a progress bar, while it is still coming.
func (*Blobs) Stored ¶ added in v0.24.0
Stored returns how many distinct chunks are held, which is what the store costs rather than the sum of the sizes of the files in it.
func (*Blobs) Sweep ¶ added in v0.24.0
Sweep removes every chunk no name refers to.
It is not safe to run while a peer may be storing something. A peer that has just put a file whose chunks this replica also had would have written no chunk operations for them — they were already here — and sweeping them leaves that peer's manifest naming chunks nobody holds. What that costs is bounded and visible: Blobs.Missing reports it, and putting the file again restores exactly the chunks that went. It is not corruption, but it is work, so sweep when a document is quiet rather than as a matter of course.
func (*Blobs) Version ¶ added in v0.24.0
func (b *Blobs) Version() crdt.CompositeVersion
Version returns what this replica holds.
type Block ¶ added in v0.27.0
type Block struct {
// ID names the block.
ID BlockID
// Type is what the block is — "paragraph", "heading", "quote", whatever
// the caller uses. It is free text rather than an enumeration, because
// this package holds no renderer and has no business deciding what a
// document can contain. The empty string is a block nobody has typed.
Type string
// Depth is how deeply the block is nested; zero is the top level.
Depth int
// Text is the block's characters, without the marker and without the
// formatting. See [Blocks.Spans] for the formatting.
Text string
}
A Block is one block as it reads now.
type BlockID ¶ added in v0.27.0
A BlockID names a block for as long as the block exists, whatever concurrent edits do to the blocks around it. It is the identity of the marker character the block begins at, minted by the RGA, so it is unique across replicas, reload-safe, and never reused.
type Blocks ¶ added in v0.27.0
type Blocks struct {
// contains filtered or unexported fields
}
A Blocks is a document made of blocks: paragraphs, headings, list items, quotes, code, each holding text that carries formatting, each nested to a depth. It is what a page in a notebook, an outline, a wiki article and the body of a message all are.
Why not one rich text per block ¶
The obvious shape is a RichText per block, held in a crdt.Composite under a part apiece. It converges, and it does not scale, for a reason that has nothing to do with merging: a part cannot be taken out of a composite, and a version carries one entry per part. A thousand-block document is then a thousand parts whose version is exchanged on every sync, and the version of an empty document that once had a thousand blocks is the same size as the version of a full one. Measured, in this package's own tests: a thousand blocks written a part apiece is 18894 bytes of version vector, and written this way it is 35. It is the same argument Ink settles the same way — one stream of points rather than one part per stroke.
How this one works ¶
There is one text, holding every block's characters in reading order, and one map holding what each block is. A block begins at a marker character in the text, and the map records that marker's type, its depth and whatever else the caller puts on it, keyed by the marker's own identity. So a document of any number of blocks is three parts: the text, the marks over it, and the blocks.
The marker is a real character rather than a boundary between two of them, and that is the whole reason this works. Two people can edit the same seam at the same moment and mean different things: one is finishing a paragraph, the other is starting the next. Both are the same offset. A boundary stored as "before the first character of this block" makes the first intention expressible and the second impossible; stored as "after the last character of the one before", the other way round. Neither can be both, because there is only one place in the sequence to insert at. A marker gives the seam two sides: text typed before it is the end of one block and text typed after it is the start of the next, and the sequence already knows how to keep two concurrent insertions at two different places apart. Nothing has to be arbitrated and nothing is lost.
The marker is U+FFFF, a Unicode noncharacter — permanently reserved, never legal in interchange, so no text a person can type contains one. Text offered to this type is refused if it does one anyway (ErrReservedRune), and a marker that arrives from a peer with no record against it reads as an untyped block rather than as an error, because what a document reads as has to be a function of the state it is in.
Why nesting is a depth and not a parent ¶
A Tree is what nesting normally wants, and it is not what this wants. The order of the blocks is already decided, by the text, and a parent pointer is a second statement about the same arrangement — one that can contradict the first, so that a block reads before the block it hangs under. There is no answer to that contradiction which is not an arbitration.
A depth cannot contradict anything. Every sequence of depths is a document somebody can read: a jump of two is a list that starts two levels in, which is what a person who indents twice meant. Two replicas indenting the same block at the same moment are two writes to one field, which crdt.Map settles the same way on both, and the loser can indent again.
Example ¶
A Blocks is a document made of blocks — headings, paragraphs, list items — however many of them, in three parts. Here two replicas edit the same seam at the same moment: one is finishing a paragraph and the other is starting the next, which are the same offset and are not the same place. Both edits land where they were meant, with nothing arbitrated.
package main
import (
"fmt"
"github.com/go-crdt/crdt/structured"
)
func main() {
ada, grace := structured.NewBlocks(1), structured.NewBlocks(2)
// Ada writes a heading and a paragraph, and shares them.
title, first, _ := ada.Insert(structured.DocStart, "heading")
second, _ := ada.InsertText(title, 0, "Rivers")
body, third, _ := ada.Insert(title, "paragraph")
fourth, _ := ada.InsertText(body, 0, "They run downhill")
grace.Apply(append(append(first, second), append(third, fourth)...)...)
// Offline: Ada finishes the heading, Grace starts the paragraph.
fromAda, _ := ada.InsertText(title, 6, " and seas")
fromGrace, _ := grace.InsertText(body, 0, "Mostly, ")
ada.Apply(fromGrace)
grace.Apply(fromAda)
for _, block := range grace.List() {
fmt.Printf("%s: %s\n", block.Type, block.Text)
}
fmt.Println(len(grace.Version()), "parts")
}
Output: heading: Rivers and seas paragraph: Mostly, They run downhill 2 parts
func BlocksOf ¶ added in v0.27.0
BlocksOf reads a composite as a block document, for a document that holds these parts among others.
func LoadBlocks ¶ added in v0.27.0
LoadBlocks rebuilds a document from a snapshot, to be edited as site.
func (*Blocks) At ¶ added in v0.27.0
At returns where a place inside a block sits in the underlying text, which is what Blocks.RichText and crdt.Doc.Anchor are in terms of.
offset may equal the block's length, which is the place after its last character.
func (*Blocks) Children ¶ added in v0.27.0
Children returns the blocks that hang directly under one block, in reading order. DocStart returns the blocks at the top level.
func (*Blocks) Composite ¶ added in v0.27.0
Composite returns the document underneath, which is what is snapshotted and what operations are applied to.
func (*Blocks) DeleteText ¶ added in v0.27.0
DeleteText takes count characters out of a block at offset.
It cannot reach past the end of the block, so a deletion can never take a marker out by accident and turn two blocks into one; that is Blocks.Merge, which is a different thing to ask for.
func (*Blocks) IDs ¶ added in v0.27.0
IDs returns the identity of every block, in reading order. It is what Blocks.List returns without reading any text, for a caller that only wants to know what is there.
func (*Blocks) Insert ¶ added in v0.27.0
Insert puts a new empty block of type typ after the block named by after, or at the top of the document for DocStart. The new block is nested as deeply as the one it follows, which is what pressing return at the end of a list item means.
func (*Blocks) InsertText ¶ added in v0.27.0
InsertText puts s into a block at offset, which may equal the block's length.
func (*Blocks) Len ¶ added in v0.27.0
Len returns how many blocks there are. It is the document's length in blocks; its length in characters is RichText.Len of Blocks.RichText, which counts the markers too.
func (*Blocks) Mark ¶ added in v0.27.0
func (b *Blocks) Mark(from BlockID, fromOff int, to BlockID, toOff int, name string, value []byte, expand Expand) (crdt.PartOps, error)
Mark puts a mark on the text from one place in the document to another, carrying value, which may be nil for a mark that is only on or off.
A mark may span blocks — a comment on two paragraphs, a sentence someone started emphasising and finished in the next one — because the text it runs over is one text. expand says whether text typed at either edge joins it; see Expand.
func (*Blocks) MarksAt ¶ added in v0.27.0
MarksAt returns the formatting of one character of a block.
func (*Blocks) Merge ¶ added in v0.27.0
Merge takes the boundary between a block and the one before it away, so that its text joins the block above. It is what pressing backspace at the start of a block does.
The first block of a document has nothing above it to join, and merging it is an error rather than a silent no-op: a caller that asks has miscounted.
func (*Blocks) OpsSince ¶ added in v0.27.0
OpsSince returns the operations a peer at v has not seen.
func (*Blocks) Outline ¶ added in v0.27.0
Outline returns the blocks with their nesting made explicit: each block paired with the block it hangs under, which is the nearest one before it at a smaller depth.
It is derived, not stored — see the type's own documentation for why nesting is a depth — so two replicas holding the same document read the same outline. A block at the top level has DocStart as its parent.
func (*Blocks) Pending ¶ added in v0.27.0
Pending reports how many operations are held back waiting for ones they depend on.
func (*Blocks) Plain ¶ added in v0.27.0
Plain returns the whole document as text, with the markers taken out and the blocks separated by sep — "\n" for something a person reads.
It is a rendering, not the state: the state is the blocks.
func (*Blocks) Preamble ¶ added in v0.31.0
Preamble returns the characters before the first block, which is nothing in any document this package wrote.
A block begins at a marker, so a document written through this type begins with one and there is nothing in front of it. Another writer — a peer running a different version, or a caller reaching past this type to the text part — can put characters there, and they belong to no block. They are returned here rather than dropped: text that is in the document but reachable through no method is worse than text somebody has to decide what to do with.
func (*Blocks) Records ¶ added in v0.27.0
Records returns the record map the blocks live in, for reading a block's own fields.
func (*Blocks) Remove ¶ added in v0.27.0
Remove takes a block and everything in it out of the document.
func (*Blocks) RichText ¶ added in v0.27.0
RichText returns the text and its formatting as one rich text, for anything this type does not wrap. Its offsets are over the whole document, markers included; see Blocks.At to convert one.
func (*Blocks) SetDepth ¶ added in v0.27.0
SetDepth says how deeply a block is nested. Zero is the top level.
func (*Blocks) SetField ¶ added in v0.27.0
SetField puts one of the caller's own fields on a block: the level of a heading, the language of a code block, whether a task is done.
A field name may be anything valid UTF-8 that does not start with a NUL, which is what this type keeps its own two under.
func (*Blocks) SetType ¶ added in v0.27.0
SetType says what a block is. The empty string takes the type off.
func (*Blocks) Spans ¶ added in v0.27.0
Spans returns one block's text broken into stretches over which the formatting does not change, in order, covering every character of it exactly once. Offsets are within the block.
A block with no characters has no spans, which is the same answer RichText.Spans gives an empty text.
func (*Blocks) Split ¶ added in v0.27.0
Split cuts a block in two at offset, and returns the new block, which holds everything from offset on and follows the one split. It is what pressing return in the middle of a paragraph does.
The new block is of type typ and is nested as deeply as the one it came from. Splitting at the end of a block leaves an empty one after it, and splitting at the start leaves an empty one before it; both are what a person pressing return at those places means.
func (*Blocks) Unmark ¶ added in v0.27.0
func (b *Blocks) Unmark(from BlockID, fromOff int, to BlockID, toOff int, name string) (crdt.PartOps, error)
Unmark takes a mark off the text between two places, on the terms RichText.Unmark describes: it is a mark of its own rather than the removal of one.
func (*Blocks) Version ¶ added in v0.27.0
func (b *Blocks) Version() crdt.CompositeVersion
Version returns what this replica has, one entry per part — three, whatever the document holds.
type Cell ¶
type Cell struct {
// Kind selects a literal or a formula.
Kind CellKind
// Text is the literal's value or the formula's source, as the caller renders
// it. It is opaque here.
Text string
// Refs are the cells a formula depends on, by stable identity. It is empty for
// a literal, and may be empty for a formula that references nothing.
Refs []CellRef
}
A Cell is the raw content of a spreadsheet cell: a literal value, or a formula's source together with the cells it references. It is what a Sheet stores and merges; a formula engine and a view are built above it and are no concern of this layer.
The zero Cell is a literal with no text, which is how an unset cell reads.
type CellKind ¶
type CellKind uint8
CellKind distinguishes a cell holding a plain value from one holding a formula.
const ( // CellLiteral is a cell whose content is its own value. CellLiteral CellKind = 1 // CellFormula is a cell whose content is an expression over other cells. Its // source text is replicated; its computed value is not — that is derived // locally, so it is never a thing two replicas can disagree about. CellFormula CellKind = 2 )
type CellRef ¶
A CellRef names a cell a formula depends on, by the stable identities of its row and column rather than by a position. A concurrent insertion or deletion of some other row renumbers positions but touches no identity, so a reference stored this way still names the same cell afterwards — which is what keeps =SUM(...) pointing where its author meant when the sheet changes shape beneath it.
type Chunker ¶ added in v0.24.0
A Chunker cuts a file into the pieces it is stored as.
The pieces are what deduplication and repeated sending work in terms of, so where the cuts fall is what decides whether changing one corner of an image rewrites one chunk or all of them. FixedChunks cuts every n bytes, which is enough for a file that is written once and never edited and is the wrong answer for one that is: inserting a single byte at the front moves every later boundary and nothing matches any more.
A chunker that cuts on the content — one that hashes a sliding window and cuts where the hash has some shape, so a boundary depends on the bytes around it and not on how far into the file it is — makes an edit rewrite only the chunks it touched. This package does not carry one, because it carries nothing: the crdt module has no dependencies and this is not the thing to spend the first one on. Pass whichever you have.
func FixedChunks ¶ added in v0.24.0
FixedChunks cuts every size bytes. A size of zero or less is one chunk.
type Counter ¶ added in v0.20.0
type Counter struct {
// contains filtered or unexported fields
}
A Counter is a number any number of replicas may add to at once, offline, in any delivery order, and every replica ends up holding the same total.
Why a register is not one ¶
The obvious way to share a number is a last-writer-wins register: read it, add one, write it back. That loses. Two replicas holding 7 both write 8, one of the two writes wins, and a vote is missed — the outcome does not depend on what either replica meant, only on which write sorted higher. The mistake is not in the register; it is that "add one" is not a value, and writing a value cannot express it.
How this one works ¶
A counter is a map keyed by site, and a replica writes only its own key. Its key holds everything that replica has ever added and everything it has ever taken away, as two numbers that only ever grow. The total is the sum of the first minus the sum of the second, over every key.
Nothing ever conflicts, because no two replicas write the same key, and a replica's own key is a number it computes from what it alone has done. That is the whole of it: the crdt.Map underneath is doing no merging at all, and the counter is correct because concurrent additions are concurrent writes to different keys.
Why two numbers rather than one ¶
A single signed total per site would be shorter and would work. Two are kept because each of them only ever increases, which is what makes a site's key safe to read back from any snapshot — an older copy of it can only be behind, never wrong in the other direction — and because a tally of what was added against what was taken away is what a vote, a stock level or a budget is actually asking for. Counter.Added and Counter.Removed return them.
func CounterOf ¶ added in v0.20.0
CounterOf reads a map as a counter, for a map that is a part of a crdt.Composite.
func LoadCounter ¶ added in v0.20.0
LoadCounter reads a snapshot back, as the given site.
func NewCounter ¶ added in v0.20.0
NewCounter returns a counter this site can add to.
func (*Counter) Add ¶ added in v0.20.0
Add moves the counter by delta, which may be negative. Adding zero is not an operation and returns ErrNoChange.
func (*Counter) Added ¶ added in v0.20.0
Added returns everything every replica has ever added, ignoring what was taken away. Removed returns the other half.
func (*Counter) Map ¶ added in v0.20.0
Map returns the map underneath, which is what is snapshotted and what operations are applied to.
func (*Counter) OpsSince ¶ added in v0.20.0
func (c *Counter) OpsSince(vv crdt.VersionVector) []crdt.MapOp
OpsSince returns the operations a peer at vv has not seen.
func (*Counter) Removed ¶ added in v0.20.0
Removed returns everything every replica has ever taken away.
func (*Counter) Value ¶ added in v0.20.0
Value returns the total: everything every replica has added, less everything every replica has taken away.
func (*Counter) Version ¶ added in v0.20.0
func (c *Counter) Version() crdt.VersionVector
Version returns what this replica has seen.
type Diagram ¶
type Diagram struct {
// contains filtered or unexported fields
}
A Diagram is a collaborative diagram of nodes and connectors — an isometric diagram is the case it was shaped for. A node is a record of fields (a grid position, a label, a colour); a connector is a record whose fields are the identities of the two nodes it joins. Editing it produces operations that any number of replicas may exchange, offline and in any order, converging to the same diagram.
It is one crdt.Composite — the same core a Sheet is — which is the point of this package: a spreadsheet and a diagram are two thin addressings of one merging substrate, not two implementations. Each node and connector field merges on its own, so one replica may move a node while another recolours it and both edits survive.
Membership is the node and connector lists: Diagram.Nodes and Diagram.Conns say what exists. A field read is by identity and independent of membership, so it still answers for a removed node — a view renders what Diagram.Nodes returns and needs no more.
A Diagram is not safe for concurrent use. Construct one with NewDiagram or LoadDiagram.
func LoadDiagram ¶
LoadDiagram rebuilds a diagram from a snapshot, to be edited as site.
func NewDiagram ¶
NewDiagram returns an empty diagram that issues operations as site. Every replica editing a diagram concurrently must pass a distinct crdt.SiteID.
func (*Diagram) AddConn ¶
AddConn adds a connector from one node to another and returns its identity and the operations to broadcast: the connector's own identity, then its endpoints. The endpoints are stored by identity and are not required to name nodes this replica already holds — the node may arrive later, or be concurrently removed.
func (*Diagram) AddNode ¶
AddNode adds a node and returns its identity and the operation to broadcast. The node has no fields yet.
func (*Diagram) Apply ¶
Apply integrates batches of operations from peers, tolerating duplicates and reordering.
func (*Diagram) Collect ¶ added in v0.33.0
func (d *Diagram) Collect(stable crdt.CompositeVersion) int
Collect gives back what a diagram that has been worked on no longer needs: the nodes and connectors that were removed, and the records saying they were.
A diagram is where this is worth asking for by hand. Its nodes live in a list and their properties in a map, and neither gives anything back on its own: a node dragged around and then deleted leaves an element and a record per property behind it, for good. On a diagram of two hundred nodes half of which were tried and removed, that is most of what the file weighs.
stable must be a version every replica has delivered, per part; see crdt.Doc.Collect for what that means and who can know it. A part it does not name is left alone.
func (*Diagram) Composite ¶ added in v0.33.0
Composite returns the document underneath, for a caller that needs the parts themselves.
func (*Diagram) ConnEndpoints ¶
ConnEndpoints returns the nodes a connector joins and whether both are set.
func (*Diagram) ConnField ¶
ConnField returns the value of an arbitrary connector field and whether it is set. The value is a copy.
func (*Diagram) NodeColour ¶
NodeColour returns a node's colour and whether it is set.
func (*Diagram) NodeField ¶
NodeField returns the value of an arbitrary node field and whether it is set. The value is a copy.
func (*Diagram) NodePosition ¶
NodePosition returns a node's grid position and whether it is set.
func (*Diagram) Nodes ¶
Nodes returns the identities of the nodes present. Two replicas holding the same operations return the same slice.
func (*Diagram) OpsSince ¶
OpsSince returns the operations this replica holds that v does not, batched by part. Pass a nil version for everything.
func (*Diagram) Pending ¶
Pending reports how many received operations are still waiting, across every part, for the operations they depend on.
func (*Diagram) RemoveConn ¶
RemoveConn removes a connector's identity and returns the operation to broadcast. Its fields are left in place, as a removed node's are. A connector not present is ErrUnknownConn.
func (*Diagram) RemoveNode ¶
RemoveNode removes a node's identity and returns the operation to broadcast. Its fields are left in place, unreferenced by any present node, exactly as a removed row's cells are in a Sheet. A node not present is ErrUnknownNode.
func (*Diagram) SetConnEndpoints ¶
SetConnEndpoints rewrites which nodes a connector joins and returns the operation to broadcast. The connector must be present.
func (*Diagram) SetConnField ¶
SetConnField writes an arbitrary field of a connector and returns the operation to broadcast — the generic form of Diagram.SetConnEndpoints for the fields a connector carries beyond its endpoints: a label, a style, an arrow, a colour, a width. Like Diagram.SetNodeField each field is its own register.
func (*Diagram) SetNodeColour ¶
SetNodeColour writes a node's colour and returns the operation to broadcast.
func (*Diagram) SetNodeField ¶
SetNodeField writes an arbitrary field of a node and returns the operation to broadcast. It is the generic form of Diagram.SetNodePosition, Diagram.SetNodeLabel and Diagram.SetNodeColour, for the scalar fields a caller's schema carries beyond those three — a shape, an icon, a layer. Each field is its own LWW-register, so a concurrent write to two of them never conflicts. The value is opaque bytes; a caller storing a number should encode it identically on every architecture, as EncodeInt does. Writing the field a typed setter uses ("pos", "label", "colour") reaches the very same register.
func (*Diagram) SetNodeLabel ¶
SetNodeLabel writes a node's label and returns the operation to broadcast.
func (*Diagram) SetNodePosition ¶
SetNodePosition writes a node's grid position and returns the operation to broadcast. Coordinates are int32 so that every replica, on any architecture, stores and compares them identically.
func (*Diagram) Snapshot ¶
Snapshot encodes the whole diagram, for a joining peer or for persistence.
func (*Diagram) Sweep ¶ added in v0.33.0
Sweep removes the properties of nodes and connectors that are no longer in the diagram.
It is the counterpart of Diagram.RemoveNode being one operation. Removing a node takes it out of the list of what exists; its label, its colour and its position stay in the map, keyed by a node nothing can reach any more. That is deliberate — a removal should be one operation, not one per property somebody happened to set — and it means a diagram worked on for a while carries every node it ever held.
This is what takes those bytes away, and it belongs where Ink.Sweep belongs: run when the drawing is quiet rather than while somebody is editing it. It has the same hazard for the same reason — a peer may be setting a property on a node this replica has been told to remove, and that arrives after the sweep and is swept by the next one. Nothing is corrupted; the node is removed, which is what was asked.
Sweeping turns unreachable records into tombstones. Diagram.Collect is what gives the tombstones back, once every replica has seen them go.
func (*Diagram) Version ¶
func (d *Diagram) Version() crdt.CompositeVersion
Version returns what this replica holds, to hand a peer that will send back what it is missing; see Diagram.OpsSince.
type Document ¶
type Document struct {
// contains filtered or unexported fields
}
A Document is a collaborative document made of several families of entities, each entity addressed by a stable identity the caller chooses. It is the shape an isometric diagram takes: nodes, connectors, zones, free text and layers all living in one merging substrate, every family a map of records and every field an independent LWW-register.
One composite, one boundary ¶
Like Sheet and Diagram a Document is exactly one crdt.Composite: every family is a RecordMap over a distinct map part of that one composite, so the whole document has a single Document.Snapshot, a single Document.Version, a single Document.OpsSince and a single Document.Apply covering all five families at once. There is nothing to bundle and no second transport to keep in step — the convergence, commutativity, idempotence and associativity the crdt package proves for its parts are inherited whole, because there is only ever one set of parts doing the merging.
Caller-chosen identities ¶
A Diagram mints a node's identity from its RGA, so two replicas that each "add a node" get two distinct nodes — which is right when the two are genuinely different. A Document instead lets the caller name an entity, because a toolkit that already has its own stable id for a shape needs the two replicas that create "the shape the user just dropped" to converge on one entity, not two. The id is the record key, so Document.Add of the same id on two replicas is the same write to the same key: they converge, with no local, non-replicated id table to keep. An id is arbitrary, non-empty UTF-8.
What exists ¶
An entity exists exactly while it holds at least one live field — a field the caller set, or the presence marker Document.Add writes so that a freshly created entity with no fields yet still shows in Document.IDs. Reading a field is by identity and independent of membership, exactly as it is for a Diagram: a view renders what Document.IDs returns.
A Document holds no widget and no geometry engine. Field values are opaque bytes; EncodeInt, DecodeInt, EncodeBool and DecodeBool are offered so that the non-string fields a diagram carries are encoded identically on every architecture, js/wasm included, but nothing here interprets a field.
A Document is not safe for concurrent use. Construct one with NewDocument or LoadDocument.
Example ¶
A Document is the five-family isometric core: nodes, connectors, zones, text and layers under one composite. Here two replicas, disconnected, each create the entity the user dropped under the same caller-chosen id and edit different fields of it; they exchange operations and converge on one entity carrying both edits.
package main
import (
"fmt"
"github.com/go-crdt/crdt/structured"
)
func main() {
ada, grace := structured.NewDocument(1), structured.NewDocument(2)
// Both name the same node — a caller-chosen id, so the two creations converge.
fromAda, _ := ada.Add(structured.Nodes, "server")
fromGrace, _ := grace.Add(structured.Nodes, "server")
// Offline, each writes a different field.
adaX, _ := ada.Set(structured.Nodes, "server", "x", structured.EncodeInt(5))
graceLabel, _ := grace.Set(structured.Nodes, "server", "label", []byte("web"))
// They swap every operation, in either order, and agree.
ada.Apply(fromGrace, graceLabel)
grace.Apply(fromAda, adaX)
x, _ := grace.Field(structured.Nodes, "server", "x")
n, _ := structured.DecodeInt(x)
label, _ := grace.Field(structured.Nodes, "server", "label")
fmt.Println(grace.IDs(structured.Nodes), n, string(label))
}
Output: [server] 5 web
func LoadDocument ¶
LoadDocument rebuilds a document from a snapshot, to be edited as site.
func NewDocument ¶
NewDocument returns an empty document that issues operations as site. Every replica editing a document concurrently must pass a distinct crdt.SiteID.
func (*Document) Add ¶
Add creates an entity in a family under the caller's id and returns the operation to broadcast. It writes a presence marker, so an entity with no fields yet is still present. Adding an id that already exists rewrites the marker and changes nothing observable, which is what lets two replicas both "create X" and converge.
func (*Document) Apply ¶
Apply integrates batches of operations from peers, tolerating duplicates and reordering, across every family at once.
func (*Document) DeleteField ¶
DeleteField removes one field of one entity and returns the operation to broadcast. Removing a field an entity does not hold is a no-op, not an error. Removing an entity's last field — the presence marker included — makes it cease to exist; use Document.Remove to clear an entity whole.
func (*Document) Field ¶
Field returns the value of one field and whether it is set. The value is a copy. An unknown family, an invalid id or an unset field reads as absent.
func (*Document) Fields ¶
Fields returns the field names an entity holds, ascending and with the internal namespace stripped, so the caller sees exactly the names it set. The presence marker and any foreign key a peer injected are not among them.
func (*Document) Has ¶
Has reports whether an entity is present — whether it holds any live field. An unknown family or an invalid id is not present rather than an error, so a view may ask freely.
func (*Document) IDs ¶
IDs returns the identities present in a family, ascending, so two replicas holding the same operations return the same slice. An unknown family has none.
func (*Document) OpsSince ¶
OpsSince returns the operations this replica holds that v does not, batched by part and covering all five families. Pass a nil version for everything.
func (*Document) Pending ¶
Pending reports how many received operations are still waiting, across every family, for the operations they depend on.
func (*Document) Remove ¶
Remove clears an entity — every field it holds, presence marker included — and returns the operations to broadcast, ascending so the batch is deterministic. An entity not present is ErrUnknownEntity. The concurrent case, a Remove racing a field write, is resolved per field by (clock, site) exactly as it is for a RecordMap: a write the Remove did not see re-establishes the entity.
func (*Document) Set ¶
Set writes value to one field of one entity and returns the operation to broadcast. The entity is created by the write if it did not exist, on the same terms as [Add], so a field may be set without a prior Add. value is opaque; use the encoders for a non-string field.
func (*Document) Snapshot ¶
Snapshot encodes the whole document — all five families — for a joining peer or for persistence. Two replicas holding the same operations produce identical bytes.
func (*Document) Version ¶
func (d *Document) Version() crdt.CompositeVersion
Version returns what this replica holds, to hand a peer that will send back what it is missing; see Document.OpsSince.
type Draft ¶ added in v0.30.0
type Draft struct {
// contains filtered or unexported fields
}
A Draft is a working copy of a document, and a replica of it: edit the composite, then hand the draft to Proposals.Put.
func (*Draft) Base ¶ added in v0.30.0
func (d *Draft) Base() crdt.CompositeVersion
Base returns the version the draft was taken at.
type Expand ¶ added in v0.23.0
type Expand uint8
Expand says whether text typed at the edge of a mark joins it.
It is the difference between bold, which continues as you type at the end of it, and a link, which does not.
const ( // ExpandNone is a mark neither edge of which grows: typing at either end // stays outside it. A link, a comment, a footnote reference. ExpandNone Expand = 0 // ExpandStart makes text typed immediately before the mark part of it. ExpandStart Expand = 1 << iota // ExpandEnd makes text typed immediately after the mark part of it, which // is what bold and italic do. ExpandEnd // ExpandBoth grows at either edge. ExpandBoth = ExpandStart | ExpandEnd )
type Family ¶
type Family string
A Family names one of the five kinds of entity a Document holds. Its value is also the name of the crdt.Composite map part the family lives in, so the set of families is the document's structural layout, fixed and the same on every replica; the fields inside a record are the caller's own schema, not this package's.
const ( // Nodes is the family of diagram nodes. Nodes Family = "node" // Connectors is the family of edges joining nodes. Connectors Family = "conn" // Zones is the family of rectangular regions. Zones Family = "zone" // TextBoxes is the family of free-standing text entities. It is not named // "Text" so as not to be mistaken for a [crdt.PartText]; a Document holds no // text part. TextBoxes Family = "text" // Layers is the family of layers entities are assigned to. Layers Family = "layer" )
type Ink ¶ added in v0.25.0
type Ink struct {
// contains filtered or unexported fields
}
Ink is what is drawn by hand: the strokes of a whiteboard, an annotation over a figure, a signature.
What a stroke needs that the other types do not give it ¶
A stroke is a list of points, and it arrives one point at a time while somebody is drawing it. Held as one value — a Sequence item whose contents are the whole path — every point sent rewrites the path: a stroke of four hundred points costs four hundred operations whose average size is two hundred points, and the person watching sees the line redrawn rather than extended.
So the points are a sequence of their own, appended to, and each point says which stroke it belongs to.
Why one stream of points and not one per stroke ¶
A crdt.Composite part per stroke would keep each stroke's points together and read more simply. It would also put a part in the document for every stroke anybody ever drew, and a part cannot be taken out again: the version two replicas exchange to find out what they are missing carries one entry per part, so a whiteboard would come to spend more on saying what it has than on what was drawn.
One stream costs a stroke identity on every point — a dozen bytes — and keeps the document two parts wide however much is drawn on it.
What is where ¶
- The strokes are a Sequence, so the order they are drawn in is the order they are painted in, and a stroke can be moved above or below another without being redrawn. Anything a stroke carries — its colour, its width, the pen it was drawn with — is a field on the item.
- The points are a crdt.List, appended to as the pen moves.
func InkOf ¶ added in v0.25.0
InkOf reads a composite as a drawing, for a document that holds one among other parts.
func (*Ink) Begin ¶ added in v0.25.0
Begin starts a stroke, above every stroke already drawn, and returns it with no points yet.
func (*Ink) Erase ¶ added in v0.25.0
Erase takes a stroke out of the drawing.
Its points stay until Ink.Sweep takes them, because erasing happens while somebody is drawing and has to cost one operation rather than one per point.
func (*Ink) Extend ¶ added in v0.25.0
Extend adds points to the end of a stroke.
It is one operation however many points are given, so a pen that reports several samples between two frames costs one operation rather than several.
func (*Ink) Paths ¶ added in v0.25.0
Paths returns every stroke that is still drawn, in the order they are painted in, with its points.
It is one walk of the points rather than one per stroke, which is what a repaint wants: Ink.Points asked for each of five hundred strokes walks the whole drawing five hundred times.
func (*Ink) Pending ¶ added in v0.25.0
Pending reports how many received operations are still waiting.
func (*Ink) Points ¶ added in v0.25.0
Points returns the points of a stroke, in the order they were drawn.
func (*Ink) Strokes ¶ added in v0.25.0
Strokes returns the sequence the strokes live in, for the order they are painted in and whatever each of them carries.
func (*Ink) Sweep ¶ added in v0.25.0
Sweep removes the points of strokes that are no longer drawn.
It is the counterpart of Ink.Erase being one operation: erasing says the stroke is gone, and this is what takes the bytes away, when the drawing is quiet rather than while somebody is drawing on it.
It has the hazard Blobs.Sweep has and for the same reason: a peer may be extending a stroke this replica has been told to erase, and those points arrive after the sweep and are swept by the next one. Nothing is corrupted — the stroke is erased, which is what was asked.
func (*Ink) Version ¶ added in v0.25.0
func (i *Ink) Version() crdt.CompositeVersion
Version returns what this replica holds.
type ItemID ¶ added in v0.21.0
An ItemID names an item. It is stable across a reload and unique across replicas, because it is the identity of the operation that created the item.
type MultiRegister ¶ added in v0.28.0
type MultiRegister struct {
// contains filtered or unexported fields
}
A MultiRegister is a value that two replicas are allowed to disagree about, where the disagreement is the answer rather than something to be settled.
What a register cannot say ¶
Register resolves every concurrent write by the (clock, site) order, which is exactly right when a losing write is of no interest: a cursor position, a window size, a colour somebody picked. It is wrong when it is. Two people rename the same file at the same moment and one of the names is gone, with nothing anywhere recording that there was ever a second one — not in the state, not in the operations, not in anything a reader could show. The register did not choose badly; it has no way of saying that a choice was made.
How this one works ¶
Each replica writes only its own key, as a Counter does, so no two replicas ever write the same one and the map underneath is doing no merging at all. What a replica writes is its value together with a version vector: how many times it has written, and how many times it had seen every other replica write when it did.
A value is live when no other value's vector strictly dominates it. Two replicas that wrote without seeing each other have vectors neither of which dominates the other, so both values are live and both are read. A replica that writes having seen the other dominates it, and its value is the only one read.
That last sentence is also the whole of resolving a conflict: choosing one of the values is writing it, and writing it dominates everything the writer could see, including the value they chose. There is no separate operation for settling, and none is needed — see MultiRegister.Set.
What it costs ¶
One key per replica that has ever written, and a vector of one entry per replica that has ever written inside each of those. That is the same shape a Counter has, for the same reason, and it is the price of being able to say that two writes did not see each other — a Lamport clock cannot, because it gives a total order, and the question is which pairs are unordered.
Example ¶
A MultiRegister keeps a disagreement instead of settling it. Here two replicas rename the same thing while disconnected: an ordinary register would throw one name away with nothing left saying it existed, and this one hands both back. Choosing between them is writing the one chosen.
package main
import (
"fmt"
"github.com/go-crdt/crdt/structured"
)
func main() {
ada, grace := structured.NewMultiRegister(1), structured.NewMultiRegister(2)
// Offline, each names the file.
fromAda, _ := ada.Set([]byte("notes.txt"))
fromGrace, _ := grace.Set([]byte("readme.txt"))
ada.Apply(fromGrace)
grace.Apply(fromAda)
fmt.Println(ada.Conflicted())
for _, name := range ada.Values() {
fmt.Printf("%s\n", name)
}
// Ada picks one. That write saw both, so it supersedes both.
chosen, _ := ada.Set([]byte("readme.txt"))
grace.Apply(chosen)
name, only := grace.Value()
fmt.Printf("%s %v\n", name, only)
}
Output: true notes.txt readme.txt readme.txt true
func LoadMultiRegister ¶ added in v0.28.0
func LoadMultiRegister(site crdt.SiteID, snapshot []byte) (*MultiRegister, error)
LoadMultiRegister rebuilds one from a snapshot, to be written as site.
func MultiRegisterOf ¶ added in v0.28.0
func MultiRegisterOf(m *crdt.Map) *MultiRegister
MultiRegisterOf reads a map as a multi-value register, for a map that is a part of a crdt.Composite.
func NewMultiRegister ¶ added in v0.28.0
func NewMultiRegister(site crdt.SiteID) *MultiRegister
NewMultiRegister returns an empty register this site can write.
func (*MultiRegister) Apply ¶ added in v0.28.0
func (r *MultiRegister) Apply(ops ...crdt.MapOp) error
Apply integrates operations from peers, tolerating duplicates and reordering.
func (*MultiRegister) Clear ¶ added in v0.28.0
func (r *MultiRegister) Clear() (crdt.MapOp, error)
Clear takes the value away, on the same terms as MultiRegister.Set: it is a writing of its own, it dominates everything this replica has seen, and a concurrent write that has not seen it stays live beside it.
func (*MultiRegister) Conflicted ¶ added in v0.28.0
func (r *MultiRegister) Conflicted() bool
Conflicted reports whether more than one writing is live: two replicas wrote without seeing each other, and both writings stand.
func (*MultiRegister) Map ¶ added in v0.28.0
func (r *MultiRegister) Map() *crdt.Map
Map returns the map underneath, which is what is snapshotted and what operations are applied to.
func (*MultiRegister) OpsSince ¶ added in v0.28.0
func (r *MultiRegister) OpsSince(vv crdt.VersionVector) []crdt.MapOp
OpsSince returns the operations this replica holds that vv does not.
func (*MultiRegister) Readings ¶ added in v0.28.0
func (r *MultiRegister) Readings() []Reading
Readings returns every writing of the register that nothing has superseded, in site order.
One reading is the ordinary case. More than one is a disagreement: two or more replicas wrote without seeing each other, and there is no fact anywhere that says which of them is the answer. Nothing here invents one.
func (*MultiRegister) Set ¶ added in v0.28.0
func (r *MultiRegister) Set(value []byte) (crdt.MapOp, error)
Set writes value, and in doing so settles every disagreement this replica can see: the write's vector dominates every reading it was made against, so the register reads as this one value until somebody who has not seen it writes again.
Choosing between the values of a conflict is therefore not a separate operation. Write the one that was chosen.
func (*MultiRegister) Site ¶ added in v0.28.0
func (r *MultiRegister) Site() crdt.SiteID
Site returns the replica this register writes as.
func (*MultiRegister) Snapshot ¶ added in v0.28.0
func (r *MultiRegister) Snapshot() []byte
Snapshot encodes the whole register, for a joining peer or for persistence.
func (*MultiRegister) Value ¶ added in v0.28.0
func (r *MultiRegister) Value() ([]byte, bool)
Value returns the one value the register holds, and whether it holds exactly one. A register nobody has written, one everybody agrees is cleared, and one two replicas disagree about all report false — so a caller that ignores the second return reads no value rather than an arbitrary one.
func (*MultiRegister) Values ¶ added in v0.28.0
func (r *MultiRegister) Values() [][]byte
Values returns the values of the live readings, in site order, leaving out any replica that cleared the register.
It is what a reader that has no way of showing a conflict should use with MultiRegister.Conflicted: one value, or a list to be offered.
func (*MultiRegister) Version ¶ added in v0.28.0
func (r *MultiRegister) Version() crdt.VersionVector
Version returns what this replica holds, to hand a peer that will send back what it is missing; see MultiRegister.OpsSince.
type NodeID ¶
A NodeID names a diagram node, on the same terms as a RowID. A node's fields are stored against it, and a connector's endpoints reference it.
func NodeSelectionOf ¶
NodeSelectionOf returns the node a peer has selected, and whether it has a valid one.
type Point ¶ added in v0.25.0
A Point is one sample of a pen: where it was, and how hard it was pressed.
Pressure is whatever the input device reports, normally between zero and one, and is carried rather than interpreted. A device with no pressure sensor should send one.
type Proposal ¶ added in v0.30.0
type Proposal struct {
// ID names it.
ID ProposalID
// Title is what it was put up as.
Title string
// Author is the replica that put it up, which is the site the proposal's
// identity was minted by. It is not the draft's site: a draft's site is a
// second identity the same person holds so that its operations do not
// collide with the ones their replica is making, and it says nothing about
// who they are.
Author crdt.SiteID
// State is what has become of it.
State State
// Base is the version of the document the draft was taken from. It is
// informational: the operations merge whatever the document has done
// since, and what a reader wants it for is whether the change still means
// what its author meant — a reworded sentence somebody has since deleted
// merges cleanly and says nothing.
Base crdt.CompositeVersion
// Ops is what accepting it would apply.
Ops []crdt.PartOps
}
A Proposal is one proposed change as it stands.
type ProposalID ¶ added in v0.30.0
A ProposalID names a proposal. It is the identity of the operation that created it, so it is unique across replicas, reload-safe, and carries the site that raised it.
func (ProposalID) String ¶ added in v0.30.0
func (p ProposalID) String() string
String renders the identity in the "seq@site" notation the crdt package uses.
type Proposals ¶ added in v0.30.0
type Proposals struct {
// contains filtered or unexported fields
}
Proposals are changes to a document that are not part of it yet: a suggested edit, a change put up for review, a branch somebody wants merged.
Why this is not a fork ¶
The obvious shape is a second copy of the document, edited on its own and reconciled later. Reconciling is where it falls apart. Two copies of a text can be compared, and what a comparison produces is a difference — insert this here, delete that — and applying a difference mints new characters. Every anchor, mark, comment and cursor hanging off the characters it replaced is then pointing at something that no longer exists, in a document nobody edited in the meantime. A review that accepts a wording change would take the comments off the paragraph around it.
What a proposal actually is ¶
A replica that has not synced. Nothing more.
Operations here commute, so a replica that goes offline, edits, and comes back a week later needs no reconciliation: its operations merge with everything that happened while it was away, and every identity it wrote is the identity the rest of the document already knows. A proposal is that same replica, kept offline on purpose, with its operations written down where reviewers can read them and applied if somebody says yes.
So accepting is crdt.Composite.Apply of the operations, and there is no rebase, no merge step and no conflict to settle: whatever the document did while the proposal was open, the proposal merges into it as concurrent work, because it is concurrent work. And nothing an anchor points at is disturbed, because the proposal is made of the document's own operations against the document's own identities rather than of a difference between two texts.
Rejecting is free for the same reason: the operations were never applied, so there is nothing to take back. That is not true of anything already in the document, which can only be undone by Undo — a new edit that has the effect of the old one not having happened.
A draft is a replica, so it needs a site of its own ¶
Proposals.Draft hands back a working copy to edit, and it takes a crdt.SiteID, for exactly the reason every replica takes one: two replicas minting operations under one site mint the same identity for different operations, and a document that received both would be holding two different things that claim to be the same. A draft is a replica. Give it its own site.
What it costs, and what it will not do ¶
A proposal is stored as one map value holding its operations, so it is one operation the size of the change. That is the right shape for a review — a wording, a paragraph, a renamed field — and the wrong one for a rewrite of the whole document, which should be a document.
A proposal is a recorded set of operations, not a live session: this package does not carry two people typing into one draft at the same time. Two people can each hold the draft as a replica and exchange its operations — which is what a draft being a replica means — and what gets written down when it is put up is whatever the draft holds then.
Example ¶
A Proposals is changes to a document that are not part of it yet. A proposal is a replica that has not synced: accepting it applies the document's own operations, so it merges with whatever happened while it was open and leaves every anchor where it was.
package main
import (
"fmt"
"github.com/go-crdt/crdt/structured"
)
func main() {
docs := structured.NewProposals(1)
text, _ := docs.Composite().Text("text")
text.Insert(0, "Hello world")
// A draft is a replica, so it gets a site of its own.
draft, _ := docs.Draft(2)
drafted, _ := draft.Composite().Text("text")
drafted.Insert(6, "beautiful ")
id, _, _ := docs.Put("an adjective", draft)
// The document has not moved, and the proposal can be read before it is in.
fmt.Println(text.String())
preview, _ := docs.Preview(id, 9)
previewed, _ := preview.Text("text")
fmt.Println(previewed.String())
// Meanwhile somebody edits the same sentence, and accepting merges.
text.Insert(11, ", and goodbye")
docs.Accept(id)
fmt.Println(text.String())
fmt.Println(docs.List()[0].State)
}
Output: Hello world Hello beautiful world Hello beautiful world, and goodbye accepted
func LoadProposals ¶ added in v0.30.0
LoadProposals rebuilds one from a snapshot, to be edited as site.
func NewProposals ¶ added in v0.30.0
NewProposals returns an empty document with proposals on it, this site can edit.
func ProposalsOf ¶ added in v0.30.0
ProposalsOf reads a composite as a document that can carry proposals. The proposals are one part of it; the rest of the document is whatever else it holds.
func (*Proposals) Accept ¶ added in v0.30.0
func (p *Proposals) Accept(id ProposalID) ([]crdt.PartOps, error)
Accept applies a proposal's operations to the document and records that it was accepted.
There is no rebase and nothing to settle. The operations merge with whatever the document has done since the draft was taken, exactly as a replica coming back from a week offline does, because that is what they are.
Accepting one that is already accepted applies the operations again, which changes nothing — they are the same operations — and writes the field again. Accepting a withdrawn one is allowed and makes it accepted: somebody changed their mind, and the document is what says so.
func (*Proposals) Composite ¶ added in v0.30.0
Composite returns the document underneath, which is what is snapshotted and what operations are applied to.
func (*Proposals) Draft ¶ added in v0.30.0
Draft returns a working copy of the document as it stands, to be edited as site.
site must be one no other replica is using, for the reason every replica's must be: a draft is a replica, and two replicas sharing a site mint one identity for two different operations. Passing the document's own site is refused, because that is the one collision this package can see coming.
func (*Proposals) Forget ¶ added in v0.30.0
func (p *Proposals) Forget(id ProposalID) ([]crdt.PartOps, error)
Forget takes a proposal's record out of the document, which is what an accepted one is once nobody needs to read what it was: its operations are in the document, and the record is a copy of them.
It does not take the change out. Nothing does; see Undo.
func (*Proposals) Get ¶ added in v0.30.0
func (p *Proposals) Get(id ProposalID) (Proposal, bool)
Get returns one proposal.
A record missing any of the three things a proposal is made of — a title, a base and its operations — is not one, and reads as absent rather than as a proposal that would do nothing. A map holds whatever key an applied operation names, so that is a state a peer can put this replica in.
func (*Proposals) List ¶ added in v0.30.0
List returns every proposal in the order they were raised.
The order is the (clock, site) one the map resolves its own writes by, taken from the write of the title — so it is a causal order where there is one, and settled by site where two replicas raised a proposal without seeing each other. Every replica reads the same list.
func (*Proposals) Open ¶ added in v0.30.0
Open returns the proposals nobody has decided about, oldest first.
func (*Proposals) OpsSince ¶ added in v0.30.0
OpsSince returns the operations a peer at v has not seen.
func (*Proposals) Pending ¶ added in v0.30.0
Pending reports how many operations are held back waiting for ones they depend on.
func (*Proposals) Preview ¶ added in v0.30.0
Preview returns the document as it would read with a proposal in it, without putting it in: a copy, loaded as site, with the operations applied.
site is what the copy would edit as if the caller went on to edit it, and is subject to the same rule Proposals.Draft states. Nothing here writes to it.
func (*Proposals) Put ¶ added in v0.30.0
Put writes a draft up as a proposal, and returns its identity.
A draft that changes nothing is ErrNoChange rather than an empty proposal: there is nothing for a reviewer to look at and nothing for accepting to do.
func (*Proposals) Snapshot ¶ added in v0.30.0
Snapshot returns the document as bytes, proposals included.
func (*Proposals) Version ¶ added in v0.30.0
func (p *Proposals) Version() crdt.CompositeVersion
Version returns what this replica holds.
func (*Proposals) Withdraw ¶ added in v0.30.0
func (p *Proposals) Withdraw(id ProposalID) (crdt.PartOps, error)
Withdraw records that a proposal was taken back or turned down. Its operations were never applied, so there is nothing to take out of the document; withdrawing costs one write and leaves the proposal readable, which is what a record of a decision is for.
A withdrawal of a proposal somebody else has already accepted is a label. See Proposals.Accept.
type Reading ¶ added in v0.28.0
type Reading struct {
// Site is the replica that wrote it.
Site crdt.SiteID
// Value is what that replica wrote, or nil if it cleared the register.
Value []byte
// Cleared is true when that replica took the value away rather than
// writing one. A clear that nobody has seen is as live as a value nobody
// has seen, which is what makes "somebody deleted this while I was
// renaming it" a thing a reader can be shown.
Cleared bool
}
A Reading is one replica's writing of the register, as it stands now.
type RecordMap ¶
type RecordMap struct {
// contains filtered or unexported fields
}
A RecordMap is a map of records built over a single crdt.Map: an observed- remove map whose values are themselves records, each a set of named fields, and each field an independent LWW-register.
Why the fields merge independently ¶
A record is not stored as one opaque value. Were it, two replicas that concurrently changed two different fields of the same record — one moved a node, the other recoloured it — would collide, and last-writer-wins would discard one of the two edits wholesale. Storing each field under its own map key makes the two edits touch different keys, so both survive and only a genuine same-field conflict is a conflict, resolved by the (clock, site) order.
Add and remove ¶
A record exists exactly while it has a live field. RecordMap.DeleteRecord removes the fields it can see, which is the whole record as this replica knows it. The policy for the race that leaves — a delete concurrent with a write to the same record — is the map's own: it is resolved per field by (clock, site), so a field written after the delete it did not see re-establishes the record, and one written before loses. That is last-writer-wins at the granularity of a field, stated once here rather than arbitrated case by case.
A RecordMap is not safe for concurrent use. The zero RecordMap is unusable — construct one with NewRecordMap, or view an existing map with RecordsOf.
func NewRecordMap ¶
NewRecordMap returns an empty record map that issues operations as site.
func RecordsOf ¶
RecordsOf views an existing map as a record map. The two share their storage, so an operation applied through one is seen by the other; it is how a document keeps a part's records beside its other parts.
func (*RecordMap) DeleteField ¶
DeleteField removes one field and returns the operation describing it. The record ceases to exist when its last field is removed.
func (*RecordMap) DeleteRecord ¶
DeleteRecord removes every live field of one record and returns the operations describing it, in ascending field order so the batch is deterministic. It removes what this replica can see; see RecordMap for the concurrent case.
func (*RecordMap) Fields ¶
Fields returns the live field names of one record, ascending, so that two replicas holding the same operations return the same slice.
func (*RecordMap) GetField ¶
GetField returns the value of one field and whether it is set. The value is a copy.
func (*RecordMap) HasRecord ¶
HasRecord reports whether a record has at least one live field — which is what it means for it to exist. It stops at the first, so it does not build the whole field list a membership test does not need. A foreign key an operation injected that this map's own writes could not have produced is not a field and does not make a record exist.
func (*RecordMap) Map ¶
Map returns the underlying map, whose Version, OpsSince, Apply and Snapshot are the record map's transport and persistence.
type Register ¶
type Register struct {
// contains filtered or unexported fields
}
A Register is a single last-writer-wins value: any number of replicas may write it at once, offline, in any delivery order, and every replica ends up holding the same value. Writes are ordered by the (Lamport clock, site) total order the crdt package defines — a maximum, so it does not depend on the order the writes arrive in — and the tie a shared clock would leave is broken by site, deterministically, without a wall clock ever being read.
It is the degenerate keyed map: one key, and nothing to merge that crdt.Map does not already merge. The whole of a Register is that restriction, so it carries no merge logic of its own and inherits the map's convergence, idempotence and wire format unchanged.
A Register is not safe for concurrent use. The zero Register is unusable — construct one with NewRegister or LoadRegister.
func LoadRegister ¶
LoadRegister rebuilds a register from a snapshot, to be written as site.
func NewRegister ¶
NewRegister returns an empty register that issues operations as site. Every replica writing to a register concurrently must pass a distinct site.
func (*Register) Apply ¶
Apply integrates operations from peers, tolerating duplicates and reordering.
func (*Register) Clear ¶
Clear removes the value and returns the operation describing it. Like a map deletion it keeps the write's clock, so a concurrent Set resolves against it by the same order rather than always winning.
func (*Register) Get ¶
Get returns the current value and whether one is set. A cleared register reads as absent. The value is a copy.
func (*Register) OpsSince ¶
func (r *Register) OpsSince(vv crdt.VersionVector) []crdt.MapOp
OpsSince returns the operations this replica holds that vv does not.
func (*Register) Set ¶
Set writes value and returns the operation describing it, already applied; send it to every peer. The register copies value.
func (*Register) Snapshot ¶
Snapshot encodes the whole register, for a joining peer or for persistence.
func (*Register) Version ¶
func (r *Register) Version() crdt.VersionVector
Version returns what this replica holds, to hand a peer that will send back what it is missing; see Register.OpsSince.
type RichText ¶ added in v0.23.0
type RichText struct {
// contains filtered or unexported fields
}
A RichText is text that carries formatting: bold, italic, a colour, a link, a comment — anything that covers a stretch of characters rather than one of them.
Why the marks are not in the text ¶
The obvious way to write formatting into a sequence CRDT is to put it there: a bold-on character, a bold-off character, or a per-character attribute. Both lose.
Markers in the sequence come apart. Two replicas that bold overlapping stretches produce interleaved on and off markers, and the text between them reads as bold on one replica and not on the other, because which marker won depends on where each landed rather than on what either person meant.
A per-character attribute does converge, and costs a write per character: a person selecting a paragraph and pressing bold sends one operation for every letter of it, forever, and each of those operations is stored forever.
How this one works ¶
A mark is one operation naming two anchors, and the formatting of the document is worked out when it is read. That is the shape Tree and Sequence use for the same reason: the answer is a function of the state, so two replicas holding the same operations read the same formatting whatever order it arrived in.
Where two marks of the same name disagree about a character — one bolding it, another taking bold away — the later of the two wins, by the (clock, site) order crdt.Map resolves its own writes by. Nothing is discarded: a mark that lost to a later one is still there, and a third mark can put it back.
What an anchor is, and why formatting grows the way it does ¶
An anchor is not an offset. An offset stored anywhere in a document people are editing means something else a moment later. An anchor names a character and a side of it: the boundary immediately before it, or immediately after it. Text typed into the gap that boundary sits in falls on one side or the other, and that is exactly the question "does what I am typing continue the bold".
Which side is the caller's to choose, because the answer differs by what the mark is. Typing at the end of a bold word should continue it, so bold ends at the boundary before the next character and grows. Typing at the end of a link should not become part of the link, so a link ends at the boundary after its last character and does not. See Expand.
func LoadRichText ¶ added in v0.23.0
LoadRichText rebuilds one from a snapshot, to be edited as site.
func NewRichText ¶ added in v0.23.0
NewRichText returns an empty rich text this site can edit.
func RichTextOf ¶ added in v0.23.0
RichTextOf reads a composite as a rich text, for a document that holds one among other parts.
func (*RichText) Composite ¶ added in v0.23.0
Composite returns the document underneath, which is what is snapshotted and what operations are applied to.
func (*RichText) Doc ¶ added in v0.23.0
Doc returns the text part, for anything this type does not wrap — anchors for a cursor, the authorship of a character.
func (*RichText) Mark ¶ added in v0.23.0
func (r *RichText) Mark(from, to int, name string, value []byte, expand Expand) (crdt.PartOps, error)
Mark puts a mark on the characters at [from, to), carrying value, which may be nil for a mark that is only on or off.
expand says whether text typed at either edge joins it; see Expand.
func (*RichText) OpsSince ¶ added in v0.23.0
OpsSince returns the operations a peer at v has not seen.
func (*RichText) Pending ¶ added in v0.23.0
Pending reports how many received operations are still waiting for the ones they depend on.
func (*RichText) Snapshot ¶ added in v0.23.0
Snapshot encodes the whole thing, text and formatting together.
func (*RichText) Spans ¶ added in v0.23.0
Spans returns the text broken into stretches over which the formatting does not change, in order, covering every character exactly once.
func (*RichText) Unmark ¶ added in v0.23.0
Unmark takes a mark off the characters at [from, to).
It is a mark of its own rather than the removal of one, because the mark it undoes may not have arrived yet, may cover more than this, and may be one of several. What it says is that these characters do not carry this name as of now, and a later mark can say otherwise.
func (*RichText) Version ¶ added in v0.23.0
func (r *RichText) Version() crdt.CompositeVersion
Version returns what this replica holds.
type RowID ¶
A RowID names a row for as long as the row exists, whatever concurrent edits do to the rows around it. It is a crdt.List element identity minted by the RGA, so it is unique across replicas, reload-safe, and never reused — the property a formula reference depends on.
type Sequence ¶ added in v0.21.0
type Sequence struct {
// contains filtered or unexported fields
}
A Sequence is an ordered collection whose items can be moved: the slides of a talk, the columns of a board, the order of a bibliography, the layers of a drawing, the rows of a list a person drags about.
Why not a list ¶
crdt.List is an RGA, and it is the right structure for text: it decides where a new character goes against every other character being typed at the same moment, and it does so per character. What it has no operation for is moving something that is already in it. Written with the operations it does have, a move is a delete and an insert — two operations, and a second replica moving the same item at the same time splits them, so the item ends up in both places or in neither.
How this one works ¶
An item carries where it sits as a value, the way a node of a Tree carries where it sits among its siblings: a rank, with another always available between any two, so moving an item is a single write of a single field. Two replicas that move the same item at once are then two writes to one field, which is a conflict crdt.Map already knows how to settle and settles the same way on both.
Items are read in order of (rank, identity). The identity is there because two replicas inserting at the same place at the same moment mint the same rank, and an order that stopped at the rank would not be an order.
What it gives up against a list ¶
The RGA's per-character judgement, which an opaque item does not need, and with it the property that an insert never has to be told about its neighbours. This has to read the ranks either side of where an item is going, which is one descent of a sorted slice rather than a walk of the collection.
func LoadSequence ¶ added in v0.21.0
LoadSequence reads a snapshot back, as the given site.
func NewSequence ¶ added in v0.21.0
NewSequence returns an empty sequence this site can edit.
func SequenceOf ¶ added in v0.21.0
SequenceOf reads a map as a sequence, for a map that is a part of a crdt.Composite.
func (*Sequence) At ¶ added in v0.21.0
At returns the item at a position, and whether there is one there.
func (*Sequence) IndexOf ¶ added in v0.21.0
IndexOf returns where an item sits, or -1 if it is not there.
func (*Sequence) Insert ¶ added in v0.21.0
Insert puts a new item holding value after the item named by after, or at the front for SeqStart.
A nil value writes no value at all, which is one operation fewer and is what a sequence whose items are identities rather than contents wants — the axes of a Sheet, where the item is the row and the row has no value of its own. It is not the same as an empty value, which is written.
func (*Sequence) Map ¶ added in v0.21.0
Map returns the map underneath, which is what is snapshotted and what operations are applied to.
func (*Sequence) Move ¶ added in v0.21.0
Move puts an item after the item named by after, or at the front for SeqStart. It is one operation: an item's place is one field.
func (*Sequence) OpsSince ¶ added in v0.21.0
func (s *Sequence) OpsSince(vv crdt.VersionVector) []crdt.MapOp
OpsSince returns the operations a peer at vv has not seen.
func (*Sequence) Records ¶ added in v0.21.0
Records returns the record map underneath, for reading an item's own fields.
func (*Sequence) SetField ¶ added in v0.21.0
SetField sets one of an item's own fields, beside what it holds.
func (*Sequence) Value ¶ added in v0.21.0
Value returns what an item holds, and whether the item exists.
func (*Sequence) Version ¶ added in v0.21.0
func (s *Sequence) Version() crdt.VersionVector
Version returns what this replica has seen.
type Set ¶ added in v0.29.0
type Set struct {
// contains filtered or unexported fields
}
A Set is a collection of names any number of replicas may add to and take from at once: the labels on a card, the people in a conversation, the layers that are showing, the tags on a document.
Why a map of flags is not one ¶
The obvious way to share a set is a crdt.Map keyed by the name, holding a flag or nothing at all. It converges, and the case it converges badly on is the one that happens: Ada adds "urgent" while Grace, who has never seen it, takes it away. Both wrote the same key, so one of the two writes wins by the (clock, site) order — and which one is nothing to do with what either of them knew. Grace can remove a label she has never been shown.
How this one works ¶
Every addition mints a tag of its own, and a name is in the set while it has at least one tag. Removing a name takes away the tags this replica can see, which is the removal that was actually asked for: these ones, the ones the person was looking at.
A tag nobody has seen is untouched by that, so an addition concurrent with a removal survives it. This is usually stated as a policy — "add wins" — and it is better read as the absence of one. A removal says what it observed. There is no rule here about an addition it did not observe, because there is nothing to base one on: neither replica knew about the other, and inventing a winner would be inventing knowledge.
It is a RecordMap with the fields used as tags and never written twice, so the merging is the map's, unchanged: a record exists while it has a live field, and a field written after a deletion it did not see re-establishes it.
What it costs ¶
One map entry per addition that has not been removed, and — because a tag is an identity, and an identity here is the identity of an operation — two operations per addition rather than one. Adding a name that is already there mints another tag rather than doing nothing, and it has to: a removal still on its way would otherwise take the name away, having seen every tag it had.
Example ¶
A Set is a collection of names, where a removal takes away what it saw and nothing else. Here two replicas edit the labels on a card at the same moment: Grace clears the labels she has been shown while Ada adds one Grace has never seen, and the new label survives — which a map keyed by the label would have settled by an order that has nothing to do with what either knew.
package main
import (
"fmt"
"github.com/go-crdt/crdt/structured"
)
func main() {
ada, grace := structured.NewSet(1), structured.NewSet(2)
shared, _ := ada.Add("draft")
grace.Apply(shared...)
// Offline: Ada labels it urgent, Grace clears what she can see.
added, _ := ada.Add("urgent")
removed, _ := grace.Remove("draft")
ada.Apply(removed...)
grace.Apply(added...)
fmt.Println(grace.Names())
fmt.Println(grace.Adders("urgent"))
}
Output: [urgent] [1]
func SetOf ¶ added in v0.29.0
SetOf reads a map as a set, for a map that is a part of a crdt.Composite.
func (*Set) Add ¶ added in v0.29.0
Add puts a name in the set.
It is two operations: one to mint the tag, which has to be an identity no replica can mint twice, and one to write it. Both must reach a peer, or the name arrives with a tag that replica could mint again after a reload.
Adding a name that is already there is not nothing. It mints another tag, because a removal still on its way has seen every tag the name had, and would otherwise take it away.
func (*Set) Adders ¶ added in v0.29.0
Adders returns the replicas whose additions of a name are still standing, in order and without repetition. It is what "who put this label on" asks, and it is free: a tag is the identity of the operation that minted it, and an identity carries its site.
A name held only by tags this version cannot read returns nothing, which is the same answer as a name nobody added.
func (*Set) Apply ¶ added in v0.29.0
Apply integrates operations from peers, tolerating duplicates and reordering.
func (*Set) Map ¶ added in v0.29.0
Map returns the map underneath, which is what is snapshotted and what operations are applied to.
func (*Set) Names ¶ added in v0.29.0
Names returns everything in the set, in order, so that two replicas holding the same set list it the same way.
func (*Set) OpsSince ¶ added in v0.29.0
func (s *Set) OpsSince(vv crdt.VersionVector) []crdt.MapOp
OpsSince returns the operations this replica holds that vv does not.
func (*Set) Records ¶ added in v0.29.0
Records returns the record map underneath, for a caller that wants to read the tags themselves.
func (*Set) Remove ¶ added in v0.29.0
Remove takes a name out of the set, by taking away the tags this replica can see. A tag added concurrently, which this replica has not seen, is not one of them and the name stays.
A name that is not in the set is ErrNoChange rather than a silent nothing, so a caller never has to decide whether an empty batch is safe to send.
func (*Set) Snapshot ¶ added in v0.29.0
Snapshot encodes the whole set, for a joining peer or for persistence.
func (*Set) Tags ¶ added in v0.29.0
Tags returns how many additions of a name are still standing. Two is two replicas that added it without seeing each other, or one replica that added it twice — and it is what a removal will have to take away.
func (*Set) Version ¶ added in v0.29.0
func (s *Set) Version() crdt.VersionVector
Version returns what this replica holds, to hand a peer that will send back what it is missing; see Set.OpsSince.
type Sheet ¶
type Sheet struct {
// contains filtered or unexported fields
}
A Sheet is a collaborative spreadsheet: rows and columns are two ordered collections of stable identities, and cells are a map keyed by a (row, column) identity pair. Editing it produces operations that any number of replicas may exchange, offline and in any order, and every replica converges to the same sheet.
It is one crdt.Composite — the same core a Diagram is — so it borrows the crdt package's convergence rather than restating it. What this type adds is the addressing: a cell named by the identities of its row and column, so that a concurrent change to the sheet's shape leaves every other cell, and every formula reference, exactly where it was.
Why the axes are sequences and not lists ¶
They were crdt.List — an RGA — and a row could then be added and removed but not moved. Dragging a row to another place had to be written as a delete and an insert, which is two operations, and a second replica dragging the same row at the same time splits them: the row ends up twice over, or not at all, and its cells follow whichever copy survives.
Sequence carries a row's place as a rank, so moving one is a single write and two replicas moving the same row are two writes to one field — a conflict crdt.Map already settles. The identity is untouched by a move, so the cells and every formula reference come with it for free.
A Sheet is not safe for concurrent use. Construct one with NewSheet or LoadSheet.
Example ¶
A spreadsheet and a diagram are two thin wrappers over one collaborative core. Here two replicas of a sheet edit two different cells while disconnected, then exchange operations and converge — the same merge a Diagram would use.
package main
import (
"fmt"
"github.com/go-crdt/crdt/structured"
)
func main() {
ada, grace := structured.NewSheet(1), structured.NewSheet(2)
// Ada lays out a row and a column and shares them.
row, r1, _ := ada.AppendRow()
col, r2, _ := ada.AppendCol()
grace.Apply(r1, r2)
// Offline, each writes a different cell.
fromAda, _ := ada.SetCell(row, col, structured.Literal("42"))
other, _, _ := grace.AppendRow()
fromGrace, _ := grace.SetCell(other, col, structured.Literal("7"))
// They swap operations, in either order, and agree.
ada.Apply(fromGrace)
grace.Apply(fromAda)
cell, _ := grace.GetCell(row, col)
fmt.Println(cell.Text, len(grace.Rows()))
}
Output: 42 2
func NewSheet ¶
NewSheet returns an empty sheet that issues operations as site. Every replica editing a sheet concurrently must pass a distinct crdt.SiteID.
func (*Sheet) AppendCol ¶
AppendCol adds a column after the last and returns its identity and the operations to broadcast.
func (*Sheet) AppendRow ¶
AppendRow adds a row after the last and returns its identity and the operations to broadcast.
func (*Sheet) Apply ¶
Apply integrates batches of operations from peers, tolerating duplicates and reordering exactly as the underlying parts do.
func (*Sheet) ClearCell ¶
ClearCell removes the cell at the intersection of row and col and returns the operation to broadcast.
func (*Sheet) DeleteCol ¶
DeleteCol removes the column at index pos and returns the operations to broadcast, on the same terms as Sheet.DeleteRow.
func (*Sheet) DeleteRow ¶
DeleteRow removes the row at index pos and returns the operations to broadcast. The cells of a removed row are left in place, unreferenced by any live row, so they neither show through Sheet.Rows nor cost a second batch to remove.
func (*Sheet) GetCell ¶
GetCell returns the cell at the intersection of row and col and whether one is set. A cell whose stored bytes are not a cell this package wrote — which a peer may inject — reads as absent.
func (*Sheet) InsertCol ¶
InsertCol adds a column at index pos and returns its identity and the operations to broadcast.
func (*Sheet) InsertRow ¶
InsertRow adds a row at index pos and returns its identity and the operations to broadcast. pos may equal Sheet.RowCount, which appends.
func (*Sheet) MoveCol ¶ added in v0.22.0
MoveCol moves the column at index from to index to, on the same terms as Sheet.MoveRow.
func (*Sheet) MoveRow ¶ added in v0.22.0
MoveRow moves the row at index from to index to, and returns the operation to broadcast. It is one operation: a row's place is one field.
The row keeps its identity, so its cells and every formula naming them come with it and nothing else in the sheet moves.
func (*Sheet) OpsSince ¶
OpsSince returns the operations this replica holds that v does not, batched by part and ready to send to the peer that produced v. Pass a nil version for everything.
func (*Sheet) Pending ¶
Pending reports how many received operations are still waiting, across every part, for the operations they depend on.
func (*Sheet) Rows ¶
Rows returns the identities of the rows present, in order. Two replicas holding the same operations return the same slice.
func (*Sheet) SetCell ¶
SetCell writes cell at the intersection of row and col and returns the operation to broadcast. The cell is addressed by identity, so the write lands on the same cell on every replica however the sheet's shape has changed.
func (*Sheet) Snapshot ¶
Snapshot encodes the whole sheet, for a joining peer or for persistence. Two replicas holding the same operations produce identical bytes.
func (*Sheet) Version ¶
func (s *Sheet) Version() crdt.CompositeVersion
Version returns what this replica holds, to hand a peer that will send back what it is missing; see Sheet.OpsSince.
type Span ¶ added in v0.23.0
type Span struct {
// Pos is where the stretch starts, in visible characters.
Pos int
// Text is what it holds.
Text string
// Marks is the formatting covering all of it, by name. A mark with no value
// of its own — bold — has a nil value; a mark that carries one — a colour,
// the target of a link — has it here.
Marks map[string][]byte
}
A Span is a stretch of the text over which the formatting does not change.
type StrokeID ¶ added in v0.25.0
type StrokeID = ItemID
A StrokeID names a stroke. It is a Sequence item, so the sequence's own ordering and fields apply to it.
type Tree ¶ added in v0.20.0
type Tree struct {
// contains filtered or unexported fields
}
A Tree is a collaborative tree whose nodes can be moved: a file tree, the outline of a document, the grouping of a diagram, a thread of replies.
Why moving is the whole problem ¶
Every other structure here is a map of records, and a tree could be one too: a node is a record, and one of its fields names its parent. That much merges on its own, because a parent is a single value and Register already says how two replicas that write one at the same time agree.
What does not merge on its own is the shape. Two replicas can each move a node under the other, and each move is fine by itself: A under B is a tree, B under A is a tree, both together are a ring floating free of the root. Nothing in the operations is wrong, and no amount of ordering them differently helps — the pair is what is wrong, and it only exists once they meet.
The same is true of deleting: one replica removes a folder while another moves a file into it, and neither operation is in conflict with the other, but the file is now under a node that is not there.
How this one answers ¶
Both are answered when the tree is read rather than when an operation is applied, by rules that are a function of the state alone. Every replica holding the same state reads the same tree out of it, whatever order the operations arrived in, which is what convergence asks for:
A node whose parent is not a live node is read as a child of the root. So deleting a folder does not delete a file a peer concurrently moved into it; the file resurfaces. That direction is deliberate: a tree that loses work to a concurrent delete cannot be trusted with a project.
A node that cannot reach the root is in a ring, or below one. The ring is broken at the node whose parent was written last, by the same (clock, site) order the map underneath resolves writes by — the move that arrived into an already-moved tree is the one that gives way — and that node is read as a child of the root.
Neither rule discards an operation. The state keeps every move that was ever made, and a later move can put back what a ring detached; what the rules decide is only what the tree looks like now.
Where a node sits among its siblings ¶
A node carries its order as a rank rather than living in a list per parent; see rank.go for why that is a single field write and a list would not be. Siblings are read in order of (rank, identity), which is total, so two replicas read the same order.
func TreeOf ¶ added in v0.20.0
TreeOf reads a map as a tree, for a map that is a part of a crdt.Composite.
func (*Tree) Children ¶ added in v0.20.0
Children returns the children of a node, in order. Passing the root returns the nodes at the top of the tree.
func (*Tree) Depth ¶ added in v0.20.0
Depth returns how far a node is below the root, the top of the tree being depth one, and whether the node exists.
func (*Tree) Insert ¶ added in v0.20.0
Insert adds a node under parent, after the sibling named by after. A root TreeID for after puts it first.
It returns the new node's identity and the operations that made it.
func (*Tree) Map ¶ added in v0.20.0
Map returns the map underneath, which is what is snapshotted and what operations are applied to.
func (*Tree) Move ¶ added in v0.20.0
Move puts node under parent, after the sibling named by after. A root TreeID for after puts it first.
Moving a node under itself or under one of its own descendants is refused here, because a replica can see that it is about to make a ring and there is no reason to make one. It is only when two replicas each make a legal move that a ring can appear, and that is what reading the tree resolves.
func (*Tree) Nodes ¶ added in v0.20.0
Nodes returns every node in the tree, in depth-first order from the root, which is the order a file tree or an outline is read in.
func (*Tree) OpsSince ¶ added in v0.20.0
func (t *Tree) OpsSince(vv crdt.VersionVector) []crdt.MapOp
OpsSince returns the operations a peer at vv has not seen.
func (*Tree) Parent ¶ added in v0.20.0
Parent returns the node's parent as the tree reads now, and whether the node exists. A node at the top of the tree has the root as its parent, which is reported as a root TreeID and true.
func (*Tree) Records ¶ added in v0.20.0
Records returns the record map underneath, for setting and reading a node's own fields.
func (*Tree) Remove ¶ added in v0.20.0
Remove deletes a node. Its children are read as children of the root, by the rule this type states: a concurrent move into a node being deleted keeps what was moved.
RemoveSubtree is what deleting a folder and its contents means.
func (*Tree) RemoveSubtree ¶ added in v0.20.0
RemoveSubtree deletes a node and everything under it, as the tree reads now.
func (*Tree) Version ¶ added in v0.20.0
func (t *Tree) Version() crdt.VersionVector
Version returns what this replica has seen.
type TreeID ¶ added in v0.20.0
A TreeID names a node.
type Undo ¶ added in v0.26.0
type Undo struct {
// contains filtered or unexported fields
}
An Undo puts back what this replica did, and only what this replica did.
Why a stack of edits is not enough ¶
Undo in a document one person is editing is a stack of states: keep the old one, put it back. That is wrong here, and quietly so. Between making an edit and undoing it, other people have been editing too — their work is in the document, and restoring a remembered state would throw it away. Worse, an undo that replaced the document would travel to them as "the document is now this", and take their work away on their screens as well.
So an undo is not a restoration. It is a new edit, made now, that has the effect of the old one not having happened — and it travels as an ordinary edit, which is why a peer needs no code to receive one.
What that means for what it can promise ¶
- Undoing an insertion removes those characters, wherever they have since moved to, and however much has been typed around them. They are named by identity, not by where they were.
- Undoing a removal puts the text back after the character it followed. If somebody has typed at that spot in the meantime, both replicas agree about the order, because the sequence decides it and not this.
- Undoing something a peer has already undone by other means does nothing, rather than doing it twice.
What it does not promise is that the document returns to a state it was in. It cannot, because that state was never the shared one.
What it records ¶
Edits go through this rather than through the document, because inverting one afterwards is not possible from what the document keeps: a removal has to know the text that went, and a document that has removed it no longer says. Undo.Insert and Undo.Delete take that note as they go.
An Undo is not safe for concurrent use.
func NewUndo ¶ added in v0.26.0
NewUndo watches a document. Only the edits made through it are undoable, which is what makes an undo this replica's own and not everybody's.
func (*Undo) Begin ¶ added in v0.26.0
func (u *Undo) Begin()
Begin starts a group: everything until Undo.Commit is put back by one Undo. It is what makes typing a word undo as a word rather than a letter at a time, and the caller decides where a word ends because only the caller knows.
Begin inside a group does nothing, so a caller need not track whether one is open.
func (*Undo) CanUndo ¶ added in v0.26.0
CanUndo and CanRedo report whether there is anything to put back or do again.
func (*Undo) Commit ¶ added in v0.26.0
func (u *Undo) Commit()
Commit closes the group. A group that turned out to be empty leaves nothing to undo, so pressing undo afterwards reaches past it to the edit before.
func (*Undo) Delete ¶ added in v0.26.0
Delete removes count characters from a visible offset and returns the operations to broadcast.
func (*Undo) Insert ¶ added in v0.26.0
Insert puts text in at a visible offset and returns the operations to broadcast, as crdt.Doc.Insert does.
func (*Undo) Redo ¶ added in v0.26.0
Redo does again what the last Undo put back. It returns ErrNoChange when there is nothing, which includes after any new edit: making one says what the document is to become, and what Redo was holding is no longer part of it.
func (*Undo) Undo ¶ added in v0.26.0
Undo puts back the last edit, or the last group, and returns the operations to broadcast. It returns ErrNoChange when there is nothing to put back.
An open group is closed first, so a caller that began one and then asked for an undo gets what it has just done rather than what came before it.