Skip to content

Changesets & lanes

A session’s persisted state is a changeset: a structured value stored in a field of the host document. Its shape mirrors the hybrid change model of datadata itself.

For every document touched by the session, the changeset holds a stage:

  • the document’s base sequence — the version the edits were computed against,
  • the document type, whether the stage creates or deletes the document, and a pending rename if there is one.

That’s all — no copy of the document’s data travels with the stage. Each staged patch embeds its own RFC 6902 test guards (the values its author saw), which is what conflict detection runs on: the patches plus the live head, no stored base. Storing no base keeps the changeset proportional to the staged edits, not to the size of the documents being staged — a session touching large documents is still a small host document. When a resolution surface needs the base value (the three-way preview), it is resolved on demand — from the live head, an in-memory pin, or a replay of the document’s event log.

Staged edits accumulate in two parallel lanes — and, deliberately, the lanes have different shapes:

  • changesJSON Patch operations against structured data: a flat record set ordered by fractional index, the same ordered-record pattern used for collections everywhere in datadata. Each record is addressable — a change can be amended or dropped by id, not just wholesale.
  • yjsCopies — staged Yjs content: one live-seeded host sub-document copy per target Y.Doc, recorded as { docId, yjsId, copyRef }. The record stores no update bytes — the copy holds the content as a real, synced sub-document, not a stored delta.

The asymmetry is the point. Structured edits are addressable, so the JSON lane is a record set a review can amend or drop from by id. Collaborative text isn’t — it merges as a whole document — so the Yjs lane is a forked copy instead. session.getYDoc hands back a copy seeded from the live target, but stages nothing: the first edit promotes it to a yjsCopies entry anchored by a deterministic copyRef, so merely opening an editor leaves the changeset untouched. Editors edit the copy directly, and live sharing, cursors, offline editing and state-vector catch-up ride the host document’s ordinary Yjs sync for free. The copy also tracks the live target during the session, so upstream edits surface to the editor as they happen rather than at commit.

Both lanes converge under concurrent authorship, by different means. Patch records from two authors interleave by fractional index. The Yjs copy needs nothing extra: copyRef is deterministic — a stable id derived from the target’s docId and yjsId — so concurrent sessions on the same target resolve to the same single copy. They collaborate on it natively, with no last-writer race over which copy is canonical.

copyRef is a yjsRef-typed value, so it’s reference-counted like any other Yjs reference. Its presence in the changeset anchors the copy sub-document against the host document’s orphan GC; clearing it reaps the sub-document. There’s no session-specific Yjs cleanup — committing or discarding just drops the entry, and the general reference GC reclaims the copy.

One more record set rides the changeset: commitIntents, written by commit() itself just before it drains. Each intent records — per stage — which staged change ids are draining under which pre-minted write event ids. It’s the changeset’s crash-safety ledger: with offline persistence enabled, a commit that dies with its page leaves journaled writes that replay on the next boot, and the intent is how a resumed session recognizes them as its own commit already in flight — it waits for their outcome and finishes the commit’s bookkeeping instead of re-applying the staged work. You never write this lane yourself; it appears during a commit and clears when the commit’s writes settle.

A staged change carries only its id, order and patch — no per-change author or originating-turn metadata. Staging is transient: the changeset is cleared once every stage drains at commit (or the session is discarded), so anything stamped on a staged change is visible only for the window the change sits staged — not where a durable record would matter. Durable authorship lives on the event log instead: every committed event records the writing principal (subject and actor). An app that wants to group staged changes by conversation turn keeps its own map keyed by staged change id — turns and tool calls are the app’s vocabulary, not the library’s.