Build Your App
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.
Use Stores When...
Other Options...
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.
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.
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:
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);
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;
}
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.
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"
}
}
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.
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 }
}
}
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);
}
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);
});