use std::collections::BTreeMap;
use std::path::Path;
use std::sync::{Arc, RwLock};
use evorule_reactor::{Fact, FactId, FactsLog, FactsLogError};
use evorule_tcb::JsonValue;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SharedFact {
pub fact_id: FactId,
pub path: String,
pub value: JsonValue,
pub source_session_id: u64,
pub version: u64,
}
#[derive(Debug, Clone)]
pub struct SharedFactsLog {
inner: Arc<RwLock<SharedFactsLogInner>>,
}
struct SharedFactsLogInner {
facts_log: FactsLog,
next_fact_id: u64,
fact_sources: BTreeMap<FactId, u64>,
used_at_startup: BTreeMap<u64, Vec<FactId>>,
}
impl std::fmt::Debug for SharedFactsLogInner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedFactsLogInner")
.field("facts_log", &self.facts_log)
.field("next_fact_id", &self.next_fact_id)
.field("fact_sources_len", &self.fact_sources.len())
.finish()
}
}
impl SharedFactsLog {
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(SharedFactsLogInner {
facts_log: FactsLog::new(),
next_fact_id: 1,
fact_sources: BTreeMap::new(),
used_at_startup: BTreeMap::new(),
})),
}
}
pub fn with_wal<P: AsRef<Path>>(path: P) -> Result<Self, FactsLogError> {
let facts_log = FactsLog::with_wal(path)?;
Ok(Self {
inner: Arc::new(RwLock::new(SharedFactsLogInner {
facts_log,
next_fact_id: 1,
fact_sources: BTreeMap::new(),
used_at_startup: BTreeMap::new(),
})),
})
}
pub fn append(
&self,
path: &str,
value: JsonValue,
source_session_id: u64,
) -> Result<u64, FactsLogError> {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
let fact_id = FactId(inner.next_fact_id);
inner.next_fact_id = inner
.next_fact_id
.checked_add(1)
.ok_or(FactsLogError::VersionOverflow)?;
let fact = Fact::PayloadUpdate {
id: fact_id,
path: path.to_string(),
value,
};
let version = inner.facts_log.append(fact)?;
inner.fact_sources.insert(fact_id, source_session_id);
Ok(version)
}
pub fn facts_by_path_prefix(&self, prefix: &str) -> Vec<SharedFact> {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner
.facts_log
.facts_by_path_prefix(prefix)
.into_iter()
.filter(|(_, fact)| matches!(fact, Fact::PayloadUpdate { .. }))
.map(|(version, fact)| {
if let Fact::PayloadUpdate { id, path, value } = fact {
let source_session_id = *inner.fact_sources.get(&id).unwrap_or(&0);
SharedFact {
fact_id: id,
path,
value,
source_session_id,
version,
}
} else {
unreachable!()
}
})
.collect()
}
pub fn source_session_id(&self, fact_id: FactId) -> Option<u64> {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner.fact_sources.get(&fact_id).copied()
}
pub fn fact_by_id(&self, fact_id: FactId) -> Option<SharedFact> {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
let source_session_id = *inner.fact_sources.get(&fact_id)?;
for (version, fact) in inner.facts_log.history_with_versions() {
if let Fact::PayloadUpdate { id, path, value } = fact {
if id == fact_id {
return Some(SharedFact {
fact_id: id,
path,
value,
source_session_id,
version,
});
}
}
}
None
}
pub fn version(&self) -> u64 {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner.facts_log.version()
}
pub fn history_len(&self) -> usize {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner.facts_log.history_len()
}
pub fn record_used_at_startup(&self, session_id: u64, fact_ids: &[FactId]) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.used_at_startup.insert(session_id, fact_ids.to_vec());
}
pub fn get_used_at_startup(&self, session_id: u64) -> Option<Vec<FactId>> {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner.used_at_startup.get(&session_id).cloned()
}
pub fn get_sessions_using_fact(&self, fact_id: FactId) -> Vec<u64> {
let inner = self.inner.read().unwrap_or_else(|e| e.into_inner());
inner
.used_at_startup
.iter()
.filter(|(_, facts)| facts.contains(&fact_id))
.map(|(session_id, _)| *session_id)
.collect()
}
pub fn reset(&self) {
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
inner.facts_log.reset();
inner.next_fact_id = 1;
inner.fact_sources.clear();
inner.used_at_startup.clear();
}
}
impl Default for SharedFactsLog {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic, clippy::expect_used)]
use super::*;
#[test]
fn test_shared_facts_log_new() {
let log = SharedFactsLog::new();
assert_eq!(log.version(), 0);
assert_eq!(log.history_len(), 0);
}
#[test]
fn test_shared_facts_log_append() {
let log = SharedFactsLog::new();
let version = log
.append("shared.research.note1", JsonValue::string("hello"), 100)
.unwrap();
assert_eq!(version, 1); assert_eq!(log.history_len(), 1);
}
#[test]
fn test_shared_facts_log_facts_by_path_prefix() {
let log = SharedFactsLog::new();
log.append("shared.research.note1", JsonValue::string("v1"), 100)
.unwrap();
log.append("shared.research.note2", JsonValue::string("v2"), 100)
.unwrap();
log.append("shared.other.data", JsonValue::string("v3"), 200)
.unwrap();
let result = log.facts_by_path_prefix("shared.research");
assert_eq!(result.len(), 2);
assert_eq!(result[0].path, "shared.research.note1");
assert_eq!(result[1].path, "shared.research.note2");
assert_eq!(result[0].source_session_id, 100);
assert_eq!(result[1].source_session_id, 100);
}
#[test]
fn test_shared_facts_log_facts_by_path_prefix_no_matches() {
let log = SharedFactsLog::new();
log.append("shared.research.note1", JsonValue::string("v1"), 100)
.unwrap();
let result = log.facts_by_path_prefix("shared.other");
assert!(result.is_empty());
}
#[test]
fn test_shared_facts_log_facts_by_path_prefix_empty() {
let log = SharedFactsLog::new();
let result = log.facts_by_path_prefix("any_prefix");
assert!(result.is_empty());
}
#[test]
fn test_shared_facts_log_source_session_id() {
let log = SharedFactsLog::new();
log.append("shared.note", JsonValue::string("v1"), 100)
.unwrap();
let result = log.facts_by_path_prefix("shared");
assert_eq!(result.len(), 1);
assert_eq!(result[0].source_session_id, 100);
}
#[test]
fn test_shared_facts_log_reset() {
let log = SharedFactsLog::new();
log.append("shared.note", JsonValue::string("v1"), 100)
.unwrap();
assert_eq!(log.history_len(), 1);
log.reset();
assert_eq!(log.history_len(), 0);
assert_eq!(log.version(), 0);
}
}