use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::collective::Collective;
use crate::experience::Experience;
use crate::insight::DerivedInsight;
use crate::relation::ExperienceRelation;
use crate::types::{CollectiveId, ExperienceId, InsightId, RelationId, Timestamp};
pub use crate::types::InstanceId;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncCursor {
pub instance_id: InstanceId,
pub last_sequence: u64,
}
impl SyncCursor {
pub fn new(instance_id: InstanceId) -> Self {
Self {
instance_id,
last_sequence: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum SyncEntityType {
Experience = 0,
Relation = 1,
Insight = 2,
Collective = 3,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SerializableExperienceUpdate {
pub importance: Option<f32>,
pub confidence: Option<f32>,
pub domain: Option<Vec<String>>,
pub related_files: Option<Vec<String>>,
pub archived: Option<bool>,
pub applications: Option<BTreeMap<InstanceId, u32>>,
pub last_reinforced: Option<Timestamp>,
}
impl From<crate::experience::ExperienceUpdate> for SerializableExperienceUpdate {
fn from(update: crate::experience::ExperienceUpdate) -> Self {
Self {
importance: update.importance,
confidence: update.confidence,
domain: update.domain,
related_files: update.related_files,
archived: update.archived,
applications: None,
last_reinforced: None,
}
}
}
impl From<SerializableExperienceUpdate> for crate::experience::ExperienceUpdate {
fn from(update: SerializableExperienceUpdate) -> Self {
Self {
importance: update.importance,
confidence: update.confidence,
domain: update.domain,
related_files: update.related_files,
archived: update.archived,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SyncPayload {
ExperienceCreated(Experience),
ExperienceUpdated {
id: ExperienceId,
update: SerializableExperienceUpdate,
timestamp: Timestamp,
},
ExperienceArchived {
id: ExperienceId,
timestamp: Timestamp,
},
ExperienceDeleted {
id: ExperienceId,
timestamp: Timestamp,
},
RelationCreated(ExperienceRelation),
RelationDeleted {
id: RelationId,
timestamp: Timestamp,
},
InsightCreated(DerivedInsight),
InsightDeleted {
id: InsightId,
timestamp: Timestamp,
},
CollectiveCreated(Collective),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SyncChange {
pub sequence: u64,
pub source_instance: InstanceId,
pub collective_id: CollectiveId,
pub entity_type: SyncEntityType,
pub payload: SyncPayload,
pub timestamp: Timestamp,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SyncStatus {
Idle,
Syncing,
Error(String),
Disconnected,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HandshakeRequest {
pub instance_id: InstanceId,
pub protocol_version: u32,
pub capabilities: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HandshakeResponse {
pub instance_id: InstanceId,
pub protocol_version: u32,
pub accepted: bool,
pub reason: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PullRequest {
pub cursor: SyncCursor,
pub batch_size: usize,
pub collectives: Option<Vec<CollectiveId>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PullResponse {
pub changes: Vec<SyncChange>,
pub has_more: bool,
pub new_cursor: SyncCursor,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PushResponse {
pub accepted: usize,
pub rejected: usize,
pub new_cursor: SyncCursor,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_instance_id_new_is_unique() {
let a = InstanceId::new();
let b = InstanceId::new();
assert_ne!(a, b);
}
#[test]
fn test_instance_id_nil() {
let id = InstanceId::nil();
assert_eq!(id, InstanceId::default());
assert_eq!(id, InstanceId::nil());
}
#[test]
fn test_instance_id_bytes_roundtrip() {
let id = InstanceId::new();
let bytes = *id.as_bytes();
let restored = InstanceId::from_bytes(bytes);
assert_eq!(id, restored);
}
#[test]
fn test_instance_id_display() {
let id = InstanceId::nil();
assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
}
#[test]
fn test_instance_id_postcard_roundtrip() {
let id = InstanceId::new();
let bytes = postcard::to_allocvec(&id).unwrap();
let restored: InstanceId = postcard::from_bytes(&bytes).unwrap();
assert_eq!(id, restored);
}
#[test]
fn test_sync_cursor_new() {
let id = InstanceId::new();
let cursor = SyncCursor::new(id);
assert_eq!(cursor.instance_id, id);
assert_eq!(cursor.last_sequence, 0);
}
#[test]
fn test_sync_cursor_postcard_roundtrip() {
let cursor = SyncCursor {
instance_id: InstanceId::new(),
last_sequence: 42,
};
let bytes = postcard::to_allocvec(&cursor).unwrap();
let restored: SyncCursor = postcard::from_bytes(&bytes).unwrap();
assert_eq!(cursor, restored);
}
#[test]
fn test_sync_entity_type_repr() {
assert_eq!(SyncEntityType::Experience as u8, 0);
assert_eq!(SyncEntityType::Relation as u8, 1);
assert_eq!(SyncEntityType::Insight as u8, 2);
assert_eq!(SyncEntityType::Collective as u8, 3);
}
#[test]
fn test_serializable_experience_update_from_conversion() {
let update = crate::experience::ExperienceUpdate {
importance: Some(0.9),
confidence: None,
domain: Some(vec!["rust".to_string()]),
related_files: None,
archived: Some(false),
};
let serializable: SerializableExperienceUpdate = update.into();
assert_eq!(serializable.importance, Some(0.9));
assert_eq!(serializable.confidence, None);
assert_eq!(serializable.domain, Some(vec!["rust".to_string()]));
assert_eq!(serializable.archived, Some(false));
}
#[test]
fn test_serializable_experience_update_into_conversion() {
let serializable = SerializableExperienceUpdate {
importance: Some(0.5),
confidence: Some(0.8),
domain: None,
related_files: Some(vec!["main.rs".to_string()]),
archived: None,
applications: None,
last_reinforced: None,
};
let update: crate::experience::ExperienceUpdate = serializable.into();
assert_eq!(update.importance, Some(0.5));
assert_eq!(update.confidence, Some(0.8));
assert_eq!(update.related_files, Some(vec!["main.rs".to_string()]));
}
#[test]
fn test_serializable_experience_update_postcard_roundtrip() {
let update = SerializableExperienceUpdate {
importance: Some(0.7),
confidence: Some(0.9),
domain: Some(vec!["test".to_string()]),
related_files: None,
archived: Some(true),
applications: Some(std::collections::BTreeMap::from([(InstanceId::new(), 2)])),
last_reinforced: Some(Timestamp::now()),
};
let bytes = postcard::to_allocvec(&update).unwrap();
let restored: SerializableExperienceUpdate = postcard::from_bytes(&bytes).unwrap();
assert_eq!(update.importance, restored.importance);
assert_eq!(update.confidence, restored.confidence);
assert_eq!(update.domain, restored.domain);
assert_eq!(update.archived, restored.archived);
assert_eq!(update.applications, restored.applications);
assert_eq!(update.last_reinforced, restored.last_reinforced);
}
#[test]
fn test_sync_status_equality() {
assert_eq!(SyncStatus::Idle, SyncStatus::Idle);
assert_eq!(SyncStatus::Error("x".into()), SyncStatus::Error("x".into()));
assert_ne!(SyncStatus::Idle, SyncStatus::Syncing);
}
#[test]
fn test_handshake_request_postcard_roundtrip() {
let req = HandshakeRequest {
instance_id: InstanceId::new(),
protocol_version: crate::sync::SYNC_PROTOCOL_VERSION,
capabilities: vec!["push".to_string(), "pull".to_string()],
};
let bytes = postcard::to_allocvec(&req).unwrap();
let restored: HandshakeRequest = postcard::from_bytes(&bytes).unwrap();
assert_eq!(req.instance_id, restored.instance_id);
assert_eq!(req.protocol_version, restored.protocol_version);
assert_eq!(req.capabilities, restored.capabilities);
}
#[test]
fn test_pull_request_postcard_roundtrip() {
let req = PullRequest {
cursor: SyncCursor::new(InstanceId::new()),
batch_size: 500,
collectives: Some(vec![CollectiveId::new()]),
};
let bytes = postcard::to_allocvec(&req).unwrap();
let restored: PullRequest = postcard::from_bytes(&bytes).unwrap();
assert_eq!(req.cursor, restored.cursor);
assert_eq!(req.batch_size, restored.batch_size);
}
#[test]
fn test_push_response_postcard_roundtrip() {
let resp = PushResponse {
accepted: 10,
rejected: 2,
new_cursor: SyncCursor {
instance_id: InstanceId::new(),
last_sequence: 100,
},
};
let bytes = postcard::to_allocvec(&resp).unwrap();
let restored: PushResponse = postcard::from_bytes(&bytes).unwrap();
assert_eq!(resp.accepted, restored.accepted);
assert_eq!(resp.rejected, restored.rejected);
assert_eq!(resp.new_cursor, restored.new_cursor);
}
}