Timeline v1
Timeline v1 keeps one keyed state on both devices of a link and sends only what changed. The
canonical text is
timeline-v1.md
in zerobyte-protocol. This page explains how it works and when to use it.
It runs on top of Relay Protocol v1 and changes nothing in it. A relay that carries envelopes carries a timeline, and it sees what it saw before: envelope sizes and times.
When to use it
Use a timeline when both devices edit the same data, such as a plan, a list or settings, and both must end up with the same copy. Use plain messages (Designing messages) when data flows one way, or when each message is an event rather than a state.
The model
| Term | Meaning |
|---|---|
| Replica | One device's copy. It names itself with a random 16-byte replica id and draws a new one whenever it has lost its state |
| Slot | A collection (1 to 64 bytes of UTF-8) and a key (1 to 1,024 bytes), both chosen by the app |
| Entry | What a slot holds: a value of bytes, or a deletion |
| Commit | One replica's changes, numbered 1, 2, 3 … per replica and chained to the one before by its SHA-256 id |
| Stamp | A commit's clock, then its replica id: the order of the timeline |
A slot holds the entry with the greatest stamp. A deletion competes like a value, so a value written before a deletion never comes back. The merge is commutative, associative and idempotent: two replicas that merged the same commits hold the same state, whatever order the commits arrived in.
The clock
The clock is a 64-bit hybrid logical clock: milliseconds since 1970 shifted left by 16 bits, plus
a 16-bit counter. A commit takes max(last + 1, now_ms << 16), where last is the greatest clock
the replica has written or merged. A change made after seeing another therefore always orders after
it, whatever the two devices' wall clocks say. Wall clocks decide only between changes made without
seeing each other.
Messages
Each message is the plaintext of one envelope: at most 1 MiB, or the lower limit a relay set for the app.
| Type | Name | Carries |
|---|---|---|
0x01 |
commits | One or more commits |
0x02 |
state | The heads this replica has merged; may ask for a catch-up |
0x03 |
catch-up | Every entry the requester has not seen, in parts; only the final part carries heads |
A replica asks for a catch-up when it is new, when it has lost its state, or when it found a gap in another replica's commits.
One run over the relay
Each replica writes in its own direction: the primary p2c, the companion c2p. One run:
- Fetch the other direction, open each envelope and merge each message in order. Drop queued
commits the other replica has confirmed, by its state or by the
seenof its commits. - Persist, then prune what was fetched.
- Commit pending changes.
- Answer a catch-up request, if there is one. A long answer goes out over several runs: the replica saves its place, and the final part carries the heads from when the answer began. A newer request replaces an answer still being written, and an answer with nothing the requester lacks is not sent.
- Write the commits and catch-up parts not yet written; then a state if the run changed something (merged a commit, replaced an entry or raised a head) and wrote no commit, or if it asks for a catch-up. A read that changed nothing is not acknowledged.
- On
quota_exceeded, stop and keep the rest for the next run.
The last envelope of a run may ask the relay for a silent push to wake the other device. A run with nothing new to send writes no envelope.
Two devices on the same network can skip the relay: each sends its state, answers the other's with a full catch-up, and closes. Commits the relay delivers later are skipped as already merged. Commits a device makes and sends during the exchange also wait for the relay, until the other device confirms them.
Errors
| Error | Means |
|---|---|
malformed |
Bytes out of order, out of range, or left over |
unsupported_version |
A commit version or a message type this replica does not know |
too_large |
A commit or message over the limit, or a single change that cannot fit in a commit of its own |
diverged |
A replica's history does not continue the one already merged: the same sequence with another id, or a wrong parent or clock |
Versioning
A commit's first byte is the Timeline version, 0x01. Timeline versions are independent of the
relay protocol's: Timeline v1 runs on Relay Protocol v1.
SDKs
Every SDK implements Timeline v1 from 1.2.0: ZeroByteTimeline in Swift,
zerobyte-timeline in Kotlin, @zerobyte-cloud/sdk/timeline in
JavaScript and ZeroByte.Timeline in .NET.
All four pass the test vectors below.
Test vectors
spec/timeline.json in zerobyte-protocol holds clock steps, three commits with their encodings and
ids, one message of each type, two merge orders that must reach the same state, a catch-up
selection and the rejections. spec/timeline.py, an independent reference that uses only the
Python standard library, produces it. Implementations copy the file; they never regenerate it.