Map Stores

Overview

A Map Store provides an agent with a store for map state that is not publically addressable. If you require addressable value state, then a Value Lane will be suitable.

A Map Store has the following properties:

For instances where a value structure is required, a Value Store is available.

Example: using a Map Store to store the highest value associated with a key using a Map Lane:

use std::collections::HashMap;
use swimos::agent::{
    agent_lifecycle::utility::HandlerContext,
    event_handler::{EventHandler, HandlerActionExt},
    lanes::MapLane,
    lifecycle, projections,
    stores::MapStore,
    AgentLaneModel,
};

#[projections]
#[derive(AgentLaneModel)]
pub struct ExampleAgent {
    lane: MapLane<String, i32>,
    store: MapStore<String, i32>,
}

#[derive(Clone)]
pub struct ExampleLifecycle;

#[lifecycle(ExampleAgent)]
impl ExampleLifecycle {
    #[on_update(lane)]
    pub fn on_update(
        &self,
        context: HandlerContext<ExampleAgent>,
        _map: &HashMap<String, i32>,
        key: String,
        _prev: Option<i32>,
        new_value: &i32,
    ) -> impl EventHandler<ExampleAgent> + '_ {
        let new_value = *new_value;

        context
            .get_entry(ExampleAgent::STORE, key.clone())
            .and_then(move |opt: Option<i32>| {
                let state = opt.unwrap_or_default();
                context.update(ExampleAgent::STORE, key, i32::max(state, new_value))
            })
    }
}

Bounds and Initialisation

Map Stores place similar bounds to the standard library’s HashMap but with the addition of requiring both the key and value types to implement the swimos_form::Form and Send traits. When the store is first intiialised, the store is initialised to an empty map. If the state of the store is to be populated using the on_start handler, then care must be taken to not overwrite any previously held state, as it is not currently possible to detect restarts; a workaround is to add a ValueStore<bool> which is set high after the store has been initialised and then it is checked during the on_start call to avoid a duplicate initialisation.

Use cases

Map Stores are useful in situtations where you need to persist data that is derived from lane events received but you do not wish for it to be available outside of the scope of the agent. While it is possible to keep state in the agent’s lifecycle, this is lost when the agent is stopped and must be rebuilt each time the agent is started. Using a Map Store, the state is retreived from the underlying persistence engine when the agent first starts. Stores have the added benefit of their usability with the HandlerContext and do not require the use of interior mutability and are usable within futures.

Handler Context Operations

The HandlerContext provided as an argument to lifecycle event handlers provides access to two functions which are used to retreive or update the state of the store.

Update

Updates an entry in the map, replacing the value if it already existed. This does not return the previous value associated with the entry and if this is required, then a preceding call to retreive the value must be made; this is because it would require an additional operation to be made against the underlying persistence engine that would not be requried if the value is discarded. Defined as:

pub fn update<Item, K, V>(
    &self,
    lane: fn(&Agent) -> &Item,
    key: K,
    value: V,
) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
where
    Item: MutableMapLikeItem<K, V>,
    K: Send + Clone + Eq + Hash + 'static,
    V: Send + 'static,
{
    //...
}

Example:

#[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context.update(ExampleAgent::STORE, "a".to_string(), 1)
}

With Entry

Updates an entry in the store. Accepts a closure that is used to update the value associatedf with the key:

pub fn with_entry<'a, Item, K, V, F>(
    &self,
    lane: fn(&Agent) -> &Item,
    key: K,
    f: F,
) -> impl HandlerAction<Agent, Completion = ()> + Send + 'a
where
    Agent: 'static,
    Item: TransformableMapLikeItem<K, V> + 'static,
    K: Send + Clone + Eq + Hash + 'static,
    V: Clone + 'static,
    F: FnOnce(Option<V>) -> Option<V> + Send + 'a,
{
    //..
}

Example:

#[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context.with_entry(
        ExampleAgent::STORE,
        "a".to_string(),
        |entry: Option<i32>| entry.map(|current| current * 2),
    )
}

Remove

Removes an entry in the map. This does not return the previous value associated with the entry and if this is required, then a preceding call to retreive the value must be made; this is because it would require an additional operation to be made against the underlying persistence engine that would not be requried if the value is discarded. Defined as:

pub fn remove<Item, K, V>(
    &self,
    lane: fn(&Agent) -> &Item,
    key: K,
) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
where
    Item: MutableMapLikeItem<K, V>,
    K: Send + Clone + Eq + Hash + 'static,
    V: Send + 'static,
{
    //..
}

Example:

#[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context.remove(ExampleAgent::STORE, "a".to_string())
}

Clear

Clears the store. This does not return the current state of the store and if this is required, then a preceding call to retreive the current state must be made; this is because it would require an additional operation to be made against the underlying persistence engine that would not be requried if the map is discarded. Defined as:

pub fn clear<Item, K, V>(
    &self,
    lane: fn(&Agent) -> &Item,
) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
where
    Item: MutableMapLikeItem<K, V>,
    K: Send + Clone + Eq + Hash + 'static,
    V: Send + 'static,
{
    //..
}

Example:

#[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context.clear(ExampleAgent::STORE)
}

Replace Map

Replaces the contents of the store with the provided iterator’s elements. Defined as:

pub fn replace_map<Item, K, V, I>(
    &self,
    lane: fn(&Agent) -> &Item,
    entries: I,
) -> impl HandlerAction<Agent, Completion = ()> + Send + 'static
where
    Item: MutableMapLikeItem<K, V> + 'static,
    K: Send + Clone + Eq + Hash + 'static,
    V: Send + 'static,
    I: IntoIterator<Item = (K, V)>,
    I::IntoIter: Send + 'static,
{
    //..
}

Example:

#[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context.replace_map(ExampleAgent::STORE, HashMap::from([("a".to_string(), 13)]))
}

Get Entry

Gets an entry from the store. Defined as:

pub fn get_entry<Item, K, V>(
    &self,
    lane: fn(&Agent) -> &Item,
    key: K,
) -> impl HandlerAction<Agent, Completion = Option<V>> + Send + 'static
where
    Item: MapLikeItem<K, V>,
    K: Send + Clone + Eq + Hash + 'static,
    V: Send + Clone + 'static,
{
    //..
}

Example:

#[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context
        .get_entry(ExampleAgent::STORE, "a".to_string())
        .discard()
}

Get Map

Retreives the entire contents of the store. Defined as:

pub fn get_map<Item, K, V>(
    &self,
    lane: fn(&Agent) -> &Item,
) -> impl HandlerAction<Agent, Completion = HashMap<K, V>> + Send + 'static
where
    Item: MapLikeItem<K, V>,
    K: Send + Clone + Eq + Hash + 'static,
    V: Send + Clone + 'static,
{
    //..
}

Example:

 #[on_start]
pub fn on_start(
    &self,
    context: HandlerContext<ExampleAgent>,
) -> impl EventHandler<ExampleAgent> {
    context.get_map(ExampleAgent::STORE).discard()
}

Choosing The Right Store

Two stores are available for agents, a Map Store and a Value Store and it’s important to consider how you will be interacting with the type that is stored. Like Map Lanes, changes are scoped to the entries and so only these changes are propagated to the underlying persistence engine. If only a small amount of data is contained in the store, it may be more efficient to use a Value Store as map stores require some additional bookkeeping.

An example demonstrating using a Value Store instead of a Map Store for map-like data:

use std::collections::HashMap;
use swimos::{
    agent::lanes::MapLane,
    agent::stores::MapStore,
    agent::{
        agent_lifecycle::utility::HandlerContext,
        event_handler::{EventHandler, HandlerActionExt},
        lifecycle, projections,
        stores::MapStore,
        AgentLaneModel,
    },
};
use swimos_form::Form;

#[projections]
#[derive(AgentLaneModel)]
pub struct ExampleAgent {
    state: MapLane<String, i32>,
    map_store: MapStore<String, i32>,
    value_store: MapStore<Record>,
}

#[derive(Default, Form, Clone)]
struct Record {
    a: i32,
    b: i32,
    c: i32,
}

#[derive(Clone)]
pub struct ExampleLifecycle;

#[lifecycle(ExampleAgent)]
impl ExampleLifecycle {
    #[on_update(state)]
    pub fn on_update(
        &self,
        context: HandlerContext<ExampleAgent>,
        _map: &HashMap<String, i32>,
        key: String,
        _prev: Option<i32>,
        new_value: &i32,
    ) -> impl EventHandler<ExampleAgent> + '_ {
        let new_value = *new_value;
        context
            .get_value(ExampleAgent::VALUE_STORE)
            .and_then(move |mut current: Record| {
                match key.as_str() {
                    "a" => current.a = new_value,
                    "b" => current.b = new_value,
                    "c" => current.c = new_value,
                    key => panic!("unknown key: {key}"),
                }

                // While we have only updated a single field in Record
                // the entire type will be flushed to the underlying
                // persistence engine.
                context
                    .set_value(ExampleAgent::VALUE_STORE, current)
                    .map(|_| key)
            })
            .and_then(move |key| {
                // Here we only update the corresponding key in the map
                // store and only that portion of the map will be
                // flushed to the underlying persistence engine.
                context.update(ExampleAgent::MAP_STORE, key, new_value)
            })
    }
}

Further reading

It is recommended to understand server persistence to understand how stores are loaded and how state changes are persisted.

Try It Yourself

A standalone project that demonstrates Map Stores is available here.