Build Your App

Store

Stores keep track of synchronized state between the client and server. They are used to manage data that needs to be updated or fetched from the server with real-time capabilities.

Comparisons

Use Stores When...

  • You want a variable that is shared between the client and server.
  • You need real-time updates to data on the client when it changes on the server.
  • You want to make sure clients have access to the latest data from the server.

Other Options...

  • You want a value that is only updatable and visible on the server: use Local.
  • You want to push data from the server to the client without storing it: use Channels.

Features

Synchronization

Stores automatically synchronize their data between the server and connected clients. When the store's data is updated on the server, all subscribed clients receive the updated data in real-time.

Additionally, when a client connects to the server, it receives the current state of the store—you do not need to manually push an update.

Selection

Stores support selection, allowing you to control what data is sent to each client. This is useful for filtering sensitive information or sending only the relevant subset of data to a client based on their context (e.g., user role).

As an example, you can model as a game of Poker as a store where each player only sees their own hand of cards, while the server maintains the full game state. When a player connects, the store's select method is used to send only the relevant data (their hand) to that player.

Implementing selection is described in more detail in the server usage section below.

Server Usage

This is a quick overview of how to create and use stores on the server. For more detailed information on how to use features described, see subpages like Store Selection.

In Rust, a store is created by:

  1. Defining a struct that implements the StoreData trait.
  2. Accessing the store in RPC functions using the Store<T> type (or others).
  3. Statically declaring the store in the App builder.

We will be using a simple counter store as an example throughout this section.

/// A simple store that keeps track of a counter.
struct Counter {
count: u32,
}

// (1) Implementing StoreData for a struct
impl StoreData for Counter {
// ... Methods to configure the store
}

// (2) Using the store in an RPC function
async fn increment_counter(store: Store<Counter>) {
// ... RPC function body
}

fn build() -> App {
App::builder()
.rpc("increment_counter", increment_counter)
// (3) Statically declaring the store
.store::<Counter>()
.build()
}

maf::register!(build);

Implementing StoreData

The StoreData trait defines the necessary methods for a store, including initialization, selection, and key generation. Only the init method and Data associated type are required, while the others can be overridden for custom behavior.

Here is the full definition of the StoreData trait:

/// Describes the data stored in a [`Store`].
pub trait StoreData: Send + Sync + 'static {
/// The type of data selected to be serialized and sent to clients.
/// This type must implement [`serde::Serialize`].
type Select<'this>: Serialize;

/// Returns the name of the store to be used by clients to identify it.
///
/// If not specified, the default is the Rust type name of `Self`.
fn name() -> impl AsRef<str> + Send {
std::any::type_name::<Self>()
}

/// Returns the key of the store used internally to identify it. You should
/// rarely need to or want to override this.
fn key() -> impl Into<StoreKey> {
StoreKey::from(Self::name().as_ref())
}

/// Selects the portion of the store data to be serialized and sent to the
/// given user.
///
/// *some comments left out*
fn select(&self, user: &User) -> Self::Select<'_>;

/// Initializes the store data with a default value.
fn init() -> Self;
}

type Select and fn select

Select is an associated type that defines what is sent to the client when the store is synchronized. It must implement serde::Serialize. This allows you to customize the data sent to clients, which is especially useful for implementing authorization and filtering.

The select method is called whenever the store needs to be serialized for a client. It receives a reference to the User representing the client, allowing you to tailor the selected data based on the user's context.

Example Implementation:

impl StoreData for Counter {
// ... other methods ...
type Select<'this> = u32;

fn select(&self, _user: &User) -> Self::Select<'_> {
self.count
}
}

👻 What is the <'this> for? The <'this> lifetime parameter allows the Select type to borrow data from the store instance if needed. This is useful for avoiding unnecessary cloning of data when selecting what to send to the client. It is completely optional to use this feature; if your Select type does not need to borrow from the store, you can ignore this lifetime parameter.

For more information on lifetimes in Rust, refer to the Rust Book.

Also see the Store Selection page for more details and examples on how to use selection effectively.

fn name

The name method returns the name of the store, which is used client-side to refer to the store. It defaults to the type name of the struct implementing StoreData.

You may want to change this if the type name is long or not descriptive enough. As an example, instead of using my_crate::players::PlayerStore, you might want to just use players.

Example Implementation:

impl StoreData for PlayerStore {
// ... other methods ...
fn name() -> impl AsRef<str> + Send {
"players"
}
}

fn key

The key method returns a unique identifier for the store, which is used to distinguish it from other stores. It defaults to the store's name. key will be used in the future to allow multiple stores of the same type.

⚠️ This feature is experimental and still being developed. You should probably not need to override this method for now.

fn init

The init method is called when the store is initialized. It should return an instance of the Data type that the store will manage. This is where you can set up the initial state of the store.

impl StoreData for Counter {
// ... other methods ...
fn init() -> Self {
Self { count: 0 }
}
}

Accessing Stores In RPC Functions

Stores can be accessed in RPC functions by declaring them as parameters to the handler. Reading and writing to the store is done using Store.read() and Store.write(), with a locking API similar to RwLock<T>.

Example:

// Access a store with Store<T: StoreData>
async fn increment_counter(store: Store<Counter>) {
// Modify the store's data with `Store.write()`
store.write().await.count += 1;

// Access the store's data with `Store.read()`
let count = store.read().await.count;
println!("counter incremented to {}", count);
}

Client Usage

Stores can be accessed on the client using the maf.store method, which returns a Store instance.

const maf = new MafClient(/* ... */);

// This should match the return type of `StoreData::serialize` on the server
interface Counter {
count: number;
}

// Pass in the type of the store data as a type parameter
const store = maf.store<Counter>("counter");

// Subscribe to changes in the store
store.on("change", (data) => {
console.log("Store data changed:", data);
});