Set up a project
A system kept in sync by magic_sync is at least two programs: the
server, on PostgreSQL, and a replica, on SQLite, that runs on every
device. They share one thing — the tables that travel, as Rust structs — and
almost nothing else. The shop is laid out so:
shop/
├── rows/ the tables, as both sides hold them: a library
├── server/ the head office: a program, and a workspace of its own
└── register/ a register: a program, and a workspace of its own
A build of magic_sync is one side: the postgres feature builds the
server’s, the sqlite feature a replica’s. A build with both is refused —
the crate does not compile — and so is a build with neither. Everything in
this chapter follows from that.
The rows: one crate, no side of its own
You write a library for the rows and their codecs, which depends on
magic_syncwithout picking a side, and lets each program pick one through a feature of its own:
[package]
name = "shop_rows"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
# No side of its own: each program that uses the rows picks one, below.
# Outside this repository, leave `path` out.
magic_sync = { version = "0.1.1", path = "../../../magic_sync" }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "uuid", "chrono"] }
uuid = { version = "1", features = ["serde", "v7"] }
[features]
# The server builds the rows with `server`, a register with `register`: what
# only one side has (where the registers are) is behind its feature.
server = ["magic_sync/postgres"]
register = ["magic_sync/sqlite"]
What only one side has is behind its feature: where the server finds the
replicas (magic_sync::plan::Replicas) exists only on the server, so the
shop’s REGISTERS is #[cfg(feature = "server")]. The rows themselves and
the codec list compile on either side (The rows,
codecs).
The server
You write a program with the
postgresside, and the rows built for it:
[package]
name = "shop_server"
version = "0.0.0"
edition = "2021"
publish = false
# A workspace of its own: a build is the server's side or a replica's, never
# both, and Cargo builds a workspace's members together.
[workspace]
[dependencies]
# The server's side. Outside this repository, leave `path` out.
magic_sync = { version = "0.1.1", path = "../../../magic_sync", features = ["postgres"] }
# The rows both sides share, built for the server.
shop_rows = { path = "../rows", features = ["server"] }
axum = "0.8"
serde = { version = "1", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6", features = ["fs"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["serde", "v7"] }
sqlx is the engine’s database layer: the pool you hand it is sqlx’s, and so
are the transactions and connections it takes. Enable its postgres driver
and a runtime; the engine runs on tokio.
A replica
You write a program with the
sqliteside:
[package]
name = "shop_register"
version = "0.0.0"
edition = "2021"
publish = false
# A workspace of its own: a build is the server's side or a replica's, never
# both, and Cargo builds a workspace's members together.
[workspace]
[dependencies]
# A replica's side. Outside this repository, leave `path` out.
magic_sync = { version = "0.1.1", path = "../../../magic_sync", features = ["sqlite"] }
# The rows both sides share, built for a replica.
shop_rows = { path = "../rows", features = ["register"] }
axum = "0.8"
chrono = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "uuid", "chrono"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1", features = ["serde", "v7"] }
One workspace per side
Cargo builds the members of a workspace together and turns on, for a crate
they share, every feature any of them asks for. Put the server and a replica
in one workspace and the first cargo build, cargo test or editor check
that builds both builds magic_sync with both sides, and the crate refuses
to compile, saying so.
So each program is a workspace of its own: an empty [workspace] table in its
manifest, as above, or one workspace for everything on the server and another
for everything on the replica. The rows are a path dependency of both, and
belong to neither. Each side’s tests run in its own workspace, and a test
never needs the other side’s build: the server’s tests drive the server,
a replica’s tests drive the replica.
A replica that is not written in Rust needs none of this: it follows the replica protocol, and only the server uses the crate.
Outside this repository
The shop reaches the engine by path, because it lives next to it; a project
of yours depends on the published crates, and leaves path out:
magic_sync = { version = "0.1.0", features = ["postgres"] }
The rest of this part writes what goes in these crates: the tables, the rows, the codecs, then each side.