use crate::value::{Js, Obj};
use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone, Debug)]
enum Spilled {
File(PathBuf),
}
pub struct Store {
rows: BTreeMap<String, Js>,
blob_bytes: usize,
budget: Option<usize>,
spill: Option<PathBuf>,
spilled: BTreeMap<String, Spilled>,
spill_count: u64,
}
impl Store {
pub fn in_memory() -> Store {
Store {
rows: BTreeMap::new(),
blob_bytes: 0,
budget: None,
spill: None,
spilled: BTreeMap::new(),
spill_count: 0,
}
}
pub fn spilling(dir: PathBuf, budget: usize) -> io::Result<Store> {
fs::create_dir_all(&dir)?;
let mut store = Store::in_memory();
store.budget = Some(budget);
store.spill = Some(dir);
Ok(store)
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn get(&self, key: &str) -> Option<Js> {
let row = self.rows.get(key)?;
if let Some(Spilled::File(path)) = self.spilled.get(key) {
let mut row = row.clone();
if let Js::Obj(o) = &mut row {
if let Some(Js::Blob(kind, _)) = o.get("blob").cloned() {
let bytes = fs::read(path).unwrap_or_default();
o.set("blob", Js::Blob(kind, Arc::new(bytes)));
}
}
return Some(row);
}
Some(row.clone())
}
pub fn peek(&self, key: &str) -> Option<&Js> {
self.rows.get(key)
}
pub fn contains(&self, key: &str) -> bool {
self.rows.contains_key(key)
}
pub fn put(&mut self, key: &str, value: Js) {
self.forget_blob(key);
let value = self.maybe_spill(key, value);
self.rows.insert(key.to_string(), value);
}
pub fn delete(&mut self, key: &str) {
self.forget_blob(key);
self.rows.remove(key);
}
pub fn keys(&self, prefix: &str) -> Vec<String> {
self.rows
.range(prefix.to_string()..)
.take_while(|(k, _)| k.starts_with(prefix))
.map(|(k, _)| k.clone())
.collect()
}
pub fn delete_prefix(&mut self, prefix: &str) {
for key in self.keys(prefix) {
self.delete(&key);
}
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Js)> {
self.rows.iter()
}
fn blob_len(value: &Js) -> usize {
match value {
Js::Obj(o) => match o.get("blob") {
Some(Js::Blob(_, b)) => b.len(),
_ => 0,
},
_ => 0,
}
}
fn forget_blob(&mut self, key: &str) {
if let Some(Spilled::File(path)) = self.spilled.remove(key) {
let _ = fs::remove_file(path);
} else if let Some(old) = self.rows.get(key) {
self.blob_bytes -= Self::blob_len(old);
}
}
fn maybe_spill(&mut self, key: &str, value: Js) -> Js {
let len = Self::blob_len(&value);
if len == 0 {
return value;
}
let (Some(budget), Some(dir)) = (self.budget, &self.spill) else {
self.blob_bytes += len;
return value;
};
if self.blob_bytes + len <= budget {
self.blob_bytes += len;
return value;
}
self.spill_count += 1;
let path = dir.join(format!("{}.bin", self.spill_count));
let Js::Obj(mut o) = value else {
unreachable!()
};
let Some(Js::Blob(kind, bytes)) = o.get("blob").cloned() else {
unreachable!()
};
if fs::write(&path, bytes.as_slice()).is_err() {
self.blob_bytes += len;
return Js::Obj(o);
}
o.set("blob", Js::Blob(kind, Arc::new(Vec::new())));
self.spilled.insert(key.to_string(), Spilled::File(path));
Js::Obj(o)
}
pub fn blob_bytes(&self) -> usize {
self.blob_bytes
}
pub fn spilled(&self) -> usize {
self.spilled.len()
}
}
impl Drop for Store {
fn drop(&mut self) {
for (_, Spilled::File(path)) in std::mem::take(&mut self.spilled) {
let _ = fs::remove_file(path);
}
}
}
pub fn obj(value: &Js) -> Option<&Obj> {
value.as_obj()
}