aweft replication protocol, v1
Status: not frozen. No deployed implementation depends on it yet. Changes go in spec/CHANGELOG.md. v1 replaces v0 whole: v0 had a host and a client, and this protocol has two equal ends (design 053).
This document specifies what two ends of a replication link say to each other. It builds on spec/format.md, which specifies the commit; nothing here changes a commit.
The key words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119.
spec/frames/ is the normative fixture corpus. Where this prose and a fixture disagree, the fixture is correct.
1. The link
A link carries frames in order between two ends. A frame is a value; whether it is bytes is a property of the transport (section 6). The two ends are equal: each runs the same protocol, and nothing in it says which end is which.
A link MUST deliver every frame it accepts, in the order it accepted them, or end.
A link MUST NOT reorder, drop or duplicate a frame. The protocol detects nothing and repairs nothing at this level: a link that cannot keep this promise ends, and a new link is a new conversation.
Delivery MUST NOT be synchronous with the send. A receiver applies commits, and applying a commit from inside another document's delivery breaks the ordering the receiver rests on.
Nothing resumes. When a link ends, everything about it is forgotten at both ends. Opening a new one, and sharing again on it, is the application's.
2. Topics
A link carries any number of documents. Each is a topic, named by a string the application chose and numbered by each end independently.
An end numbers the topics it opens, starting at 1, unique on that link for as long as the link lives. A number is never reused on one link.
A frame names a topic by the sender's number for it, with one exception: a
faultthat answers a frame names the topic by the number that frame carried, which is the other end's, because the other end is the one that has to find the topic that ended. Each end therefore keeps two maps: its own numbers, and the numbers the other end announced withopen.An
openis the only frame that carries a name. It states the sender's number, the name, the sender's root id and kind, and whether the senderwants the other end's state. An end that holds nothing under the name states no root (both null) and MUST setwant.Two ends that both hold nothing under a name each answer the other's
openwithfaultnamingno-document, and the topic never becomes live. Nothing waits in silence.A topic is live at an end once that end has both shared the name and received the other end's
openfor it. An end MUST NOT sendcommitsorstatefor a topic that is not live.An
opennaming a root id that differs from the root id this end shares under that name MUST be answered withfaultnamingroot-mismatch, and the topic never becomes live. Two documents are only ever kept in step when they are one document.An end sets
wantonly when it holds nothing for that name: a document it minted from the other end's root, or one it has chosen to throw away. An end that receivesopenwithwanttrue MUST answer withstateonce the topic is live.Commits an end made before the topic was live are sent when it becomes live, as ordinary
commits, unless the other end asked for state, in which case thestatecovers them and they are not sent.leaveends a topic at both ends. A frame about a topic that is not open MUST be answered withfaultnamingno-topic, and the link carries on.
3. Sequence numbers
Each end numbers the commits it sends on a topic, starting at 1. A commits frame states the sequence of its first commit; the rest follow in order. The numbers exist so a refused can name a commit. They are not used to detect loss: a link delivers in order or ends (section 1).
4. What an end does
On
commits: for each commit in order, ask the application whether to accept it, then apply it. A commit that is not accepted, or that the applier refuses, is reported to this end's application and answered withrefusednaming its sequence and the reasons. The next commit in the frame is still applied. A refusal refuses the commit and never the link.On
state: move this document to the state the frame describes, applying the difference against what it holds. A receiver MUST NOT replace its document, and MUST drop anything it holds that the state does not mention. Astatewith no commit means the other end's document is empty.On
refused: report it to this end's application, with the commit and its inverse when this end still holds them. An end keeps a bounded window of the commits it sent; a refusal naming a commit outside the window is reported with its sequence only.On
leave: forget the topic. Onfault: report it; afaultnaming a topic ends that topic, afaultnaming topic 0 ends the link.A commit that arrived over a link MUST NOT be sent back over the same link. It MUST be delivered to every other consumer of the document (another link, a store, a watcher) as an ordinary commit made at this end (design 055).
The protocol never decides which end yields when two ends conflict. That is the application's, and it acts by asking for the other end's state: an end MAY send
openagain for a live topic withwanttrue, and the other end answers withstate.
5. Frames
A frame is an array. Element 0 is the kind, as an integer. The encoding of every element is spec/format.md section 6.
| kind | frame | elements |
|---|---|---|
| 0 | open | topic, name, root id, root kind, want |
| 1 | state | topic, commit or null |
| 2 | commits | topic, first, list of commits, each as bytes |
| 3 | refused | topic, seq, list of reasons |
| 4 | leave | topic |
| 5 | fault | topic, reason, message |
A root kind is 0 for object, 1 for array, 2 for map. In an
openthe root id and kind are both null when the sender holds nothing; one null without the other isbad-frame, and so is no root withoutwant.wantis a boolean. Topic numbers are integers from 1; topic 0 in afaultmeans the link itself.A reason is
[code, message, path or null], where a path is a list of text steps. Codes are the apply-stage vocabulary ofspec/format.mdsection 7, or whatever the application'sacceptgave.In
refused,topicis the refusing end's number andseqis a sequence the other end assigned (section 3).A
commitsframe MUST carry at least one commit; arefusedat least one reason.A frame with the wrong number of elements for its kind, an unknown kind, or an element of the wrong type MUST be refused with the reason
bad-frame.Frames MUST NOT be compressed one at a time. Measured on a real commit stream, gzip per frame is larger than the frames themselves. Use the transport's shared-context compression.
A receiver MUST treat a frame that does not decode as the end of the link, not as an exception to raise into a delivery.
6. Transports
A frame is a value. A transport that carries values (an in-process pair, a structured-clone port) carries frames as they are; a transport that carries bytes encodes each frame with section 5 and spec/format.md section 6. Both are the one protocol.
A WebSocket carries a frame as one binary message. A text message on the same socket is not a frame and is not this protocol's: a receiver ignores it, and whatever else shares the socket ignores binary messages in turn. That is how a request channel rides beside a link without the link carrying anything but commits (section 7).
7. Not specified
Authority. Nothing here says who may write what. An end's
acceptis the application's and the protocol does not know what it checks.Who yields. Section 4, last item.
Resuming. Section 1, last item.
Application messages. A link carries commits and nothing else.
Read filtering. Every end sees the whole document it shares.