lattice_sequence/sequence
A generic sequence CRDT using stable item IDs and YATA-style origins.
Each live item is stored with a stable internal ID plus left and right
origins for deterministic ordering. Deletes are represented as tombstones
that record the delete’s op ID; values returns only non-deleted items.
The public editing API exposes index-based insert, delete, and move operations while resolving stable item IDs internally, so callers do not need to construct or manage item identifiers. Moves preserve item identity and converge with single-winner semantics for concurrent moves of the same item.
Compaction
Long-lived sequences never shrink on their own: every delete leaves a
tombstone and every item carries origins. compact takes a stability
frontier — a VersionVector meaning “everything causally at or below
this is stable; no in-flight or future op references it” — and rewrites
the stable region: stable tombstones are dropped, runs of adjacent stable
items from the same replica with sequential counters are merged into
compact blocks, and origins of stable items are discarded. Items above
the frontier keep their full YATA representation.
Every dropped ID gets a forwarding entry pointing at its retained neighbors, so anchors and rebased operations that still hold the ID can resolve to the gap it left behind. The forwarding map and the applied frontier travel with the state. Deriving a correct frontier is the host’s job (e.g. from a global sequencer’s acknowledgement floor); the frontier must be a causal cut over the ops applied to the sequence.
Example
import lattice_core/replica_id
import lattice_sequence/sequence
let list =
sequence.new(replica_id.new("node-a"))
|> sequence.insert(0, "hello")
|> sequence.insert(1, "world")
|> sequence.move(0, 1)
sequence.values(list) // -> ["world", "hello"]
Types
A stable position between items (positions 0..length inclusive).
Anchors are created from a visible index with anchor_at and resolved
back to a current index with resolve after any sequence of local edits
and merges. They live outside the CRDT state: creating and resolving
anchors never mutates the sequence.
pub opaque type Anchor
An error returned when an anchor cannot be created or resolved.
pub type AnchorError {
AnchorIndexOutOfBounds(index: Int, length: Int)
UnknownAnchorTarget
}
Constructors
-
AnchorIndexOutOfBounds(index: Int, length: Int) -
UnknownAnchorTargetThe anchor references an item this replica cannot locate: either it was created remotely and not merged yet, or it was compacted away and its forwarding entry has expired. Treat this as “re-anchor”.
Which side of its gap an anchor sticks to when content is inserted exactly at the anchor position.
pub type Bias {
Before
After
}
Constructors
-
BeforeAttach to the item after the gap: inserts at the gap push the anchor right, so it stays glued to its item.
-
AfterAttach to the item before the gap: inserts at the gap land after the anchor, so it stays put.
An error returned when a delete cannot be applied.
pub type DeleteError {
DeleteIndexOutOfBounds(index: Int, length: Int)
}
Constructors
-
DeleteIndexOutOfBounds(index: Int, length: Int)
The forwarding entries emitted by one compact pass.
Each entry maps a dropped item ID to the gap it left behind. The library
keeps the cumulative map inside the sequence for anchor resolution and
origin translation; retention is the host’s policy — keep the map returned
by each compact round and expire old rounds with remove_forwardings.
pub opaque type ForwardingMap
An error returned when an insert cannot be applied.
pub type InsertError {
IndexOutOfBounds(index: Int, length: Int)
}
Constructors
-
IndexOutOfBounds(index: Int, length: Int)
An error returned when a move cannot be applied.
pub type MoveError {
MoveFromIndexOutOfBounds(index: Int, length: Int)
MoveToIndexOutOfBounds(index: Int, length_after_removal: Int)
}
Constructors
-
MoveFromIndexOutOfBounds(index: Int, length: Int) -
MoveToIndexOutOfBounds(index: Int, length_after_removal: Int)
An error returned when a delta’s origins cannot be translated onto a compacted state.
pub type TranslateError {
UnknownOriginTarget
}
Constructors
-
UnknownOriginTargetAn origin references an ID that is neither present nor forwarded — its forwarding entry has expired. The host must degrade the op (e.g. re-insert by position) or drop it.
Values
pub fn anchor_at(
sequence: Sequence(a),
index: Int,
bias: Bias,
) -> Anchor
Create an anchor at the gap before the visible item at index.
Before bias binds the anchor to the item at index; After bias binds
it to the item at index - 1. Boundary positions with no item on the
chosen side degrade to the start / end sentinels.
pub fn anchor_from_json(
json_string: String,
) -> Result(Anchor, json.DecodeError)
Decode an anchor from a JSON string produced by anchor_to_json.
pub fn anchor_to_json(anchor: Anchor) -> json.Json
Encode an anchor as a self-describing JSON value.
Produces an envelope with type, v (schema version), and anchor, so
anchors can travel between replicas (e.g. shared cursors).
pub fn compact(
sequence: Sequence(a),
stable: version_vector.VersionVector,
) -> #(Sequence(a), ForwardingMap)
Compact everything at or below a stability frontier.
stable must describe a causal cut the host knows no in-flight or future
op can reference (e.g. the version vector accumulated by replaying ops up
to a global sequencer’s acknowledgement floor). For the stable region the
pass drops tombstones, merges runs of adjacent same-replica items with
sequential counters into blocks, and strips origins and move slots.
Returns the compacted sequence and the forwarding entries emitted by this
pass (one per dropped ID). The cumulative forwarding map is also carried
in the sequence; hosts bound its growth with remove_forwardings.
Compacting at the current frontier, at an older one, or at one concurrent with it is a no-op — frontiers only advance.
A state holding ANY move record is left unchanged, even when the move op is
already covered by stable. This guard is load-bearing for convergence, not
just conservatism: baking a settled move into the compact skeleton fixes its
position from only the compacting replica’s items, but a peer’s still-live
concurrent inserts above the frontier integrate against the moved item’s
origins — which stabilization strips. The two then order those inserts
differently and merge stops commuting (see the property test
merge_commutes_with_compaction_for_deltas_above_frontier). Because move
records are never cleared locally, a replica that uses move cannot compact
until it merges a peer that has already stabilized the item into a block; a
safe stabilization path for moves is future work (see issue #98).
pub fn delete(sequence: Sequence(a), index: Int) -> Sequence(a)
Delete the value at the visible item index.
Panics with DeleteIndexOutOfBounds when index is outside
[0, length). Use try_delete_with_delta to handle an untrusted index
without crashing.
pub fn delete_with_delta(
sequence: Sequence(a),
index: Int,
) -> #(Sequence(a), Sequence(a))
Delete a value and return both the updated sequence and deletion delta.
Panics with DeleteIndexOutOfBounds when index is outside
[0, length). Use try_delete_with_delta to handle an untrusted index
without crashing.
pub fn end_anchor() -> Anchor
Create an anchor at the end of the sequence. Always resolves to the current visible length, tracking growth.
pub fn forwarding_size(map: ForwardingMap) -> Int
The number of entries in a forwarding map.
pub fn from_json(
json_string: String,
value_decoder: decode.Decoder(a),
) -> Result(Sequence(a), json.DecodeError)
Decode a sequence CRDT from a JSON string produced by to_json.
Returns Ok(Sequence) on success, or Error(json.DecodeError) if the
input is not a valid sequence JSON envelope. Live items are reordered
deterministically from their stable origins before the Sequence is
returned.
pub fn frontier(
sequence: Sequence(a),
) -> version_vector.VersionVector
The stability frontier this sequence was last compacted at.
Empty until the first compact call. Carried in the state so merging can
tell which side is compacted further.
pub fn insert(
sequence: Sequence(a),
index: Int,
value: a,
) -> Sequence(a)
Insert a value at the visible item index.
Panics with IndexOutOfBounds when index is outside [0, length]. Use
try_insert_with_delta to handle an untrusted index without crashing.
pub fn insert_many(
sequence: Sequence(a),
index: Int,
values: List(a),
) -> Sequence(a)
Insert several values at consecutive visible indices starting at index.
values are placed in order — the first at index, the next at
index + 1, and so on — exactly as looping insert would, but the whole
run is spliced in a single pass and reported as one delta. Panics with
IndexOutOfBounds when index is outside [0, length]. Use
try_insert_many_with_delta to handle an untrusted index without crashing.
pub fn insert_many_with_delta(
sequence: Sequence(a),
index: Int,
values: List(a),
) -> #(Sequence(a), Sequence(a))
Insert several values and return both the updated sequence and the merged insertion delta covering every new item.
Panics with IndexOutOfBounds when index is outside [0, length]. Use
try_insert_many_with_delta to handle an untrusted index without crashing.
pub fn insert_with_delta(
sequence: Sequence(a),
index: Int,
value: a,
) -> #(Sequence(a), Sequence(a))
Insert a value and return both the updated sequence and insertion delta.
Panics with IndexOutOfBounds when index is outside [0, length]. Use
try_insert_with_delta to handle an untrusted index without crashing.
pub fn merge(a: Sequence(a), b: Sequence(a)) -> Sequence(a)
Merge two sequence CRDT states.
Items are joined by their stable IDs. Concurrent deletes are preserved by keeping the winning delete op, and the merged item set is deterministically reordered using each item’s left and right origins. Stable blocks form a fixed skeleton that live items are ordered around.
States compacted at different frontiers merge as long as one frontier dominates the other (with a global sequencer, floors are totally ordered so this always holds). An item absent from the further-compacted side and covered by its frontier is treated as compacted away and stays dropped.
pub fn move(
sequence: Sequence(a),
from_index: Int,
to_index: Int,
) -> Sequence(a)
Move a visible item to another visible index.
The to_index is interpreted after removing the item from from_index.
Panics with a MoveError when either index is out of bounds. Use
try_move_with_delta to handle untrusted indices without crashing.
pub fn move_with_delta(
sequence: Sequence(a),
from_index: Int,
to_index: Int,
) -> #(Sequence(a), Sequence(a))
Move a visible item and return both the updated sequence and move delta.
The to_index is interpreted after removing the item from from_index.
Panics with a MoveError when either index is out of bounds. Use
try_move_with_delta to handle untrusted indices without crashing.
pub fn new(replica_id: replica_id.ReplicaId) -> Sequence(a)
Create an empty sequence for a replica.
pub fn remove_forwardings(
sequence: Sequence(a),
map: ForwardingMap,
) -> Sequence(a)
Remove previously emitted forwarding entries from the sequence.
Forwardings are bounded by the host’s retention policy: keep the map
returned by each compact round and expire old rounds by passing them
here. Anchors and deltas referencing removed entries hard-fail
(UnknownAnchorTarget / UnknownOriginTarget) and must re-anchor or
resync.
pub fn resolve(sequence: Sequence(a), anchor: Anchor) -> Int
Resolve an anchor to a current visible index in [0, length].
Anchors on deleted items still resolve: both biases collapse to the gap
where the item used to be. Anchors follow moved items. Panics with
UnknownAnchorTarget when the target was never merged or was compacted
and its forwarding has expired — hosts holding anchors across compaction
rounds should use try_resolve and treat failure as “re-anchor”.
pub fn start_anchor() -> Anchor
Create an anchor at the start of the sequence. Always resolves to 0.
pub fn to_json(
sequence: Sequence(a),
encode_value: fn(a) -> json.Json,
) -> json.Json
Encode a sequence CRDT as a self-describing JSON value.
Produces an envelope with type, v (schema version), and state. The
state includes this replica ID, local counter, applied compaction
frontier, forwarding entries, and every segment: compact blocks of stable
values and full items including tombstones.
pub fn translate_origins(
delta: Sequence(a),
onto: Sequence(a),
) -> Result(Sequence(a), TranslateError)
Translate a delta’s origins onto a compacted state.
Rebase support for evicted clients: origins (including move origins)
referencing compacted IDs are rewritten through onto’s forwarding map to
the gap the ID left behind. Items whose own ID was compacted away are
dropped from the delta — the op is already settled. Returns
Error(UnknownOriginTarget) when an origin is neither present, part of
the delta itself, nor forwarded (the forwarding expired); the host must
degrade the op to a positional edit or discard it.
pub fn try_anchor_at(
sequence: Sequence(a),
index: Int,
bias: Bias,
) -> Result(Anchor, AnchorError)
Safely create an anchor at the gap before the visible item at index.
Valid positions are 0 <= index <= length.
pub fn try_delete_with_delta(
sequence: Sequence(a),
index: Int,
) -> Result(#(Sequence(a), Sequence(a)), DeleteError)
Safely delete a value and return both the updated sequence and deletion delta.
Deletes mint an op ID (bumping this replica’s counter) so a compaction frontier can distinguish acknowledged deletes from in-flight ones.
pub fn try_insert_many_with_delta(
sequence: Sequence(a),
index: Int,
values: List(a),
) -> Result(#(Sequence(a), Sequence(a)), InsertError)
Safely insert several values at consecutive visible indices starting at
index, returning the updated sequence and a single delta of all new
items.
Each new item’s left origin is the previous new item (the first pins to the visible left neighbor) and every item shares the same right origin — the left neighbor’s canonical successor — so the run integrates contiguously on every replica. When the state holds no live move record, stored order is already the canonical order, so the run is spliced directly in place rather than re-deriving the whole order; otherwise it falls back to a full rebuild.
pub fn try_insert_with_delta(
sequence: Sequence(a),
index: Int,
value: a,
) -> Result(#(Sequence(a), Sequence(a)), InsertError)
Safely insert a value and return both the updated sequence and insertion delta.
pub fn try_move_with_delta(
sequence: Sequence(a),
from_index: Int,
to_index: Int,
) -> Result(#(Sequence(a), Sequence(a)), MoveError)
Safely move a visible item and return both the updated sequence and move delta.
pub fn try_resolve(
sequence: Sequence(a),
anchor: Anchor,
) -> Result(Int, AnchorError)
Safely resolve an anchor to a current visible index in [0, length].
Anchors to compacted items resolve through the forwarding map to the gap the item left behind — semantically the same as tombstone collapse.
Returns Error(UnknownAnchorTarget) when the anchor references an item
this replica has never seen (created remotely and not yet merged), or one
that was compacted away and whose forwarding entry has since been removed
by the host’s retention policy. Either way the anchor is unusable and the
holder should re-anchor.