magic_sync 0.1.1

Keeps partial copies of PostgreSQL tables on SQLite replicas — offline-first, by comparing what each replica holds with what it should hold
Documentation
// The sync engine's messages, for a replica written in any language.
// PROTOCOL.md says what each one means and what a replica must do with it.
// The engine's Rust types (src/envelope/sync.rs) are generated from this file,
// and a test fails while they differ from what it generates.
syntax = "proto3";

package sync;

// A row's key: its parts as text, in primary key order — a uuid part
// lowercase and hyphenated, an integer part decimal.
message Key {
  repeated string parts = 1;
}

// Everything the server sends a replica: one message, whichever it is.
message ToReplica {
  // The version of the protocol the server speaks. A replica that receives a
  // later one than its own does not act on it, and does not acknowledge it.
  uint32 protocol = 1;
  oneof body {
    Down down = 2;
    EndSnapshot end_snapshot = 3;
    Filed filed = 4;
  }
}

// One table's part of a message: rows to hold, then rows to let go.
message Table {
  string table = 1;
  // Rows to hold, each encoded as the table's own row.
  repeated bytes upserts = 2;
  // Rows to let go of.
  repeated Key deletes = 3;
  // The key of each upsert, in the same order, one per upsert: a side that
  // cannot read a row can still say which one it was.
  repeated Key keys = 4;
  // Up only: the replica re-states these rows, of a setting or answers to
  // wishes, as it holds them now (PROTOCOL.md section 7) — the server takes
  // them whatever their version.
  bool restated = 5;
  // Down only, in a snapshot: the rows of a setting the server records this
  // replica holding (PROTOCOL.md section 4.2) — the replica deletes, as its
  // own write, each it does not hold.
  repeated Key recorded = 6;
}

// Server to replica: rows to hold and rows to let go, in the order given —
// parents' upserts first, children's deletes first — applied in one
// transaction.
message Down {
  // Set while the replica is provisioned from scratch: it remembers every
  // row the snapshot names, and lets go of the rest when the snapshot ends.
  string snapshot_id = 1;
  repeated Table tables = 2;
  // Rises with every message the server sends, from 1 to 2^63 - 1: a replica
  // applies nothing at or below the last it applied from the same epoch.
  uint64 sequence = 3;
  // The server installation the sequence is from, a uuid.
  string epoch = 4;
  // The sequence of the message sent this replica before this one, in this
  // epoch: a replica that did not apply that one missed it. 0 only on the
  // message that begins a snapshot, which follows nothing.
  uint64 previous = 5;
  // A number of this message's own, drawn at random from 1 to 2^63 - 1: a
  // server restored from an older copy numbers its messages again, and
  // draws other nonces for them.
  uint64 nonce = 6;
  // The nonce of the message `previous` names: the one this follows is that
  // sequence with that nonce. 0 when `previous` is 0.
  uint64 previous_nonce = 7;
  // The server's seal on this message for this replica, opaque to it: the
  // replica gives it back as it came when it says it applied the message.
  bytes mac = 8;
}

// Server to replica: a snapshot is complete. Every row of `tables` the
// replica holds and the snapshot did not name goes, children first: in
// reverse of the order `tables` lists them.
message EndSnapshot {
  string snapshot_id = 1;
  repeated string tables = 2;
  // As Down's.
  uint64 sequence = 3;
  // As Down's.
  string epoch = 4;
  // As Down's: 0 when it ends a snapshot of nothing, which it begins.
  uint64 previous = 5;
  // As Down's.
  uint64 nonce = 6;
  uint64 previous_nonce = 7;
  bytes mac = 8;
}

// Server to replica: these uploads are filed, or kept to be filed once they
// can be. The notes they carried are done.
message Filed {
  repeated uint64 uploads = 1;
}

// Replica to server: rows it wrote, rows it could not take, what it holds,
// and the last message it applied. The transport says which replica sent it.
message Up {
  reserved 1;
  reserved "replica_id";
  repeated Table tables = 2;
  repeated Rejected rejected = 3;
  // What the replica holds of each table sent down, to compare with what the
  // server records it holds.
  repeated Digest digests = 4;
  // The last message the replica applied: its epoch, sequence, nonce and
  // mac, as the message carried them. Set on the first lane when the server
  // has not heard of it, and always with digests, which are only comparable
  // once the replica applied every message recorded for it, and with a gap.
  string epoch = 5;
  uint64 sequence = 6;
  uint64 nonce = 11;
  bytes mac = 12;
  // The number of this upload, when it carries rows: a fresh one on every
  // send, drawn at random from 1 to 2^63 - 1, so no answer meant for another
  // replica, or for this one before a restore, names it. The server answers
  // with Filed once it filed it.
  repeated uint64 uploads = 7;
  // The version of the protocol the replica speaks.
  uint32 protocol = 8;
  // What the replica wrote that the server has not filed, when it says: on
  // the first lane, when it changed. Absent: it did not say this time.
  Backlog backlog = 9;
  // It missed a message: the sequence of the newest one that arrived not
  // following the last it applied, and that it therefore did not apply. 0:
  // none. Said on the first lane, with the last applied, until a snapshot
  // begins, and told again whenever it grows; the server answers with a
  // snapshot, unless one that began after this sequence is on its way.
  uint64 gap = 10;
}

// What a replica wrote that the server has not filed yet.
message Backlog {
  repeated TableBacklog tables = 1;
}

// One table's part of a backlog.
message TableBacklog {
  string table = 1;
  // Rows not sent yet.
  uint64 unsent = 2;
  // Rows sent the server has not said it filed.
  uint64 unfiled = 3;
  // Rows that cannot go up as they are — larger than a message, refused by
  // the transport, of a table or a key this build cannot read — kept until
  // the replica runs again.
  uint64 set_aside = 4;
}

// A row a replica could not take, or could not let go of, and why. The key
// is required.
message Rejected {
  string table = 1;
  Key key = 2;
  string reason = 3;
  // The message whose row, or delete, the replica refused: the server
  // ignores a rejection of a message other than the last one that carried
  // the row, or of another epoch.
  string epoch = 4;
  uint64 sequence = 5;
  // The replica did not take the row because it deleted it, a row of its
  // own, and the server has not filed the delete: no fault of the row's,
  // nothing a fix cures. The server sends the row again, should it keep it.
  bool held_back = 6;
}

// What a replica holds of one table: how many rows, and a fingerprint of
// their keys. Rows it retired, or rejected, are left out.
message Digest {
  string table = 1;
  uint64 count = 2;
  uint64 fingerprint = 3;
}

// A row of the engine's table `sync_wishes`, which travels as any table
// written on both sides does: down in a `Down`'s tables, up in an `Up`'s. It
// is what the server wants one setting of the replica to hold: a row of a
// table the replica owns, whose desired columns the server may wish other
// values (PROTOCOL.md 4.5). The server owns the first fields; the replica the
// answer, which the server never sends down.
message Wish {
  reserved 2;
  reserved "replica_id";
  // A uuid: the row's key.
  string id = 1;
  // The setting's table, and its row's key.
  string table = 3;
  Key key = 4;
  // The row as wished, encoded as the setting's table's own row: the replica
  // takes its desired columns, and compares its version when `if_unchanged`.
  bytes wanted = 5;
  // Drawn at random for every wish, from 1 to 2^63 - 1: an answer is of the
  // wish whose version it names.
  uint64 version = 6;
  // Carried out only while the replica holds the version `wanted` carries.
  bool if_unchanged = 7;
  // The server's words on it: who asked, why.
  string note = 8;
  // The replica's answer: the version it answers, 0 for none yet.
  uint64 answered = 9;
  // "applied", "refused", "superseded" or "waiting"; empty with no answer.
  string outcome = 10;
  string reason = 11;
  // When it answered, RFC 3339, by the replica's clock.
  string answered_at = 12;
}