Moradok Lanna
Dev notes

Live collaborative editing by paragraph, on shared hosting

Several people can open the same board (a shared Lanna document) and write at once, and see each other's changes within a couple of seconds. It runs on ordinary shared hosting: no websockets, no server-sent events, no long-running process, only short PHP requests and a database.

This page explains the design and the trade-offs, including what it deliberately does not do yet.

Constraints

The unit of collaboration is the paragraph

Instead of syncing characters, the system syncs paragraphs. Every paragraph in a document gets a stable id the first time it is seen, and the id survives edits, splits and merges of neighbours. A document is then an ordered list of (id, html) pairs plus a version number.

doc = { version: 41,
        paras: [ {id: "p7", html: "..."}, {id: "p9", html: "..."} ] }

Paragraph granularity is a deliberate middle path. It is far simpler than character-level merging, and it matches how people actually write: usually one person is working in one paragraph at a time.

Presence and paragraph locks

While you edit, the client tells the server which paragraph holds your caret. The server keeps a small presence record per person (display name, colour, last seen, current paragraph) and grants a lock on that paragraph:

Locks are advisory in the interface and enforced on the server. A client that ignores them still cannot overwrite a locked paragraph.

Polling that is polite

Without a push channel, the client asks. The rules keep the load low:

  1. Poll only while editing. A reader looking at a board does not poll fast. At most a slow check asks "has this changed?" and shows a "new edits, load" notice instead of changing the text under them.
  2. Adaptive back-off. The interval starts at about two seconds and grows when nothing happens, then snaps back as soon as there is activity.
  3. Tiny replies. If nothing changed since the version the client already has, the answer is a few bytes: "same".
  4. Send only what changed. The client sends only the paragraphs it touched, and the reply contains only the paragraphs that changed.
every ~2s (backing off when idle), while editing:
    send   { since: myVersion, dirty: [changed paragraphs], caret: pid }
    reply  { same: true }                  // most of the time
        or { version: n, changed: [...], presence: [...], locks: [...] }

Applying someone else's change must not disturb you. Remote paragraphs are swapped in place, your caret and IME composition are never touched, and a paragraph you are typing in is never replaced.

There is also a cap on simultaneous live editors per board. Past it, new arrivals fall back to the simple save-and-compare flow described next.

Writes are applied against a version, and conflicts lose nothing

The server receives paragraph-level operations tagged with the version they were based on, and decides:

apply(op, baseVersion):
    p = paragraph(op.id)
    if p.lockedByOther or p.changedSince(baseVersion):
        keepSideVersion(op.text, author)    // the loser's text is never dropped
        return reject(currentVersion)
    p.html = op.html;  version += 1

The client shows a calm message with the choice to see the latest text or to overwrite it deliberately. "Never silently lose what someone wrote" is the rule that all other choices serve.

Versions and history

Every save creates a version. To keep the history readable, saves by the same person within a short window are merged into one entry, so a long editing session is not a thousand rows. Two versions are special:

History shows who (as a name and colour), when, and a restore button. Restoring creates a new version rather than rewriting the past.

Anonymous identities

Most contributors are not signed in. So identity is light:

Why not a CRDT?

Conflict-free replicated data types (CRDTs) and operational transforms give character-level merging, the Google Docs experience. They are the right answer for the long run, but not for this milestone:

A CRDT is a future milestone, likely alongside a host that can keep connections open. The paragraph id, the version number and the side-version model are designed so that a later move does not throw away existing documents.

What is not verified

Live editing has been tested with scripted multi-user runs (several simulated editors, locks, conflicts, expiry, fallback) and with a real browser, but not yet with large groups on poor mobile networks. That is on the list.

Back to Overview · Previous: Jaan Editor · Next: AI