use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use async_trait::async_trait;
use serde_json::Value;
#[async_trait]
pub trait State: Send + Sync {
async fn append(&self, key: &str, item: Value, max_len: Option<usize>, ttl: Option<Duration>);
async fn set(&self, key: &str, value: Value, ttl: Option<Duration>);
async fn get(&self, key: &str) -> Option<Value>;
}
pub struct NamespacedState {
inner: Arc<dyn State>,
prefix: String,
}
impl NamespacedState {
pub fn new(inner: Arc<dyn State>, namespace: impl std::fmt::Display) -> Self {
Self {
inner,
prefix: format!("{namespace}."),
}
}
pub fn prefix(&self) -> &str {
&self.prefix
}
fn key(&self, key: &str) -> String {
format!("{}{key}", self.prefix)
}
}
#[async_trait]
impl State for NamespacedState {
async fn append(&self, key: &str, item: Value, max_len: Option<usize>, ttl: Option<Duration>) {
self.inner.append(&self.key(key), item, max_len, ttl).await;
}
async fn set(&self, key: &str, value: Value, ttl: Option<Duration>) {
self.inner.set(&self.key(key), value, ttl).await;
}
async fn get(&self, key: &str) -> Option<Value> {
self.inner.get(&self.key(key)).await
}
}
struct Entry {
value: Value,
expires_at: Option<Instant>,
}
impl Entry {
fn is_expired(&self, now: Instant) -> bool {
self.expires_at.map(|e| now >= e).unwrap_or(false)
}
}
#[derive(Default)]
pub struct MemoryState {
map: Mutex<HashMap<String, Entry>>,
}
impl MemoryState {
pub fn new() -> Self {
Self::default()
}
pub fn from_snapshot(entries: serde_json::Map<String, Value>) -> Self {
Self {
map: Mutex::new(
entries
.into_iter()
.map(|(key, value)| {
(
key,
Entry {
value,
expires_at: None,
},
)
})
.collect(),
),
}
}
pub fn snapshot(&self) -> serde_json::Map<String, Value> {
let now = Instant::now();
self.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.filter(|(_, entry)| !entry.is_expired(now))
.map(|(key, entry)| (key.clone(), entry.value.clone()))
.collect()
}
fn expiry(ttl: Option<Duration>) -> Option<Instant> {
ttl.map(|d| Instant::now() + d)
}
}
#[async_trait]
impl State for MemoryState {
async fn append(&self, key: &str, item: Value, max_len: Option<usize>, ttl: Option<Duration>) {
let now = Instant::now();
let mut map = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(e) = map.get(key) {
if e.is_expired(now) {
map.remove(key);
}
}
let entry = map.entry(key.to_string()).or_insert_with(|| Entry {
value: Value::Array(Vec::new()),
expires_at: Self::expiry(ttl),
});
if !entry.value.is_array() {
entry.value = Value::Array(Vec::new());
}
if let Value::Array(list) = &mut entry.value {
list.push(item);
if let Some(cap) = max_len {
while list.len() > cap {
list.remove(0);
}
}
}
if ttl.is_some() {
entry.expires_at = Self::expiry(ttl);
}
}
async fn set(&self, key: &str, value: Value, ttl: Option<Duration>) {
let mut map = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
map.insert(
key.to_string(),
Entry {
value,
expires_at: Self::expiry(ttl),
},
);
}
async fn get(&self, key: &str) -> Option<Value> {
let now = Instant::now();
let mut map = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match map.get(key) {
Some(e) if e.is_expired(now) => {
map.remove(key);
None
}
Some(e) => Some(e.value.clone()),
None => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn append_respects_max_len() {
let s = MemoryState::new();
for i in 0..5 {
s.append("k", json!(i), Some(3), None).await;
}
let v = s.get("k").await.unwrap();
assert_eq!(v, json!([2, 3, 4]));
}
#[tokio::test]
async fn set_and_get() {
let s = MemoryState::new();
s.set("k", json!({"a": 1}), None).await;
assert_eq!(s.get("k").await.unwrap(), json!({"a": 1}));
assert!(s.get("missing").await.is_none());
}
#[tokio::test]
async fn namespaces_isolate_instances() {
let shared: Arc<dyn State> = Arc::new(MemoryState::new());
let first = NamespacedState::new(shared.clone(), "first");
let second = NamespacedState::new(shared, "second");
first.set("branch.value", json!(1), None).await;
second.set("branch.value", json!(2), None).await;
assert_eq!(first.get("branch.value").await, Some(json!(1)));
assert_eq!(second.get("branch.value").await, Some(json!(2)));
}
}