doc

package
v1.13.0 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package doc owns the Doc and Transaction types — the document container plus its mutation-lifecycle wrapper.

A Doc is the smallest self-contained CRDT replica: it holds a client identifier, a block store of every Item this replica has produced or received, and a single RWMutex that gates concurrent access. Mutations occur inside a Transaction (write) or are observed inside a Transaction (read).

See docs/yrs-port-notes/transaction.md for the per-method contract and the 11-step commit lifecycle yrs runs at TransactionMut.Commit. Most of that lifecycle is not yet implemented; see tech-debt.md.

Index

Constants

View Source
const MaxClientID uint64 = (1 << 53) - 1

MaxClientID is the upper bound on Doc.ClientID values. Set to 2^53-1 to match the JS-safe-integer range that JS Yjs uses for its clientID; all values produced by NewDoc fit in [1, MaxClientID].

Variables

View Source
var TypeEventHook func(t *TransactionMut)

TypeEventHook, when set, is invoked during Commit (after observers' AfterTransaction handlers, before GC) to dispatch shared-type observe events. The types layer installs it in an init, bridging the doc -> types direction without an import cycle (types imports doc, not the reverse). nil in tests that never register an observer.

Functions

This section is empty.

Types

type AfterTransactionHandler added in v0.10.0

type AfterTransactionHandler func(*TransactionMut)

AfterTransactionHandler is invoked once per committed write transaction, after the change-tracking state on TransactionMut has been finalised. The handler runs with the doc's write lock still held; it must not acquire ReadTxn or WriteTxn on the same doc. Heavy or blocking work should be dispatched to a goroutine.

type Doc

type Doc struct {
	// contains filtered or unexported fields
}

Doc is a single CRDT replica. Construct with NewDoc.

Doc is safe for concurrent access through ReadTxn / WriteTxn — those methods acquire the internal RWMutex appropriately. Direct field access from outside the doc package is not safe.

func NewDoc

func NewDoc() *Doc

NewDoc returns a fresh Doc with default options and a random client identifier in [1, MaxClientID].

func NewDocWithOptions

func NewDocWithOptions(opts Options) *Doc

NewDocWithOptions returns a fresh Doc with the given options. A zero Options{} is equivalent to NewDoc().

func (*Doc) Branch

func (d *Doc) Branch(name string) *block.Branch

Branch returns the root branch with the given name, creating it lazily on first access. The returned *block.Branch is the underlying state shared by all types-layer wrappers (Map, Array, Text) constructed against this name on this Doc.

Concurrency: Branch acquires the doc's write lock to insert into the root-branch registry. It MUST be called outside any active Transaction or TransactionMut on this Doc — calling it inside a transaction deadlocks (sync.RWMutex is non-reentrant).

Typical usage:

d := doc.NewDoc()
settingsBranch := d.Branch("settings")  // outside any txn
m := types.NewMap(settingsBranch)        // wrap once
// later, inside a txn:
txn := d.WriteTxn()
m.Set(txn, "color", "red")
txn.Commit()

func (*Doc) ClientID

func (d *Doc) ClientID() uint64

ClientID returns the random per-replica identifier. Stable for the life of the Doc; used as the Client field in every ID this replica produces.

func (*Doc) GC

func (d *Doc) GC() bool

GC reports whether garbage collection of fully-observed deleted items will run at transaction commit.

func (*Doc) GUID added in v0.10.0

func (d *Doc) GUID() string

GUID returns the document's stable string identity.

func (*Doc) Load added in v1.0.0

func (d *Doc) Load()

Load marks this subdocument to be loaded. A provider watching the parent's transactions sees the GUID in SubdocsLoaded and can then fetch and apply the subdocument's own update stream. Idempotent.

func (*Doc) OnAfterTransaction added in v0.10.0

func (d *Doc) OnAfterTransaction(fn AfterTransactionHandler) func()

OnAfterTransaction registers fn to fire at the end of every future TransactionMut.Commit. Returns an unsubscribe function that removes the handler; calling it more than once is a no-op.

Registration order is preserved; handlers fire in registration order. Panics in a handler propagate and are NOT caught — they leave the doc write lock held by the panicking goroutine, which is the same behaviour as any other panic during Commit.

func (*Doc) OnSubdocs added in v1.0.0

func (d *Doc) OnSubdocs(fn func(SubdocsEvent)) func()

OnSubdocs registers fn to fire after any transaction that changed the document's subdocuments. Returns an unsubscribe function. A sync provider uses this to learn which nested documents to start or stop syncing. Convenience layer over OnAfterTransaction.

func (*Doc) PutSubdoc added in v0.10.0

func (d *Doc) PutSubdoc(sub *Doc)

PutSubdoc registers an existing Doc as a subdocument under its GUID, so later Subdoc / GetDoc lookups return the same instance. A no-op if a different instance is already registered for that GUID.

func (*Doc) ReadTxn

func (d *Doc) ReadTxn() *Transaction

ReadTxn acquires the doc's read lock and returns a Transaction. The caller MUST call Close (typically via defer) to release the lock.

Multiple read transactions may coexist; they block any concurrent WriteTxn until all read transactions close.

func (*Doc) RemoveSubdoc added in v1.0.0

func (d *Doc) RemoveSubdoc(guid string)

RemoveSubdoc drops the in-memory handle for the subdocument with the given GUID from the registry, so later Subdoc / GetDoc lookups no longer return a stale instance. Called when a ContentDoc reference is tombstoned. A no-op if no handle is registered. Safe for concurrent use. CRDT state is unaffected; this only releases the cached handle.

func (*Doc) ShouldLoad added in v1.0.0

func (d *Doc) ShouldLoad() bool

ShouldLoad reports whether this (sub)document is marked to load.

func (*Doc) Subdoc added in v0.10.0

func (d *Doc) Subdoc(guid string) *Doc

Subdoc returns the in-memory handle for the subdocument with the given GUID, creating and registering one (GC disabled, matching subdoc semantics) on first access. Safe for concurrent use.

func (*Doc) Subdocs added in v0.10.0

func (d *Doc) Subdocs() []string

Subdocs returns the GUIDs of all registered subdocuments.

func (*Doc) WriteTxn

func (d *Doc) WriteTxn() *TransactionMut

WriteTxn acquires the doc's write lock and returns a TransactionMut. The caller MUST call Commit (typically via defer) to release the lock and run the commit lifecycle.

WriteTxn blocks until all concurrent ReadTxn / WriteTxn close. Calling WriteTxn while already holding a write lock on this Doc from the same goroutine deadlocks (Go RWMutex is not re-entrant), matching yrs's transact_mut behaviour (transact.rs:255 explicit "this will hang forever" comment).

type Options

type Options struct {
	// DisableGC turns OFF garbage collection of fully-observed
	// deleted items at transaction commit. Default (false) means GC
	// is enabled, matching JS Yjs and yrs. Set to true only if you
	// intend to use snapshots; otherwise GC may drop content the
	// snapshot needs to materialize.
	DisableGC bool

	// ClientID overrides the random client identifier. When zero
	// (the default), NewDocWithOptions generates one via crypto/rand.
	// Pass a fixed value only for deterministic tests.
	ClientID uint64

	// GUID is the document's stable string identity. Subdocuments are
	// referenced by GUID on the wire. When empty (the default), a
	// random GUID is generated. Two Docs sharing a GUID are treated as
	// the same logical (sub)document.
	GUID string
}

Options bundles per-Doc settings. The zero value (Options{}) gives the safe defaults that JS Yjs and yrs use: GC enabled, random ClientID. Fields are inverted from their natural English where needed so that Go's zero-value default matches the recommended configuration.

type SubdocsEvent added in v1.0.0

type SubdocsEvent struct {
	Added   []string
	Removed []string
	Loaded  []string
}

SubdocsEvent carries the subdocument lifecycle changes observed in a single transaction: the GUIDs added (a ContentDoc surfaced), removed (its reference was tombstoned), and loaded (autoLoad or an explicit Load). Mirrors yjs's "subdocs" event payload.

type Transaction

type Transaction struct {
	// contains filtered or unexported fields
}

Transaction is a read-only transaction holding the doc's read lock for its lifetime. Created by Doc.ReadTxn; released by Close.

Mirrors yrs Transaction<'doc>. The lock is held until Close runs; Go has no Drop trait, so Close must be called explicitly. Use a `defer txn.Close()` immediately after acquisition.

A Transaction value must not be retained past its Close call. yrs enforces this with a 'doc lifetime parameter; we document the contract and trust callers (see tech-debt.md).

func (*Transaction) Close

func (t *Transaction) Close()

Close releases the read lock. Safe to call more than once; only the first call unlocks.

func (*Transaction) Doc

func (t *Transaction) Doc() *Doc

Doc returns the Doc this transaction was created from.

func (*Transaction) PendingState

func (t *Transaction) PendingState() any

PendingState returns the opaque pending-update state stored on the doc, read-only. Concrete type is *encoding.Pending; see TransactionMut.PendingState for the rationale.

func (*Transaction) Store

func (t *Transaction) Store() *store.BlockStore

Store returns the doc's BlockStore for read access. Mutations through this pointer would race with WriteTxn writers; do not mutate from within a read transaction.

type TransactionMut

type TransactionMut struct {

	// Origin is an opaque caller-supplied value attached to this
	// transaction. Mirrors yrs Origin (transaction.rs:1210-1288).
	// Visible in observer events to distinguish e.g. local edits
	// from updates applied via ApplyUpdate.
	Origin any
	// contains filtered or unexported fields
}

TransactionMut is a write transaction holding the doc's write lock for its lifetime. Created by Doc.WriteTxn; released by Commit.

Mirrors yrs TransactionMut<'doc>. Accumulates change-tracking state during the transaction; consumes it at Commit time to run the post-commit lifecycle (squash, GC, observers, update emission).

Most lifecycle steps are not yet implemented — see tech-debt.md. Commit currently only releases the lock and marks the txn closed.

func (*TransactionMut) AddChangedType

func (t *TransactionMut) AddChangedType(parent *block.Branch, parentSub *string)

AddChangedType records that a Branch saw user-observable changes during this transaction, tracking the changed map keys (parentSub != nil) and whether positional content changed (parentSub == nil) separately. Drives observer dispatch at Commit. Mirrors yjs transaction.changed (Map<AbstractType, Set<string|null>>).

func (*TransactionMut) AfterState added in v0.10.0

func (t *TransactionMut) AfterState() store.StateVector

AfterState returns the per-client clock-head snapshot taken at the start of Commit. Populated only after Commit runs; before Commit the returned map is empty.

func (*TransactionMut) BeforeState added in v0.10.0

func (t *TransactionMut) BeforeState() store.StateVector

BeforeState returns the per-client clock-head snapshot taken when this transaction acquired the write lock. Read-only; do not mutate the returned map.

Together with AfterState, the per-transaction insertion ranges are `afterState[client] - beforeState[client]` per client.

func (*TransactionMut) ChangedKeys added in v1.2.0

func (t *TransactionMut) ChangedKeys(b *block.Branch) map[string]struct{}

ChangedKeys returns the set of map keys changed on branch this transaction. Returns nil for a branch with no recorded key changes.

func (*TransactionMut) ChangedTypes

func (t *TransactionMut) ChangedTypes() []*block.Branch

ChangedTypes returns the branches with recorded changes in this transaction. Order is non-deterministic (map iteration). Primarily for tests and the future observer dispatcher.

func (*TransactionMut) Commit

func (t *TransactionMut) Commit()

Commit runs the post-commit lifecycle and releases the write lock. Safe to call more than once; subsequent calls are no-ops.

Lifecycle steps (mostly stubbed today; tech-debt.md tracks each):

  1. Squash mergeBlocks against their left neighbours.
  2. GC fully-observed deleted items if Doc.GC is true.
  3. Fire pre-emit observers on changedTypes.
  4. Emit the update event with V1 (or V2) bytes for the diff.
  5. Emit subdoc events.
  6. Fire after-commit observers.

Today: only step 0 (release the lock) runs.

func (*TransactionMut) Delete

func (t *TransactionMut) Delete(item *block.Item)

Delete tombstones an Item and records its ID for inclusion in the transaction's eventual delete-set emission. The Item must already be in the store.

Note: the recursive-delete-of-Type-children path is not yet implemented (tracked in tech-debt.md). This implementation handles the simple case integrate uses for map-key LWW tombstoning.

func (*TransactionMut) DeleteRange added in v0.10.0

func (t *TransactionMut) DeleteRange(client, start, end uint64) int

DeleteRange tombstones the clock range [start, end) for client, splitting items at the range boundaries so a merged or partially covered item is sliced and only the matching part is tombstoned. Items already deleted are skipped. Clocks the store has not seen are skipped (the range may extend past this replica's state). Returns the number of items newly tombstoned.

This is the split-aware delete shared by the wire delete-set path and the UndoManager. After commit-time squash a single item can span several clocks that belong to distinct logical edits; deleting by range with boundary splitting is what keeps undo and remote deletes correct in the presence of merged blocks.

func (*TransactionMut) DeletedIDs

func (t *TransactionMut) DeletedIDs() []block.ID

DeletedIDs returns the IDs of items tombstoned during this transaction so far. Returned slice aliases internal state; do not mutate. Primarily for tests and the future delete-set emitter.

func (*TransactionMut) DeletedThisTxn added in v1.2.0

func (t *TransactionMut) DeletedThisTxn(id block.ID) bool

DeletedThisTxn reports whether the item at id was tombstoned during this transaction, the y-protocols "deletes(struct)" predicate the observer event logic needs. Built lazily from deletedIDs on first call. For map-key items (Len 1) the head-ID match is exact.

func (*TransactionMut) Doc

func (t *TransactionMut) Doc() *Doc

Doc returns the Doc this transaction was created from.

func (*TransactionMut) GetItem

func (t *TransactionMut) GetItem(id block.ID) *block.Item

GetItem looks up the Item containing id in the doc's BlockStore. Returns nil for GC cells or unknown IDs.

func (*TransactionMut) GetOrCreateBranch

func (t *TransactionMut) GetOrCreateBranch(name string) *block.Branch

GetOrCreateBranch returns the root branch with the given name from the doc's root-branch registry. Used by block.Repair to resolve ParentNamed references arriving from wire updates.

We do not call Doc.Branch here because Doc.Branch acquires the write lock, which we already hold inside this transaction. Touch the registry directly under the existing lock instead.

func (*TransactionMut) MaterializeCleanEnd

func (t *TransactionMut) MaterializeCleanEnd(id block.ID) *block.Item

MaterializeCleanEnd returns an Item ending exactly at id.Clock (inclusive), splitting if needed.

func (*TransactionMut) MaterializeCleanStart

func (t *TransactionMut) MaterializeCleanStart(id block.ID) *block.Item

MaterializeCleanStart returns an Item starting exactly at id.Clock, splitting the underlying block in the store if id lands mid-block. Returns nil if id is in a GC cell or unknown.

func (*TransactionMut) PendingState

func (t *TransactionMut) PendingState() any

PendingState returns the opaque pending-update state stored on the doc. Returns nil when no encoding-layer state has been installed yet. The concrete type is *encoding.Pending; the doc package does not depend on encoding so this stays any-typed at the boundary. Callers (encoding.ApplyUpdate) type-assert.

func (*TransactionMut) PositionalChanged added in v1.2.0

func (t *TransactionMut) PositionalChanged(b *block.Branch) bool

PositionalChanged reports whether branch saw positional (Array / Text) content changes this transaction.

func (*TransactionMut) SetPendingState

func (t *TransactionMut) SetPendingState(s any)

SetPendingState replaces the opaque pending-update state on the doc. Pass nil to drop pending state entirely (e.g. when the queue drains to empty and the encoding layer wants to release the allocation).

func (*TransactionMut) Store

func (t *TransactionMut) Store() *store.BlockStore

Store returns the doc's BlockStore. Safe to mutate from within this transaction; the write lock prevents concurrent access.

func (*TransactionMut) SubdocsAdded added in v1.0.0

func (t *TransactionMut) SubdocsAdded() []string

SubdocsAdded returns the GUIDs of subdocuments first surfaced during this transaction (local SetDoc or a decoded remote ContentDoc).

func (*TransactionMut) SubdocsLoaded added in v1.0.0

func (t *TransactionMut) SubdocsLoaded() []string

SubdocsLoaded returns the GUIDs of subdocuments marked to load during this transaction (autoLoad option or an explicit Load call).

func (*TransactionMut) SubdocsRemoved added in v1.0.0

func (t *TransactionMut) SubdocsRemoved() []string

SubdocsRemoved returns the GUIDs of subdocuments whose reference was tombstoned during this transaction.

Jump to

Keyboard shortcuts

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