Skip to main content

neuron_state_memory/
lib.rs

1#![deny(missing_docs)]
2//! In-memory implementation of layer0's StateStore trait.
3//!
4//! Uses a `HashMap` behind a `RwLock` for concurrent access.
5//! Scopes are serialized to strings for use as key prefixes,
6//! providing full scope isolation. Search always returns empty
7//! (no semantic search support in the in-memory backend).
8
9use async_trait::async_trait;
10use layer0::effect::Scope;
11use layer0::error::StateError;
12use layer0::state::{SearchResult, StateStore};
13use std::collections::HashMap;
14use tokio::sync::RwLock;
15
16/// In-memory state store backed by a `HashMap` behind a `RwLock`.
17///
18/// Suitable for testing, prototyping, and single-process use cases
19/// where persistence across restarts is not required.
20pub struct MemoryStore {
21    data: RwLock<HashMap<String, serde_json::Value>>,
22}
23
24impl MemoryStore {
25    /// Create a new empty in-memory store.
26    pub fn new() -> Self {
27        Self {
28            data: RwLock::new(HashMap::new()),
29        }
30    }
31}
32
33impl Default for MemoryStore {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39/// Build a composite key from scope + key to ensure isolation.
40fn composite_key(scope: &Scope, key: &str) -> String {
41    let scope_str = serde_json::to_string(scope).unwrap_or_else(|_| "unknown".to_string());
42    format!("{scope_str}\0{key}")
43}
44
45/// Extract the user-facing key from a composite key, if it belongs to the given scope.
46fn extract_key<'a>(composite: &'a str, scope_prefix: &str) -> Option<&'a str> {
47    composite
48        .strip_prefix(scope_prefix)
49        .and_then(|rest| rest.strip_prefix('\0'))
50}
51
52#[async_trait]
53impl StateStore for MemoryStore {
54    async fn read(
55        &self,
56        scope: &Scope,
57        key: &str,
58    ) -> Result<Option<serde_json::Value>, StateError> {
59        let ck = composite_key(scope, key);
60        let data = self.data.read().await;
61        Ok(data.get(&ck).cloned())
62    }
63
64    async fn write(
65        &self,
66        scope: &Scope,
67        key: &str,
68        value: serde_json::Value,
69    ) -> Result<(), StateError> {
70        let ck = composite_key(scope, key);
71        let mut data = self.data.write().await;
72        data.insert(ck, value);
73        Ok(())
74    }
75
76    async fn delete(&self, scope: &Scope, key: &str) -> Result<(), StateError> {
77        let ck = composite_key(scope, key);
78        let mut data = self.data.write().await;
79        data.remove(&ck);
80        Ok(())
81    }
82
83    async fn list(&self, scope: &Scope, prefix: &str) -> Result<Vec<String>, StateError> {
84        let scope_prefix = serde_json::to_string(scope).unwrap_or_else(|_| "unknown".to_string());
85        let data = self.data.read().await;
86        let keys: Vec<String> = data
87            .keys()
88            .filter_map(|ck| {
89                extract_key(ck, &scope_prefix).and_then(|k| {
90                    if k.starts_with(prefix) {
91                        Some(k.to_string())
92                    } else {
93                        None
94                    }
95                })
96            })
97            .collect();
98        Ok(keys)
99    }
100
101    async fn search(
102        &self,
103        _scope: &Scope,
104        _query: &str,
105        _limit: usize,
106    ) -> Result<Vec<SearchResult>, StateError> {
107        // In-memory store does not support semantic search.
108        Ok(vec![])
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use serde_json::json;
116
117    #[tokio::test]
118    async fn write_and_read() {
119        let store = MemoryStore::new();
120        let scope = Scope::Global;
121
122        store.write(&scope, "key1", json!("value1")).await.unwrap();
123        let val = store.read(&scope, "key1").await.unwrap();
124        assert_eq!(val, Some(json!("value1")));
125    }
126
127    #[tokio::test]
128    async fn read_nonexistent_returns_none() {
129        let store = MemoryStore::new();
130        let scope = Scope::Global;
131
132        let val = store.read(&scope, "missing").await.unwrap();
133        assert_eq!(val, None);
134    }
135
136    #[tokio::test]
137    async fn write_overwrites_existing() {
138        let store = MemoryStore::new();
139        let scope = Scope::Global;
140
141        store.write(&scope, "key1", json!("first")).await.unwrap();
142        store.write(&scope, "key1", json!("second")).await.unwrap();
143        let val = store.read(&scope, "key1").await.unwrap();
144        assert_eq!(val, Some(json!("second")));
145    }
146
147    #[tokio::test]
148    async fn delete_removes_key() {
149        let store = MemoryStore::new();
150        let scope = Scope::Global;
151
152        store.write(&scope, "key1", json!("value1")).await.unwrap();
153        store.delete(&scope, "key1").await.unwrap();
154        let val = store.read(&scope, "key1").await.unwrap();
155        assert_eq!(val, None);
156    }
157
158    #[tokio::test]
159    async fn delete_nonexistent_is_ok() {
160        let store = MemoryStore::new();
161        let scope = Scope::Global;
162
163        let result = store.delete(&scope, "missing").await;
164        assert!(result.is_ok());
165    }
166
167    #[tokio::test]
168    async fn list_keys_with_prefix() {
169        let store = MemoryStore::new();
170        let scope = Scope::Global;
171
172        store
173            .write(&scope, "user:name", json!("Alice"))
174            .await
175            .unwrap();
176        store.write(&scope, "user:age", json!(30)).await.unwrap();
177        store
178            .write(&scope, "system:version", json!("1.0"))
179            .await
180            .unwrap();
181
182        let mut keys = store.list(&scope, "user:").await.unwrap();
183        keys.sort();
184        assert_eq!(keys, vec!["user:age", "user:name"]);
185    }
186
187    #[tokio::test]
188    async fn list_empty_prefix_returns_all() {
189        let store = MemoryStore::new();
190        let scope = Scope::Global;
191
192        store.write(&scope, "a", json!(1)).await.unwrap();
193        store.write(&scope, "b", json!(2)).await.unwrap();
194
195        let keys = store.list(&scope, "").await.unwrap();
196        assert_eq!(keys.len(), 2);
197    }
198
199    #[tokio::test]
200    async fn scopes_are_isolated() {
201        let store = MemoryStore::new();
202        let global = Scope::Global;
203        let session = Scope::Session(layer0::SessionId::new("s1"));
204
205        store
206            .write(&global, "key", json!("global_val"))
207            .await
208            .unwrap();
209        store
210            .write(&session, "key", json!("session_val"))
211            .await
212            .unwrap();
213
214        let global_val = store.read(&global, "key").await.unwrap();
215        let session_val = store.read(&session, "key").await.unwrap();
216
217        assert_eq!(global_val, Some(json!("global_val")));
218        assert_eq!(session_val, Some(json!("session_val")));
219    }
220
221    #[tokio::test]
222    async fn search_returns_empty() {
223        let store = MemoryStore::new();
224        let scope = Scope::Global;
225
226        let results = store.search(&scope, "query", 10).await.unwrap();
227        assert!(results.is_empty());
228    }
229
230    #[test]
231    fn default_store_is_empty() {
232        let store = MemoryStore::default();
233        let _ = store; // Just verify it constructs
234    }
235
236    #[test]
237    fn memory_store_implements_state_store() {
238        fn _assert_state_store<T: StateStore>() {}
239        _assert_state_store::<MemoryStore>();
240    }
241}