Aper is a Rust library for representing data that can be read and written to by multiple users in real time.
Use-cases of Aper include managing the state of an application with real-time collaboration features, creating an timestamped audit trail of an arbitrary data structure, and synchronizing the game state of a multiplayer game.
aper library implements the underlying data structures and algorithms, but is agnostic to the
actual mechanism for transfering data on a network. The crates
aper-serve provide a client
and server implementation aimed at synchronizing state across multiple
WebAssembly clients using
For Aper to synchronize state, it must be represented as a state machine. This means that:
- It implements the
StateMachinetrait, which has two type arguments (
Conflict) and one method:
- All changes to its internal state flow through this
- Updates to state are entirely deterministic. They may depend on the current state and any data that is contained in the transition value, and nothing else.
- If a conflict arises (i.e. if
applyreturns anything other than
Ok(())), the state machine is not mutated.
#1 is enforced by Rust's type system, but it's your responsibility to satisfy the other three. In particular,
accessing the current time, non-determistic random number generators, or external data in
a violation of #3.
In principle, the way that Aper keeps state in sync is pretty simple: when a client connects, they receive a
full copy of the latest copy of the state. Thereafter, they receive a real-time stream of
client receives the same transitions in the same order, so their states are updated in lockstep. This is why
it's important that
apply is deterministic. If it were not, states could become divergent even if they
received the same transition stream.
Note that for this model to work, the client can't immediately apply a transition to its local state, because the client doesn't know whether another client is sending the server a transition at the same time. The client has two options:
- Wait to receive its own transition back from the server, and accept the latency associated with it. Depending on the use-case, a few hundred milliseconds of latency might be tolerable and the simpler model is easier to reason about.
- Keep two copies of the state, called
well as a FIFO queue
stashof local transitions. As local transitions fire, push them to
stashand proactively apply them to
optimistis used to render the view, so local transitions appear automatically. When the server sends a transition, check if it is the next transition in our
stash. If so, pop it from the
stashand apply it to
pessimist. Otherwise, we apply the server-sent transition to
pessimist, then clone it and apply every transition in the
stashto the clone. This clone becomes the new value of
optimist, and the old value is discarded.