Crate aper[][src]

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 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 TransitionEvent (which represents a StateMachine::Transition wrapped in some metadata), the result of applying that transition must always result in the same underlying state.

This is similar to the state representation in frameworks like Redux and Yew.

How it works

When a client connects to the server, it receives a complete serialized copy of the current state. After that, it sends StateMachine::Transitions to the server and receives only TransitionEvents. 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.

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

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

impl StateMachine for Counter {
    type Transition = CounterTransition;

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

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 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 automic 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. State machines may use the "who" and "when" data to determine how a transition is applied. Events may also not have a player associated with them (as in suspended events), but always have a time.
  • 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.

Structs

PlayerID

An opaque identifier for a single connected user.

SuspendedEvent

Represents a transition that a crate::StateMachine would like to receive in the future from the server, as well as the time that it would like to receive it. Each state machine can have either zero or one transition suspended at any point in time, and may change or remove that suspended event every time its state is updated (and only then).

TransitionEvent

A transition with associated metadata: which player triggered it and when. The player ID is optional, since SuspendedEvents do not have a player associated with them.

Enums

StateUpdateMessage

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

Traits

StateMachine

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

StateMachineFactory

A trait indicating that a struct can be used to create a StateMachine for a given type. If your StateMachine does not need to be initialized with any external data or state, implement std::default::Default on it to avoid the need for a factory.