youros

How it worksUsing it

Files

The files service on a node watches a folder and keeps a Loro doc beside it. The spikes (files-sync, git-roundtrip, git-import and shallow, in spikes/) tried each part; what they found is in their READMEs, and what they couldn't solve is under Open.

Open, keep, hold

Three verbs, three tiers of state (D-22, D-28 in Choices):

  • Open is the moment's. The files service on the machine keeps a room per open path, in memory, for as long as anyone has it open. It forwards and never reads: what one member says (the editor's Loro updates and presence, as opaque bytes, in pieces under the relay's frame limit) goes to every other member, sealed in each one's own session, so Loro stays out of the machine. The editor's sync runs over the room beside the browser's other tabs, so a late joiner catches up from whoever is there. The first in the room opens the page as a single editor does and is its writer: the only one that saves, over the version it last saw, and takes changes on disk back in, so N editors never race to write the file or each absorb the same change. Anyone after starts empty and takes the page from the room, since two copies made from the file apart would merge into a doubled page. When the writer leaves, the earliest still there writes; a member silent for 10 s (the editor speaks every second) is dropped, so a closed tab never holds the writing, and one that speaks again joins again. The last to leave ends the room, and nothing stays on the machine.
  • Keep is the account's: a list of kept folders, each an id, a name and its ignore rules, in an account doc every device syncs. Keeping a folder makes its doc (A Loro doc beside the folder, below) on the machine where it was kept.
  • Hold is the device's: which kept folders this device holds and where, in its device doc. A machine holds a folder as files on disk with the doc beside them; a browser holds the doc in its own storage, text only, binary files fetched by hash when opened. Holding nothing is a policy like any other.

The pane reads all three: the kept list and the holdings give each folder's mark and its count of holders; a path under a kept folder but matched by its ignore rules is left out. A file in the kept view opens from whichever holder answers first; with none online it says which devices hold it.

Stars, for now on the machine

A star is { path, kind } in a list the machine's files service keeps beside the node's key (stars.json), outside the root, so it never shows in a listing and a browser can't reach it as a file. stars.get reads the list and its version; stars.set replaces it only over the version it was read at, as a write's base does, so two browsers starring at once both land. Each browser reads every machine's list when it reaches it and keeps a copy, which is how a machine that is away still shows its stars. The list is the account's, not the machine's: it moves to the account's doc (D-22) once there is one, and the machine keeps none.

A Loro doc beside the folder

The folder is a Loro tree, a node per file or directory, and a text file is Loro text, so a change on disk becomes the few operations that make it, and an edit from another device is written back to disk by temporary file and rename. A rename is a move in the tree, not a delete and a create. Binary files are kept whole by hash, the newest winning. The service remembers what it wrote, so its own writes don't come back as edits. A doc is a folder, or a project: the unit of sharing (D-22, in Choices).

Git mirrors the working tree

The doc mirrors the working tree, not git's own store, and git is where code is reviewed and built (D-13, narrowed by D-23). Two things cross between them: a version committed to a branch, and what git did taken in as operations, diffed from the commit the doc last mirrored, so a checkout and back is a round trip; an ancestor that merely carries a version is not enough.

Two edits to one line merge into something that still compiles: 30 edited to 45 in git and 60 live became 645. So a change from git is taken in on a fork, merged at once only when a three-way check (the last mirrored commit, the live doc, the new commit) finds no line both changed, and otherwise held and shown as a conflict.

A repo's history comes in as one doc. Each commit is made on its parents' version, so branches are concurrent changes and a merge is Loro's merge corrected to the merge commit's tree. An author is a chain of peers, one per line of work, since one peer writing on two branches would make one depend on the other. An old version opens by replaying history up to it into a fresh doc, since going back in place is slow (below).

Cutting history

A doc keeps every change, and that only grows. So every change is kept for a window, then the doc is cut, and coarser checkpoints are kept outside it, text stored by content so an unchanged file costs nothing. Loro's own shallow snapshot roots the cut where it can, which with several devices taking turns can be far earlier than asked; ours rebuilds the state at the cut and replays what came after. A device away longer than the window can't merge its operations into the cut doc, so it checks its version against the cut before syncing, and carries its edits over as a three-way text merge from the state it last shared.

Open

  • Keeping a folder inside another kept folder: one doc or two. Probably refused, with the outer folder's ignore rule offered instead.
  • Live collaboration on a file whose machine goes away: the room lives only there, so the others keep editing their copies and merge when it returns (their sync catches up), but nobody saves meanwhile.
  • A browser joining a room drops its own copy of the page, so an edit it made alone and never saved is lost; and if those in the room are slow to answer (15 s) it makes the page from the file, which merges cleanly only if theirs was made from the same text.
  • A writer's tab left hidden is throttled to a timer a minute, so it is dropped after 10 s of silence and the writing moves to someone visible; it rejoins at its next word.
  • Pruning back to git: when a version is committed and pushed, cut the doc's history at that commit, so git holds everything before it. Possible (a single-head version can be a cut), not decided.
  • A rename done in git arrives as a delete and a create, and a live edit to the file is lost; git doesn't record renames.
  • An editor that saves a stale buffer undoes a remote edit; only the editor can know its buffer is stale.
  • A save landing between the service's scan and its write is overwritten (a window of microseconds); a delete beats an edit.
  • Case-insensitive filesystems; a scan per change rather than per path.
  • Where Loro's shallow root lands with several peers: whether it is meant to, asked upstream.
  • Overlapping edits in a rejoin keep every insert but can differ by a few characters from a merge with full history.
  • File modes, symlinks, submodules, and very large files.
  • Going back is slow in Loro: on that repo, checkout or fork_at to an older version took 28 to 49 s, against 66 ms going forward. Replaying the history up to the version into a fresh doc takes 1.2 s, so files/old-version is kept that way until the cause is known.
  • Renames inside git merges, and copies, come in as a delete and a create; a hand-resolved conflict is stored as undoing Loro's merge then applying the person's version, right but odd for blame. Only one ref is imported.
  • Loro's own whole-file text update is too slow for code (7 s on one 74 KB file); a line diff, then a character diff inside each changed hunk, does the same in a fraction of the time. A diff crate's insert positions were wrong in a way only checking against git caught.