Crate aper[][src]

Expand description

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:

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct Counter(i64);

#[derive(Transition, Serialize, Deserialize, Clone, Debug, PartialEq)]
enum CounterTransition {
    Reset,
    Increment(i64),
    Decrement(i64),
}

impl StateMachine for Counter {
    type Transition = CounterTransition;

    fn apply(&mut self, event: CounterTransition) {
        match event {
            CounterTransition::Reset => { self.0 = 0 }
            CounterTransition::Increment(amount) => { self.0 += amount }
            CounterTransition::Decrement(amount) => { self.0 -= amount }
        }
    }
}

State Programs

StateMachines 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 aper::StateMachineContainerProgram;
let state_program = StateMachineContainerProgram(Counter::default());

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 TransitionEvents to/from the server. By applying these TransitionEvents 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 TransitionEvents 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.

Modules

Structs

An opaque identifier for a single connected user.

A StateProgram implementation that can be built from any StateMachine. Transitions are stripped of their metadata and passed down to the underlying state machine.

Enums

A message from the server to a client that tells it to update its state.

Traits

This trait provides the methods that Aper needs to be able to interact with an object as a state machine.

This trait can be added to a StateMachine which takes a TransitionEvent as its transition. Only state machines with this trait can be used directly with the aper client/server infrastructure.

This trait indicates that a type can be used as the transition of a StateMachine.

Type Definitions

Derive Macros