use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, Notify};
use crate::WalletError;
pub const MANIFEST_CACHE_TTL: Duration = Duration::from_secs(300);
pub const RECENT_GRANT_WINDOW: Duration = Duration::from_secs(15);
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub granted: bool,
pub expires_at: Instant,
}
struct ActiveRequest {
notify: Arc<Notify>,
result: Arc<Mutex<Option<Result<bool, String>>>>,
}
pub enum DeduplicateResult {
Proceed(ActiveRequestHandle),
Wait(Arc<Notify>, Arc<Mutex<Option<Result<bool, String>>>>),
}
pub struct ActiveRequestHandle {
key: String,
}
impl ActiveRequestHandle {
pub fn key(&self) -> &str {
&self.key
}
}
pub struct PermissionCache {
manifest_cache: Mutex<HashMap<String, CacheEntry>>,
recent_grants: Mutex<HashMap<String, Instant>>,
active_requests: Mutex<HashMap<String, ActiveRequest>>,
}
impl PermissionCache {
pub fn new() -> Self {
Self {
manifest_cache: Mutex::new(HashMap::new()),
recent_grants: Mutex::new(HashMap::new()),
active_requests: Mutex::new(HashMap::new()),
}
}
pub async fn check_cache(&self, key: &str) -> Option<bool> {
let mut cache = self.manifest_cache.lock().await;
if let Some(entry) = cache.get(key) {
if Instant::now() < entry.expires_at {
return Some(entry.granted);
}
cache.remove(key);
}
None
}
pub async fn insert_cache(&self, key: &str, granted: bool, ttl: Duration) {
let entry = CacheEntry {
granted,
expires_at: Instant::now() + ttl,
};
self.manifest_cache
.lock()
.await
.insert(key.to_string(), entry);
}
pub async fn is_recent_grant(&self, key: &str) -> bool {
let grants = self.recent_grants.lock().await;
if let Some(when) = grants.get(key) {
Instant::now().duration_since(*when) < RECENT_GRANT_WINDOW
} else {
false
}
}
pub async fn record_recent_grant(&self, key: &str) {
self.recent_grants
.lock()
.await
.insert(key.to_string(), Instant::now());
}
pub async fn try_deduplicate(&self, key: &str) -> DeduplicateResult {
let mut active = self.active_requests.lock().await;
if let Some(existing) = active.get(key) {
return DeduplicateResult::Wait(
Arc::clone(&existing.notify),
Arc::clone(&existing.result),
);
}
let notify = Arc::new(Notify::new());
let result = Arc::new(Mutex::new(None));
active.insert(
key.to_string(),
ActiveRequest {
notify: Arc::clone(¬ify),
result: Arc::clone(&result),
},
);
DeduplicateResult::Proceed(ActiveRequestHandle {
key: key.to_string(),
})
}
pub async fn complete_request(&self, key: &str, result: Result<bool, WalletError>) {
let mut active = self.active_requests.lock().await;
if let Some(req) = active.remove(key) {
let stored = result.map_err(|e| format!("{}", e));
*req.result.lock().await = Some(stored);
req.notify.notify_waiters();
}
}
pub async fn clear(&self) {
self.manifest_cache.lock().await.clear();
self.recent_grants.lock().await.clear();
let mut active = self.active_requests.lock().await;
for (_, req) in active.drain() {
*req.result.lock().await = Some(Err("Cache cleared".to_string()));
req.notify.notify_waiters();
}
}
pub fn build_cache_key(
permission_type: &super::types::PermissionType,
originator: &str,
details: &str,
) -> String {
format!("{:?}:{}:{}", permission_type, originator, details)
}
}
impl Default for PermissionCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::permissions::types::PermissionType;
#[tokio::test]
async fn test_cache_insert_and_check() {
let cache = PermissionCache::new();
cache
.insert_cache("proto:test", true, Duration::from_secs(60))
.await;
assert_eq!(cache.check_cache("proto:test").await, Some(true));
}
#[tokio::test]
async fn test_cache_expiry() {
let cache = PermissionCache::new();
cache
.insert_cache("proto:expired", true, Duration::from_secs(0))
.await;
tokio::time::sleep(Duration::from_millis(1)).await;
assert_eq!(cache.check_cache("proto:expired").await, None);
}
#[tokio::test]
async fn test_recent_grant_within_window() {
let cache = PermissionCache::new();
cache.record_recent_grant("proto:recent").await;
assert!(cache.is_recent_grant("proto:recent").await);
}
#[tokio::test]
async fn test_recent_grant_not_present() {
let cache = PermissionCache::new();
assert!(!cache.is_recent_grant("proto:missing").await);
}
#[tokio::test]
async fn test_deduplication_proceed_first() {
let cache = PermissionCache::new();
let result = cache.try_deduplicate("proto:dedup").await;
assert!(matches!(result, DeduplicateResult::Proceed(_)));
}
#[tokio::test]
async fn test_deduplication_wait_second() {
let cache = PermissionCache::new();
let _handle = match cache.try_deduplicate("proto:dedup2").await {
DeduplicateResult::Proceed(h) => h,
_ => panic!("Expected Proceed for first caller"),
};
let result = cache.try_deduplicate("proto:dedup2").await;
assert!(matches!(result, DeduplicateResult::Wait(..)));
cache.complete_request("proto:dedup2", Ok(true)).await;
}
#[tokio::test]
async fn test_build_cache_key_deterministic() {
let key1 = PermissionCache::build_cache_key(
&PermissionType::ProtocolPermission,
"example.com",
"proto:1:self",
);
let key2 = PermissionCache::build_cache_key(
&PermissionType::ProtocolPermission,
"example.com",
"proto:1:self",
);
assert_eq!(key1, key2);
let key3 = PermissionCache::build_cache_key(
&PermissionType::BasketAccess,
"example.com",
"my-basket",
);
assert_ne!(key1, key3);
}
#[tokio::test]
async fn test_clear_clears_all() {
let cache = PermissionCache::new();
cache
.insert_cache("k1", true, Duration::from_secs(300))
.await;
cache.record_recent_grant("k2").await;
cache.clear().await;
assert_eq!(cache.check_cache("k1").await, None);
assert!(!cache.is_recent_grant("k2").await);
}
}