1use std::collections::BTreeMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::collective::Collective;
12use crate::experience::Experience;
13use crate::insight::DerivedInsight;
14use crate::relation::ExperienceRelation;
15use crate::types::{CollectiveId, ExperienceId, InsightId, RelationId, Timestamp};
16
17pub use crate::types::InstanceId;
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct SyncCursor {
30 pub instance_id: InstanceId,
32
33 pub last_sequence: u64,
35}
36
37impl SyncCursor {
38 pub fn new(instance_id: InstanceId) -> Self {
40 Self {
41 instance_id,
42 last_sequence: 0,
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[repr(u8)]
54pub enum SyncEntityType {
55 Experience = 0,
57 Relation = 1,
59 Insight = 2,
61 Collective = 3,
63}
64
65#[derive(Clone, Debug, Default, Serialize, Deserialize)]
75pub struct SerializableExperienceUpdate {
76 pub importance: Option<f32>,
78
79 pub confidence: Option<f32>,
81
82 pub domain: Option<Vec<String>>,
84
85 pub related_files: Option<Vec<String>>,
87
88 pub archived: Option<bool>,
90
91 pub applications: Option<BTreeMap<InstanceId, u32>>,
93
94 pub last_reinforced: Option<Timestamp>,
96}
97
98impl From<crate::experience::ExperienceUpdate> for SerializableExperienceUpdate {
99 fn from(update: crate::experience::ExperienceUpdate) -> Self {
100 Self {
101 importance: update.importance,
102 confidence: update.confidence,
103 domain: update.domain,
104 related_files: update.related_files,
105 archived: update.archived,
106 applications: None,
107 last_reinforced: None,
108 }
109 }
110}
111
112impl From<SerializableExperienceUpdate> for crate::experience::ExperienceUpdate {
113 fn from(update: SerializableExperienceUpdate) -> Self {
114 Self {
115 importance: update.importance,
116 confidence: update.confidence,
117 domain: update.domain,
118 related_files: update.related_files,
119 archived: update.archived,
120 }
121 }
122}
123
124#[derive(Clone, Debug, Serialize, Deserialize)]
134pub enum SyncPayload {
135 ExperienceCreated(Experience),
137
138 ExperienceUpdated {
140 id: ExperienceId,
142 update: SerializableExperienceUpdate,
144 timestamp: Timestamp,
146 },
147
148 ExperienceArchived {
150 id: ExperienceId,
152 timestamp: Timestamp,
154 },
155
156 ExperienceDeleted {
158 id: ExperienceId,
160 timestamp: Timestamp,
162 },
163
164 RelationCreated(ExperienceRelation),
166
167 RelationDeleted {
169 id: RelationId,
171 timestamp: Timestamp,
173 },
174
175 InsightCreated(DerivedInsight),
177
178 InsightDeleted {
180 id: InsightId,
182 timestamp: Timestamp,
184 },
185
186 CollectiveCreated(Collective),
188}
189
190#[derive(Clone, Debug, Serialize, Deserialize)]
199pub struct SyncChange {
200 pub sequence: u64,
202
203 pub source_instance: InstanceId,
205
206 pub collective_id: CollectiveId,
208
209 pub entity_type: SyncEntityType,
211
212 pub payload: SyncPayload,
214
215 pub timestamp: Timestamp,
217}
218
219#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
225pub enum SyncStatus {
226 Idle,
228 Syncing,
230 Error(String),
232 Disconnected,
234}
235
236#[derive(Clone, Debug, Serialize, Deserialize)]
242pub struct HandshakeRequest {
243 pub instance_id: InstanceId,
245 pub protocol_version: u32,
247 pub capabilities: Vec<String>,
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize)]
253pub struct HandshakeResponse {
254 pub instance_id: InstanceId,
256 pub protocol_version: u32,
258 pub accepted: bool,
260 pub reason: Option<String>,
262}
263
264#[derive(Clone, Debug, Serialize, Deserialize)]
270pub struct PullRequest {
271 pub cursor: SyncCursor,
273 pub batch_size: usize,
275 pub collectives: Option<Vec<CollectiveId>>,
277}
278
279#[derive(Clone, Debug, Serialize, Deserialize)]
281pub struct PullResponse {
282 pub changes: Vec<SyncChange>,
284 pub has_more: bool,
286 pub new_cursor: SyncCursor,
288}
289
290#[derive(Clone, Debug, Serialize, Deserialize)]
296pub struct PushResponse {
297 pub accepted: usize,
299 pub rejected: usize,
301 pub new_cursor: SyncCursor,
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn test_instance_id_new_is_unique() {
311 let a = InstanceId::new();
312 let b = InstanceId::new();
313 assert_ne!(a, b);
314 }
315
316 #[test]
317 fn test_instance_id_nil() {
318 let id = InstanceId::nil();
319 assert_eq!(id, InstanceId::default());
320 assert_eq!(id, InstanceId::nil());
321 }
322
323 #[test]
324 fn test_instance_id_bytes_roundtrip() {
325 let id = InstanceId::new();
326 let bytes = *id.as_bytes();
327 let restored = InstanceId::from_bytes(bytes);
328 assert_eq!(id, restored);
329 }
330
331 #[test]
332 fn test_instance_id_display() {
333 let id = InstanceId::nil();
334 assert_eq!(id.to_string(), "00000000-0000-0000-0000-000000000000");
335 }
336
337 #[test]
338 fn test_instance_id_postcard_roundtrip() {
339 let id = InstanceId::new();
340 let bytes = postcard::to_allocvec(&id).unwrap();
341 let restored: InstanceId = postcard::from_bytes(&bytes).unwrap();
342 assert_eq!(id, restored);
343 }
344
345 #[test]
346 fn test_sync_cursor_new() {
347 let id = InstanceId::new();
348 let cursor = SyncCursor::new(id);
349 assert_eq!(cursor.instance_id, id);
350 assert_eq!(cursor.last_sequence, 0);
351 }
352
353 #[test]
354 fn test_sync_cursor_postcard_roundtrip() {
355 let cursor = SyncCursor {
356 instance_id: InstanceId::new(),
357 last_sequence: 42,
358 };
359 let bytes = postcard::to_allocvec(&cursor).unwrap();
360 let restored: SyncCursor = postcard::from_bytes(&bytes).unwrap();
361 assert_eq!(cursor, restored);
362 }
363
364 #[test]
365 fn test_sync_entity_type_repr() {
366 assert_eq!(SyncEntityType::Experience as u8, 0);
367 assert_eq!(SyncEntityType::Relation as u8, 1);
368 assert_eq!(SyncEntityType::Insight as u8, 2);
369 assert_eq!(SyncEntityType::Collective as u8, 3);
370 }
371
372 #[test]
373 fn test_serializable_experience_update_from_conversion() {
374 let update = crate::experience::ExperienceUpdate {
375 importance: Some(0.9),
376 confidence: None,
377 domain: Some(vec!["rust".to_string()]),
378 related_files: None,
379 archived: Some(false),
380 };
381 let serializable: SerializableExperienceUpdate = update.into();
382 assert_eq!(serializable.importance, Some(0.9));
383 assert_eq!(serializable.confidence, None);
384 assert_eq!(serializable.domain, Some(vec!["rust".to_string()]));
385 assert_eq!(serializable.archived, Some(false));
386 }
387
388 #[test]
389 fn test_serializable_experience_update_into_conversion() {
390 let serializable = SerializableExperienceUpdate {
391 importance: Some(0.5),
392 confidence: Some(0.8),
393 domain: None,
394 related_files: Some(vec!["main.rs".to_string()]),
395 archived: None,
396 applications: None,
397 last_reinforced: None,
398 };
399 let update: crate::experience::ExperienceUpdate = serializable.into();
400 assert_eq!(update.importance, Some(0.5));
401 assert_eq!(update.confidence, Some(0.8));
402 assert_eq!(update.related_files, Some(vec!["main.rs".to_string()]));
403 }
404
405 #[test]
406 fn test_serializable_experience_update_postcard_roundtrip() {
407 let update = SerializableExperienceUpdate {
408 importance: Some(0.7),
409 confidence: Some(0.9),
410 domain: Some(vec!["test".to_string()]),
411 related_files: None,
412 archived: Some(true),
413 applications: Some(std::collections::BTreeMap::from([(InstanceId::new(), 2)])),
414 last_reinforced: Some(Timestamp::now()),
415 };
416 let bytes = postcard::to_allocvec(&update).unwrap();
417 let restored: SerializableExperienceUpdate = postcard::from_bytes(&bytes).unwrap();
418 assert_eq!(update.importance, restored.importance);
419 assert_eq!(update.confidence, restored.confidence);
420 assert_eq!(update.domain, restored.domain);
421 assert_eq!(update.archived, restored.archived);
422 assert_eq!(update.applications, restored.applications);
423 assert_eq!(update.last_reinforced, restored.last_reinforced);
424 }
425
426 #[test]
427 fn test_sync_status_equality() {
428 assert_eq!(SyncStatus::Idle, SyncStatus::Idle);
429 assert_eq!(SyncStatus::Error("x".into()), SyncStatus::Error("x".into()));
430 assert_ne!(SyncStatus::Idle, SyncStatus::Syncing);
431 }
432
433 #[test]
434 fn test_handshake_request_postcard_roundtrip() {
435 let req = HandshakeRequest {
436 instance_id: InstanceId::new(),
437 protocol_version: crate::sync::SYNC_PROTOCOL_VERSION,
438 capabilities: vec!["push".to_string(), "pull".to_string()],
439 };
440 let bytes = postcard::to_allocvec(&req).unwrap();
441 let restored: HandshakeRequest = postcard::from_bytes(&bytes).unwrap();
442 assert_eq!(req.instance_id, restored.instance_id);
443 assert_eq!(req.protocol_version, restored.protocol_version);
444 assert_eq!(req.capabilities, restored.capabilities);
445 }
446
447 #[test]
448 fn test_pull_request_postcard_roundtrip() {
449 let req = PullRequest {
450 cursor: SyncCursor::new(InstanceId::new()),
451 batch_size: 500,
452 collectives: Some(vec![CollectiveId::new()]),
453 };
454 let bytes = postcard::to_allocvec(&req).unwrap();
455 let restored: PullRequest = postcard::from_bytes(&bytes).unwrap();
456 assert_eq!(req.cursor, restored.cursor);
457 assert_eq!(req.batch_size, restored.batch_size);
458 }
459
460 #[test]
461 fn test_push_response_postcard_roundtrip() {
462 let resp = PushResponse {
463 accepted: 10,
464 rejected: 2,
465 new_cursor: SyncCursor {
466 instance_id: InstanceId::new(),
467 last_sequence: 100,
468 },
469 };
470 let bytes = postcard::to_allocvec(&resp).unwrap();
471 let restored: PushResponse = postcard::from_bytes(&bytes).unwrap();
472 assert_eq!(resp.accepted, restored.accepted);
473 assert_eq!(resp.rejected, restored.rejected);
474 assert_eq!(resp.new_cursor, restored.new_cursor);
475 }
476}