use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::numeric::count_u32;
mod time;
pub use time::{TOMBSTONE_RETENTION_MS, now_ms, parse_since};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileState {
Active,
Tombstoned,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeEntry {
pub path: String,
pub last_indexed_at: u64,
pub state: FileState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeFeed {
pub added: Vec<ChangeEntry>,
pub modified: Vec<ChangeEntry>,
pub deleted: Vec<ChangeEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TombstoneEntry {
pub path: String,
pub deleted_at: u64,
pub last_indexed_at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexMetadata {
pub schema_version: u32,
pub last_indexed_at: u64,
pub last_seen_at: u64,
pub active_notes: u32,
pub chunk_count: u32,
pub tombstone_count: u32,
}
impl Default for IndexMetadata {
fn default() -> Self {
Self {
schema_version: 1,
last_indexed_at: 0,
last_seen_at: 0,
active_notes: 0,
chunk_count: 0,
tombstone_count: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FileChangeState {
pub path: String,
pub last_indexed_at: u64,
pub last_seen_at: u64,
pub mtime: u64,
pub tombstoned: bool,
pub tombstoned_at: Option<u64>,
}
#[allow(clippy::missing_const_for_fn)]
impl FileChangeState {
#[must_use]
pub fn active(path: String, mtime: u64) -> Self {
Self {
path,
last_indexed_at: 0,
last_seen_at: 0,
mtime,
tombstoned: false,
tombstoned_at: None,
}
}
pub fn mark_indexed(&mut self, timestamp: u64) {
self.last_indexed_at = timestamp;
self.last_seen_at = timestamp;
}
pub fn mark_seen(&mut self, timestamp: u64) {
self.last_seen_at = timestamp;
}
pub fn update_mtime(&mut self, mtime: u64) {
self.mtime = mtime;
}
pub fn tombstone(&mut self, timestamp: u64) {
self.tombstoned = true;
self.tombstoned_at = Some(timestamp);
}
#[must_use]
pub fn is_modified(&self) -> bool {
self.last_indexed_at > 0 && self.mtime > self.last_indexed_at
}
#[must_use]
pub fn is_active(&self) -> bool {
!self.tombstoned
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChangeIndex {
pub states: BTreeMap<String, FileChangeState>,
pub tombstones: BTreeMap<String, TombstoneEntry>,
}
impl ChangeIndex {
pub fn register_active(&mut self, path: String, mtime: u64, timestamp: u64) {
let mut state = FileChangeState::active(path.clone(), mtime);
state.mark_indexed(timestamp);
state.mark_seen(timestamp);
self.states.insert(path, state);
}
pub fn update_mtime(&mut self, path: &str, mtime: u64) {
if let Some(state) = self.states.get_mut(path) {
state.update_mtime(mtime);
state.mark_seen(mtime);
}
}
pub fn mark_seen(&mut self, path: &str, timestamp: u64) {
if let Some(state) = self.states.get_mut(path) {
state.mark_seen(timestamp);
}
}
pub fn tombstone(&mut self, path: &str, timestamp: u64) {
if let Some(state) = self.states.get_mut(path) {
state.tombstone(timestamp);
self.tombstones.insert(
path.to_string(),
TombstoneEntry {
path: path.to_string(),
deleted_at: timestamp,
last_indexed_at: state.last_indexed_at,
},
);
}
}
pub fn remove(&mut self, path: &str) {
self.states.remove(path);
self.tombstones.remove(path);
}
#[must_use]
pub fn get_changes_since(&self, since: u64) -> (Vec<String>, Vec<String>) {
let mut added = Vec::new();
let mut modified = Vec::new();
for (path, state) in &self.states {
if state.last_indexed_at < since && state.last_seen_at >= since {
if state.is_modified() {
modified.push(path.clone());
} else {
added.push(path.clone());
}
}
}
added.sort();
modified.sort();
(added, modified)
}
#[must_use]
pub fn get_tombstones(&self) -> Vec<&TombstoneEntry> {
self.tombstones.values().collect()
}
pub fn prune_tombstones(&mut self, max_age_ms: u64, current_time: u64) -> Vec<String> {
let mut pruned = Vec::new();
self.tombstones.retain(|path, entry| {
if current_time - entry.deleted_at > max_age_ms {
pruned.push(path.clone());
false
} else {
true
}
});
pruned
}
#[must_use]
pub fn compute_change_feed(&self, since: u64) -> ChangeFeed {
let mut added = Vec::new();
let mut modified = Vec::new();
let mut deleted = Vec::new();
for (path, state) in &self.states {
if state.last_seen_at >= since {
let entry = ChangeEntry {
path: path.clone(),
last_indexed_at: state.last_indexed_at,
state: FileState::Active,
};
if state.is_modified() {
modified.push(entry);
} else {
added.push(entry);
}
}
}
for (path, entry) in &self.tombstones {
if entry.deleted_at >= since {
deleted.push(ChangeEntry {
path: path.clone(),
last_indexed_at: entry.last_indexed_at,
state: FileState::Tombstoned,
});
}
}
added.sort_by_key(|e| e.path.clone());
modified.sort_by_key(|e| e.path.clone());
deleted.sort_by_key(|e| e.path.clone());
ChangeFeed {
added,
modified,
deleted,
}
}
#[must_use]
pub fn to_metadata(&self) -> IndexMetadata {
IndexMetadata {
schema_version: 1,
last_indexed_at: self
.states
.values()
.map(|s| s.last_indexed_at)
.max()
.unwrap_or(0),
last_seen_at: self
.states
.values()
.map(|s| s.last_seen_at)
.max()
.unwrap_or(0),
active_notes: count_u32(self.states.values().filter(|s| s.is_active()).count()),
chunk_count: 0,
tombstone_count: count_u32(self.tombstones.len()),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests;