use std::sync::{Arc, Mutex};
use crate::{BlobDownload, BlobUploadGrant, SegmentRequest, Transport, TransportError};
pub enum Inbound {
Text(String),
Binary(Vec<u8>),
}
pub struct InboundBuffer {
frames: Mutex<Vec<Inbound>>,
notify: Option<Arc<dyn Fn() + Send + Sync>>,
}
impl Default for InboundBuffer {
fn default() -> Self {
Self {
frames: Mutex::new(Vec::new()),
notify: None,
}
}
}
impl InboundBuffer {
pub fn with_notify(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
Self {
frames: Mutex::new(Vec::new()),
notify: Some(notify),
}
}
pub fn push(&self, frame: Inbound) {
self.frames.lock().expect("inbound lock").push(frame);
if let Some(notify) = &self.notify {
notify();
}
}
fn take(&self) -> Vec<Inbound> {
std::mem::take(&mut *self.frames.lock().expect("inbound lock"))
}
}
pub enum HostTransport {
Null {
signed_urls: bool,
inbound: Arc<InboundBuffer>,
},
#[cfg(feature = "native-transport")]
Native(native::NativeTransport),
}
impl HostTransport {
pub fn from_config(config: &serde_json::Value) -> Result<Self, String> {
Self::new_from_config(config)
}
pub fn new_from_config(config: &serde_json::Value) -> Result<Self, String> {
Self::from_config_with_notify(config, None)
}
pub fn from_config_with_notify(
config: &serde_json::Value,
notify: Option<Arc<dyn Fn() + Send + Sync>>,
) -> Result<Self, String> {
#[cfg(feature = "native-transport")]
{
if let Some(base_url) = config.get("baseUrl").and_then(|v| v.as_str()) {
return Ok(HostTransport::Native(native::NativeTransport::new(
base_url, config, notify,
)?));
}
}
#[cfg(not(feature = "native-transport"))]
{
if config.get("baseUrl").is_some() {
return Err(
"this build has no native transport (rebuild with --features native-transport)"
.to_owned(),
);
}
}
Ok(HostTransport::Null {
signed_urls: false,
inbound: Arc::new(match notify {
Some(notify) => InboundBuffer::with_notify(notify),
None => InboundBuffer::default(),
}),
})
}
pub fn fork_round(&self) -> Self {
match self {
Self::Null {
signed_urls,
inbound,
} => Self::Null {
signed_urls: *signed_urls,
inbound: inbound.clone(),
},
#[cfg(feature = "native-transport")]
Self::Native(t) => Self::Native(t.fork_round()),
}
}
pub fn set_signed_urls(&mut self, value: bool) {
match self {
HostTransport::Null { signed_urls, .. } => *signed_urls = value,
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.signed_urls = value,
}
}
pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
match self {
HostTransport::Null { .. } => drop(headers),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.set_headers(headers),
}
}
pub fn take_inbound(&mut self) -> Vec<Inbound> {
match self {
HostTransport::Null { inbound, .. } => inbound.take(),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.inbound.take(),
}
}
pub fn shutdown(&mut self) {
match self {
HostTransport::Null { .. } => {}
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.shutdown(),
}
}
}
fn unavailable(op: &str) -> TransportError {
TransportError::new(
"transport.unavailable",
format!("{op} needs the native transport (build with --features native-transport)"),
)
}
#[cfg_attr(not(feature = "native-transport"), allow(unused_variables))]
impl Transport for HostTransport {
fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("sync")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.sync(request),
}
}
fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("remoteOperation")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.remote_operation(request),
}
}
fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.realtime_sync(request),
}
}
fn download_segment(
&mut self,
request: &SegmentRequest,
on_progress: &mut dyn FnMut(u64),
) -> Result<Vec<u8>, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.download_segment(request, on_progress),
}
}
fn supports_url_fetch(&self) -> bool {
match self {
HostTransport::Null { signed_urls, .. } => *signed_urls,
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.signed_urls,
}
}
fn fetch_url(
&mut self,
url: &str,
on_progress: &mut dyn FnMut(u64),
) -> Result<Vec<u8>, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.fetch_url(url, on_progress),
}
}
fn blob_upload(
&mut self,
blob_id: &str,
bytes: &[u8],
media_type: Option<&str>,
) -> Result<(), TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("blobUpload")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
}
}
fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("blobDownload")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.blob_download(blob_id),
}
}
fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.fetch_blob_url(url),
}
}
fn blob_upload_grant(
&mut self,
blob_id: &str,
byte_length: u64,
media_type: Option<&str>,
) -> Result<BlobUploadGrant, TransportError> {
match self {
HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
}
}
fn blob_put_url(
&mut self,
url: &str,
bytes: &[u8],
media_type: Option<&str>,
) -> Result<(), TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
}
}
fn realtime_connect(&mut self) -> Result<(), TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => {
t.set_realtime_client_id(None);
t.realtime_connect()
}
}
}
fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => {
t.set_realtime_client_id(Some(client_id));
t.realtime_connect()
}
}
}
fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
match self {
HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.realtime_send(text),
}
}
fn realtime_close(&mut self) -> Result<(), TransportError> {
match self {
HostTransport::Null { .. } => Ok(()),
#[cfg(feature = "native-transport")]
HostTransport::Native(t) => t.realtime_close(),
}
}
}
#[cfg(feature = "native-transport")]
mod native {
use std::net::TcpStream;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use polling::{Event, Events, Poller};
use tungstenite::stream::MaybeTlsStream;
use tungstenite::Message;
use super::{Inbound, InboundBuffer};
use crate::{
BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
TransportError,
};
struct SocketRegistration {
poller: Arc<Poller>,
stream: TcpStream,
}
impl Drop for SocketRegistration {
fn drop(&mut self) {
let _ = self.poller.delete(&self.stream);
}
}
#[derive(Clone)]
struct SocketSender {
queue: mpsc::SyncSender<Outgoing>,
poller: Arc<Poller>,
}
struct Outgoing {
message: Message,
completed: mpsc::Sender<Result<(), TransportError>>,
}
const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Default)]
pub(super) struct RoundChannel {
state: Mutex<RoundState>,
ready: Condvar,
}
#[derive(Default)]
struct RoundState {
round: RealtimeRound,
outcome: Option<Result<Vec<u8>, TransportError>>,
}
impl RoundChannel {
fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
let mut state = self.state.lock().expect("round lock");
state.outcome = None;
state.round.begin(request)
}
fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
let mut state = self.state.lock().expect("round lock");
match state.round.route_binary(frame) {
Ok(RoundInbound::Delta(body)) => Some(body),
Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
Ok(RoundInbound::RoundComplete(bytes)) => {
state.outcome = Some(Ok(bytes));
self.ready.notify_all();
None
}
Err(error) => {
state.outcome = Some(Err(error));
self.ready.notify_all();
None
}
}
}
fn fail_in_flight(&self, error: TransportError) {
let mut state = self.state.lock().expect("round lock");
if state.round.in_flight() && state.outcome.is_none() {
state.round.abort();
state.outcome = Some(Err(error));
self.ready.notify_all();
}
}
fn wait(&self) -> Result<Vec<u8>, TransportError> {
let mut state = self.state.lock().expect("round lock");
let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
while state.outcome.is_none() {
let now = std::time::Instant::now();
if now >= deadline {
state.round.abort();
return Err(TransportError::new(
"sync.transport_failed",
"realtime sync round timed out (§8.7)",
));
}
let (guard, _timeout) = self
.ready
.wait_timeout(state, deadline - now)
.expect("round wait");
state = guard;
}
state.outcome.take().expect("outcome present")
}
}
fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
TransportError::new("transport.failed", format!("{op}: {e}"))
}
fn segment_error(
path: Option<&str>,
http_status: Option<u16>,
cause: impl std::fmt::Display,
) -> TransportError {
let mut details = serde_json::json!({ "causeMessage": cause.to_string() });
if let Some(path) = path {
details["path"] = path.into();
}
if let Some(status) = http_status {
details["httpStatus"] = status.into();
}
let mut error = TransportError::new("sync.transport_failed", "segment transfer failed");
error.details = Some(details);
error
}
fn is_would_block(e: &tungstenite::Error) -> bool {
matches!(
e,
tungstenite::Error::Io(io) if matches!(
io.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
)
)
}
pub struct NativeTransport {
base_url: String,
ws_url: String,
headers: Vec<(String, String)>,
agent: ureq::Agent,
pub signed_urls: bool,
pub inbound: Arc<InboundBuffer>,
outgoing: Option<Box<SocketSender>>,
reader: Option<JoinHandle<()>>,
reader_stop: Arc<AtomicBool>,
round: Arc<RoundChannel>,
realtime_client_id: Option<String>,
}
fn derive_ws_url(base_url: &str) -> String {
let ws = if let Some(rest) = base_url.strip_prefix("https://") {
format!("wss://{rest}")
} else if let Some(rest) = base_url.strip_prefix("http://") {
format!("ws://{rest}")
} else {
base_url.to_owned()
};
let trimmed = ws.trim_end_matches('/');
format!("{trimmed}/realtime")
}
impl NativeTransport {
pub fn fork_round(&self) -> Self {
Self {
base_url: self.base_url.clone(),
ws_url: self.ws_url.clone(),
headers: self.headers.clone(),
agent: self.agent.clone(),
signed_urls: self.signed_urls,
inbound: self.inbound.clone(),
outgoing: self.outgoing.clone(),
reader: None,
reader_stop: self.reader_stop.clone(),
round: self.round.clone(),
realtime_client_id: self.realtime_client_id.clone(),
}
}
pub fn new(
base_url: &str,
config: &serde_json::Value,
notify: Option<Arc<dyn Fn() + Send + Sync>>,
) -> Result<Self, String> {
let mut headers = Vec::new();
if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
for (k, v) in map {
if let Some(s) = v.as_str() {
headers.push((k.clone(), s.to_owned()));
}
}
}
let ws_url = config
.get("wsUrl")
.and_then(|v| v.as_str())
.map(str::to_owned)
.unwrap_or_else(|| derive_ws_url(base_url));
Ok(NativeTransport {
base_url: base_url.trim_end_matches('/').to_owned(),
ws_url,
headers,
agent: ureq::Agent::new_with_defaults(),
signed_urls: false,
inbound: Arc::new(match notify {
Some(notify) => InboundBuffer::with_notify(notify),
None => InboundBuffer::default(),
}),
outgoing: None,
reader: None,
reader_stop: Arc::new(AtomicBool::new(false)),
round: Arc::new(RoundChannel::default()),
realtime_client_id: None,
})
}
fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
let url = format!("{}{}", self.base_url, path);
let mut req = self
.agent
.post(&url)
.header("content-type", "application/vnd.syncular.sync.v2");
for (k, v) in &self.headers {
req = req.header(k.as_str(), v.as_str());
}
let resp = req.send(body).map_err(|e| http_err("POST", e))?;
read_body(resp, None)
}
fn post_operation(&self, body: &[u8]) -> Result<Vec<u8>, TransportError> {
let url = format!("{}/operations", self.base_url);
let mut req = self.agent.post(&url).header(
"content-type",
"application/vnd.syncular.operations.v1+json",
);
for (key, value) in &self.headers {
req = req.header(key.as_str(), value.as_str());
}
let response = req
.send(body)
.map_err(|error| http_err("POST operation", error))?;
read_body(response, None)
}
fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
let mut req = self.agent.get(url);
if with_headers {
for (k, v) in &self.headers {
req = req.header(k.as_str(), v.as_str());
}
}
let resp = req.call().map_err(|e| http_err("GET", e))?;
read_body(resp, None)
}
pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
self.headers = headers;
}
pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
self.realtime_client_id = client_id.map(str::to_owned);
}
pub fn shutdown(&mut self) {
self.reader_stop.store(true, Ordering::SeqCst);
self.round.fail_in_flight(TransportError::new(
"sync.transport_failed",
"realtime disconnected mid-round (§8.7)",
));
if let Some(outgoing) = &self.outgoing {
let _ = outgoing.poller.notify();
}
if let Some(handle) = self.reader.take() {
let _ = handle.join();
}
self.outgoing = None;
}
fn send_message(&mut self, message: Message) -> Result<(), TransportError> {
let result = (|| {
let Some(outgoing) = &self.outgoing else {
return Err(TransportError::new(
"transport.failed",
"realtime not connected",
));
};
let (completed, result) = mpsc::channel();
outgoing
.queue
.try_send(Outgoing { message, completed })
.map_err(|_| {
TransportError::new("transport.failed", "realtime send queue unavailable")
})?;
outgoing
.poller
.notify()
.map_err(|e| http_err("ws wake", e))?;
result.recv_timeout(ROUND_TIMEOUT).map_err(|_| {
TransportError::new("transport.failed", "realtime send did not complete")
})?
})();
if result.is_err() {
self.shutdown();
}
result
}
}
fn read_body(
resp: ureq::http::Response<ureq::Body>,
mut progress: Option<&mut dyn FnMut(u64)>,
) -> Result<Vec<u8>, TransportError> {
use std::io::Read;
let mut reader = resp.into_body().into_with_config().reader();
let mut bytes = Vec::new();
let mut chunk = [0u8; 64 * 1024];
let mut reported = 0;
loop {
let n = reader.read(&mut chunk).map_err(|e| http_err("read", e))?;
if n == 0 {
break;
}
bytes.extend_from_slice(&chunk[..n]);
if bytes.len() - reported >= 64 * 1024 {
if let Some(callback) = progress.as_mut() {
callback(bytes.len() as u64);
}
reported = bytes.len();
}
}
if bytes.len() != reported {
if let Some(callback) = progress {
callback(bytes.len() as u64);
}
}
Ok(bytes)
}
impl Transport for NativeTransport {
fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
self.post_sync("/sync", request)
}
fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
self.post_operation(request)
}
fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
if self.outgoing.is_none() {
return self.post_sync("/sync", request);
}
let framed = self.round.begin(request)?;
if let Err(error) = self.send_message(Message::Binary(framed.into())) {
self.round.fail_in_flight(error);
}
self.round.wait()
}
fn download_segment(
&mut self,
request: &SegmentRequest,
on_progress: &mut dyn FnMut(u64),
) -> Result<Vec<u8>, TransportError> {
let url = format!("{}/segments/{}", self.base_url, request.segment_id);
let parsed = url::Url::parse(&url).map_err(|error| segment_error(None, None, error))?;
let mut req = self
.agent
.get(&url)
.header("x-syncular-scopes", &request.requested_scopes_json);
for (k, v) in &self.headers {
req = req.header(k.as_str(), v.as_str());
}
let resp = req.call().map_err(|error| {
let status = match &error {
ureq::Error::StatusCode(status) => Some(*status),
_ => None,
};
segment_error(Some(parsed.path()), status, error)
})?;
let status = resp.status().as_u16();
read_body(resp, Some(on_progress))
.map_err(|error| segment_error(Some(parsed.path()), Some(status), error.message))
}
fn supports_url_fetch(&self) -> bool {
self.signed_urls
}
fn fetch_url(
&mut self,
url: &str,
on_progress: &mut dyn FnMut(u64),
) -> Result<Vec<u8>, TransportError> {
let parsed = url::Url::parse(url).map_err(|error| segment_error(None, None, error))?;
let resp = self.agent.get(url).call().map_err(|error| {
let status = match &error {
ureq::Error::StatusCode(status) => Some(*status),
_ => None,
};
segment_error(Some(parsed.path()), status, error)
})?;
let status = resp.status().as_u16();
read_body(resp, Some(on_progress))
.map_err(|error| segment_error(Some(parsed.path()), Some(status), error.message))
}
fn blob_upload(
&mut self,
blob_id: &str,
bytes: &[u8],
media_type: Option<&str>,
) -> Result<(), TransportError> {
let url = format!("{}/blobs/{}", self.base_url, blob_id);
let mut req = self.agent.put(&url).header(
"content-type",
media_type.unwrap_or("application/octet-stream"),
);
for (k, v) in &self.headers {
req = req.header(k.as_str(), v.as_str());
}
req.send(bytes).map_err(|e| http_err("PUT blob", e))?;
Ok(())
}
fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
let url = format!("{}/blobs/{}", self.base_url, blob_id);
let mut req = self.agent.get(&url);
for (k, v) in &self.headers {
req = req.header(k.as_str(), v.as_str());
}
let resp = req.call().map_err(|e| {
let e = http_err("GET blob", e);
if e.code == "transport.failed" {
TransportError::new("blob.not_found", e.message)
} else {
e
}
})?;
let is_json = resp
.headers()
.get(ureq::http::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.contains("application/json"));
let body = read_body(resp, None)?;
if is_json {
if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
return Ok(BlobDownload::Url {
url: u.to_owned(),
url_expires_at_ms: parsed
.get("urlExpiresAtMs")
.and_then(|v| v.as_i64()),
});
}
}
}
Ok(BlobDownload::Bytes(body))
}
fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
self.get_bytes(url, false)
}
fn blob_upload_grant(
&mut self,
blob_id: &str,
byte_length: u64,
media_type: Option<&str>,
) -> Result<BlobUploadGrant, TransportError> {
let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
let mut req = self
.agent
.post(&url)
.header("content-type", "application/json");
for (k, v) in &self.headers {
req = req.header(k.as_str(), v.as_str());
}
let body = serde_json::json!({
"byteLength": byte_length,
"mediaType": media_type,
});
let resp = req
.send(body.to_string())
.map_err(|e| http_err("POST upload-grant", e))?;
let grant_body = read_body(resp, None)?;
let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
.map_err(|e| TransportError::new("transport.failed", format!("read grant: {e}")))?;
if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
return Ok(BlobUploadGrant::Url {
url: u.to_owned(),
url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
});
}
if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
return Ok(BlobUploadGrant::Present);
}
Ok(BlobUploadGrant::None)
}
fn blob_put_url(
&mut self,
url: &str,
bytes: &[u8],
media_type: Option<&str>,
) -> Result<(), TransportError> {
let req = self.agent.put(url).header(
"content-type",
media_type.unwrap_or("application/octet-stream"),
);
req.send(bytes).map_err(|e| http_err("PUT blob url", e))?;
Ok(())
}
fn realtime_connect(&mut self) -> Result<(), TransportError> {
if self.outgoing.is_some() {
if self.reader_stop.load(Ordering::SeqCst)
|| self
.reader
.as_ref()
.is_some_and(|reader| reader.is_finished())
{
self.shutdown();
} else {
return Ok(());
}
}
use tungstenite::client::IntoClientRequest;
let mut url = url::Url::parse(&self.ws_url)
.map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
if let Some(client_id) = self.realtime_client_id.as_deref() {
let retained_query: Vec<(String, String)> = url
.query_pairs()
.filter(|(key, _)| key != "clientId")
.map(|(key, value)| (key.into_owned(), value.into_owned()))
.collect();
url.set_query(None);
url.query_pairs_mut()
.extend_pairs(retained_query)
.append_pair("clientId", client_id);
}
let mut request = url
.as_str()
.into_client_request()
.map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
{
let out = request.headers_mut();
for (k, v) in &self.headers {
if let (Ok(name), Ok(value)) = (
tungstenite::http::HeaderName::try_from(k.as_str()),
tungstenite::http::HeaderValue::try_from(v.as_str()),
) {
out.insert(name, value);
}
}
}
let (mut ws, _resp) = tungstenite::connect(request)
.map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
let stream = match ws.get_mut() {
MaybeTlsStream::Plain(stream) => stream.try_clone(),
MaybeTlsStream::Rustls(stream) => stream.get_ref().try_clone(),
_ => {
return Err(TransportError::new(
"transport.failed",
"unsupported websocket stream",
))
}
}
.map_err(|e| http_err("ws clone", e))?;
stream
.set_nonblocking(true)
.map_err(|e| http_err("ws nonblocking", e))?;
let poller = Arc::new(Poller::new().map_err(|e| http_err("ws poller", e))?);
unsafe { poller.add(&stream, Event::readable(0)) }
.map_err(|e| http_err("ws register", e))?;
let registration = SocketRegistration {
poller: Arc::clone(&poller),
stream,
};
let (outgoing, queued) = mpsc::sync_channel::<Outgoing>(1);
self.outgoing = Some(Box::new(SocketSender {
queue: outgoing,
poller,
}));
self.reader_stop = Arc::new(AtomicBool::new(false));
self.round = Arc::new(RoundChannel::default());
let inbound = Arc::clone(&self.inbound);
let stop = Arc::clone(&self.reader_stop);
let round = Arc::clone(&self.round);
self.reader = Some(std::thread::spawn(move || {
let mut events = Events::new();
let mut pending: Option<mpsc::Sender<Result<(), TransportError>>> = None;
let failure = 'io: loop {
if stop.load(Ordering::SeqCst) {
let _ = ws.close(None);
let _ = ws.flush();
break TransportError::new(
"sync.transport_failed",
"realtime disconnected mid-round (§8.7)",
);
}
if pending.is_none() {
if let Ok(outgoing) = queued.try_recv() {
pending = Some(outgoing.completed);
if let Err(error) = ws.write(outgoing.message) {
if !is_would_block(&error) {
break http_err("ws write", error);
}
}
}
}
let mut drained = false;
for _ in 0..64 {
match ws.read() {
Ok(Message::Text(text)) => {
inbound.push(Inbound::Text(text.to_string()))
}
Ok(Message::Binary(bytes)) => {
if let Some(delta) = round.route_binary(&bytes) {
inbound.push(Inbound::Binary(delta));
}
}
Err(error) if is_would_block(&error) => {
drained = true;
break;
}
Ok(Message::Close(_)) => {
let _ = ws.flush();
break 'io TransportError::new(
"sync.transport_failed",
"realtime disconnected mid-round (§8.7)",
);
}
Err(_) => {
break 'io TransportError::new(
"sync.transport_failed",
"realtime disconnected mid-round (§8.7)",
);
}
Ok(_) => {}
}
}
let interest = match ws.flush() {
Ok(()) => {
if let Some(completed) = pending.take() {
let _ = completed.send(Ok(()));
}
Event::readable(0)
}
Err(error) if is_would_block(&error) => Event::all(0),
Err(error) => break http_err("ws flush", error),
};
if !drained {
continue;
}
if let Err(error) = registration.poller.modify(®istration.stream, interest) {
break http_err("ws rearm", error);
}
events.clear();
if let Err(error) = registration.poller.wait(&mut events, None) {
if error.kind() != std::io::ErrorKind::Interrupted {
break http_err("ws wait", error);
}
}
};
if let Some(completed) = pending {
let _ = completed.send(Err(failure.clone()));
}
while let Ok(outgoing) = queued.try_recv() {
let _ = outgoing.completed.send(Err(failure.clone()));
}
round.fail_in_flight(failure);
}));
Ok(())
}
fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
self.send_message(Message::Text(text.to_owned().into()))
}
fn realtime_close(&mut self) -> Result<(), TransportError> {
self.shutdown();
Ok(())
}
}
}
#[cfg(all(test, feature = "native-transport"))]
mod tests {
use super::*;
use std::io::{BufRead, BufReader, Write};
#[test]
fn detached_round_uses_the_owned_socket_and_shutdown_releases_it() {
use ssp2::model::{Frame, Message, MsgKind};
use std::sync::mpsc::channel;
for cancelled in [false, true] {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
let (entered_tx, entered_rx) = channel();
let (release_tx, release_rx) = channel();
let server = std::thread::spawn(move || {
let (socket, _) = listener.accept().unwrap();
let mut socket = tungstenite::accept(socket).unwrap();
let request = socket.read().unwrap().into_data();
assert_eq!(request[0], crate::REALTIME_TAG_ROUND);
assert_eq!(
ssp2::decode_message(&request[1..]).unwrap().msg_kind,
MsgKind::Request
);
entered_tx.send(()).unwrap();
release_rx.recv().unwrap();
if !cancelled {
let response = ssp2::encode_message(&Message {
wire_version: ssp2::decode::WIRE_VERSION,
msg_kind: MsgKind::Response,
frames: vec![Frame::RespHeader {
required_schema_version: None,
latest_schema_version: None,
log_epoch: Some("epoch".into()),
reset_required: Some(true),
}],
});
let mut chunk = vec![crate::REALTIME_TAG_ROUND];
chunk.extend(response);
socket
.send(tungstenite::Message::Binary(chunk.into()))
.unwrap();
}
});
let mut owner =
HostTransport::new_from_config(&serde_json::json!({"baseUrl":base})).unwrap();
owner.realtime_connect_for_client("detached-round").unwrap();
let mut detached = owner.fork_round();
let network = std::thread::spawn(move || {
let request = ssp2::encode_message(&Message {
wire_version: ssp2::decode::WIRE_VERSION,
msg_kind: MsgKind::Request,
frames: vec![
Frame::ReqHeader {
client_id: "detached-round".into(),
schema_version: 1,
log_epoch: None,
},
Frame::PullHeader {
limit_commits: 0,
limit_snapshot_rows: 0,
max_snapshot_pages: 0,
accept: 3,
},
],
});
detached.realtime_sync(&request)
});
entered_rx.recv().unwrap();
owner.set_headers(vec![("authorization".into(), "Bearer rotated".into())]);
assert!(owner.take_inbound().is_empty());
if cancelled {
owner.shutdown();
}
release_tx.send(()).unwrap();
let response = network.join().unwrap();
if cancelled {
assert!(response.is_err());
} else {
assert_eq!(
ssp2::decode_message(&response.unwrap()).unwrap().msg_kind,
MsgKind::Response
);
owner.shutdown();
}
server.join().unwrap();
}
}
#[test]
fn segment_http_failures_have_named_codes_and_request_details() {
for signed in [false, true] {
for body_failure in [false, true] {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
let server = std::thread::spawn(move || {
let (mut socket, _) = listener.accept().unwrap();
let mut reader = BufReader::new(socket.try_clone().unwrap());
loop {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
if line == "\r\n" {
break;
}
assert!(!line.is_empty());
}
socket.write_all(if body_failure {
b"HTTP/1.1 200 OK\r\nContent-Length: 999\r\nConnection: close\r\n\r\nshort"
} else {
b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
}).unwrap();
});
let mut transport =
HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
let mut progress = |_| {};
let error = if signed {
transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
} else {
transport.download_segment(
&SegmentRequest {
segment_id: "sha256:test".into(),
table: "tasks".into(),
requested_scopes_json: "{}".into(),
},
&mut progress,
)
}
.unwrap_err();
assert_eq!(error.code, "sync.transport_failed");
assert_eq!(error.message, "segment transfer failed");
let details = error.details.unwrap();
assert_eq!(
details["path"],
if signed {
"/signed"
} else {
"/segments/sha256:test"
}
);
assert_eq!(details["httpStatus"], if body_failure { 200 } else { 403 });
server.join().unwrap();
}
}
}
#[test]
fn refused_segment_connection_has_a_named_code_without_an_invented_status() {
for signed in [false, true] {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
drop(listener);
let mut transport =
HostTransport::new_from_config(&serde_json::json!({"baseUrl": base})).unwrap();
let mut progress = |_| {};
let error = if signed {
transport.fetch_url(&format!("{base}/signed?signature=secret"), &mut progress)
} else {
transport.download_segment(
&SegmentRequest {
segment_id: "sha256:test".into(),
table: "tasks".into(),
requested_scopes_json: "{}".into(),
},
&mut progress,
)
}
.unwrap_err();
assert_eq!(error.code, "sync.transport_failed");
assert!(error.details.as_ref().unwrap().get("httpStatus").is_none());
}
}
#[test]
fn segment_http_reports_intermediate_bytes_for_direct_and_signed_urls() {
for signed in [false, true] {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
let server = std::thread::spawn(move || {
let (mut socket, _) = listener.accept().unwrap();
let mut reader = BufReader::new(socket.try_clone().unwrap());
let mut headers = String::new();
loop {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
if line == "\r\n" {
break;
}
assert!(!line.is_empty());
headers.push_str(&line.to_ascii_lowercase());
}
assert_eq!(headers.contains("authorization: bearer test"), !signed);
assert_eq!(headers.contains("x-syncular-scopes:"), !signed);
let bytes = vec![7; 128 * 1024 + 17];
write!(
socket,
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
bytes.len()
)
.unwrap();
socket.write_all(&bytes).unwrap();
});
let mut transport = HostTransport::from_config(
&serde_json::json!({"baseUrl": base, "headers": {"authorization": "Bearer test"}}),
)
.unwrap();
let mut updates = Vec::new();
let mut progress = |bytes| updates.push(bytes);
let bytes = if signed {
transport
.fetch_url(&format!("{base}/signed"), &mut progress)
.unwrap()
} else {
transport
.download_segment(
&SegmentRequest {
segment_id: "sha256:test".into(),
table: "tasks".into(),
requested_scopes_json: "{}".into(),
},
&mut progress,
)
.unwrap()
};
assert_eq!(bytes.len(), 128 * 1024 + 17);
assert!(updates.iter().any(|n| *n > 0 && *n < bytes.len() as u64));
assert_eq!(updates.last(), Some(&(bytes.len() as u64)));
assert!(updates.windows(2).all(|pair| pair[0] < pair[1]));
server.join().unwrap();
}
}
}