use std::sync::Arc;
use tracing::{debug, instrument, trace, warn};
use crate::db::PulseDB;
use crate::storage::schema::{EntityTypeTag, WatchEventRecord, WatchEventTypeTag};
use crate::types::{CollectiveId, ExperienceId, InsightId, RelationId, Timestamp};
use crate::watch::ChangePoller;
use super::config::SyncConfig;
use super::error::SyncError;
use super::transport::SyncTransport;
use super::types::{
InstanceId, SerializableExperienceUpdate, SyncChange, SyncCursor, SyncEntityType, SyncPayload,
};
pub(crate) struct LocalChangePusher {
db: Arc<PulseDB>,
transport: Arc<dyn SyncTransport>,
config: SyncConfig,
poller: ChangePoller,
local_instance_id: InstanceId,
peer_instance_id: InstanceId,
}
impl LocalChangePusher {
pub fn new(
db: Arc<PulseDB>,
transport: Arc<dyn SyncTransport>,
config: SyncConfig,
local_instance_id: InstanceId,
peer_instance_id: InstanceId,
start_sequence: u64,
) -> Self {
Self {
db,
transport,
config,
poller: ChangePoller::from_sequence(start_sequence),
local_instance_id,
peer_instance_id,
}
}
#[instrument(skip(self), fields(peer = %self.peer_instance_id))]
pub async fn push_pending(&mut self) -> Result<usize, SyncError> {
let storage = self.db.storage_for_test(); let events = self
.poller
.poll_sync_events(storage)
.map_err(|e| SyncError::transport(format!("Failed to poll WAL events: {}", e)))?;
if events.is_empty() {
return Ok(0);
}
let mut changes = Vec::with_capacity(events.len());
for (sequence, record) in &events {
if let Some(change) = self.record_to_change(*sequence, record)? {
changes.push(change);
}
}
if changes.is_empty() {
self.save_push_cursor()?;
return Ok(0);
}
let count = changes.len();
let _response = self.transport.push_changes(changes).await?;
self.save_push_cursor()?;
debug!(count, "Pushed local changes to remote");
Ok(count)
}
fn record_to_change(
&self,
sequence: u64,
record: &WatchEventRecord,
) -> Result<Option<SyncChange>, SyncError> {
let collective_id = CollectiveId::from_bytes(record.collective_id);
let timestamp = Timestamp::from_millis(record.timestamp_ms);
if let Some(ref allowed) = self.config.collectives {
if !allowed.contains(&collective_id) {
trace!(seq = sequence, "Skipping change: collective filtered");
return Ok(None);
}
}
match record.entity_type {
EntityTypeTag::Relation if !self.config.sync_relations => {
trace!(seq = sequence, "Skipping relation: sync_relations=false");
return Ok(None);
}
EntityTypeTag::Insight if !self.config.sync_insights => {
trace!(seq = sequence, "Skipping insight: sync_insights=false");
return Ok(None);
}
_ => {}
}
let entity_type = match record.entity_type {
EntityTypeTag::Experience => SyncEntityType::Experience,
EntityTypeTag::Relation => SyncEntityType::Relation,
EntityTypeTag::Insight => SyncEntityType::Insight,
EntityTypeTag::Collective => SyncEntityType::Collective,
};
let payload = self.build_payload(record)?;
let payload = match payload {
Some(p) => p,
None => {
trace!(seq = sequence, "Skipping change: entity no longer exists");
return Ok(None);
}
};
Ok(Some(SyncChange {
sequence,
source_instance: self.local_instance_id,
collective_id,
entity_type,
payload,
timestamp,
}))
}
fn build_payload(&self, record: &WatchEventRecord) -> Result<Option<SyncPayload>, SyncError> {
let map_err = |e: crate::error::PulseDBError| {
SyncError::transport(format!("Failed to load entity for sync: {}", e))
};
match (record.entity_type, record.event_type) {
(EntityTypeTag::Experience, WatchEventTypeTag::Created) => {
let id = ExperienceId::from_bytes(record.entity_id);
match self.db.get_experience(id).map_err(map_err)? {
Some(exp) => Ok(Some(SyncPayload::ExperienceCreated(exp))),
None => Ok(None), }
}
(EntityTypeTag::Experience, WatchEventTypeTag::Updated) => {
let id = ExperienceId::from_bytes(record.entity_id);
match self.db.get_experience(id).map_err(map_err)? {
Some(exp) => {
let update = SerializableExperienceUpdate {
importance: Some(exp.importance),
confidence: Some(exp.confidence),
domain: Some(exp.domain.clone()),
related_files: Some(exp.related_files.clone()),
archived: Some(exp.archived),
applications: Some(exp.applications.clone()),
last_reinforced: Some(exp.last_reinforced),
};
Ok(Some(SyncPayload::ExperienceUpdated {
id,
update,
timestamp: Timestamp::from_millis(record.timestamp_ms),
}))
}
None => Ok(None),
}
}
(EntityTypeTag::Experience, WatchEventTypeTag::Archived) => {
let id = ExperienceId::from_bytes(record.entity_id);
Ok(Some(SyncPayload::ExperienceArchived {
id,
timestamp: Timestamp::from_millis(record.timestamp_ms),
}))
}
(EntityTypeTag::Experience, WatchEventTypeTag::Deleted) => {
let id = ExperienceId::from_bytes(record.entity_id);
Ok(Some(SyncPayload::ExperienceDeleted {
id,
timestamp: Timestamp::from_millis(record.timestamp_ms),
}))
}
(EntityTypeTag::Relation, WatchEventTypeTag::Created) => {
let id = RelationId::from_bytes(record.entity_id);
match self.db.get_relation(id).map_err(map_err)? {
Some(rel) => Ok(Some(SyncPayload::RelationCreated(rel))),
None => Ok(None),
}
}
(EntityTypeTag::Relation, WatchEventTypeTag::Deleted) => {
let id = RelationId::from_bytes(record.entity_id);
Ok(Some(SyncPayload::RelationDeleted {
id,
timestamp: Timestamp::from_millis(record.timestamp_ms),
}))
}
(EntityTypeTag::Insight, WatchEventTypeTag::Created) => {
let id = InsightId::from_bytes(record.entity_id);
match self.db.get_insight(id).map_err(map_err)? {
Some(insight) => Ok(Some(SyncPayload::InsightCreated(insight))),
None => Ok(None),
}
}
(EntityTypeTag::Insight, WatchEventTypeTag::Deleted) => {
let id = InsightId::from_bytes(record.entity_id);
Ok(Some(SyncPayload::InsightDeleted {
id,
timestamp: Timestamp::from_millis(record.timestamp_ms),
}))
}
(EntityTypeTag::Collective, WatchEventTypeTag::Created) => {
let id = CollectiveId::from_bytes(record.entity_id);
match self.db.get_collective(id).map_err(map_err)? {
Some(collective) => Ok(Some(SyncPayload::CollectiveCreated(collective))),
None => Ok(None),
}
}
(entity_type, event_type) => {
warn!(
?entity_type,
?event_type,
"Unexpected WAL event combination, skipping"
);
Ok(None)
}
}
}
fn save_push_cursor(&self) -> Result<(), SyncError> {
let cursor = SyncCursor {
instance_id: self.peer_instance_id,
last_sequence: self.poller.last_sequence(),
};
self.db
.storage_for_test()
.save_sync_cursor(&cursor)
.map_err(|e| SyncError::transport(format!("Failed to save push cursor: {}", e)))
}
}