nodedb_lite/sync/client/
handshake.rs1use nodedb_types::sync::wire::{HandshakeAckMsg, HandshakeMsg};
4use nodedb_types::wire_version::WIRE_FORMAT_VERSION;
5
6use super::config::SyncState;
7use super::state::SyncClient;
8
9impl SyncClient {
10 pub async fn build_handshake(&self, identity: &crate::identity::LiteIdentity) -> HandshakeMsg {
17 let clock = self.clock.lock().await;
18 let shapes = self.shapes.lock().await;
19
20 let wire_clock = clock.to_wire();
21 let mut vector_clock = std::collections::HashMap::new();
22 vector_clock.insert("_global".to_string(), wire_clock);
25
26 HandshakeMsg {
27 jwt_token: self.config.jwt_token.clone(),
28 vector_clock,
29 subscribed_shapes: shapes.active_shape_ids(),
30 client_version: self.config.client_version.clone(),
31 lite_id: identity.lite_id.clone(),
32 epoch: identity.epoch,
33 wire_version: WIRE_FORMAT_VERSION,
34 }
35 }
36
37 pub async fn handle_handshake_ack(&self, ack: &HandshakeAckMsg) -> bool {
39 if !ack.success {
40 tracing::warn!(
41 error = ack.error.as_deref().unwrap_or("unknown"),
42 "handshake rejected by Origin"
43 );
44 return false;
45 }
46
47 *self.session_id.lock().await = Some(ack.session_id.clone());
48 self.announced_collections.lock().await.clear();
52
53 let mut clock = self.clock.lock().await;
54 for (peer_hex, &counter) in &ack.server_clock {
55 if let Ok(peer_id) = u64::from_str_radix(peer_hex, 16) {
56 clock.advance(peer_id, counter);
57 }
58 }
59 drop(clock);
60
61 self.set_producer_id(ack.producer_id).await;
62 self.set_accepted_epoch(ack.accepted_epoch).await;
63
64 *self.state.lock().await = SyncState::Connected;
65 tracing::info!(
66 session = %ack.session_id,
67 producer_id = ack.producer_id,
68 accepted_epoch = ack.accepted_epoch,
69 "sync handshake accepted"
70 );
71 true
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use crate::sync::client::SyncConfig;
79
80 fn make_config() -> SyncConfig {
81 SyncConfig::new("wss://localhost:9090/sync", "test.jwt.token")
82 }
83
84 #[tokio::test]
85 async fn build_handshake() {
86 let client = SyncClient::new(make_config());
87
88 {
89 let mut shapes = client.shapes().lock().await;
90 shapes.subscribe(nodedb_types::sync::shape::ShapeDefinition {
91 shape_id: "s1".into(),
92 tenant_id: 1,
93 shape_type: nodedb_types::sync::shape::ShapeType::Document {
94 collection: "orders".into(),
95 predicate: Vec::new(),
96 },
97 description: "test".into(),
98 field_filter: vec![],
99 });
100 }
101
102 let identity = crate::identity::LiteIdentity {
103 lite_id: "lite-1".into(),
104 epoch: 3,
105 peer_id: 42,
106 };
107 let hs = client.build_handshake(&identity).await;
108 assert_eq!(hs.jwt_token, "test.jwt.token");
109 assert_eq!(hs.subscribed_shapes, vec!["s1"]);
110 }
111
112 #[tokio::test]
113 async fn handle_handshake_ack_success() {
114 let client = SyncClient::new(make_config());
115 let ack = HandshakeAckMsg {
116 success: true,
117 session_id: "sess-123".into(),
118 server_clock: std::collections::HashMap::new(),
119 error: None,
120 fork_detected: false,
121 server_wire_version: 1,
122 producer_id: 0,
123 accepted_epoch: 0,
124 delta_signing_key: [0u8; 32],
126 };
127
128 assert!(client.handle_handshake_ack(&ack).await);
129 assert_eq!(client.state().await, SyncState::Connected);
130 }
131
132 #[tokio::test]
133 async fn handle_handshake_ack_failure() {
134 let client = SyncClient::new(make_config());
135 let ack = HandshakeAckMsg {
136 success: false,
137 session_id: String::new(),
138 server_clock: std::collections::HashMap::new(),
139 error: Some("invalid token".into()),
140 fork_detected: false,
141 server_wire_version: 1,
142 producer_id: 0,
143 accepted_epoch: 0,
144 delta_signing_key: [0u8; 32],
146 };
147
148 assert!(!client.handle_handshake_ack(&ack).await);
149 assert_eq!(client.state().await, SyncState::Disconnected);
150 }
151}