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.
Stages
Section titled “Stages”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.
Two lanes, two shapes
Section titled “Two lanes, two shapes”Staged edits accumulate in two parallel lanes — and, deliberately, the lanes have different shapes:
changes— JSON 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.
Commit intents
Section titled “Commit intents”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.
Authorship is not staged
Section titled “Authorship is not staged”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.