1use std::sync::{Arc, Mutex};
20
21use crate::{BlobDownload, BlobUploadGrant, SegmentRequest, Transport, TransportError};
22
23pub enum Inbound {
25 Text(String),
26 Binary(Vec<u8>),
27}
28
29pub struct InboundBuffer {
33 frames: Mutex<Vec<Inbound>>,
34 notify: Option<Arc<dyn Fn() + Send + Sync>>,
35}
36
37impl Default for InboundBuffer {
38 fn default() -> Self {
39 Self {
40 frames: Mutex::new(Vec::new()),
41 notify: None,
42 }
43 }
44}
45
46impl InboundBuffer {
47 pub fn with_notify(notify: Arc<dyn Fn() + Send + Sync>) -> Self {
48 Self {
49 frames: Mutex::new(Vec::new()),
50 notify: Some(notify),
51 }
52 }
53
54 pub fn push(&self, frame: Inbound) {
55 self.frames.lock().expect("inbound lock").push(frame);
56 if let Some(notify) = &self.notify {
57 notify();
58 }
59 }
60 fn take(&self) -> Vec<Inbound> {
61 std::mem::take(&mut *self.frames.lock().expect("inbound lock"))
62 }
63}
64
65pub enum HostTransport {
66 Null {
68 signed_urls: bool,
69 inbound: Arc<InboundBuffer>,
70 },
71 #[cfg(feature = "native-transport")]
72 Native(native::NativeTransport),
73}
74
75impl HostTransport {
76 pub fn from_config(config: &serde_json::Value) -> Result<Self, String> {
79 Self::new_from_config(config)
80 }
81
82 pub fn new_from_config(config: &serde_json::Value) -> Result<Self, String> {
85 Self::from_config_with_notify(config, None)
86 }
87
88 pub fn from_config_with_notify(
92 config: &serde_json::Value,
93 notify: Option<Arc<dyn Fn() + Send + Sync>>,
94 ) -> Result<Self, String> {
95 #[cfg(feature = "native-transport")]
96 {
97 if let Some(base_url) = config.get("baseUrl").and_then(|v| v.as_str()) {
98 return Ok(HostTransport::Native(native::NativeTransport::new(
99 base_url, config, notify,
100 )?));
101 }
102 }
103 #[cfg(not(feature = "native-transport"))]
104 {
105 if config.get("baseUrl").is_some() {
106 return Err(
107 "this build has no native transport (rebuild with --features native-transport)"
108 .to_owned(),
109 );
110 }
111 }
112 Ok(HostTransport::Null {
113 signed_urls: false,
114 inbound: Arc::new(match notify {
115 Some(notify) => InboundBuffer::with_notify(notify),
116 None => InboundBuffer::default(),
117 }),
118 })
119 }
120
121 pub fn set_signed_urls(&mut self, value: bool) {
122 match self {
123 HostTransport::Null { signed_urls, .. } => *signed_urls = value,
124 #[cfg(feature = "native-transport")]
125 HostTransport::Native(t) => t.signed_urls = value,
126 }
127 }
128
129 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
132 match self {
133 HostTransport::Null { .. } => drop(headers),
134 #[cfg(feature = "native-transport")]
135 HostTransport::Native(t) => t.set_headers(headers),
136 }
137 }
138
139 pub fn take_inbound(&mut self) -> Vec<Inbound> {
141 match self {
142 HostTransport::Null { inbound, .. } => inbound.take(),
143 #[cfg(feature = "native-transport")]
144 HostTransport::Native(t) => t.inbound.take(),
145 }
146 }
147
148 pub fn shutdown(&mut self) {
150 match self {
151 HostTransport::Null { .. } => {}
152 #[cfg(feature = "native-transport")]
153 HostTransport::Native(t) => t.shutdown(),
154 }
155 }
156}
157
158fn unavailable(op: &str) -> TransportError {
159 TransportError::new(
160 "transport.unavailable",
161 format!("{op} needs the native transport (build with --features native-transport)"),
162 )
163}
164
165#[cfg_attr(not(feature = "native-transport"), allow(unused_variables))]
168impl Transport for HostTransport {
169 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
170 match self {
171 HostTransport::Null { .. } => Err(unavailable("sync")),
172 #[cfg(feature = "native-transport")]
173 HostTransport::Native(t) => t.sync(request),
174 }
175 }
176
177 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
178 match self {
179 HostTransport::Null { .. } => Err(unavailable("remoteOperation")),
180 #[cfg(feature = "native-transport")]
181 HostTransport::Native(t) => t.remote_operation(request),
182 }
183 }
184
185 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
186 match self {
187 HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
188 #[cfg(feature = "native-transport")]
189 HostTransport::Native(t) => t.realtime_sync(request),
190 }
191 }
192
193 fn download_segment(
194 &mut self,
195 request: &SegmentRequest,
196 on_progress: &mut dyn FnMut(u64),
197 ) -> Result<Vec<u8>, TransportError> {
198 match self {
199 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
200 #[cfg(feature = "native-transport")]
201 HostTransport::Native(t) => t.download_segment(request, on_progress),
202 }
203 }
204
205 fn supports_url_fetch(&self) -> bool {
206 match self {
207 HostTransport::Null { signed_urls, .. } => *signed_urls,
208 #[cfg(feature = "native-transport")]
209 HostTransport::Native(t) => t.signed_urls,
210 }
211 }
212
213 fn fetch_url(
214 &mut self,
215 url: &str,
216 on_progress: &mut dyn FnMut(u64),
217 ) -> Result<Vec<u8>, TransportError> {
218 match self {
219 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
220 #[cfg(feature = "native-transport")]
221 HostTransport::Native(t) => t.fetch_url(url, on_progress),
222 }
223 }
224
225 fn blob_upload(
226 &mut self,
227 blob_id: &str,
228 bytes: &[u8],
229 media_type: Option<&str>,
230 ) -> Result<(), TransportError> {
231 match self {
232 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
233 #[cfg(feature = "native-transport")]
234 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
235 }
236 }
237
238 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
239 match self {
240 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
241 #[cfg(feature = "native-transport")]
242 HostTransport::Native(t) => t.blob_download(blob_id),
243 }
244 }
245
246 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
247 match self {
248 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
249 #[cfg(feature = "native-transport")]
250 HostTransport::Native(t) => t.fetch_blob_url(url),
251 }
252 }
253
254 fn blob_upload_grant(
255 &mut self,
256 blob_id: &str,
257 byte_length: u64,
258 media_type: Option<&str>,
259 ) -> Result<BlobUploadGrant, TransportError> {
260 match self {
261 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
264 #[cfg(feature = "native-transport")]
265 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
266 }
267 }
268
269 fn blob_put_url(
270 &mut self,
271 url: &str,
272 bytes: &[u8],
273 media_type: Option<&str>,
274 ) -> Result<(), TransportError> {
275 match self {
276 HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
277 #[cfg(feature = "native-transport")]
278 HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
279 }
280 }
281
282 fn realtime_connect(&mut self) -> Result<(), TransportError> {
283 match self {
284 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
285 #[cfg(feature = "native-transport")]
286 HostTransport::Native(t) => {
287 t.set_realtime_client_id(None);
288 t.realtime_connect()
289 }
290 }
291 }
292
293 fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
294 match self {
295 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
296 #[cfg(feature = "native-transport")]
297 HostTransport::Native(t) => {
298 t.set_realtime_client_id(Some(client_id));
299 t.realtime_connect()
300 }
301 }
302 }
303
304 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
305 match self {
306 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
307 #[cfg(feature = "native-transport")]
308 HostTransport::Native(t) => t.realtime_send(text),
309 }
310 }
311
312 fn realtime_close(&mut self) -> Result<(), TransportError> {
313 match self {
314 HostTransport::Null { .. } => Ok(()),
315 #[cfg(feature = "native-transport")]
316 HostTransport::Native(t) => t.realtime_close(),
317 }
318 }
319}
320
321#[cfg(feature = "native-transport")]
322mod native {
323 use std::net::TcpStream;
335 use std::sync::atomic::{AtomicBool, Ordering};
336 use std::sync::{mpsc, Arc, Condvar, Mutex};
337 use std::thread::JoinHandle;
338 use std::time::Duration;
339
340 use polling::{Event, Events, Poller};
341 use tungstenite::stream::MaybeTlsStream;
342 use tungstenite::Message;
343
344 use super::{Inbound, InboundBuffer};
345 use crate::{
346 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
347 TransportError,
348 };
349
350 struct SocketRegistration {
352 poller: Arc<Poller>,
353 stream: TcpStream,
354 }
355
356 impl Drop for SocketRegistration {
357 fn drop(&mut self) {
358 let _ = self.poller.delete(&self.stream);
359 }
360 }
361
362 struct SocketSender {
363 queue: mpsc::SyncSender<Outgoing>,
364 poller: Arc<Poller>,
365 }
366
367 struct Outgoing {
368 message: Message,
369 completed: mpsc::Sender<Result<(), TransportError>>,
370 }
371
372 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
376 #[derive(Default)]
383 pub(super) struct RoundChannel {
384 state: Mutex<RoundState>,
385 ready: Condvar,
386 }
387
388 #[derive(Default)]
389 struct RoundState {
390 round: RealtimeRound,
391 outcome: Option<Result<Vec<u8>, TransportError>>,
393 }
394
395 impl RoundChannel {
396 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
400 let mut state = self.state.lock().expect("round lock");
401 state.outcome = None;
402 state.round.begin(request)
403 }
404
405 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
409 let mut state = self.state.lock().expect("round lock");
410 match state.round.route_binary(frame) {
411 Ok(RoundInbound::Delta(body)) => Some(body),
412 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
413 Ok(RoundInbound::RoundComplete(bytes)) => {
414 state.outcome = Some(Ok(bytes));
415 self.ready.notify_all();
416 None
417 }
418 Err(error) => {
419 state.outcome = Some(Err(error));
420 self.ready.notify_all();
421 None
422 }
423 }
424 }
425
426 fn fail_in_flight(&self, error: TransportError) {
428 let mut state = self.state.lock().expect("round lock");
429 if state.round.in_flight() && state.outcome.is_none() {
430 state.round.abort();
431 state.outcome = Some(Err(error));
432 self.ready.notify_all();
433 }
434 }
435
436 fn wait(&self) -> Result<Vec<u8>, TransportError> {
438 let mut state = self.state.lock().expect("round lock");
439 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
440 while state.outcome.is_none() {
441 let now = std::time::Instant::now();
442 if now >= deadline {
443 state.round.abort();
444 return Err(TransportError::new(
445 "sync.transport_failed",
446 "realtime sync round timed out (§8.7)",
447 ));
448 }
449 let (guard, _timeout) = self
450 .ready
451 .wait_timeout(state, deadline - now)
452 .expect("round wait");
453 state = guard;
454 }
455 state.outcome.take().expect("outcome present")
456 }
457 }
458
459 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
460 TransportError::new("transport.failed", format!("{op}: {e}"))
461 }
462
463 fn is_would_block(e: &tungstenite::Error) -> bool {
465 matches!(
466 e,
467 tungstenite::Error::Io(io) if matches!(
468 io.kind(),
469 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
470 )
471 )
472 }
473
474 pub struct NativeTransport {
475 base_url: String,
476 ws_url: String,
477 headers: Vec<(String, String)>,
479 agent: ureq::Agent,
480 pub signed_urls: bool,
481 pub inbound: Arc<InboundBuffer>,
482 outgoing: Option<Box<SocketSender>>,
484 reader: Option<JoinHandle<()>>,
485 reader_stop: Arc<AtomicBool>,
486 round: Arc<RoundChannel>,
488 realtime_client_id: Option<String>,
489 }
490
491 fn derive_ws_url(base_url: &str) -> String {
492 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
495 format!("wss://{rest}")
496 } else if let Some(rest) = base_url.strip_prefix("http://") {
497 format!("ws://{rest}")
498 } else {
499 base_url.to_owned()
500 };
501 let trimmed = ws.trim_end_matches('/');
502 format!("{trimmed}/realtime")
503 }
504
505 impl NativeTransport {
506 pub fn new(
507 base_url: &str,
508 config: &serde_json::Value,
509 notify: Option<Arc<dyn Fn() + Send + Sync>>,
510 ) -> Result<Self, String> {
511 let mut headers = Vec::new();
512 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
513 for (k, v) in map {
514 if let Some(s) = v.as_str() {
515 headers.push((k.clone(), s.to_owned()));
516 }
517 }
518 }
519 let ws_url = config
520 .get("wsUrl")
521 .and_then(|v| v.as_str())
522 .map(str::to_owned)
523 .unwrap_or_else(|| derive_ws_url(base_url));
524 Ok(NativeTransport {
525 base_url: base_url.trim_end_matches('/').to_owned(),
526 ws_url,
527 headers,
528 agent: ureq::Agent::new_with_defaults(),
529 signed_urls: false,
530 inbound: Arc::new(match notify {
531 Some(notify) => InboundBuffer::with_notify(notify),
532 None => InboundBuffer::default(),
533 }),
534 outgoing: None,
535 reader: None,
536 reader_stop: Arc::new(AtomicBool::new(false)),
537 round: Arc::new(RoundChannel::default()),
538 realtime_client_id: None,
539 })
540 }
541
542 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
543 let url = format!("{}{}", self.base_url, path);
544 let mut req = self
547 .agent
548 .post(&url)
549 .header("content-type", "application/vnd.syncular.sync.v2");
550 for (k, v) in &self.headers {
551 req = req.header(k.as_str(), v.as_str());
552 }
553 let resp = req.send(body).map_err(|e| http_err("POST", e))?;
554 read_body(resp, None)
555 }
556
557 fn post_operation(&self, body: &[u8]) -> Result<Vec<u8>, TransportError> {
558 let url = format!("{}/operations", self.base_url);
559 let mut req = self.agent.post(&url).header(
560 "content-type",
561 "application/vnd.syncular.operations.v1+json",
562 );
563 for (key, value) in &self.headers {
564 req = req.header(key.as_str(), value.as_str());
565 }
566 let response = req
567 .send(body)
568 .map_err(|error| http_err("POST operation", error))?;
569 read_body(response, None)
570 }
571
572 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
573 let mut req = self.agent.get(url);
574 if with_headers {
575 for (k, v) in &self.headers {
576 req = req.header(k.as_str(), v.as_str());
577 }
578 }
579 let resp = req.call().map_err(|e| http_err("GET", e))?;
580 read_body(resp, None)
581 }
582
583 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
584 self.headers = headers;
585 }
586
587 pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
588 self.realtime_client_id = client_id.map(str::to_owned);
589 }
590
591 pub fn shutdown(&mut self) {
592 self.reader_stop.store(true, Ordering::SeqCst);
593 self.round.fail_in_flight(TransportError::new(
596 "sync.transport_failed",
597 "realtime disconnected mid-round (§8.7)",
598 ));
599 if let Some(outgoing) = &self.outgoing {
600 let _ = outgoing.poller.notify();
601 }
602 if let Some(handle) = self.reader.take() {
603 let _ = handle.join();
604 }
605 self.outgoing = None;
606 }
607
608 fn send_message(&mut self, message: Message) -> Result<(), TransportError> {
609 let result = (|| {
610 let Some(outgoing) = &self.outgoing else {
611 return Err(TransportError::new(
612 "transport.failed",
613 "realtime not connected",
614 ));
615 };
616 let (completed, result) = mpsc::channel();
617 outgoing
618 .queue
619 .try_send(Outgoing { message, completed })
620 .map_err(|_| {
621 TransportError::new("transport.failed", "realtime send queue unavailable")
622 })?;
623 outgoing
624 .poller
625 .notify()
626 .map_err(|e| http_err("ws wake", e))?;
627 result.recv_timeout(ROUND_TIMEOUT).map_err(|_| {
628 TransportError::new("transport.failed", "realtime send did not complete")
629 })?
630 })();
631 if result.is_err() {
634 self.shutdown();
635 }
636 result
637 }
638 }
639
640 fn read_body(
641 resp: ureq::http::Response<ureq::Body>,
642 mut progress: Option<&mut dyn FnMut(u64)>,
643 ) -> Result<Vec<u8>, TransportError> {
644 use std::io::Read;
645 let mut reader = resp.into_body().into_with_config().reader();
646 let mut bytes = Vec::new();
647 let mut chunk = [0u8; 64 * 1024];
648 let mut reported = 0;
649 loop {
650 let n = reader.read(&mut chunk).map_err(|e| http_err("read", e))?;
651 if n == 0 {
652 break;
653 }
654 bytes.extend_from_slice(&chunk[..n]);
655 if bytes.len() - reported >= 64 * 1024 {
656 if let Some(callback) = progress.as_mut() {
657 callback(bytes.len() as u64);
658 }
659 reported = bytes.len();
660 }
661 }
662 if bytes.len() != reported {
663 if let Some(callback) = progress {
664 callback(bytes.len() as u64);
665 }
666 }
667 Ok(bytes)
668 }
669
670 impl Transport for NativeTransport {
671 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
672 self.post_sync("/sync", request)
673 }
674
675 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
676 self.post_operation(request)
677 }
678
679 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
680 if self.outgoing.is_none() {
690 return self.post_sync("/sync", request);
691 }
692 let framed = self.round.begin(request)?;
693 if let Err(error) = self.send_message(Message::Binary(framed.into())) {
694 self.round.fail_in_flight(error);
695 }
696 self.round.wait()
697 }
698
699 fn download_segment(
700 &mut self,
701 request: &SegmentRequest,
702 on_progress: &mut dyn FnMut(u64),
703 ) -> Result<Vec<u8>, TransportError> {
704 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
711 let mut req = self
712 .agent
713 .get(&url)
714 .header("x-syncular-scopes", &request.requested_scopes_json);
715 for (k, v) in &self.headers {
716 req = req.header(k.as_str(), v.as_str());
717 }
718 let resp = req.call().map_err(|e| http_err("GET segment", e))?;
719 read_body(resp, Some(on_progress))
720 }
721
722 fn supports_url_fetch(&self) -> bool {
723 self.signed_urls
724 }
725
726 fn fetch_url(
727 &mut self,
728 url: &str,
729 on_progress: &mut dyn FnMut(u64),
730 ) -> Result<Vec<u8>, TransportError> {
731 read_body(
733 self.agent
734 .get(url)
735 .call()
736 .map_err(|e| http_err("GET segment URL", e))?,
737 Some(on_progress),
738 )
739 }
740
741 fn blob_upload(
742 &mut self,
743 blob_id: &str,
744 bytes: &[u8],
745 media_type: Option<&str>,
746 ) -> Result<(), TransportError> {
747 let url = format!("{}/blobs/{}", self.base_url, blob_id);
750 let mut req = self.agent.put(&url).header(
751 "content-type",
752 media_type.unwrap_or("application/octet-stream"),
753 );
754 for (k, v) in &self.headers {
755 req = req.header(k.as_str(), v.as_str());
756 }
757 req.send(bytes).map_err(|e| http_err("PUT blob", e))?;
758 Ok(())
759 }
760
761 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
762 let url = format!("{}/blobs/{}", self.base_url, blob_id);
765 let mut req = self.agent.get(&url);
766 for (k, v) in &self.headers {
767 req = req.header(k.as_str(), v.as_str());
768 }
769 let resp = req.call().map_err(|e| {
770 let e = http_err("GET blob", e);
771 if e.code == "transport.failed" {
773 TransportError::new("blob.not_found", e.message)
774 } else {
775 e
776 }
777 })?;
778 let is_json = resp
781 .headers()
782 .get(ureq::http::header::CONTENT_TYPE)
783 .and_then(|value| value.to_str().ok())
784 .is_some_and(|value| value.contains("application/json"));
785 let body = read_body(resp, None)?;
786 if is_json {
787 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
788 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
789 return Ok(BlobDownload::Url {
790 url: u.to_owned(),
791 url_expires_at_ms: parsed
792 .get("urlExpiresAtMs")
793 .and_then(|v| v.as_i64()),
794 });
795 }
796 }
797 }
798 Ok(BlobDownload::Bytes(body))
799 }
800
801 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
802 self.get_bytes(url, false)
804 }
805
806 fn blob_upload_grant(
807 &mut self,
808 blob_id: &str,
809 byte_length: u64,
810 media_type: Option<&str>,
811 ) -> Result<BlobUploadGrant, TransportError> {
812 let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
813 let mut req = self
814 .agent
815 .post(&url)
816 .header("content-type", "application/json");
817 for (k, v) in &self.headers {
818 req = req.header(k.as_str(), v.as_str());
819 }
820 let body = serde_json::json!({
821 "byteLength": byte_length,
822 "mediaType": media_type,
823 });
824 let resp = req
825 .send(body.to_string())
826 .map_err(|e| http_err("POST upload-grant", e))?;
827 let grant_body = read_body(resp, None)?;
828 let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
829 .map_err(|e| TransportError::new("transport.failed", format!("read grant: {e}")))?;
830 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
831 return Ok(BlobUploadGrant::Url {
832 url: u.to_owned(),
833 url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
834 });
835 }
836 if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
837 return Ok(BlobUploadGrant::Present);
838 }
839 Ok(BlobUploadGrant::None)
840 }
841
842 fn blob_put_url(
843 &mut self,
844 url: &str,
845 bytes: &[u8],
846 media_type: Option<&str>,
847 ) -> Result<(), TransportError> {
848 let req = self.agent.put(url).header(
850 "content-type",
851 media_type.unwrap_or("application/octet-stream"),
852 );
853 req.send(bytes).map_err(|e| http_err("PUT blob url", e))?;
854 Ok(())
855 }
856
857 fn realtime_connect(&mut self) -> Result<(), TransportError> {
858 if self.outgoing.is_some() {
859 return Ok(());
860 }
861 use tungstenite::client::IntoClientRequest;
867 let mut url = url::Url::parse(&self.ws_url)
868 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
869 if let Some(client_id) = self.realtime_client_id.as_deref() {
870 let retained_query: Vec<(String, String)> = url
871 .query_pairs()
872 .filter(|(key, _)| key != "clientId")
873 .map(|(key, value)| (key.into_owned(), value.into_owned()))
874 .collect();
875 url.set_query(None);
876 url.query_pairs_mut()
877 .extend_pairs(retained_query)
878 .append_pair("clientId", client_id);
879 }
880 let mut request = url
881 .as_str()
882 .into_client_request()
883 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
884 {
885 let out = request.headers_mut();
886 for (k, v) in &self.headers {
887 if let (Ok(name), Ok(value)) = (
888 tungstenite::http::HeaderName::try_from(k.as_str()),
889 tungstenite::http::HeaderValue::try_from(v.as_str()),
890 ) {
891 out.insert(name, value);
892 }
893 }
894 }
895 let (mut ws, _resp) = tungstenite::connect(request)
896 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
897 let stream = match ws.get_mut() {
898 MaybeTlsStream::Plain(stream) => stream.try_clone(),
899 MaybeTlsStream::Rustls(stream) => stream.get_ref().try_clone(),
900 _ => {
901 return Err(TransportError::new(
902 "transport.failed",
903 "unsupported websocket stream",
904 ))
905 }
906 }
907 .map_err(|e| http_err("ws clone", e))?;
908 stream
909 .set_nonblocking(true)
910 .map_err(|e| http_err("ws nonblocking", e))?;
911 let poller = Arc::new(Poller::new().map_err(|e| http_err("ws poller", e))?);
912 unsafe { poller.add(&stream, Event::readable(0)) }
914 .map_err(|e| http_err("ws register", e))?;
915 let registration = SocketRegistration {
916 poller: Arc::clone(&poller),
917 stream,
918 };
919 let (outgoing, queued) = mpsc::sync_channel::<Outgoing>(1);
920 self.outgoing = Some(Box::new(SocketSender {
921 queue: outgoing,
922 poller,
923 }));
924 self.reader_stop.store(false, Ordering::SeqCst);
925 let inbound = Arc::clone(&self.inbound);
926 let stop = Arc::clone(&self.reader_stop);
927 let round = Arc::clone(&self.round);
928 self.reader = Some(std::thread::spawn(move || {
929 let mut events = Events::new();
930 let mut pending: Option<mpsc::Sender<Result<(), TransportError>>> = None;
931 let failure = 'io: loop {
932 if stop.load(Ordering::SeqCst) {
933 let _ = ws.close(None);
934 let _ = ws.flush();
935 break TransportError::new(
936 "sync.transport_failed",
937 "realtime disconnected mid-round (§8.7)",
938 );
939 }
940 if pending.is_none() {
941 if let Ok(outgoing) = queued.try_recv() {
942 pending = Some(outgoing.completed);
943 if let Err(error) = ws.write(outgoing.message) {
945 if !is_would_block(&error) {
946 break http_err("ws write", error);
947 }
948 }
949 }
950 }
951 let mut drained = false;
953 for _ in 0..64 {
954 match ws.read() {
955 Ok(Message::Text(text)) => {
956 inbound.push(Inbound::Text(text.to_string()))
957 }
958 Ok(Message::Binary(bytes)) => {
959 if let Some(delta) = round.route_binary(&bytes) {
960 inbound.push(Inbound::Binary(delta));
961 }
962 }
963 Err(error) if is_would_block(&error) => {
964 drained = true;
965 break;
966 }
967 Ok(Message::Close(_)) => {
968 let _ = ws.flush();
969 break 'io TransportError::new(
970 "sync.transport_failed",
971 "realtime disconnected mid-round (§8.7)",
972 );
973 }
974 Err(_) => {
975 break 'io TransportError::new(
976 "sync.transport_failed",
977 "realtime disconnected mid-round (§8.7)",
978 );
979 }
980 Ok(_) => {}
981 }
982 }
983 let interest = match ws.flush() {
984 Ok(()) => {
985 if let Some(completed) = pending.take() {
986 let _ = completed.send(Ok(()));
987 }
988 Event::readable(0)
989 }
990 Err(error) if is_would_block(&error) => Event::all(0),
991 Err(error) => break http_err("ws flush", error),
992 };
993 if !drained {
994 continue;
995 }
996 if let Err(error) = registration.poller.modify(®istration.stream, interest) {
997 break http_err("ws rearm", error);
998 }
999 events.clear();
1000 if let Err(error) = registration.poller.wait(&mut events, None) {
1001 if error.kind() != std::io::ErrorKind::Interrupted {
1002 break http_err("ws wait", error);
1003 }
1004 }
1005 };
1006 if let Some(completed) = pending {
1007 let _ = completed.send(Err(failure.clone()));
1008 }
1009 while let Ok(outgoing) = queued.try_recv() {
1010 let _ = outgoing.completed.send(Err(failure.clone()));
1011 }
1012 round.fail_in_flight(failure);
1013 }));
1014 Ok(())
1015 }
1016
1017 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
1018 self.send_message(Message::Text(text.to_owned().into()))
1019 }
1020
1021 fn realtime_close(&mut self) -> Result<(), TransportError> {
1022 self.shutdown();
1023 Ok(())
1024 }
1025 }
1026}
1027
1028#[cfg(all(test, feature = "native-transport"))]
1029mod tests {
1030 use super::*;
1031 use std::io::{BufRead, BufReader, Write};
1032
1033 #[test]
1034 fn segment_http_reports_intermediate_bytes_for_direct_and_signed_urls() {
1035 for signed in [false, true] {
1036 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1037 let base = format!("http://{}", listener.local_addr().unwrap());
1038 let server = std::thread::spawn(move || {
1039 let (mut socket, _) = listener.accept().unwrap();
1040 let mut reader = BufReader::new(socket.try_clone().unwrap());
1041 let mut headers = String::new();
1042 loop {
1043 let mut line = String::new();
1044 reader.read_line(&mut line).unwrap();
1045 if line == "\r\n" {
1046 break;
1047 }
1048 assert!(!line.is_empty());
1049 headers.push_str(&line.to_ascii_lowercase());
1050 }
1051 assert_eq!(headers.contains("authorization: bearer test"), !signed);
1052 assert_eq!(headers.contains("x-syncular-scopes:"), !signed);
1053 let bytes = vec![7; 128 * 1024 + 17];
1054 write!(
1055 socket,
1056 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1057 bytes.len()
1058 )
1059 .unwrap();
1060 socket.write_all(&bytes).unwrap();
1061 });
1062 let mut transport = HostTransport::from_config(
1063 &serde_json::json!({"baseUrl": base, "headers": {"authorization": "Bearer test"}}),
1064 )
1065 .unwrap();
1066 let mut updates = Vec::new();
1067 let mut progress = |bytes| updates.push(bytes);
1068 let bytes = if signed {
1069 transport
1070 .fetch_url(&format!("{base}/signed"), &mut progress)
1071 .unwrap()
1072 } else {
1073 transport
1074 .download_segment(
1075 &SegmentRequest {
1076 segment_id: "sha256:test".into(),
1077 table: "tasks".into(),
1078 requested_scopes_json: "{}".into(),
1079 },
1080 &mut progress,
1081 )
1082 .unwrap()
1083 };
1084 assert_eq!(bytes.len(), 128 * 1024 + 17);
1085 assert!(updates.iter().any(|n| *n > 0 && *n < bytes.len() as u64));
1086 assert_eq!(updates.last(), Some(&(bytes.len() as u64)));
1087 assert!(updates.windows(2).all(|pair| pair[0] < pair[1]));
1088 server.join().unwrap();
1089 }
1090 }
1091}