use serde::{Deserialize, Serialize};
use crate::error::AppError;
use crate::store::KeyspaceHandle;
use vta_sdk::protocols::memory::MemoryItem;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct MemoryRecord {
key: String,
value: String,
}
fn store_key(context_id: &str, key: &str) -> String {
format!("mem:{context_id}:{key}")
}
fn context_prefix(context_id: &str) -> String {
format!("mem:{context_id}:")
}
pub async fn put(
memory_ks: &KeyspaceHandle,
context_id: &str,
key: &str,
value: &str,
) -> Result<(), AppError> {
let record = MemoryRecord {
key: key.to_string(),
value: value.to_string(),
};
memory_ks.insert(store_key(context_id, key), &record).await
}
pub async fn list(
memory_ks: &KeyspaceHandle,
context_id: &str,
) -> Result<Vec<MemoryItem>, AppError> {
let pairs = memory_ks
.prefix_iter_raw(context_prefix(context_id))
.await?;
let mut items = Vec::with_capacity(pairs.len());
for (_key_bytes, value_bytes) in pairs {
let record: MemoryRecord = serde_json::from_slice(&value_bytes)
.map_err(|e| AppError::Internal(format!("decode memory record: {e}")))?;
items.push(MemoryItem {
key: record.key,
value: record.value,
});
}
Ok(items)
}
pub async fn delete(
memory_ks: &KeyspaceHandle,
context_id: &str,
key: &str,
) -> Result<(), AppError> {
let sk = store_key(context_id, key);
if memory_ks.get_raw(sk.clone()).await?.is_none() {
return Err(AppError::NotFound(format!(
"memory entry `{key}` not found in context `{context_id}`"
)));
}
memory_ks.remove(sk).await
}
#[cfg(test)]
mod tests {
use super::*;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
async fn open() -> (tempfile::TempDir, KeyspaceHandle) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.unwrap();
let ks = store.keyspace(crate::keyspaces::MEMORY).unwrap();
(dir, ks)
}
#[tokio::test]
async fn put_then_list_returns_the_entry() {
let (_d, ks) = open().await;
put(&ks, "ctx-a", "name", "Ada").await.unwrap();
let items = list(&ks, "ctx-a").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].key, "name");
assert_eq!(items[0].value, "Ada");
}
#[tokio::test]
async fn put_same_key_twice_upserts() {
let (_d, ks) = open().await;
put(&ks, "ctx-a", "name", "Ada").await.unwrap();
put(&ks, "ctx-a", "name", "Grace").await.unwrap();
let items = list(&ks, "ctx-a").await.unwrap();
assert_eq!(items.len(), 1, "second put must replace, not append");
assert_eq!(items[0].value, "Grace");
}
#[tokio::test]
async fn delete_removes_and_unknown_is_not_found() {
let (_d, ks) = open().await;
put(&ks, "ctx-a", "k", "v").await.unwrap();
delete(&ks, "ctx-a", "k").await.unwrap();
assert!(list(&ks, "ctx-a").await.unwrap().is_empty());
let err = delete(&ks, "ctx-a", "k").await.unwrap_err();
assert!(matches!(err, AppError::NotFound(_)), "{err:?}");
}
#[tokio::test]
async fn context_a_memory_is_not_returned_listing_context_b() {
let (_d, ks) = open().await;
put(&ks, "ctx-a", "secret", "a-only").await.unwrap();
put(&ks, "ctx-b", "secret", "b-only").await.unwrap();
let a = list(&ks, "ctx-a").await.unwrap();
let b = list(&ks, "ctx-b").await.unwrap();
assert_eq!(a.len(), 1);
assert_eq!(a[0].value, "a-only");
assert_eq!(b.len(), 1);
assert_eq!(b[0].value, "b-only");
}
#[tokio::test]
async fn prefix_scan_is_context_exact() {
let (_d, ks) = open().await;
put(&ks, "ctx", "k", "short").await.unwrap();
put(&ks, "ctx-extra", "k", "long").await.unwrap();
let items = list(&ks, "ctx").await.unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].value, "short");
}
}