/// HTTP client for talking to a remote Engram peer. /// /// All peer-to-peer communication goes through this client. /// Authentication is via `Authorization: Bearer {api_key}` on every request. use crate::types::{Peer, SerializableActivatedNode, SyncDelta}; use anyhow::Context; use uuid::Uuid; pub struct SyncClient { peer: Peer, http: reqwest::Client, our_id: Uuid, } impl SyncClient { pub fn new(peer: Peer, our_id: Uuid) -> Self { let http = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(30)) .build() .expect("failed to build reqwest client"); Self { peer, http, our_id } } /// Pull a delta from the remote peer containing everything since `since` (Unix ms). /// /// GET {address}/sync/delta?since={since}&peer_id={our_id} pub async fn pull_delta(&self, since: i64) -> anyhow::Result { let url = format!( "{}/sync/delta?since={}&peer_id={}", self.peer.address, since, self.our_id ); let resp = self .http .get(&url) .header("Authorization", format!("Bearer {}", self.peer.api_key)) .send() .await .context("pull_delta: request failed")?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); anyhow::bail!("pull_delta: peer returned {}: {}", status, body); } resp.json::() .await .context("pull_delta: failed to decode response") } /// Push our delta to the remote peer. /// /// POST {address}/sync/push pub async fn push_delta(&self, delta: &SyncDelta) -> anyhow::Result<()> { let url = format!("{}/sync/push", self.peer.address); let resp = self .http .post(&url) .header("Authorization", format!("Bearer {}", self.peer.api_key)) .json(delta) .send() .await .context("push_delta: request failed")?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); anyhow::bail!("push_delta: peer returned {}: {}", status, body); } Ok(()) } /// Fan spreading activation out to a remote peer. /// /// POST {address}/swarm/activate — the remote peer runs activation locally /// (include_peers=false so it does not fan out further, preventing cycles). pub async fn remote_activate( &self, seeds: &[Uuid], query_embedding: &[f32], max_depth: u8, limit: usize, ) -> anyhow::Result> { use crate::types::SwarmActivateRequest; let url = format!("{}/swarm/activate", self.peer.address); let req = SwarmActivateRequest { seeds: seeds.to_vec(), query_embedding: query_embedding.to_vec(), max_depth, limit, include_peers: false, // no further fan-out — prevents cycles }; let resp = self .http .post(&url) .header("Authorization", format!("Bearer {}", self.peer.api_key)) .json(&req) .send() .await .context("remote_activate: request failed")?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); anyhow::bail!("remote_activate: peer returned {}: {}", status, body); } // The remote returns a SwarmActivateResponse; we only want local_results let response: crate::types::SwarmActivateResponse = resp .json() .await .context("remote_activate: failed to decode response")?; Ok(response.local_results) } pub fn peer(&self) -> &Peer { &self.peer } }