Sessions and history
decided in principle, draft in detail.
One mechanism for undo, proposals and previews
Section titled “One mechanism for undo, proposals and previews”A session is a fork of a document’s history at a point given by a snapshot: a branch, in git’s terms. Sessions are meant to be cheap enough to make one per person, per proposal or per visit. That one idea covers several features:
- A personal session is one user’s own state, which may never leave that user’s own muppet: a private scratch copy, a draft, or the state an ordinary web session would keep today. See use cases.
- Undo forks a session from an earlier point, so history is never thrown away.
- A proposed edit is a session that hasn’t been merged yet.
- A preview draws a session over its parent.
- A paused queue is a local session whose automatic merge is switched off. Releasing it is an ordinary merge.
- Infinite, branchable undo falls out of keeping every session’s operations. How that history is stored long-term is on the roadmap.
A session is identified by where it lives: its address, {host}/{path}@{name}. There are no generated IDs. Each session also records its parent (another session’s address) and the snapshot at the fork.
Naming a session
Section titled “Naming a session”The creator names a session, and the name must be unique on its host. Because the name is an address:
- Asking for a name that already exists opens that session, like checking out an existing branch in git, subject to its read and write grants.
- Creating a new fork on a name that’s taken is rejected, and so is asking for a session you have no access to.
Nobody can overwrite or hijack a session by naming, and two people who pick the same name for the same thing end up working in the same session.
Prior art: cheap sessions, merges as replays and an authoritative upstream all come from git (prior art). The undo side builds on Inkscape’s transaction-based undo stack (prior art). Access to sessions is covered in Identity and access.
Undo in practice
Section titled “Undo in practice”Two kinds of undo meet here:
- Personal undo (Ctrl+Z) reverses your own recent actions with inverse operations, leaving collaborators’ edits alone. One step is one action, grouped the way Inkscape groups an action into a transaction, including merging small repeated steps such as a drag or a spin button.
- History undo goes back to an earlier state of the whole document. That’s a session forked from an earlier snapshot.
Where they differ from Inkscape: in Inkscape, undoing and then making a new edit clears the redo stack, and the undone work is lost. In XTXP, the undone work is still in the document’s history, so it can be reached again through a session from before the undo.
Hosting a session
Section titled “Hosting a session”Any muppet can host a session. A host holds the session’s Xmesh document like any other muppet, and also:
- admits whole sessions into it against its grants, in arrival order (see below)
- routes admitted updates to everyone subscribed to the session
- persists the session and hands new clients its full state (see below)
On a server, each hosted session runs headless, with no views and no user of its own, as a lightweight actor: an async task that owns one session and talks to the rest only by messages. Actors are cheap enough to spawn one per proposed edit or undo fork, and a failure in one session stays contained.
Admission is by session
Section titled “Admission is by session”Every connection edits in a session of its own, with a client ID that’s fresh for that session. Its edits reach a shared session such as @main by being merged. A host never accepts some of a client’s updates and refuses others, because Yrs needs each client’s updates without gaps, and one refused update would leave all of that client’s later ones stuck.
- A host admits or refuses whole sessions. For a writer’s session, admission is an automatic merge as edits arrive. A refused session simply isn’t merged.
- Nothing is lost by refusal. A refused or not-yet-admitted session stays a session in its own right, and shows as an onion skin over the one it would merge into. If it’s admitted later, merging carries on from where it stopped.
- A host’s copy of a session only ever holds what was admitted to it. Previews are drawn from the other sessions, never mixed in.
- Dependencies merge together. If one session builds on another that hasn’t been admitted, merging it would bring that one in too. The host admits both or neither, and says why when it holds a merge back.
Sessions on other hosts, and trust between them
Section titled “Sessions on other hosts, and trust between them”Every host holds its own sessions. A session never lives on two hosts at once: a copy elsewhere is a different session, with its own address, in the way every git clone has its own sessions.
- Only a session’s own host admits edits to it, in its own arrival order.
- Trust between sessions drives automatic merging. If session A trusts session B, B’s changes merge into A as they arrive, by ordinary state-vector sync. Trust can run one way or both ways, so a studio’s session on its local network and
mainon a server can keep each other up to date. - A paused queue is a session whose automatic merge is switched off until it’s released.
- Merges converge in any direction. CRDT merges are commutative and idempotent, so two sessions that trust each other can’t loop or diverge.
For example, xtxp.example.org/drawing@main can be forked onto a laptop as my-laptop.lan/drawing@sketch, with the fork trusting main so it stays current, and main trusting the fork only once it’s ready.
Storage: toolchain first
Section titled “Storage: toolchain first”The Yrs ecosystem already has the building blocks a host needs:
- A full state as one update. Yrs can encode a whole document as a single update, so a new client loads one compact blob instead of replaying history.
- Append, then compact. Storing each incoming update and periodically merging them into one is the usual pattern for persisting Yjs documents.
XTXP starts with those, and y-sweet already implements them for Yjs documents. A dedicated ledger (a chronological patch log in cold storage) and keyframes (hot snapshots tuned for instant loading) are on the roadmap, to be built only if the toolchain’s own approach falls short.
Fork, preview, merge
Section titled “Fork, preview, merge”Merging is mechanical. CRDT operations commute, so merging a session means applying its updates to the parent. Nobody has to resolve conflicts by hand, and the redline is exactly the set of operations being applied, grouped by author.
On Yrs: a fork is a new document seeded with the parent’s state at the fork’s snapshot (which needs Yrs’s garbage collection off). A merge encodes the session’s updates since the fork and applies them to the parent.
Conflicts above the CRDT
Section titled “Conflicts above the CRDT”draft (decision 23)
A merge always converges: there’s never a conflict in the CRDT sense. But the merged document can still break a rule of the document type. The first case is duplicate ids: two sessions can each create path123, as git lets two branches hold different files at the same path, and once merged, url(#path123) references become ambiguous.
- Documents are never silently rewritten. What’s in a session is exactly what its people made.
- A merge that produces a duplicate
idis flagged as a conflict to resolve. The resolution algorithm is open, possibly with a person deciding. - Partitioned ids, such as
path-123with a per-session part, would be an explicit, permanent part of the document. They’re left to a future experiment.
Lifecycle
Section titled “Lifecycle”Still open
Section titled “Still open”- How long abandoned sessions stay loaded before they’re only kept in storage.
- Permissions: who may fork, who may merge into which session.
- Whether a merge records itself as an operation, so it can be undone by branching from before it.
- Exactly how personal undo groups operations, and how far back it reaches once collaborators have built on your changes.