7 releases (4 breaking)

0.5.0 Sep 1, 2024
0.3.0 May 5, 2024
0.2.2 Sep 18, 2022
0.2.0 Aug 21, 2022
0.0.2 Sep 14, 2021

#6 in #synchronized

Download history 3/week @ 2024-07-01 36/week @ 2024-07-08 16/week @ 2024-07-15 10/week @ 2024-07-22 44/week @ 2024-07-29 15/week @ 2024-08-05 4/week @ 2024-08-12 145/week @ 2024-08-26 103/week @ 2024-09-02 5/week @ 2024-09-09 14/week @ 2024-09-16 20/week @ 2024-09-23 14/week @ 2024-09-30 7/week @ 2024-10-07 21/week @ 2024-10-14

62 downloads per month
Used in 4 crates (via aper)

MIT license

11KB
146 lines

Aper

GitHub Repo stars crates.io docs.rs wokflow state

Cartoonized face of an ape.

Aper is a Rust library for data synchronization using state machines. Aper provides mechanisms to represent common data structures in terms of state machines, as well as a transport-agnostic protocol for keeping multiple instances of a state machine synchronized across a network.

Use-cases include real-time multiplayer applications that operate on shared state, client-server applications that want to share state updates incrementally and bidirectionally, and multiplayer turn-based games.

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.
  • It defines a StateMachine::Conflict type, which describes a conflict which may occur when a transition is applied that is not valid at the time it is applied. For simple types where a conflict is impossible, you can use NeverConflict for this.
  • 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 aper::{Aper, AperSync};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Clone, Debug, Default, AperSync)]
struct Counter { value: i64 }

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

impl Aper for Counter {
    type Transition = CounterTransition;
    type Conflict = NeverConflict;

    fn apply(&self, event: &CounterTransition) -> Result<Counter, NeverConflict> {
        match event {
            CounterTransition::Reset => Ok(Counter {value: 0}),
            CounterTransition::Increment(amount) => Ok(Counter {value: self.value + amount}),
            CounterTransition::Decrement(amount) => Ok(Counter {value: self.value - 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 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.


Aper is rapidly evolving. Consider this a technology preview. See the list of issues outstanding for version 1.0

Dependencies

~250–700KB
~17K SLoC