1use std::sync::{Arc, Mutex};
18
19use syncular_client::{BlobDownload, BlobUploadGrant, SegmentRequest, Transport, TransportError};
20
21pub enum Inbound {
23 Text(String),
24 Binary(Vec<u8>),
25}
26
27pub struct InboundBuffer {
30 frames: Mutex<Vec<Inbound>>,
31 notify: Option<Arc<dyn Fn() + Send + Sync>>,
32}
33
34impl Default for InboundBuffer {
35 fn default() -> Self {
36 Self {
37 frames: Mutex::new(Vec::new()),
38 notify: None,
39 }
40 }
41}
42
43impl InboundBuffer {
44 pub fn with_notify(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
45 Self {
46 frames: Mutex::new(Vec::new()),
47 notify: Some(notify),
48 }
49 }
50
51 pub fn push(&self, frame: Inbound) {
52 self.frames.lock().expect("inbound lock").push(frame);
53 if let Some(notify) = &self.notify {
54 notify();
55 }
56 }
57 fn take(&self) -> Vec<Inbound> {
58 std::mem::take(&mut *self.frames.lock().expect("inbound lock"))
59 }
60}
61
62pub enum HostTransport {
63 Null {
65 signed_urls: bool,
66 inbound: std::sync::Arc<InboundBuffer>,
67 },
68 #[cfg(feature = "native-transport")]
69 Native(native::NativeTransport),
70}
71
72impl HostTransport {
73 pub fn from_config(config: &serde_json::Value) -> Result<Self, String> {
76 Self::from_config_with_notify(config, None)
77 }
78
79 pub fn from_config_with_notify(
80 config: &serde_json::Value,
81 notify: Option<Arc<dyn Fn() + Send + Sync>>,
82 ) -> Result<Self, String> {
83 #[cfg(feature = "native-transport")]
84 {
85 if let Some(base_url) = config.get("baseUrl").and_then(|v| v.as_str()) {
86 return Ok(HostTransport::Native(native::NativeTransport::new(
87 base_url, config, notify,
88 )?));
89 }
90 }
91 #[cfg(not(feature = "native-transport"))]
92 {
93 if config.get("baseUrl").is_some() {
94 return Err("this build has no native transport (enable the \
95 native-transport feature on tauri-plugin-syncular)"
96 .to_owned());
97 }
98 }
99 Ok(HostTransport::Null {
100 signed_urls: false,
101 inbound: Arc::new(match notify {
102 Some(notify) => InboundBuffer::with_notify(notify),
103 None => InboundBuffer::default(),
104 }),
105 })
106 }
107
108 pub fn set_signed_urls(&mut self, value: bool) {
109 match self {
110 HostTransport::Null { signed_urls, .. } => *signed_urls = value,
111 #[cfg(feature = "native-transport")]
112 HostTransport::Native(t) => t.signed_urls = value,
113 }
114 }
115
116 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
122 match self {
123 HostTransport::Null { .. } => drop(headers),
124 #[cfg(feature = "native-transport")]
125 HostTransport::Native(t) => t.set_headers(headers),
126 }
127 }
128
129 pub fn take_inbound(&mut self) -> Vec<Inbound> {
131 match self {
132 HostTransport::Null { inbound, .. } => inbound.take(),
133 #[cfg(feature = "native-transport")]
134 HostTransport::Native(t) => t.inbound.take(),
135 }
136 }
137
138 pub fn shutdown(&mut self) {
140 match self {
141 HostTransport::Null { .. } => {}
142 #[cfg(feature = "native-transport")]
143 HostTransport::Native(t) => t.shutdown(),
144 }
145 }
146}
147
148fn unavailable(op: &str) -> TransportError {
149 TransportError::new(
150 "transport.unavailable",
151 format!("{op} needs the native transport (enable the native-transport feature)"),
152 )
153}
154
155#[cfg_attr(not(feature = "native-transport"), allow(unused_variables))]
156impl Transport for HostTransport {
157 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
158 match self {
159 HostTransport::Null { .. } => Err(unavailable("sync")),
160 #[cfg(feature = "native-transport")]
161 HostTransport::Native(t) => t.sync(request),
162 }
163 }
164
165 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
166 match self {
167 HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
168 #[cfg(feature = "native-transport")]
169 HostTransport::Native(t) => t.realtime_sync(request),
170 }
171 }
172
173 fn download_segment(&mut self, request: &SegmentRequest) -> Result<Vec<u8>, TransportError> {
174 match self {
175 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
176 #[cfg(feature = "native-transport")]
177 HostTransport::Native(t) => t.download_segment(request),
178 }
179 }
180
181 fn supports_url_fetch(&self) -> bool {
182 match self {
183 HostTransport::Null { signed_urls, .. } => *signed_urls,
184 #[cfg(feature = "native-transport")]
185 HostTransport::Native(t) => t.signed_urls,
186 }
187 }
188
189 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
190 match self {
191 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
192 #[cfg(feature = "native-transport")]
193 HostTransport::Native(t) => t.fetch_url(url),
194 }
195 }
196
197 fn blob_upload(
198 &mut self,
199 blob_id: &str,
200 bytes: &[u8],
201 media_type: Option<&str>,
202 ) -> Result<(), TransportError> {
203 match self {
204 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
205 #[cfg(feature = "native-transport")]
206 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
207 }
208 }
209
210 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
211 match self {
212 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
213 #[cfg(feature = "native-transport")]
214 HostTransport::Native(t) => t.blob_download(blob_id),
215 }
216 }
217
218 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
219 match self {
220 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
221 #[cfg(feature = "native-transport")]
222 HostTransport::Native(t) => t.fetch_blob_url(url),
223 }
224 }
225
226 fn blob_upload_grant(
227 &mut self,
228 blob_id: &str,
229 byte_length: u64,
230 media_type: Option<&str>,
231 ) -> Result<BlobUploadGrant, TransportError> {
232 match self {
233 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
234 #[cfg(feature = "native-transport")]
235 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
236 }
237 }
238
239 fn realtime_connect(&mut self) -> Result<(), TransportError> {
240 match self {
241 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
242 #[cfg(feature = "native-transport")]
243 HostTransport::Native(t) => t.realtime_connect(),
244 }
245 }
246
247 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
248 match self {
249 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
250 #[cfg(feature = "native-transport")]
251 HostTransport::Native(t) => t.realtime_send(text),
252 }
253 }
254
255 fn realtime_close(&mut self) -> Result<(), TransportError> {
256 match self {
257 HostTransport::Null { .. } => Ok(()),
258 #[cfg(feature = "native-transport")]
259 HostTransport::Native(t) => t.realtime_close(),
260 }
261 }
262}
263
264#[cfg(feature = "native-transport")]
265mod native {
266 use std::io::Read;
271 use std::net::TcpStream;
272 use std::sync::atomic::{AtomicBool, Ordering};
273 use std::sync::{Arc, Condvar, Mutex};
274 use std::thread::JoinHandle;
275 use std::time::Duration;
276
277 use tungstenite::stream::MaybeTlsStream;
278 use tungstenite::{Message, WebSocket};
279
280 use super::{Inbound, InboundBuffer};
281 use syncular_client::{
282 BlobDownload, RealtimeRound, RoundInbound, SegmentRequest, Transport, TransportError,
283 };
284
285 type Ws = WebSocket<MaybeTlsStream<TcpStream>>;
286
287 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
294 const READ_TIMEOUT: Duration = Duration::from_millis(50);
295
296 #[derive(Default)]
299 pub(super) struct RoundChannel {
300 state: Mutex<RoundState>,
301 ready: Condvar,
302 }
303
304 #[derive(Default)]
305 struct RoundState {
306 round: RealtimeRound,
307 outcome: Option<Result<Vec<u8>, TransportError>>,
308 }
309
310 impl RoundChannel {
311 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
312 let mut state = self.state.lock().expect("round lock");
313 state.outcome = None;
314 state.round.begin(request)
315 }
316
317 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
318 let mut state = self.state.lock().expect("round lock");
319 match state.round.route_binary(frame) {
320 Ok(RoundInbound::Delta(body)) => Some(body),
321 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
322 Ok(RoundInbound::RoundComplete(bytes)) => {
323 state.outcome = Some(Ok(bytes));
324 self.ready.notify_all();
325 None
326 }
327 Err(error) => {
328 state.outcome = Some(Err(error));
329 self.ready.notify_all();
330 None
331 }
332 }
333 }
334
335 fn fail_in_flight(&self, error: TransportError) {
336 let mut state = self.state.lock().expect("round lock");
337 if state.round.in_flight() && state.outcome.is_none() {
338 state.round.abort();
339 state.outcome = Some(Err(error));
340 self.ready.notify_all();
341 }
342 }
343
344 fn wait(&self) -> Result<Vec<u8>, TransportError> {
345 let mut state = self.state.lock().expect("round lock");
346 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
347 while state.outcome.is_none() {
348 let now = std::time::Instant::now();
349 if now >= deadline {
350 state.round.abort();
351 return Err(TransportError::new(
352 "sync.transport_failed",
353 "realtime sync round timed out (§8.7)",
354 ));
355 }
356 let (guard, _timeout) = self
357 .ready
358 .wait_timeout(state, deadline - now)
359 .expect("round wait");
360 state = guard;
361 }
362 state.outcome.take().expect("outcome present")
363 }
364 }
365
366 fn is_would_block(e: &tungstenite::Error) -> bool {
367 matches!(
368 e,
369 tungstenite::Error::Io(io) if matches!(
370 io.kind(),
371 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
372 )
373 )
374 }
375
376 fn set_read_timeout(ws: &mut Ws, timeout: Option<Duration>) {
377 match ws.get_mut() {
378 MaybeTlsStream::Plain(s) => {
379 let _ = s.set_read_timeout(timeout);
380 }
381 MaybeTlsStream::Rustls(s) => {
382 let _ = s.get_ref().set_read_timeout(timeout);
383 }
384 _ => {}
385 }
386 }
387
388 pub struct NativeTransport {
389 base_url: String,
390 ws_url: String,
391 headers: Vec<(String, String)>,
392 agent: ureq::Agent,
393 pub signed_urls: bool,
394 pub inbound: Arc<InboundBuffer>,
395 socket: Option<Arc<Mutex<Ws>>>,
396 reader: Option<JoinHandle<()>>,
397 reader_stop: Arc<AtomicBool>,
398 round: Arc<RoundChannel>,
399 }
400
401 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
402 TransportError::new("transport.failed", format!("{op}: {e}"))
403 }
404
405 fn derive_ws_url(base_url: &str) -> String {
406 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
407 format!("wss://{rest}")
408 } else if let Some(rest) = base_url.strip_prefix("http://") {
409 format!("ws://{rest}")
410 } else {
411 base_url.to_owned()
412 };
413 let trimmed = ws.trim_end_matches('/');
414 format!("{trimmed}/realtime")
415 }
416
417 impl NativeTransport {
418 pub fn new(
419 base_url: &str,
420 config: &serde_json::Value,
421 notify: Option<Arc<dyn Fn() + Send + Sync>>,
422 ) -> Result<Self, String> {
423 let mut headers = Vec::new();
424 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
425 for (k, v) in map {
426 if let Some(s) = v.as_str() {
427 headers.push((k.clone(), s.to_owned()));
428 }
429 }
430 }
431 let ws_url = config
432 .get("wsUrl")
433 .and_then(|v| v.as_str())
434 .map(str::to_owned)
435 .unwrap_or_else(|| derive_ws_url(base_url));
436 Ok(NativeTransport {
437 base_url: base_url.trim_end_matches('/').to_owned(),
438 ws_url,
439 headers,
440 agent: ureq::AgentBuilder::new().build(),
441 signed_urls: false,
442 inbound: Arc::new(match notify {
443 Some(notify) => InboundBuffer::with_notify(notify),
444 None => InboundBuffer::default(),
445 }),
446 socket: None,
447 reader: None,
448 reader_stop: Arc::new(AtomicBool::new(false)),
449 round: Arc::new(RoundChannel::default()),
450 })
451 }
452
453 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
454 let url = format!("{}{}", self.base_url, path);
455 let mut req = self
458 .agent
459 .post(&url)
460 .set("content-type", "application/vnd.syncular.sync.v2");
461 for (k, v) in &self.headers {
462 req = req.set(k, v);
463 }
464 let resp = req.send_bytes(body).map_err(|e| http_err("POST", e))?;
465 read_body(resp)
466 }
467
468 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
469 let mut req = self.agent.get(url);
470 if with_headers {
471 for (k, v) in &self.headers {
472 req = req.set(k, v);
473 }
474 }
475 let resp = req.call().map_err(|e| http_err("GET", e))?;
476 read_body(resp)
477 }
478
479 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
481 self.headers = headers;
482 }
483
484 pub fn shutdown(&mut self) {
485 self.reader_stop.store(true, Ordering::SeqCst);
486 self.round.fail_in_flight(TransportError::new(
487 "sync.transport_failed",
488 "realtime disconnected mid-round (§8.7)",
489 ));
490 if let Some(socket) = &self.socket {
491 if let Ok(mut ws) = socket.lock() {
492 let _ = ws.close(None);
493 let _ = ws.flush();
494 }
495 }
496 if let Some(handle) = self.reader.take() {
497 let _ = handle.join();
498 }
499 self.socket = None;
500 }
501 }
502
503 fn read_body(resp: ureq::Response) -> Result<Vec<u8>, TransportError> {
504 let mut buf = Vec::new();
505 resp.into_reader()
506 .read_to_end(&mut buf)
507 .map_err(|e| http_err("read", e))?;
508 Ok(buf)
509 }
510
511 impl Transport for NativeTransport {
512 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
513 self.post_sync("/sync", request)
514 }
515
516 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
517 let Some(socket) = self.socket.clone() else {
522 return self.post_sync("/sync", request);
523 };
524 let framed = self.round.begin(request)?;
525 let send = {
526 let mut ws = socket
527 .lock()
528 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
529 ws.send(Message::Binary(framed))
530 .map_err(|e| http_err("ws round send", &e))
531 .and_then(|()| ws.flush().map_err(|e| http_err("ws round flush", &e)))
532 };
533 if let Err(e) = send {
534 self.round.fail_in_flight(e);
535 }
536 self.round.wait()
537 }
538
539 fn download_segment(
540 &mut self,
541 request: &SegmentRequest,
542 ) -> Result<Vec<u8>, TransportError> {
543 let id = request
544 .segment_id
545 .strip_prefix("sha256:")
546 .unwrap_or(&request.segment_id);
547 let url = format!("{}/segments/{}", self.base_url, id);
548 self.get_bytes(&url, true)
549 }
550
551 fn supports_url_fetch(&self) -> bool {
552 self.signed_urls
553 }
554
555 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
556 self.get_bytes(url, false)
557 }
558
559 fn blob_upload(
560 &mut self,
561 blob_id: &str,
562 bytes: &[u8],
563 media_type: Option<&str>,
564 ) -> Result<(), TransportError> {
565 let id = blob_id.strip_prefix("sha256:").unwrap_or(blob_id);
566 let url = format!("{}/blobs/{}", self.base_url, id);
567 let mut req = self.agent.put(&url).set(
568 "content-type",
569 media_type.unwrap_or("application/octet-stream"),
570 );
571 for (k, v) in &self.headers {
572 req = req.set(k, v);
573 }
574 req.send_bytes(bytes).map_err(|e| http_err("PUT blob", e))?;
575 Ok(())
576 }
577
578 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
579 let id = blob_id.strip_prefix("sha256:").unwrap_or(blob_id);
580 let url = format!("{}/blobs/{}", self.base_url, id);
581 let mut req = self.agent.get(&url);
582 for (k, v) in &self.headers {
583 req = req.set(k, v);
584 }
585 let resp = req.call().map_err(|e| {
586 let e = http_err("GET blob", e);
587 if e.code == "transport.failed" {
588 TransportError::new("blob.not_found", e.message)
589 } else {
590 e
591 }
592 })?;
593 let is_json = resp
596 .header("content-type")
597 .is_some_and(|ct| ct.contains("application/json"));
598 let body = read_body(resp)?;
599 if is_json {
600 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
601 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
602 return Ok(BlobDownload::Url {
603 url: u.to_owned(),
604 url_expires_at_ms: parsed
605 .get("urlExpiresAtMs")
606 .and_then(|v| v.as_i64()),
607 });
608 }
609 }
610 }
611 Ok(BlobDownload::Bytes(body))
612 }
613
614 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
615 self.get_bytes(url, false)
617 }
618
619 fn realtime_connect(&mut self) -> Result<(), TransportError> {
620 if self.socket.is_some() {
621 return Ok(());
622 }
623 use tungstenite::client::IntoClientRequest;
627 let mut request = self
628 .ws_url
629 .as_str()
630 .into_client_request()
631 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
632 {
633 let out = request.headers_mut();
634 for (k, v) in &self.headers {
635 if let (Ok(name), Ok(value)) = (
636 tungstenite::http::HeaderName::try_from(k.as_str()),
637 tungstenite::http::HeaderValue::try_from(v.as_str()),
638 ) {
639 out.insert(name, value);
640 }
641 }
642 }
643 let (mut ws, _resp) = tungstenite::connect(request)
644 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
645 set_read_timeout(&mut ws, Some(READ_TIMEOUT));
646 let socket = Arc::new(Mutex::new(ws));
647 self.socket = Some(Arc::clone(&socket));
648 self.reader_stop.store(false, Ordering::SeqCst);
649 let inbound = Arc::clone(&self.inbound);
650 let stop = Arc::clone(&self.reader_stop);
651 let reader_socket = Arc::clone(&socket);
652 let round = Arc::clone(&self.round);
653 self.reader = Some(std::thread::spawn(move || loop {
654 if stop.load(Ordering::SeqCst) {
655 break;
656 }
657 let msg = {
658 let mut ws = match reader_socket.lock() {
659 Ok(ws) => ws,
660 Err(_) => break,
661 };
662 ws.read()
663 };
664 match msg {
665 Ok(Message::Text(text)) => inbound.push(Inbound::Text(text)),
666 Ok(Message::Binary(bytes)) => {
667 if let Some(delta) = round.route_binary(&bytes) {
671 inbound.push(Inbound::Binary(delta));
672 }
673 }
674 Err(e) if is_would_block(&e) => continue,
675 Ok(Message::Close(_)) | Err(_) => {
676 round.fail_in_flight(TransportError::new(
677 "sync.transport_failed",
678 "realtime disconnected mid-round (§8.7)",
679 ));
680 break;
681 }
682 Ok(_) => {}
683 }
684 }));
685 Ok(())
686 }
687
688 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
689 let Some(socket) = &self.socket else {
690 return Err(TransportError::new(
691 "transport.failed",
692 "realtime not connected",
693 ));
694 };
695 let mut ws = socket
696 .lock()
697 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
698 ws.send(Message::Text(text.to_owned()))
699 .map_err(|e| http_err("ws send", e))?;
700 ws.flush().map_err(|e| http_err("ws flush", e))?;
701 Ok(())
702 }
703
704 fn realtime_close(&mut self) -> Result<(), TransportError> {
705 self.shutdown();
706 Ok(())
707 }
708 }
709}