use crate::error::MnemesError;
use rusqlite::Connection;
use semantic_memory::{MemoryConfig, MemoryStore};
use sha2::{Digest, Sha256};
use std::path::Path;
use std::sync::Arc;
#[derive(Clone)]
pub struct ReplicaStore {
inner: Arc<MemoryStore>,
device_id: String,
store_id: String,
}
impl ReplicaStore {
pub fn open(db_path: &Path, device_id: &str, store_id: &str) -> Result<Self, MnemesError> {
let path_str = db_path.to_string_lossy();
let uri = format!("file:{}?mode=ro", path_str);
let conn = Connection::open_with_flags(
&uri,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY
| rusqlite::OpenFlags::SQLITE_OPEN_URI
| rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.map_err(|e| {
MnemesError::Replication(format!(
"cannot open replica {}:{} at {}: {e}",
device_id,
store_id,
db_path.display()
))
})?;
conn.execute_batch("PRAGMA query_only = ON;").map_err(|e| {
MnemesError::Replication(format!("cannot set query_only on replica: {e}"))
})?;
let mut config = MemoryConfig::default();
config.base_dir = db_path.parent().unwrap_or(db_path).to_path_buf();
drop(conn);
let store = MemoryStore::open(config).map_err(|e| {
MnemesError::Replication(format!(
"cannot open MemoryStore for replica {}:{}: {e:?}",
device_id, store_id
))
})?;
Ok(Self {
inner: Arc::new(store),
device_id: device_id.to_string(),
store_id: store_id.to_string(),
})
}
pub fn store(&self) -> &MemoryStore {
&self.inner
}
pub fn device_id(&self) -> &str {
&self.device_id
}
pub fn store_id(&self) -> &str {
&self.store_id
}
}
pub struct ReplicaApplier {
db_path: Box<Path>,
device_id: String,
store_id: String,
}
impl ReplicaApplier {
pub fn new(db_path: &Path, device_id: &str, store_id: &str) -> Self {
Self {
db_path: db_path.to_path_buf().into_boxed_path(),
device_id: device_id.to_string(),
store_id: store_id.to_string(),
}
}
pub fn apply_entry(
&self,
journal_sequence: i64,
operation_kind: &str,
payload: &[u8],
replay_fn: &dyn Fn(&Connection) -> Result<(), MnemesError>,
) -> Result<ApplyOutcome, MnemesError> {
let conn = Connection::open(&*self.db_path)
.map_err(|e| MnemesError::Replication(format!("cannot open replica for apply: {e}")))?;
let stored_payload: Option<Vec<u8>> = match conn.query_row(
"SELECT payload FROM mutation_journal
WHERE home_device_id = ?1 AND store_id = ?2 AND sequence = ?3",
rusqlite::params![self.device_id, self.store_id, journal_sequence],
|row| row.get(0),
) {
Ok(payload) => Some(payload),
Err(rusqlite::Error::QueryReturnedNoRows) => None,
Err(error) => {
return Err(MnemesError::Replication(format!(
"cannot check replay state for sequence {journal_sequence}: {error}"
)))
}
};
if let Some(stored_payload) = stored_payload {
if Sha256::digest(&stored_payload) != Sha256::digest(payload) {
return Err(MnemesError::Replication(format!(
"replay payload mismatch for sequence {journal_sequence}"
)));
}
return Ok(ApplyOutcome::AlreadyApplied {
sequence: journal_sequence,
});
}
let tx = conn
.unchecked_transaction()
.map_err(|e| MnemesError::Replication(format!("begin apply tx: {e}")))?;
replay_fn(&tx)?;
tx.execute(
"INSERT INTO mutation_journal
(home_device_id, store_id, sequence, operation_kind, payload)
VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![
self.device_id,
self.store_id,
journal_sequence,
operation_kind,
payload
],
)
.map_err(|e| MnemesError::Replication(format!("journal record on apply: {e}")))?;
tx.commit()
.map_err(|e| MnemesError::Replication(format!("commit apply tx: {e}")))?;
Ok(ApplyOutcome::Applied {
sequence: journal_sequence,
})
}
pub fn device_id(&self) -> &str {
&self.device_id
}
pub fn store_id(&self) -> &str {
&self.store_id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyOutcome {
Applied { sequence: i64 },
AlreadyApplied { sequence: i64 },
}