use std::collections::HashMap;
use std::path::Path;
use std::sync::{Arc, OnceLock, RwLock};
use crate::graph::index_freshness::IndexFreshness;
use super::property_index;
#[derive(Debug)]
pub(crate) struct DiskIndexFreshness {
baseline: Arc<IndexFreshness>,
typed: RwLock<HashMap<(String, String), Arc<IndexFreshness>>>,
global: RwLock<HashMap<String, Arc<IndexFreshness>>>,
tracked: OnceLock<bool>,
}
impl DiskIndexFreshness {
pub(crate) fn covering(node_bound: u32) -> Self {
Self {
baseline: Arc::new(IndexFreshness::covering(node_bound, None)),
typed: RwLock::new(HashMap::new()),
global: RwLock::new(HashMap::new()),
tracked: OnceLock::new(),
}
}
#[inline]
pub(crate) fn tracks_anything(&self, data_dir: &Path) -> bool {
*self
.tracked
.get_or_init(|| directory_holds_a_bundle(data_dir))
}
pub(crate) fn mark_tracked(&self) {
let _ = self.tracked.set(true);
}
pub(crate) fn note_created(&self, slot: u32, node_type: &str) {
self.baseline.note_created(slot, true);
for (key, freshness) in self.typed.read().unwrap_or_else(|e| e.into_inner()).iter() {
freshness.note_created(slot, key.0 == node_type);
}
for freshness in self
.global
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
{
freshness.note_created(slot, true);
}
}
pub(crate) fn note_property_written(&self, slot: u32, node_type: Option<&str>) {
self.baseline.note_changed(slot);
for (key, freshness) in self.typed.read().unwrap_or_else(|e| e.into_inner()).iter() {
if node_type.is_none_or(|written| written == key.0) {
freshness.note_changed(slot);
}
}
for freshness in self
.global
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
{
freshness.note_changed(slot);
}
}
pub(crate) fn note_removed(&self, slot: u32) {
self.note_property_written(slot, None);
}
pub(crate) fn typed_is_fresh(&self, key: &(String, String), node_bound: u32) -> bool {
let registered = {
let read = self.typed.read().unwrap_or_else(|e| e.into_inner());
read.get(key).cloned()
};
let freshness = match registered {
Some(freshness) => freshness,
None => {
let inherited = Arc::new((*self.baseline).clone());
let mut write = self.typed.write().unwrap_or_else(|e| e.into_inner());
Arc::clone(write.entry(key.clone()).or_insert(inherited))
}
};
!freshness.is_stale(node_bound)
}
pub(crate) fn global_is_fresh(&self, property: &str, node_bound: u32) -> bool {
let registered = {
let read = self.global.read().unwrap_or_else(|e| e.into_inner());
read.get(property).cloned()
};
let freshness = match registered {
Some(freshness) => freshness,
None => {
let inherited = Arc::new((*self.baseline).clone());
let mut write = self.global.write().unwrap_or_else(|e| e.into_inner());
Arc::clone(write.entry(property.to_string()).or_insert(inherited))
}
};
!freshness.is_stale(node_bound)
}
pub(crate) fn mark_typed_built(&self, key: (String, String), node_bound: u32) {
self.mark_tracked();
self.typed
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(key, Arc::new(IndexFreshness::covering(node_bound, None)));
}
pub(crate) fn mark_global_built(&self, property: &str, node_bound: u32) {
self.mark_tracked();
self.global
.write()
.unwrap_or_else(|e| e.into_inner())
.insert(
property.to_string(),
Arc::new(IndexFreshness::covering(node_bound, None)),
);
}
pub(crate) fn forget_typed(&self, key: &(String, String)) {
self.typed
.write()
.unwrap_or_else(|e| e.into_inner())
.remove(key);
}
}
impl Clone for DiskIndexFreshness {
fn clone(&self) -> Self {
fn deep<K: Clone + Eq + std::hash::Hash>(
map: &RwLock<HashMap<K, Arc<IndexFreshness>>>,
) -> RwLock<HashMap<K, Arc<IndexFreshness>>> {
let read = map.read().unwrap_or_else(|e| e.into_inner());
RwLock::new(
read.iter()
.map(|(key, freshness)| (key.clone(), Arc::new((**freshness).clone())))
.collect(),
)
}
let tracked = OnceLock::new();
if let Some(value) = self.tracked.get() {
let _ = tracked.set(*value);
}
Self {
baseline: Arc::new((*self.baseline).clone()),
typed: deep(&self.typed),
global: deep(&self.global),
tracked,
}
}
}
fn directory_holds_a_bundle(data_dir: &Path) -> bool {
let Ok(entries) = std::fs::read_dir(data_dir) else {
return false;
};
entries.flatten().any(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
property_index::is_bundle_file_name(&name)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> (String, String) {
("Doc".to_string(), "tag".to_string())
}
#[test]
fn a_lazily_discovered_bundle_inherits_the_writes_it_missed() {
let state = DiskIndexFreshness::covering(4);
state.note_property_written(1, Some("Doc"));
assert!(
!state.typed_is_fresh(&key(), 4),
"a bundle opened after the write must inherit it"
);
}
#[test]
fn a_rebuild_clears_only_its_own_bundle() {
let state = DiskIndexFreshness::covering(4);
assert!(state.typed_is_fresh(&key(), 4));
assert!(state.global_is_fresh("title", 4));
state.note_created(4, "Doc");
assert!(!state.typed_is_fresh(&key(), 5));
assert!(!state.global_is_fresh("title", 5));
state.mark_typed_built(key(), 5);
assert!(state.typed_is_fresh(&key(), 5));
assert!(
!state.global_is_fresh("title", 5),
"rebuilding one bundle says nothing about another"
);
}
#[test]
fn a_creation_of_an_uncovered_type_leaves_the_typed_bundle_fresh() {
let state = DiskIndexFreshness::covering(4);
assert!(state.typed_is_fresh(&key(), 4), "register the bundle");
state.note_created(4, "Person");
assert!(
state.typed_is_fresh(&key(), 5),
"bulk-loading another type must not stale this one"
);
assert!(
!state.global_is_fresh("title", 5),
"a cross-type bundle covers it"
);
}
#[test]
fn a_removal_stales_every_bundle() {
let state = DiskIndexFreshness::covering(4);
assert!(state.typed_is_fresh(&key(), 4));
state.note_removed(2);
assert!(!state.typed_is_fresh(&key(), 4));
assert!(!state.global_is_fresh("title", 4));
}
#[test]
fn a_clone_shares_no_state_with_its_source() {
let state = DiskIndexFreshness::covering(4);
assert!(state.typed_is_fresh(&key(), 4));
let copy = state.clone();
copy.note_property_written(1, None);
assert!(!copy.typed_is_fresh(&key(), 4));
assert!(state.typed_is_fresh(&key(), 4), "the source must not move");
}
}