use async_trait::async_trait;
use reqwest::Client;
use tracing::debug;
use super::error::SyncError;
use super::transport::SyncTransport;
use super::types::{
HandshakeRequest, HandshakeResponse, PullRequest, PullResponse, PushResponse, SyncChange,
};
use super::{read_wire_preamble, write_wire_preamble};
pub struct HttpSyncTransport {
client: Client,
base_url: String,
auth_token: Option<String>,
}
impl HttpSyncTransport {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
client: Client::new(),
base_url: base_url.into(),
auth_token: None,
}
}
pub fn with_auth(base_url: impl Into<String>, token: impl Into<String>) -> Self {
Self {
client: Client::new(),
base_url: base_url.into(),
auth_token: Some(token.into()),
}
}
async fn post_raw(&self, path: &str, body: Vec<u8>) -> Result<Vec<u8>, SyncError> {
let url = format!("{}{}", self.base_url, path);
let mut req = self
.client
.post(&url)
.header("Content-Type", "application/octet-stream")
.body(body);
if let Some(ref token) = self.auth_token {
req = req.header("Authorization", format!("Bearer {}", token));
}
let response = req.send().await.map_err(|e| {
if e.is_timeout() {
SyncError::Timeout
} else if e.is_connect() {
SyncError::ConnectionLost
} else {
SyncError::transport(e.to_string())
}
})?;
let status = response.status();
if !status.is_success() {
let body_text = response.text().await.unwrap_or_else(|_| "unknown".into());
return Err(if status.is_client_error() {
SyncError::invalid_payload(format!("HTTP {}: {}", status, body_text))
} else {
SyncError::transport(format!("HTTP {}: {}", status, body_text))
});
}
response
.bytes()
.await
.map(|b| b.to_vec())
.map_err(|e| SyncError::transport(format!("Failed to read response body: {}", e)))
}
async fn post_body<Req, Resp>(&self, path: &str, request: &Req) -> Result<Resp, SyncError>
where
Req: serde::Serialize,
Resp: serde::de::DeserializeOwned,
{
let body =
postcard::to_allocvec(request).map_err(|e| SyncError::serialization(e.to_string()))?;
let response_bytes = self.post_raw(path, body).await?;
postcard::from_bytes(&response_bytes)
.map_err(|e| SyncError::serialization(format!("Response deserialization: {}", e)))
}
}
#[async_trait]
impl SyncTransport for HttpSyncTransport {
async fn handshake(&self, request: HandshakeRequest) -> Result<HandshakeResponse, SyncError> {
debug!(url = %self.base_url, "HTTP sync handshake");
let encoded =
postcard::to_allocvec(&request).map_err(|e| SyncError::serialization(e.to_string()))?;
let framed = write_wire_preamble(&encoded);
let response_bytes = self.post_raw("/sync/handshake", framed).await?;
let payload = read_wire_preamble(&response_bytes)?;
postcard::from_bytes(payload)
.map_err(|e| SyncError::serialization(format!("Response deserialization: {}", e)))
}
async fn push_changes(&self, changes: Vec<SyncChange>) -> Result<PushResponse, SyncError> {
let count = changes.len();
debug!(count, "HTTP sync push");
self.post_body("/sync/push", &changes).await
}
async fn pull_changes(&self, request: PullRequest) -> Result<PullResponse, SyncError> {
debug!("HTTP sync pull");
self.post_body("/sync/pull", &request).await
}
async fn health_check(&self) -> Result<(), SyncError> {
let url = format!("{}/sync/health", self.base_url);
let mut req = self.client.get(&url);
if let Some(ref token) = self.auth_token {
req = req.header("Authorization", format!("Bearer {}", token));
}
let response = req.send().await.map_err(|e| {
if e.is_timeout() {
SyncError::Timeout
} else if e.is_connect() {
SyncError::ConnectionLost
} else {
SyncError::transport(e.to_string())
}
})?;
if response.status().is_success() {
Ok(())
} else {
Err(SyncError::transport(format!(
"Health check failed: HTTP {}",
response.status()
)))
}
}
}