use crate::error::MnemesError;
use crate::replica::{ApplyOutcome, ReplicaApplier};
use crate::replication::TrustedKeyRegistry;
use rusqlite::Connection;
#[derive(Debug)]
pub struct SyncResult {
pub entries_synced: usize,
pub next_sequence: i64,
pub has_more: bool,
pub errors: Vec<String>,
}
pub fn export_device_journal(
conn: &Connection,
home_device_id: &str,
store_id: &str,
start_sequence: i64,
limit: usize,
) -> Result<(Vec<JournalPayload>, i64, bool), MnemesError> {
let mut stmt = conn
.prepare(
"SELECT sequence, operation_kind, payload
FROM mutation_journal
WHERE home_device_id = ?1 AND store_id = ?2 AND sequence >= ?3
ORDER BY sequence ASC
LIMIT ?4",
)
.map_err(|e| MnemesError::Replication(format!("journal export: {e}")))?;
let rows = stmt
.query_map(
rusqlite::params![home_device_id, store_id, start_sequence, limit as i64],
|row| {
Ok(JournalPayload {
sequence: row.get(0)?,
operation_kind: row.get(1)?,
payload: row.get(2)?,
})
},
)
.map_err(|e| MnemesError::Replication(format!("export query: {e}")))?;
let mut entries = Vec::new();
let mut expected = start_sequence;
let mut has_gap = false;
for row in rows {
let entry = row.map_err(|e| MnemesError::Replication(format!("row: {e}")))?;
if entry.sequence != expected {
has_gap = true;
break;
}
expected = entry.sequence + 1;
entries.push(entry);
}
Ok((
entries.clone(),
expected,
!has_gap && entries.len() >= limit,
))
}
pub fn export_operation_journal(
conn: &Connection,
_home_device_id: &str,
_store_id: &str,
_start_sequence: i64,
_limit: usize,
) -> Result<(Vec<JournalPayload>, i64, bool), MnemesError> {
let exists: bool = conn
.query_row(
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='operation_journal')",
[],
|row| row.get(0),
)
.map_err(|e| MnemesError::Replication(format!("canonical journal probe: {e}")))?;
if !exists {
return Err(MnemesError::Replication(
"canonical operation_journal is unavailable".into(),
));
}
Err(MnemesError::Replication(
"operation_journal has no replayable payload; replication is unavailable until the canonical owner exports payload bytes".into(),
))
}
#[derive(Debug, Clone)]
pub struct JournalPayload {
pub sequence: i64,
pub operation_kind: String,
pub payload: Vec<u8>,
}
pub fn sync_batch(
device_conn: &Connection,
replica_applier: &ReplicaApplier,
registry: &TrustedKeyRegistry,
home_device_id: &str,
store_id: &str,
start_sequence: i64,
max_batch: usize,
dispatch: &dyn Fn(&Connection, &str, &[u8]) -> Result<(), MnemesError>,
) -> Result<SyncResult, MnemesError> {
let (entries, next_seq, has_more) = export_device_journal(
device_conn,
home_device_id,
store_id,
start_sequence,
max_batch,
)?;
let mut synced = 0;
let mut errors = Vec::new();
for entry in &entries {
let _ = registry;
match replica_applier.apply_entry(
entry.sequence,
&entry.operation_kind,
&entry.payload,
&|conn| dispatch(conn, &entry.operation_kind, &entry.payload),
) {
Ok(ApplyOutcome::Applied { .. }) | Ok(ApplyOutcome::AlreadyApplied { .. }) => {
synced += 1;
}
Err(e) => {
errors.push(format!("seq {} apply failed: {e}", entry.sequence));
}
}
}
Ok(SyncResult {
entries_synced: synced,
next_sequence: next_seq,
has_more,
errors,
})
}
pub fn has_pending(
conn: &Connection,
home_device_id: &str,
store_id: &str,
last_applied_sequence: i64,
) -> Result<bool, MnemesError> {
let count: i64 = conn
.query_row(
"SELECT COUNT(*) FROM mutation_journal
WHERE home_device_id = ?1 AND store_id = ?2 AND sequence > ?3",
rusqlite::params![home_device_id, store_id, last_applied_sequence],
|row| row.get(0),
)
.map_err(|e| MnemesError::Replication(format!("pending check: {e}")))?;
Ok(count > 0)
}