|
5 | 5 | [](https://docs.rs/aper/)
|
6 | 6 | [](https://github.com/drifting-in-space/aper/actions/workflows/rust.yml)
|
7 | 7 |
|
8 |
| -<img src="https://aper.dev/ape.svg" alt="Cartoonized face of an ape." width="200px" /> |
| 8 | +Aper is a Rust library for data synchronization over a network. |
9 | 9 |
|
10 |
| -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. |
| 10 | +Aper supports optimistic updates and arbitrary business logic, making it useful for real-time collabrative and agentic use cases. |
11 | 11 |
|
12 |
| -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. |
| 12 | +## Introduction |
13 | 13 |
|
14 |
| -## What is a state machine? |
| 14 | +(More docs coming soon) |
15 | 15 |
|
16 |
| -For the purposes of Aper, a state machine is simply a `struct` or `enum` that |
17 |
| -implements `StateMachine` and has the following properties: |
18 |
| -- It defines a `StateMachine::Transition` type, through which every |
19 |
| - possible change to the state can be described. It is usually useful, |
20 |
| - though not required, that this be an `enum` type. |
21 |
| -- It defines a `StateMachine::Conflict` type, which describes a conflict which |
22 |
| - may occur when a transition is applied that is not valid at the time it is |
23 |
| - applied. For simple types where a conflict is impossible, you can use |
24 |
| - `NeverConflict` for this. |
25 |
| -- All state updates are deterministic: if you clone a `StateMachine` and a |
26 |
| - `Transition`, the result of applying the cloned transition to the cloned |
27 |
| - state must be identical to applying the original transition to the original |
28 |
| - state. |
| 16 | +Types marked with the `AperSync` trait can be stored in the `Store`, Aper's synchronizable data store. |
| 17 | +Aper includes several data structures that implement `AperSync` in the `aper::data_structures` module, which |
| 18 | +can be used as building blocks to build your own synchronizable types. |
29 | 19 |
|
30 |
| -Here's an example `StateMachine` implementing a counter: |
| 20 | +You can use these, along with the `AperSync` derive macro, to compose structs that also implement `AperSync`. |
31 | 21 |
|
32 | 22 | ```rust
|
33 |
| -use aper::{Aper, AperSync}; |
34 |
| -use serde::{Serialize, Deserialize}; |
| 23 | +use aper::{AperSync, data_structures::{Atom, Map}}; |
| 24 | +use uuid::Uuid; |
35 | 25 |
|
36 |
| -#[derive(Serialize, Deserialize, Clone, Debug, Default, AperSync)] |
37 |
| -struct Counter { value: i64 } |
| 26 | +#[derive(AperSync)] |
| 27 | +struct ToDoItem { |
| 28 | + pub done: Atom<bool>, |
| 29 | + pub name: Atom<String>, |
| 30 | +} |
38 | 31 |
|
39 |
| -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] |
40 |
| -enum CounterTransition { |
41 |
| - Reset, |
42 |
| - Increment(i64), |
43 |
| - Decrement(i64), |
| 32 | +#[derive(AperSync)] |
| 33 | +struct ToDoList { |
| 34 | + pub items: Map<Uuid, ToDoItem>, |
44 | 35 | }
|
| 36 | +``` |
| 37 | + |
| 38 | +To synchronize from the server to clients, Aper replicates changes to the `Store` when it receives them. To synchronize |
| 39 | +from clients to servers, we instead send *intents* to the server. |
| 40 | + |
| 41 | +Intents are represented as a serializable `enum` representing every possible action a user might take on the data. |
| 42 | +For example, in our to-do list, that represents creating a task, renaming a task, marking a task as (not) done, or |
| 43 | +removing completed items. |
45 | 44 |
|
46 |
| -impl Aper for Counter { |
47 |
| - type Transition = CounterTransition; |
48 |
| - type Conflict = NeverConflict; |
| 45 | +```rust |
| 46 | +use aper::Aper; |
| 47 | + |
| 48 | +#[derive(Serialize, Deserialize, Clone, std::cmp::PartialEq)] |
| 49 | +enum ToDoIntent { |
| 50 | + CreateTask { |
| 51 | + id: Uuid, |
| 52 | + name: String, |
| 53 | + }, |
| 54 | + RenameTask { |
| 55 | + id: Uuid, |
| 56 | + name: String, |
| 57 | + }, |
| 58 | + MarkDone { |
| 59 | + id: Uuid, |
| 60 | + done: bool, |
| 61 | + }, |
| 62 | + RemoveCompleted, |
| 63 | +} |
49 | 64 |
|
50 |
| - fn apply(&self, event: &CounterTransition) -> Result<Counter, NeverConflict> { |
51 |
| - match event { |
52 |
| - CounterTransition::Reset => Ok(Counter {value: 0}), |
53 |
| - CounterTransition::Increment(amount) => Ok(Counter {value: self.value + amount}), |
54 |
| - CounterTransition::Decrement(amount) => Ok(Counter {value: self.value - amount}), |
| 65 | +impl Aper for ToDoList { |
| 66 | + type Intent = ToDoIntent; |
| 67 | + type Error = (); |
| 68 | + |
| 69 | + fn apply(&mut self, intent: &ToDoIntent) -> Result<(), ()> { |
| 70 | + match intent { |
| 71 | + ToDoIntent::CreateTask { id, name } => { |
| 72 | + let mut item = self.items.get_or_create(id); |
| 73 | + item.name.set(name.to_string()); |
| 74 | + item.done.set(false); |
| 75 | + }, |
| 76 | + ToDoIntent::RenameTask { id, name } => { |
| 77 | + // Unlike CreateTask, we bail early with an `Err` if |
| 78 | + // the item doesn't exist. Most likely, the server has |
| 79 | + // seen a `RemoveCompleted` that removed the item, but |
| 80 | + // a client attempted to rename it before the removal |
| 81 | + // was synced to it. |
| 82 | + let mut item = self.items.get(id).ok_or(())?; |
| 83 | + item.name.set(name.to_string()); |
| 84 | + } |
| 85 | + ToDoIntent::MarkDone { id, done } => { |
| 86 | + let mut item = self.items.get(id).ok_or(())?; |
| 87 | + item.done.set(*done); |
| 88 | + } |
| 89 | + ToDoIntent::RemoveCompleted => { |
| 90 | + // TODO: need to implement .iter() on Map first. |
| 91 | + } |
55 | 92 | }
|
| 93 | + |
| 94 | + Ok(()) |
56 | 95 | }
|
57 | 96 | }
|
58 | 97 | ```
|
59 | 98 |
|
60 |
| -## Why not CRDT? |
61 |
| -[Conflict-free replicated data types](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) |
62 |
| -are a really neat way of representing data that's shared between peers. |
63 |
| -In order to avoid the need for a central “source of truth”, CRDTs require |
64 |
| -that update operations (i.e. state transitions) be [commutative](https://en.wikipedia.org/wiki/Commutative_property). |
65 |
| -This allows them to represent a bunch of common data structures, but doesn't |
66 |
| -allow you to represent arbitrarily complex update logic. |
67 |
| -By relying on a central authority, a state-machine approach allows you to |
68 |
| -implement data structures with arbitrary update logic, such as atomic moves |
69 |
| -of a value between two data structures, or the rules of a board game. |
70 |
| - |
71 | 99 | ---
|
72 | 100 |
|
73 | 101 | **Aper is rapidly evolving. Consider this a technology preview.** See the [list of issues outstanding for version 1.0](https://github.com/drifting-in-space/aper/labels/v1-milestone)
|
74 | 102 |
|
75 | 103 | - [Documentation](https://docs.rs/aper/)
|
76 |
| -- [Getting Started with Aper guide](https://aper.dev/guide/) |
77 | 104 | - [Examples](https://github.com/drifting-in-space/aper/tree/main/examples)
|
78 | 105 | - [Talk on Aper for Rust Berlin (20 minute video)](https://www.youtube.com/watch?v=HNzeouj0eKc&t=1852s)
|
0 commit comments