use crate::resolver::ResolvedData;
use std::cell::RefCell;
use std::collections::HashMap;
pub fn content_id(store_id: &str, root: &str, resource_key: &str, salt: Option<&str>) -> String {
format!("{store_id}:{root}:{resource_key}:{}", salt.unwrap_or(""))
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DiskArtifacts {
pub ciphertext: Vec<u8>,
pub proof_b64: String,
pub chunk_lens: Vec<u32>,
}
pub struct MemoryCache {
max_entries: usize,
max_bytes: usize,
inner: RefCell<Inner>,
}
struct Inner {
tick: u64,
bytes: usize,
map: HashMap<String, Entry>,
}
struct Entry {
last_used: u64,
data: ResolvedData,
}
impl MemoryCache {
pub fn new(max_entries: usize, max_bytes: usize) -> Self {
MemoryCache {
max_entries,
max_bytes,
inner: RefCell::new(Inner {
tick: 0,
bytes: 0,
map: HashMap::new(),
}),
}
}
pub fn get(&self, id: &str) -> Option<ResolvedData> {
let mut inner = self.inner.borrow_mut();
inner.tick += 1;
let tick = inner.tick;
let entry = inner.map.get_mut(id)?;
entry.last_used = tick;
Some(entry.data.clone())
}
pub fn put(&self, id: String, data: ResolvedData) {
let size = data.bytes.len();
if self.max_entries == 0 || size > self.max_bytes {
return;
}
let mut inner = self.inner.borrow_mut();
inner.tick += 1;
let tick = inner.tick;
if let Some(prev) = inner.map.insert(
id,
Entry {
last_used: tick,
data,
},
) {
inner.bytes -= prev.data.bytes.len();
}
inner.bytes += size;
self.evict(&mut inner);
}
fn evict(&self, inner: &mut Inner) {
while inner.map.len() > self.max_entries || inner.bytes > self.max_bytes {
let Some(victim) = inner
.map
.iter()
.min_by_key(|(_, e)| e.last_used)
.map(|(k, _)| k.clone())
else {
break;
};
if let Some(removed) = inner.map.remove(&victim) {
inner.bytes -= removed.data.bytes.len();
}
}
}
}
pub const DEFAULT_MEMORY_ENTRIES: usize = 256;
pub const DEFAULT_MEMORY_BYTES: usize = 32 * 1024 * 1024;
#[cfg(feature = "native")]
pub use disk::DiskCache;
#[cfg(all(feature = "wasm", not(feature = "native")))]
pub use disk_wasm::DiskCache;
#[cfg(feature = "native")]
mod disk {
use super::DiskArtifacts;
use digstore_core::hash::sha256;
use std::path::PathBuf;
pub struct DiskCache {
dir: PathBuf,
}
impl DiskCache {
pub fn new(dir: impl Into<PathBuf>) -> Self {
let dir = dir.into();
let _ = std::fs::create_dir_all(&dir);
DiskCache { dir }
}
fn path(&self, id: &str) -> PathBuf {
self.dir
.join(format!("{}.json", sha256(id.as_bytes()).to_hex()))
}
pub fn get(&self, id: &str) -> Option<DiskArtifacts> {
let raw = std::fs::read(self.path(id)).ok()?;
serde_json::from_slice(&raw).ok()
}
pub fn put(&self, id: &str, artifacts: &DiskArtifacts) {
if let Ok(bytes) = serde_json::to_vec(artifacts) {
let _ = std::fs::write(self.path(id), bytes);
}
}
pub fn remove(&self, id: &str) {
let _ = std::fs::remove_file(self.path(id));
}
}
}
#[cfg(all(feature = "wasm", not(feature = "native")))]
mod disk_wasm {
use super::DiskArtifacts;
use crate::node_fs;
use digstore_core::hash::sha256;
pub struct DiskCache {
dir: String,
}
impl DiskCache {
pub fn new(dir: impl AsRef<str>) -> Self {
let dir = dir.as_ref().trim_end_matches('/').to_string();
node_fs::mkdir_all(&dir);
DiskCache { dir }
}
fn path(&self, id: &str) -> String {
format!("{}/{}.json", self.dir, sha256(id.as_bytes()).to_hex())
}
pub fn get(&self, id: &str) -> Option<DiskArtifacts> {
let raw = node_fs::read_file(&self.path(id))?;
serde_json::from_slice(&raw).ok()
}
pub fn put(&self, id: &str, artifacts: &DiskArtifacts) {
if let Ok(bytes) = serde_json::to_vec(artifacts) {
node_fs::write_file(&self.path(id), &bytes);
}
}
pub fn remove(&self, id: &str) {
node_fs::remove_file(&self.path(id));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resolver::ResolvedData;
fn data(n: usize) -> ResolvedData {
ResolvedData::new(vec![0u8; n], "image/png".into())
}
#[test]
fn content_id_is_stable_and_distinguishes_salt_and_root() {
assert_eq!(
content_id("s", "r", "a.png", None),
content_id("s", "r", "a.png", None)
);
assert_ne!(
content_id("s", "r1", "a.png", None),
content_id("s", "r2", "a.png", None)
);
assert_ne!(
content_id("s", "r", "a.png", Some("aa")),
content_id("s", "r", "a.png", None)
);
}
#[test]
fn memory_cache_hits_and_misses() {
let c = MemoryCache::new(8, 1 << 20);
assert!(c.get("k").is_none());
c.put("k".into(), data(10));
assert_eq!(c.get("k").unwrap().bytes.len(), 10);
}
#[test]
fn memory_cache_evicts_lru_at_entry_cap() {
let c = MemoryCache::new(2, 1 << 20);
c.put("a".into(), data(1));
c.put("b".into(), data(1));
let _ = c.get("a"); c.put("c".into(), data(1)); assert!(c.get("a").is_some());
assert!(c.get("c").is_some());
assert!(c.get("b").is_none(), "LRU entry evicted");
}
#[test]
fn memory_cache_evicts_at_byte_cap() {
let c = MemoryCache::new(100, 100);
c.put("a".into(), data(60));
c.put("b".into(), data(60)); assert!(c.get("a").is_none());
assert!(c.get("b").is_some());
c.put("big".into(), data(200));
assert!(c.get("big").is_none());
}
}