encoding

package
v0.9.0 Latest Latest
Warning

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

Go to latest
Published: May 17, 2026 License: MIT Imports: 9 Imported by: 0

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

View Source
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

View Source
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.

View Source
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

func ApplyUpdate(d *doc.Doc, raw []byte) error

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

func ApplyUpdateV2(d *doc.Doc, raw []byte) error

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

func DecodeAny(buf []byte) (any, []byte, error)

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

func DecodeContent(buf []byte, refNum uint8) (block.Content, []byte, error)

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

func DecodeContentV2(dec *DecoderV2, refNum uint8) (block.Content, error)

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

func EncodeAny(buf []byte, v any) []byte

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

func EncodeContent(buf []byte, c block.Content) []byte

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

func EncodeContentV2(enc *EncoderV2, c block.Content)

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

func EncodeStateAsUpdate(d *doc.Doc) []byte

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

func EncodeStateAsUpdateV2(d *doc.Doc) []byte

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.

const (
	WireBlockItem BlockKind = 0
	WireBlockGC   BlockKind = 1
	WireBlockSkip BlockKind = 2
)

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

func NewDecoderV2(buf []byte) (*DecoderV2, error)

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

func (d *DecoderV2) AdvanceRest(n int)

AdvanceRest moves the rest cursor forward by n bytes. Content decoders call this after consuming bytes via ReadRestBytes.

func (*DecoderV2) ReadAny

func (d *DecoderV2) ReadAny() (block.Any, error)

ReadAny consumes the next lib0 Any TLV from the rest stream.

func (*DecoderV2) ReadClient

func (d *DecoderV2) ReadClient() (uint64, error)

ReadClient returns the next client ID from the shared client column.

func (*DecoderV2) ReadInfo

func (d *DecoderV2) ReadInfo() (uint8, error)

ReadInfo returns the next info byte from the info RLE column.

func (*DecoderV2) ReadJSON

func (d *DecoderV2) ReadJSON() (block.Any, error)

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

func (d *DecoderV2) ReadKey() (string, error)

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

func (d *DecoderV2) ReadLeftID() (block.ID, error)

ReadLeftID pulls one (client, clock) pair from the shared client column + left-clock column.

func (*DecoderV2) ReadLen

func (d *DecoderV2) ReadLen() (uint64, error)

ReadLen returns the next length value from the length column.

func (*DecoderV2) ReadParentInfo

func (d *DecoderV2) ReadParentInfo() (bool, error)

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

func (d *DecoderV2) ReadRestBytes() []byte

ReadRestBytes returns the unconsumed rest stream — used by content decoders that need raw access.

func (*DecoderV2) ReadRightID

func (d *DecoderV2) ReadRightID() (block.ID, error)

ReadRightID pulls from the shared client + right-clock columns.

func (*DecoderV2) ReadString

func (d *DecoderV2) ReadString() (string, error)

ReadString returns the next string from the string column.

func (*DecoderV2) ReadTypeRef

func (d *DecoderV2) ReadTypeRef() (uint8, error)

ReadTypeRef returns the next TypeRef byte from the type-ref RLE column.

func (*DecoderV2) ReadVarBuf

func (d *DecoderV2) ReadVarBuf() ([]byte, error)

ReadVarBuf consumes the next varbuffer from the rest stream.

func (*DecoderV2) ReadVarUint

func (d *DecoderV2) ReadVarUint() (uint64, error)

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

func (e *EncoderV2) Bytes() []byte

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

func (e *EncoderV2) WriteAny(v block.Any)

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

func (e *EncoderV2) WriteClient(client uint64)

WriteClient routes a standalone client ID (e.g. per-client block-run header) to the shared client column.

func (*EncoderV2) WriteInfo

func (e *EncoderV2) WriteInfo(info uint8)

WriteInfo routes an info byte to the per-byte RLE column.

func (*EncoderV2) WriteJSON

func (e *EncoderV2) WriteJSON(v block.Any)

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

func (e *EncoderV2) WriteKey(key string)

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

func (e *EncoderV2) WriteLeftID(id block.ID)

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

func (e *EncoderV2) WriteLen(l uint64)

WriteLen routes a run-length (GC range, ContentDeleted, content element count) to the length column.

func (*EncoderV2) WriteParentInfo

func (e *EncoderV2) WriteParentInfo(isRootName bool)

WriteParentInfo routes the "parent is name vs item-ID" boolean tag to the parent-info RLE column.

func (*EncoderV2) WriteRestBytes

func (e *EncoderV2) WriteRestBytes(b []byte)

WriteRestBytes appends raw bytes directly to the rest stream. Used by Content encoders (ContentAny, ContentBinary, etc.) that produce arbitrary byte payloads.

func (*EncoderV2) WriteRightID

func (e *EncoderV2) WriteRightID(id block.ID)

WriteRightID routes to the shared client column + the right-clock column.

func (*EncoderV2) WriteString

func (e *EncoderV2) WriteString(s string)

WriteString routes a string to the string column. Used for parent root-type names and ContentString payloads.

func (*EncoderV2) WriteTypeRef

func (e *EncoderV2) WriteTypeRef(ref uint8)

WriteTypeRef routes a TypeRef tag (Array=0, Map=1, Text=2, ...) to the type-ref RLE column. Used inside ContentType payloads.

func (*EncoderV2) WriteVarBuf

func (e *EncoderV2) WriteVarBuf(b []byte)

WriteVarBuf routes a length-prefixed byte array to the rest stream. Used by ContentBinary.

func (*EncoderV2) WriteVarUint

func (e *EncoderV2) WriteVarUint(n uint64)

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

func DecodeIdSet(buf []byte) (*IdSet, []byte, error)

DecodeIdSet parses a V1 wire-encoded IdSet from buf and returns the IdSet plus the unconsumed tail.

func NewIdSet

func NewIdSet() *IdSet

NewIdSet returns an empty IdSet.

func (*IdSet) ClientCount

func (s *IdSet) ClientCount() int

ClientCount returns the number of distinct clients with at least one range in the set.

func (*IdSet) Contains

func (s *IdSet) Contains(client, clock uint64) bool

Contains reports whether the (client, clock) pair is covered by some range in the set.

func (*IdSet) Encode

func (s *IdSet) Encode(buf []byte) []byte

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).

func (*IdSet) Insert

func (s *IdSet) Insert(client, start, length uint64)

Insert adds the half-open range [start, start+length) for client. Overlapping or adjacent existing ranges are merged.

Mirrors yrs IdRanges::insert (id_set.rs:34-90 area).

func (*IdSet) Iterate

func (s *IdSet) Iterate(fn func(client uint64, ranges []Range))

Iterate calls fn once per client, in ascending client ID order. The ranges slice handed to fn must NOT be retained or mutated; it aliases internal state.

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 NewPending

func NewPending() *Pending

NewPending returns an empty buffer.

func (*Pending) BlockCount

func (p *Pending) BlockCount() int

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) IsEmpty

func (p *Pending) IsEmpty() bool

IsEmpty reports whether the buffer holds zero queued items.

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

type Range struct {
	Start  uint64
	Length uint64
}

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.

func (Range) End

func (r Range) End() uint64

End returns the (exclusive) upper bound of the range.

type Update

type Update struct {
	Blocks    map[uint64][]Block
	DeleteSet *IdSet
}

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

func DecodeUpdate(buf []byte) (*Update, []byte, error)

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

func DecodeUpdateV2(buf []byte) (*Update, error)

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 NewUpdate

func NewUpdate() *Update

NewUpdate returns an empty Update.

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`.

Jump to

Keyboard shortcuts

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