Aper
Aper is a framework for real-time sharing of arbitrary application state over WebSockets.
With Aper, you represent your program state as a state machine by implementing the [StateMachine] trait. Aper then provides the infrastructure to keep clones of this state synchronized across multiple clients, including clients running in WebAssembly via WebSockets.
Organization
The Aper project is divided over a number of crates. This crate,
aper
, does not provide any functionality, it merely defines a spec
that is used by the other crates.
The other crates, aper-yew
and aper-actix
, provide client and server
implementations (respectively).
What is a state machine?
For the purposes of Aper, a state machine is simply a struct
or enum
that
implements [StateMachine] and has the following properties:
- It defines a [StateMachine::Transition] type, through which every
possible change to the state can be described. It is usually useful,
though not required, that this be an
enum
type. - All state updates are deterministic: if you clone a [StateMachine] and a [Transition], the result of applying the cloned transition to the cloned state must be identical to applying the original transition to the original state.
Here's an example [StateMachine] implementing a counter:
# use ;
# use ;
;
State Programs
[StateMachine]s can take whatever transition types they want, but in order to interface with the Aper client/server infrastructure, a [StateMachine] must have a [TransitionEvent] transition type. This wraps up a regular [Transition] with metadata that the client produces (namely, the ID of the player who initiated the event and the timestamp of the event).
In order to tell the Rust typesystem that a [StateMachine] is compatible, it must also implement the [StateProgram] trait. This also gives you a way to implement suspended events.
Typically, a program in Aper will have only one trait that implements [StateProgram], but may have multiple traits that implement [StateMachine] used in the underlying representation of [StateProgram].
If you just want to serve a [StateMachine] data structure and don't need transition metadata, you can construct a [StateMachineContainerProgram] which simply strips the metadata and passes the raw transition into the state machine, i.e.:
# use ;
# use ;
#
# ;
#
#
# ;
#
#
#
#
How it works
When a client first connects to the server, the server sends back a complete serialized copy of the current state. After that, it sends and receives only [TransitionEvent]s to/from the server. By applying these [TransitionEvent]s to its local copy, each client keeps its local copy of the state synchronized with the server.
It is important that the server guarantees that each client receives [TransitionEvent]s in the same order, since the way a transition is applied may depend on previous state. For example, if a transition pushes a value to the end of a list, two clients receiving the transitions in a different order would have internal states which represented different orders of the list.
Why not CRDT?
Conflict-free replicated data types are a really neat way of representing data that's shared between peers. In order to avoid the need for a central “source of truth”, CRDTs require that update operations (i.e. state transitions) be commutative. This allows them to represent a bunch of common data structures, but doesn't allow you to represent arbitrarily complex update logic.
By relying on a central authority, a state-machine approach allows you to implement data structures with arbitrary update logic, such as atomic moves of a value between two data structures, or the rules of a board game.
Vocabulary Conventions
- A player is a connection to the service. The term user is probably a more conventional description, but multiplayer is often used in the context of non-game multi-user apps, and I've chosen to adopt it here because I think our users should be having fun.
- A transition represents a way to update the state. For example, “draw a circle at (4, 6)” is a transition.
- An event (or transition event) is a specific invocation of a transition by a user at a time. For example, “player A drew a circle at (4, 6) at 10:04 PM” is an event.
- A channel is the combination of a state object and the players currently connected to it. You can think of this as analogous to a room or channel in a chat app, except that the state is an arbitrary state machine instead of a sequential list of messages. The state of each channel is independent from one another: state changes in one channel do not impact the state in another, much like messages in one chat room do not appear in another.