jamsocket/aper

Synchronizable structs for Rust.

JavaScript

312

392 commits

updated Oct 5, 2024

See the code

README

Aper

GitHub Repo stars crates.io docs.rs wokflow state

Aper is a Rust library for data synchronization over a network.

Aper supports optimistic updates and arbitrary business logic, making it useful for real-time collabrative and agentic use cases.

Introduction

(Aper is mid-refactor. Docs and examples may be out of date.)

Types marked with the AperSync trait can be stored in the Store, Aper's synchronizable data store. Aper includes several data structures that implement AperSync in the aper::data_structures module, which can be used as building blocks to build your own synchronizable types.

You can use these, along with the AperSync derive macro, to compose structs that also implement AperSync.

use aper::{AperSync, data_structures::{Atom, Map}};
use uuid::Uuid;

#[derive(AperSync)]
struct ToDoItem {
   pub done: Atom<bool>,
   pub name: Atom<String>,
}

#[derive(AperSync)]
struct ToDoList {
   pub items: Map<Uuid, ToDoItem>,
}

To synchronize from the server to clients, Aper replicates changes to the Store when it receives them. To synchronize from clients to servers, we instead send intents to the server.

Intents are represented as a serializable enum representing every possible action a user might take on the data. For example, in our to-do list, that represents creating a task, renaming a task, marking a task as (not) done, or removing completed items.

use aper::Aper;

#[derive(Serialize, Deserialize, Clone, std::cmp::PartialEq)]
enum ToDoIntent {
    CreateTask {
        id: Uuid,
        name: String,
    },
    RenameTask {
        id: Uuid,
        name: String,
    },
    MarkDone {
        id: Uuid,
        done: bool,
    },
    RemoveCompleted,
}

impl Aper for ToDoList {
    type Intent = ToDoIntent;
    type Error = ();

    fn apply(&mut self, intent: &ToDoIntent) -> Result<(), ()> {
        match intent {
            ToDoIntent::CreateTask { id, name } => {
                let mut item = self.items.get_or_create(id);
                item.name.set(name.to_string());
                item.done.set(false);
            },
            ToDoIntent::RenameTask { id, name } => {
                // Unlike CreateTask, we bail early with an `Err` if
                // the item doesn't exist. Most likely, the server has
                // seen a `RemoveCompleted` that removed the item, but
                // a client attempted to rename it before the removal
                // was synced to it.
                let mut item = self.items.get(id).ok_or(())?;
                item.name.set(name.to_string());
            }
            ToDoIntent::MarkDone { id, done } => {
                let mut item = self.items.get(id).ok_or(())?;
                item.done.set(*done);
            }
            ToDoIntent::RemoveCompleted => {
                // TODO: need to implement .iter() on Map first.
            }
        }

        Ok(())
    }
}

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

rust
state
state-machine

Contributors

paulgb

381 commits

seanchen1991

7 commits

joonazan

2 commits

ckaran

1 commits

jamsocket/aper

Synchronizable structs for Rust.

JavaScript

312

392 commits

updated Oct 5, 2024

See the code

README

Aper

GitHub Repo stars crates.io docs.rs wokflow state

Aper is a Rust library for data synchronization over a network.

Aper supports optimistic updates and arbitrary business logic, making it useful for real-time collabrative and agentic use cases.

Introduction

(Aper is mid-refactor. Docs and examples may be out of date.)

Types marked with the AperSync trait can be stored in the Store, Aper's synchronizable data store. Aper includes several data structures that implement AperSync in the aper::data_structures module, which can be used as building blocks to build your own synchronizable types.

You can use these, along with the AperSync derive macro, to compose structs that also implement AperSync.

use aper::{AperSync, data_structures::{Atom, Map}};
use uuid::Uuid;

#[derive(AperSync)]
struct ToDoItem {
   pub done: Atom<bool>,
   pub name: Atom<String>,
}

#[derive(AperSync)]
struct ToDoList {
   pub items: Map<Uuid, ToDoItem>,
}

To synchronize from the server to clients, Aper replicates changes to the Store when it receives them. To synchronize from clients to servers, we instead send intents to the server.

Intents are represented as a serializable enum representing every possible action a user might take on the data. For example, in our to-do list, that represents creating a task, renaming a task, marking a task as (not) done, or removing completed items.

use aper::Aper;

#[derive(Serialize, Deserialize, Clone, std::cmp::PartialEq)]
enum ToDoIntent {
    CreateTask {
        id: Uuid,
        name: String,
    },
    RenameTask {
        id: Uuid,
        name: String,
    },
    MarkDone {
        id: Uuid,
        done: bool,
    },
    RemoveCompleted,
}

impl Aper for ToDoList {
    type Intent = ToDoIntent;
    type Error = ();

    fn apply(&mut self, intent: &ToDoIntent) -> Result<(), ()> {
        match intent {
            ToDoIntent::CreateTask { id, name } => {
                let mut item = self.items.get_or_create(id);
                item.name.set(name.to_string());
                item.done.set(false);
            },
            ToDoIntent::RenameTask { id, name } => {
                // Unlike CreateTask, we bail early with an `Err` if
                // the item doesn't exist. Most likely, the server has
                // seen a `RemoveCompleted` that removed the item, but
                // a client attempted to rename it before the removal
                // was synced to it.
                let mut item = self.items.get(id).ok_or(())?;
                item.name.set(name.to_string());
            }
            ToDoIntent::MarkDone { id, done } => {
                let mut item = self.items.get(id).ok_or(())?;
                item.done.set(*done);
            }
            ToDoIntent::RemoveCompleted => {
                // TODO: need to implement .iter() on Map first.
            }
        }

        Ok(())
    }
}

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

rust
state
state-machine

Contributors

paulgb

381 commits

seanchen1991

7 commits

joonazan

2 commits

ckaran

1 commits

Languages

JavaScript

39.2%

Rust

30.8%

HTML

27.5%

CSS

2.2%