Documentation
¶
Overview ¶
Package encoding implements the V1 wire format yrs and JS Yjs use for state vectors, delete sets, and document updates.
Per docs/yrs-port-notes/update-v1.md the V1 wire format has three load-bearing pieces:
- StateVector: varuint count followed by (clientID, clock) pairs.
- IdSet (the wire DeleteSet): per-client (id, range count, ranges of (start, length)) — half-open ranges, written as length not end.
- Update: per-client block runs followed by an embedded IdSet.
This package owns StateVector and IdSet end-to-end. Update encode and decode arrive in subsequent commits; they will compose StateVector and IdSet here.
All primitive byte operations route through internal/lib0, which is already verified byte-equivalent with JS lib0 across 40 fixtures.
Determinism: encoding sorts clients ascending by clientID before emission. yrs's StateVector and IdSet iterate HashMap / BTreeMap (BTreeMap = ascending; HashMap = arbitrary). JS Yjs iterates JS Map in insertion order. To produce reproducible bytes that round-trip across runs and runtimes we always sort here. Decoders accept any order — the wire is unambiguous.
Index ¶
- Constants
- Variables
- func ApplyUpdate(d *doc.Doc, raw []byte) error
- func ApplyUpdateV2(d *doc.Doc, raw []byte) error
- func DecodeAny(buf []byte) (any, []byte, error)
- func DecodeContent(buf []byte, refNum uint8) (block.Content, []byte, error)
- func DecodeContentV2(dec *DecoderV2, refNum uint8) (block.Content, error)
- func DecodeStateVector(buf []byte) (store.StateVector, []byte, error)
- func EncodeAny(buf []byte, v any) []byte
- func EncodeContent(buf []byte, c block.Content) []byte
- func EncodeContentV2(enc *EncoderV2, c block.Content)
- func EncodeDiff(d *doc.Doc, txn *doc.Transaction, remoteSV store.StateVector) []byte
- func EncodeDiffV2(d *doc.Doc, txn *doc.Transaction, remoteSV store.StateVector) []byte
- func EncodeStateAsUpdate(d *doc.Doc) []byte
- func EncodeStateAsUpdateV2(d *doc.Doc) []byte
- func EncodeStateVector(sv store.StateVector, buf []byte) []byte
- func HasPending(t *doc.Transaction) bool
- func MissingSV(t *doc.Transaction) store.StateVector
- type Block
- type BlockKind
- type DecoderV2
- func (d *DecoderV2) AdvanceRest(n int)
- func (d *DecoderV2) ReadAny() (block.Any, error)
- func (d *DecoderV2) ReadClient() (uint64, error)
- func (d *DecoderV2) ReadInfo() (uint8, error)
- func (d *DecoderV2) ReadJSON() (block.Any, error)
- func (d *DecoderV2) ReadKey() (string, error)
- func (d *DecoderV2) ReadLeftID() (block.ID, error)
- func (d *DecoderV2) ReadLen() (uint64, error)
- func (d *DecoderV2) ReadParentInfo() (bool, error)
- func (d *DecoderV2) ReadRestBytes() []byte
- func (d *DecoderV2) ReadRightID() (block.ID, error)
- func (d *DecoderV2) ReadString() (string, error)
- func (d *DecoderV2) ReadTypeRef() (uint8, error)
- func (d *DecoderV2) ReadVarBuf() ([]byte, error)
- func (d *DecoderV2) ReadVarUint() (uint64, error)
- type EncoderV2
- func (e *EncoderV2) Bytes() []byte
- func (e *EncoderV2) WriteAny(v block.Any)
- func (e *EncoderV2) WriteClient(client uint64)
- func (e *EncoderV2) WriteInfo(info uint8)
- func (e *EncoderV2) WriteJSON(v block.Any)
- func (e *EncoderV2) WriteKey(key string)
- func (e *EncoderV2) WriteLeftID(id block.ID)
- func (e *EncoderV2) WriteLen(l uint64)
- func (e *EncoderV2) WriteParentInfo(isRootName bool)
- func (e *EncoderV2) WriteRestBytes(b []byte)
- func (e *EncoderV2) WriteRightID(id block.ID)
- func (e *EncoderV2) WriteString(s string)
- func (e *EncoderV2) WriteTypeRef(ref uint8)
- func (e *EncoderV2) WriteVarBuf(b []byte)
- func (e *EncoderV2) WriteVarUint(n uint64)
- type IdSet
- type Pending
- type Range
- type Update
Constants ¶
const ( AnyTagBinary uint8 = 116 AnyTagArray uint8 = 117 AnyTagObject uint8 = 118 AnyTagString uint8 = 119 AnyTagTrue uint8 = 120 AnyTagFalse uint8 = 121 AnyTagBigInt uint8 = 122 AnyTagFloat64 uint8 = 123 AnyTagFloat32 uint8 = 124 AnyTagInteger uint8 = 125 AnyTagNull uint8 = 126 AnyTagUndefined uint8 = 127 )
lib0 Any TLV type tags. Tag bytes are written as a single uint8 followed by the variant-specific payload. Constants reproduced verbatim from lib0/encoding.js writeAny (and confirmed against docs/yrs-port-notes/update-v1.md content table).
Variables ¶
var ErrUnsupportedAnyTag = errors.New("encoding: unsupported Any tag")
ErrUnsupportedAnyTag is returned when DecodeAny encounters a type tag that cannot be mapped to a Go value.
var ErrV2BadFeatureFlag = errors.New("encoding: V2 update has non-zero feature flag")
ErrV2BadFeatureFlag is returned when the leading byte isn't the expected 0x00. Indicates wire-format drift (a future V3?) or that the bytes aren't actually V2 (could be V1 or junk).
Functions ¶
func ApplyUpdate ¶
ApplyUpdate is the top-level convenience entry point. It opens a write transaction on d, decodes raw, integrates the result, and returns. Equivalent to:
upd, _, err := DecodeUpdate(raw)
if err != nil { return err }
txn := d.WriteTxn()
defer txn.Commit()
return upd.Apply(txn)
Returns the decode error verbatim when raw is malformed. The pending buffer absorbs any missing-dependency items silently; inspect with GetPending afterwards if the caller cares.
func ApplyUpdateV2 ¶
ApplyUpdateV2 is the top-level convenience entry point for V2 wire bytes. Opens a write transaction on d, decodes raw, applies, returns. Pending-buffer semantics identical to ApplyUpdate (V1): items missing causal dependencies queue silently and drain on subsequent ApplyUpdate / ApplyUpdateV2 calls.
V1 and V2 wire formats are NOT interchangeable. Calling ApplyUpdateV2 on V1 bytes (or vice versa) is undefined behaviour.
func DecodeAny ¶
DecodeAny reads one lib0-Any-encoded value from buf and returns the value plus the unconsumed tail.
Tag → Go type mapping:
binary (116) → []byte (slice is copied; never aliases buf)
array (117) → []any (recursive)
object (118) → map[string]any (recursive)
string (119) → string
true (120) → true
false (121) → false
bigint (122) → int64 (8-byte BE; loses precision above int64 range,
matching lib0's writeBigInt64 wire format)
float64 (123) → float64
float32 (124) → float64 (widened; downstream callers rarely care
about the source-width distinction)
integer (125) → int64
null (126) → nil
undefined (127) → nil
Unknown tags return ErrUnsupportedAnyTag.
func DecodeContent ¶
DecodeContent reads a Content payload from buf given the content ref-number (the low nibble of the info byte). Returns the parsed Content plus the unconsumed tail.
Mirrors yrs/src/block.rs ItemContent::decode dispatch.
func DecodeContentV2 ¶
DecodeContentV2 reads a Content payload from V2 column streams given the content ref-number (low nibble of info byte). Mirror of EncodeContentV2.
func DecodeStateVector ¶
func DecodeStateVector(buf []byte) (store.StateVector, []byte, error)
DecodeStateVector parses a V1 wire-encoded StateVector from buf and returns the StateVector plus the unconsumed tail.
Mirrors yrs StateVector::decode (state_vector.rs ~line 100).
func EncodeAny ¶
EncodeAny appends the lib0 Any TLV encoding of v to buf.
Supported variants:
- nil → null (tag 126)
- bool → true/false (tags 120/121)
- string → string (tag 119, varstring payload)
- int / int32 / int64 fitting in 32 bits → integer (tag 125, varint payload)
- int / int64 outside 32-bit range → float64 (tag 123, 8-byte BE payload) — matches lib0's BITS31 sniff for safe-integer range
- float32 → float32 (tag 124, 4-byte BE payload)
- float64 → float64 (tag 123, 8-byte BE payload)
- []byte → binary (tag 116, varuint length + bytes)
- []any → array (tag 117, varuint count + each element recursively)
- map[string]any → object (tag 118, varuint count + each (varstring key + Any value)). Keys are sorted alphabetically so the wire bytes are deterministic across runs — Go map iteration order is randomized, which would otherwise break the fixture-determinism guarantee.
Not yet supported (panics):
- math/big BigInt encoding (decode side returns int64 from the 8-byte BE payload, matching lib0 writeBigInt64; encoding from Go side is deferred until an adopter actually needs it)
Mirrors lib0/encoding.js writeAny.
func EncodeContent ¶
EncodeContent appends the wire-format payload of c to buf. Caller has already emitted the info byte; this writes only the content field that follows.
Mirrors yrs/src/block.rs:1844-1872 ItemContent::encode.
Supported in this commit: KindAny, KindString, KindBinary, KindDeleted, KindType, KindFormat, KindEmbed. Skipped: KindDoc, KindMove, KindJSON. All deferred kinds panic on encode rather than emit silently-wrong bytes.
func EncodeContentV2 ¶
EncodeContentV2 writes a Content payload via the V2 column API. Mirrors EncodeContent (V1) but routes per the per-ContentKind wire layout documented in yjs/src/structs/Content*.js.
func EncodeDiff ¶
func EncodeDiff(d *doc.Doc, txn *doc.Transaction, remoteSV store.StateVector) []byte
EncodeDiff returns the V1 wire bytes for the blocks the local doc has that the remote doc (per remoteSV) does not. A nil remoteSV is treated as the empty SV — emit everything.
In this first port pass EncodeDiff emits whole blocks (no slicing at the SV boundary). yrs's Store::write_blocks_from clamps the first block of each client run via find_pivot + slice-trim; we emit the entire client list. The wire format is identical when the remote SV either knows the client fully (we skip them) or not at all (we emit from clock 0). Partial-knowledge clients (remote knows clocks [0, k) but not [k, end)) still get all blocks for that client emitted, so the receiver may see redundant blocks for clocks it already has. Those are silently rejected by integrate (state-vector check) so the result is correct, just chattier than yrs. Tracked in tech-debt.md.
Block runs are emitted in DESCENDING clientID order per docs/yrs-port-notes/update-v1.md gotcha 1.
func EncodeDiffV2 ¶
func EncodeDiffV2(d *doc.Doc, txn *doc.Transaction, remoteSV store.StateVector) []byte
EncodeDiffV2 returns V2 wire bytes for the blocks the local doc has that the remote (per remoteSV) does not. Slice-trimming at the SV boundary is deferred (same as V1's EncodeDiff — see tech-debt.md "EncodeDiff doesn't slice at SV boundary").
Per-client run order is DESCENDING clientID, matching writeClientsStructs in yjs/src/utils/encoding.js (the comment there reads "heavily improves the conflict algorithm").
func EncodeStateAsUpdate ¶
EncodeStateAsUpdate returns the V1 wire bytes for the doc's full state — equivalent to encoding against an empty remote state vector. The caller may pass an existing transaction for read access; if nil a fresh ReadTxn is acquired and released.
Mirrors yrs Doc::encode_state_as_update_v1, simplified to omit the pending-update merge path (we have no pending buffer yet).
func EncodeStateAsUpdateV2 ¶
EncodeStateAsUpdateV2 returns V2 wire bytes for the doc's full state. Mirrors EncodeStateAsUpdate. Caller-side dispatch: V1 vs V2 is the caller's choice (no autodetect on the wire side).
func EncodeStateVector ¶
func EncodeStateVector(sv store.StateVector, buf []byte) []byte
EncodeStateVector appends the V1 wire encoding of sv to buf and returns the extended slice. Wire layout:
varuint clientCount clientCount × (varuint clientID, varuint clock)
Clients are sorted ascending by clientID for deterministic output. JS Yjs / yrs accept any order on decode; the canonical sort keeps our round-trip byte-equality tests stable.
Mirrors yrs StateVector::encode (state_vector.rs ~line 80).
func HasPending ¶
func HasPending(t *doc.Transaction) bool
HasPending reports whether the doc has any queued items awaiting dependencies.
func MissingSV ¶
func MissingSV(t *doc.Transaction) store.StateVector
MissingSV returns the state-vector of clocks the doc needs to receive in order to drain its pending buffer. Sync-protocol servers send this back to peers as a re-fetch request.
Returns an empty SV when the pending buffer is empty.
Types ¶
type Block ¶
type Block struct {
Kind BlockKind
Item *block.Item // WireBlockItem
ID block.ID // WireBlockGC, WireBlockSkip
Len uint64 // WireBlockGC, WireBlockSkip
}
Block is one block-shaped record inside an Update. For WireBlockItem the Item field carries the parsed *block.Item with Origin / RightOrigin set but Left / Right nil (Repair fixes those at apply time). For WireBlockGC and WireBlockSkip only ID and Len matter.
type BlockKind ¶
type BlockKind uint8
BlockKind discriminates the three on-the-wire block variants: regular Item, GC tombstone range, Skip range.
type DecoderV2 ¶
type DecoderV2 struct {
// contains filtered or unexported fields
}
DecoderV2 is the column-oriented update decoder matching yjs UpdateDecoderV2 (testdata/gen/node_modules/yjs/src/utils/ UpdateDecoder.js:96-180). The constructor reads all 9 column decoders upfront from the wire (each as a length-prefixed varbuffer); per-field Read* methods then pull one value from the appropriate column per block iteration.
Top-level update headers (block-count, start-clock, client-count) come from the raw rest stream via ReadVarUint — only per-block field reads pull from columns.
Pattern: construct with NewDecoderV2, call Read* in the SAME order the encoder called Write*. Wire format has no length self-check; any iteration-order mismatch silently corrupts.
func NewDecoderV2 ¶
NewDecoderV2 wraps buf as a V2 decoder. Reads the feature flag + 9 column lengths upfront so subsequent Read* calls are O(1) per value.
func (*DecoderV2) AdvanceRest ¶
AdvanceRest moves the rest cursor forward by n bytes. Content decoders call this after consuming bytes via ReadRestBytes.
func (*DecoderV2) ReadClient ¶
ReadClient returns the next client ID from the shared client column.
func (*DecoderV2) ReadJSON ¶
ReadJSON consumes the next JSON-encoded payload (varstring) from the rest stream and JSON-unmarshals it. Used by ContentFormat / ContentEmbed (mirrors V1 readJSON path).
func (*DecoderV2) ReadKey ¶
ReadKey resolves a map-key reference. Unlike the encoder side, the decoder DOES populate a key table — see port-note gotcha 5 for the asymmetry. First read of a given keyClock value reads a string from the string column AND caches it; subsequent reads of the same keyClock return the cached string.
func (*DecoderV2) ReadLeftID ¶
ReadLeftID pulls one (client, clock) pair from the shared client column + left-clock column.
func (*DecoderV2) ReadParentInfo ¶
ReadParentInfo returns true if the parent is referenced by root-name, false if by item ID. Maps the 0/1 byte from the parent-info RLE column.
func (*DecoderV2) ReadRestBytes ¶
ReadRestBytes returns the unconsumed rest stream — used by content decoders that need raw access.
func (*DecoderV2) ReadRightID ¶
ReadRightID pulls from the shared client + right-clock columns.
func (*DecoderV2) ReadString ¶
ReadString returns the next string from the string column.
func (*DecoderV2) ReadTypeRef ¶
ReadTypeRef returns the next TypeRef byte from the type-ref RLE column.
func (*DecoderV2) ReadVarBuf ¶
ReadVarBuf consumes the next varbuffer from the rest stream.
func (*DecoderV2) ReadVarUint ¶
ReadVarUint returns the next varuint from the raw rest stream. Used for top-level structure headers.
type EncoderV2 ¶
type EncoderV2 struct {
// contains filtered or unexported fields
}
EncoderV2 is the column-oriented update encoder matching yjs UpdateEncoderV2 (testdata/gen/node_modules/yjs/src/utils/ UpdateEncoder.js:115-220) and yrs EncoderV2. Block-write methods route field writes to 9 per-column buffers + 1 raw rest stream; Bytes() concatenates them with length prefixes behind a 0x00 feature flag byte.
Wire layout — see docs/yrs-port-notes/update-v2.md §1. Column order is wire-load-bearing; DO NOT reorder.
Top-level update headers (per-client block-count, start-clock, outer client-count) go to the rest stream via WriteVarUint — only per-block field writes get column-routed. See port note §4 gotcha "Top-level block-run structure stays the same as V1".
Pattern: construct with NewEncoderV2, call the per-field Write* methods + WriteVarUint for headers, then call Bytes() exactly once at the end to flush all columns and assemble the wire bytes. EncoderV2 is single-use; do not call Bytes() twice.
func NewEncoderV2 ¶
func NewEncoderV2() *EncoderV2
NewEncoderV2 returns a fresh EncoderV2 with all column buffers initialized.
func (*EncoderV2) Bytes ¶
Bytes flushes every column and assembles the final V2 wire bytes per port-note §1. Must be called exactly once; do not call any Write* method afterwards.
Layout:
[0x00 feature flag] [varuint len | keyClockBytes] [varuint len | clientBytes] [varuint len | leftClockBytes] [varuint len | rightClockBytes] [varuint len | infoBytes] [varuint len | stringBytes] [varuint len | parentInfoBytes] [varuint len | typeRefBytes] [varuint len | lenBytes] [restBytes] (NO length prefix; reader takes the remainder)
func (*EncoderV2) WriteAny ¶
WriteAny routes a lib0 Any TLV value to the rest stream. Used by ContentAny / ContentEmbed when V2 hits them — note V2 keeps Any in rest (no column for it).
func (*EncoderV2) WriteClient ¶
WriteClient routes a standalone client ID (e.g. per-client block-run header) to the shared client column.
func (*EncoderV2) WriteJSON ¶
WriteJSON routes a JSON-encoded value to the rest stream as a varstring. Used by ContentFormat / ContentEmbed (matches the V1 encoder.writeJSON path — V2 doesn't column-encode JSON payloads either).
func (*EncoderV2) WriteKey ¶
WriteKey emits a map-key reference. Mirrors yjs's "bug" per port-note gotcha 5: the encoder-side key table is NEVER populated, so every WriteKey writes a fresh string into the string column and increments the keyClock counter. The decoder side DOES build a key table — asymmetric but wire-compatible.
func (*EncoderV2) WriteLeftID ¶
WriteLeftID routes the (client, clock) pair to the shared client column + the left-clock column. Mirrors UpdateEncoderV2.writeLeftID (UpdateEncoder.js:156-159).
func (*EncoderV2) WriteLen ¶
WriteLen routes a run-length (GC range, ContentDeleted, content element count) to the length column.
func (*EncoderV2) WriteParentInfo ¶
WriteParentInfo routes the "parent is name vs item-ID" boolean tag to the parent-info RLE column.
func (*EncoderV2) WriteRestBytes ¶
WriteRestBytes appends raw bytes directly to the rest stream. Used by Content encoders (ContentAny, ContentBinary, etc.) that produce arbitrary byte payloads.
func (*EncoderV2) WriteRightID ¶
WriteRightID routes to the shared client column + the right-clock column.
func (*EncoderV2) WriteString ¶
WriteString routes a string to the string column. Used for parent root-type names and ContentString payloads.
func (*EncoderV2) WriteTypeRef ¶
WriteTypeRef routes a TypeRef tag (Array=0, Map=1, Text=2, ...) to the type-ref RLE column. Used inside ContentType payloads.
func (*EncoderV2) WriteVarBuf ¶
WriteVarBuf routes a length-prefixed byte array to the rest stream. Used by ContentBinary.
func (*EncoderV2) WriteVarUint ¶
WriteVarUint routes a generic varuint to the raw rest stream. Used for top-level structure headers: outer client count, per-client block count, start clock.
type IdSet ¶
type IdSet struct {
// contains filtered or unexported fields
}
IdSet is a per-client set of half-open clock ranges. Used as the V1 wire DeleteSet. Insert auto-merges overlapping or adjacent ranges so the per-client list stays sorted and non-overlapping at all times.
Mirrors yrs IdSet (id_set.rs:121-135), simplified — yrs has a generic IdMapInner<V> with a () value type for the wire DeleteSet case; we just store []Range directly.
func DecodeIdSet ¶
DecodeIdSet parses a V1 wire-encoded IdSet from buf and returns the IdSet plus the unconsumed tail.
func (*IdSet) ClientCount ¶
ClientCount returns the number of distinct clients with at least one range in the set.
func (*IdSet) Contains ¶
Contains reports whether the (client, clock) pair is covered by some range in the set.
func (*IdSet) Encode ¶
Encode appends the V1 wire encoding of s to buf and returns the extended slice. Wire layout:
varuint clientCount
clientCount × (
varuint clientID
varuint rangeCount
rangeCount × (varuint start, varuint length)
)
Clients are emitted in ascending order (matches yrs BTreeMap iteration). Ranges within a client are already sorted by Insert.
Mirrors yrs IdSet::encode (id_set.rs:323 area).
type Pending ¶
type Pending struct {
// Blocks groups queued wire-level Block records by clientID.
// Per-client entries are kept in clock-ascending order to
// preserve causal-prefix ordering — yrs relies on this when
// dropping a contiguous prefix that has become satisfied.
Blocks map[uint64][]Block
// DeleteSet stores ranges whose target IDs are not yet present
// in the local store. yrs calls this `pending_ds`. Each Drain
// pass re-scans these ranges; those whose target now exists
// get applied.
DeleteSet *IdSet
}
Pending buffers items and delete-set entries that arrived before their causal dependencies (Origin / RightOrigin / Parent-by-ID referring to clocks the local BlockStore has not yet seen).
yrs's equivalent state lives in `Store::pending` (`store.rs` field `pending: Option<PendingUpdate>`) plus `pending_ds` (`Option<DeleteSet>`). We collapse both into a single Pending struct attached to the Doc via the opaque `pendingState any` slot.
Lifecycle: ApplyUpdate folds the incoming update's blocks/DS into Pending, then runs Pending.Drain in a loop until no further progress is made. Items that successfully integrate are removed from Pending; the leftover stays for the next ApplyUpdate call.
Concurrency: Pending is mutated only under the doc's write lock (acquired by the surrounding TransactionMut). No internal mutex.
func GetPending ¶
func GetPending(t *doc.Transaction) *Pending
GetPending returns the doc's current pending buffer, or nil if no pending state is installed (queue is empty). Caller MUST hold at least a read transaction on d.
The returned pointer is the live buffer — mutate at your peril. Use Pending.MissingSV and Pending.BlockCount for safe inspection.
func (*Pending) BlockCount ¶
BlockCount returns the total number of queued blocks across all clients. Useful for tests and observability ("how many items are stuck waiting?").
func (*Pending) Drain ¶
func (p *Pending) Drain(txn *doc.TransactionMut) int
Drain attempts to integrate every queued block whose dependencies are now satisfied. Returns the number of blocks that successfully integrated this pass. Apply callers loop on Drain until it returns 0, which means the queue has reached its fixed point for this transaction.
The order of work within a pass: iterate clients in ascending clientID, items within a client in clock-ascending order. A block with a still-missing dep is skipped (left in the queue) — the causal-prefix property of yrs updates means a later clock for the same client may yet integrate (depends on a different chain), so we do not short-circuit on first failure within a client.
func (*Pending) MissingSV ¶
func (p *Pending) MissingSV(bs *store.BlockStore) store.StateVector
MissingSV returns the state vector describing what the local store is missing to satisfy every queued item's dependencies. Sync-protocol callers use this to request the gap from the peer ("send me everything since (client, clock) for each entry").
For each pending item we walk Origin and RightOrigin; for each reference that points outside the current BlockStore, we record (clientID, clock+1) — the receiver wants every clock starting at that target, and the SV convention is "the smallest clock NOT yet seen". Parent-by-ID references contribute the same way.
Returns an empty map when nothing is missing.
type Range ¶
Range is a half-open clock interval [Start, Start+Length). The wire form encodes (Start, Length), not (Start, End). yrs's in-memory Range<u32> is half-open; the wire form is unambiguous.
Per docs/yrs-port-notes/update-v1.md gotcha 2.
type Update ¶
Update is the parsed-but-not-yet-applied form of a V1 wire update. Blocks groups records by client; per-client lists are stored in emission order (clock-ascending within a client).
func DecodeUpdate ¶
DecodeUpdate parses V1 wire bytes into an Update. The returned Update is ready for Apply but its blocks have nil Left/Right — Repair fills those at apply time.
Mirrors yrs Update::decode (update.rs:497-516).
func DecodeUpdateV2 ¶
DecodeUpdateV2 parses V2 wire bytes into an Update. The returned Update is ready for Apply but its blocks have nil Left/Right — Repair fills those at apply time. Mirrors DecodeUpdate (V1) but pulls per-field bytes from the column decoders.
func (*Update) Apply ¶
func (u *Update) Apply(txn *doc.TransactionMut) error
Apply integrates every block and delete-set entry in u into the doc represented by txn. Items whose causal dependencies the local store has not yet seen — Origin, RightOrigin, or Parent-by-ID references to unseen clocks — are queued in the doc's pending buffer (Pending) for automatic retry on subsequent Apply calls that satisfy them. Delete-set ranges targeting unseen IDs are likewise queued.
Always returns nil. The previous ErrMissingDependency contract is retired; see docs/tech-debt.md "Pending update buffer not implemented" (now resolved). The signature stays `(*TransactionMut) error` for backwards compatibility with callers that check the error path.
Mirrors yrs Update::integrate (update.rs:234-301) plus the pending-buffer retry loop yrs runs in `Store::try_integrate_pending`.