Projects live at URLs, have owners, and are edited together live
A project is only ever at /p/<id>/<slug>; / is the index of the projects
you own or edit. Every project has an owner, who can name editors;
anyone with the link can view. Every edit saves itself, one request in
flight at a time, as a patch of the leaves that changed, and a websocket
(channels + daphne) carries presence and each committed write to
everyone else in the project. The first write to a leaf wins, and the
loser is told.
Undo is per person: a step undoes only if the leaves it touched still
hold what it left, so it never takes a collaborator's work with it.
Named snapshots replace saving, and restore as an ordinary write.
An empty symbol now survives the leaf round trip with `:nodes {}`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c17ee138f2
commit
6a53adb5e0
31 changed files with 1918 additions and 158 deletions
|
|
@ -675,6 +675,45 @@ and `~` is then refused inside a name. That is the whole of the escaping.
|
|||
That maps onto the tiers exactly — the server stores tier 1 and snapshots tier 1,
|
||||
with tiers 2 and 3 as content-addressed blobs beside it.
|
||||
|
||||
### As built
|
||||
|
||||
- **Addresses.** `/` is the index of the projects you own or edit. A project
|
||||
is only ever at `/p/<uuid>/<slug>`: the id finds it, the slug is its name and
|
||||
follows a rename without a history entry. "new" makes the project on the
|
||||
server first and opens it; a built-in example opened in the editor is saved
|
||||
at once as a project of its own. There is no bare project. The address, the
|
||||
title and the socket follow `[:project :id]` through one global interceptor
|
||||
(`events/collab`).
|
||||
- **Ownership.** Every project has an owner (`Project.owner`, not nullable) and
|
||||
`editors`. Anyone with the link reads; the owner and editors write. Making a
|
||||
project needs you signed in. A reader can make a copy of their own.
|
||||
`/api/{me,login,signup,logout}`, `/api/projects/<id>/editors[/<username>]`.
|
||||
- **Every edit saves.** No save button. The same interceptor sees
|
||||
`:paint/revision` move and saves; one request is in flight at a time, and an
|
||||
edit made meanwhile goes when it lands — so a drag reaches the room as fast
|
||||
as the round trip allows. A save sends only the leaves that differ from what
|
||||
was last synced, and a clean document sends nothing. Analyses and blocks
|
||||
already put on the server are not asked about again.
|
||||
- **Saves are patches.** With `base` (the seq last caught up to) a save names
|
||||
only its changed and removed leaves. A named leaf somebody else changed after
|
||||
`base`, to something else, fails the whole save with 409.
|
||||
- **The first write wins.** A remote write to a leaf with a local change not
|
||||
yet sent waits in the entry's `:behind`; the next save, or a 409, puts theirs
|
||||
on screen over ours and says so. Ours stays in the undo list.
|
||||
- **The socket** (`/ws/projects/<uuid>`, channels + daphne) carries presence,
|
||||
the delta each committed write broadcasts, and `access` when the editor list
|
||||
changes. A gap in `seq`, a welcome, or a 409 refetches the document.
|
||||
- **Undo** is per person, recorded in `events/edit` as leaf befores and afters
|
||||
(`domain/history`), and applied as an ordinary edit. A step undoes only if
|
||||
every leaf it touched still holds what it left there: somebody else's edit
|
||||
since refuses it rather than being undone with it. Edits to the same leaves
|
||||
within a second, each starting where the last left off, are one step.
|
||||
- **Snapshots** are named revisions (`/api/projects/<id>/revisions`), with each
|
||||
clip's block keys. Restoring one is an ordinary write, broadcast like any.
|
||||
|
||||
Not yet: follow mode, frame/selection in presence, the advisory `:editing`
|
||||
lease, the durable outbox.
|
||||
|
||||
### Why this model, and not a CRDT
|
||||
|
||||
The usual reason to reach for Yjs or Automerge is automatic convergence without
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue