use std::collections::HashSet;
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
use super::file_table::FileId;
use super::state::{mtime_ms_of, now_unix_ms, relative_path_under_root, RefreshOutcome};
use super::walker::walk_indexable;
use super::IndexState;
use crate::HarnReferenceResolver;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(super) struct ReconcileProgress {
git_head: Option<String>,
verified_files: HashSet<FileId>,
}
#[cfg(test)]
#[path = "reconcile_tests.rs"]
mod tests;
pub(super) struct CheckpointPolicy {
pub files: usize,
pub interval: Duration,
}
impl Default for CheckpointPolicy {
fn default() -> Self {
Self {
files: 512,
interval: Duration::from_secs(30),
}
}
}
impl CheckpointPolicy {
pub fn disabled() -> Self {
Self {
files: usize::MAX,
interval: Duration::MAX,
}
}
}
impl IndexState {
pub(super) fn refresh_with_checkpoints(
&mut self,
resolver: Option<&HarnReferenceResolver>,
policy: CheckpointPolicy,
mut checkpoint: impl FnMut(&Self),
) -> RefreshOutcome {
let root = self.root.clone();
let live_head = super::git_head::read_git_head(&root);
let mut outcome = RefreshOutcome {
git_head_changed: self.git_head != live_head
|| self
.reconcile_progress
.as_ref()
.is_some_and(|p| p.git_head != live_head),
..RefreshOutcome::default()
};
if outcome.git_head_changed {
if self
.reconcile_progress
.as_ref()
.is_none_or(|p| p.git_head != live_head)
{
self.reconcile_progress = Some(ReconcileProgress {
git_head: live_head.clone(),
verified_files: HashSet::new(),
});
}
} else {
self.reconcile_progress = None;
}
let mut seen = HashSet::with_capacity(self.files.len());
let mut graph_dirty = false;
let mut deps_dirty = false;
let mut harn_touched = false;
let mut completed = 0;
let mut last_checkpoint = Instant::now();
walk_indexable(&root, |abs, meta| {
outcome.files_scanned += 1;
let Some(rel) = relative_path_under_root(&root, abs) else {
return;
};
let known = self.path_to_id.get(&rel).copied();
if let Some(id) = known {
let verified = self
.reconcile_progress
.as_ref()
.is_none_or(|p| p.verified_files.contains(&id));
if verified
&& self.files.get(&id).is_some_and(|file| {
file.size_bytes == meta.len() && file.mtime_ms == mtime_ms_of(meta)
})
{
outcome.files_unchanged += 1;
seen.insert(rel);
return;
}
}
match self.ingest(abs, Some(meta)) {
Some(ingested) => {
let id = ingested.id;
let changed = ingested.changed;
seen.insert(rel.clone());
if known.is_none() {
outcome.files_added += 1;
deps_dirty = true;
} else if changed {
self.rebuild_deps(id, &rel);
}
if changed {
outcome.files_reindexed += 1;
completed += 1;
harn_touched |= self.files.get(&id).is_some_and(|f| f.language == "harn");
self.rebuild_symbol_graph_for(id, &ingested.source);
graph_dirty = true;
} else {
outcome.files_touched_only += 1;
}
if let Some(progress) = &mut self.reconcile_progress {
progress.verified_files.insert(id);
}
if completed >= policy.files || last_checkpoint.elapsed() >= policy.interval {
self.finish_reconcile_batch(&mut graph_dirty, &mut deps_dirty);
checkpoint(self);
completed = 0;
last_checkpoint = Instant::now();
}
}
None => outcome.files_skipped += 1,
}
});
let stale: Vec<_> = self
.path_to_id
.keys()
.filter(|rel| !seen.contains(*rel))
.cloned()
.collect();
for rel in stale {
harn_touched |= self
.path_to_id
.get(&rel)
.and_then(|id| self.files.get(id))
.is_some_and(|f| f.language == "harn");
self.remove_relative_path(&rel);
outcome.files_removed += 1;
}
if outcome.files_removed > 0 {
deps_dirty = true;
}
self.finish_reconcile_batch(&mut graph_dirty, &mut deps_dirty);
self.git_head = live_head;
self.reconcile_progress = None;
if harn_touched {
self.relink_harn_references(resolver);
}
if !outcome.is_noop() {
self.last_built_unix_ms = now_unix_ms();
}
outcome
}
fn finish_reconcile_batch(&mut self, graph_dirty: &mut bool, deps_dirty: &mut bool) {
if *deps_dirty {
self.rebuild_all_deps();
*deps_dirty = false;
*graph_dirty = true;
}
if !*graph_dirty {
return;
}
self.link_symbol_imports();
self.relink_dirty_calls();
*graph_dirty = false;
}
}