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 realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
178 match self {
179 HostTransport::Null { .. } => Err(unavailable("realtimeSync")),
180 #[cfg(feature = "native-transport")]
181 HostTransport::Native(t) => t.realtime_sync(request),
182 }
183 }
184
185 fn download_segment(&mut self, request: &SegmentRequest) -> Result<Vec<u8>, TransportError> {
186 match self {
187 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
188 #[cfg(feature = "native-transport")]
189 HostTransport::Native(t) => t.download_segment(request),
190 }
191 }
192
193 fn supports_url_fetch(&self) -> bool {
194 match self {
195 HostTransport::Null { signed_urls, .. } => *signed_urls,
196 #[cfg(feature = "native-transport")]
197 HostTransport::Native(t) => t.signed_urls,
198 }
199 }
200
201 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
202 match self {
203 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
204 #[cfg(feature = "native-transport")]
205 HostTransport::Native(t) => t.fetch_url(url),
206 }
207 }
208
209 fn blob_upload(
210 &mut self,
211 blob_id: &str,
212 bytes: &[u8],
213 media_type: Option<&str>,
214 ) -> Result<(), TransportError> {
215 match self {
216 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
217 #[cfg(feature = "native-transport")]
218 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
219 }
220 }
221
222 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
223 match self {
224 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
225 #[cfg(feature = "native-transport")]
226 HostTransport::Native(t) => t.blob_download(blob_id),
227 }
228 }
229
230 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
231 match self {
232 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
233 #[cfg(feature = "native-transport")]
234 HostTransport::Native(t) => t.fetch_blob_url(url),
235 }
236 }
237
238 fn blob_upload_grant(
239 &mut self,
240 blob_id: &str,
241 byte_length: u64,
242 media_type: Option<&str>,
243 ) -> Result<BlobUploadGrant, TransportError> {
244 match self {
245 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
248 #[cfg(feature = "native-transport")]
249 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
250 }
251 }
252
253 fn blob_put_url(
254 &mut self,
255 url: &str,
256 bytes: &[u8],
257 media_type: Option<&str>,
258 ) -> Result<(), TransportError> {
259 match self {
260 HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
261 #[cfg(feature = "native-transport")]
262 HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
263 }
264 }
265
266 fn realtime_connect(&mut self) -> Result<(), TransportError> {
267 match self {
268 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
269 #[cfg(feature = "native-transport")]
270 HostTransport::Native(t) => {
271 t.set_realtime_client_id(None);
272 t.realtime_connect()
273 }
274 }
275 }
276
277 fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
278 match self {
279 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
280 #[cfg(feature = "native-transport")]
281 HostTransport::Native(t) => {
282 t.set_realtime_client_id(Some(client_id));
283 t.realtime_connect()
284 }
285 }
286 }
287
288 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
289 match self {
290 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
291 #[cfg(feature = "native-transport")]
292 HostTransport::Native(t) => t.realtime_send(text),
293 }
294 }
295
296 fn realtime_close(&mut self) -> Result<(), TransportError> {
297 match self {
298 HostTransport::Null { .. } => Ok(()),
299 #[cfg(feature = "native-transport")]
300 HostTransport::Native(t) => t.realtime_close(),
301 }
302 }
303}
304
305#[cfg(feature = "native-transport")]
306mod native {
307 use std::io::Read;
319 use std::net::TcpStream;
320 use std::sync::atomic::{AtomicBool, Ordering};
321 use std::sync::{Arc, Condvar, Mutex};
322 use std::thread::JoinHandle;
323 use std::time::Duration;
324
325 use tungstenite::stream::MaybeTlsStream;
326 use tungstenite::{Message, WebSocket};
327
328 use super::{Inbound, InboundBuffer};
329 use crate::{
330 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
331 TransportError,
332 };
333
334 type Ws = WebSocket<MaybeTlsStream<TcpStream>>;
335
336 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
340 const READ_TIMEOUT: Duration = Duration::from_millis(5);
347 const READ_YIELD: Duration = Duration::from_micros(500);
353
354 #[derive(Default)]
361 pub(super) struct RoundChannel {
362 state: Mutex<RoundState>,
363 ready: Condvar,
364 }
365
366 #[derive(Default)]
367 struct RoundState {
368 round: RealtimeRound,
369 outcome: Option<Result<Vec<u8>, TransportError>>,
371 }
372
373 impl RoundChannel {
374 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
378 let mut state = self.state.lock().expect("round lock");
379 state.outcome = None;
380 state.round.begin(request)
381 }
382
383 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
387 let mut state = self.state.lock().expect("round lock");
388 match state.round.route_binary(frame) {
389 Ok(RoundInbound::Delta(body)) => Some(body),
390 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
391 Ok(RoundInbound::RoundComplete(bytes)) => {
392 state.outcome = Some(Ok(bytes));
393 self.ready.notify_all();
394 None
395 }
396 Err(error) => {
397 state.outcome = Some(Err(error));
398 self.ready.notify_all();
399 None
400 }
401 }
402 }
403
404 fn fail_in_flight(&self, error: TransportError) {
406 let mut state = self.state.lock().expect("round lock");
407 if state.round.in_flight() && state.outcome.is_none() {
408 state.round.abort();
409 state.outcome = Some(Err(error));
410 self.ready.notify_all();
411 }
412 }
413
414 fn wait(&self) -> Result<Vec<u8>, TransportError> {
416 let mut state = self.state.lock().expect("round lock");
417 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
418 while state.outcome.is_none() {
419 let now = std::time::Instant::now();
420 if now >= deadline {
421 state.round.abort();
422 return Err(TransportError::new(
423 "sync.transport_failed",
424 "realtime sync round timed out (§8.7)",
425 ));
426 }
427 let (guard, _timeout) = self
428 .ready
429 .wait_timeout(state, deadline - now)
430 .expect("round wait");
431 state = guard;
432 }
433 state.outcome.take().expect("outcome present")
434 }
435 }
436
437 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
438 TransportError::new("transport.failed", format!("{op}: {e}"))
439 }
440
441 fn is_would_block(e: &tungstenite::Error) -> bool {
444 matches!(
445 e,
446 tungstenite::Error::Io(io) if matches!(
447 io.kind(),
448 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
449 )
450 )
451 }
452
453 fn set_read_timeout(ws: &mut Ws, timeout: Option<Duration>) {
456 match ws.get_mut() {
457 MaybeTlsStream::Plain(s) => {
458 let _ = s.set_read_timeout(timeout);
459 }
460 MaybeTlsStream::Rustls(s) => {
461 let _ = s.get_ref().set_read_timeout(timeout);
462 }
463 _ => {}
464 }
465 }
466
467 pub struct NativeTransport {
468 base_url: String,
469 ws_url: String,
470 headers: Vec<(String, String)>,
472 agent: ureq::Agent,
473 pub signed_urls: bool,
474 pub inbound: Arc<InboundBuffer>,
475 socket: Option<Arc<Mutex<Ws>>>,
477 reader: Option<JoinHandle<()>>,
478 reader_stop: Arc<AtomicBool>,
479 round: Arc<RoundChannel>,
481 realtime_client_id: Option<String>,
482 }
483
484 fn derive_ws_url(base_url: &str) -> String {
485 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
488 format!("wss://{rest}")
489 } else if let Some(rest) = base_url.strip_prefix("http://") {
490 format!("ws://{rest}")
491 } else {
492 base_url.to_owned()
493 };
494 let trimmed = ws.trim_end_matches('/');
495 format!("{trimmed}/realtime")
496 }
497
498 impl NativeTransport {
499 pub fn new(
500 base_url: &str,
501 config: &serde_json::Value,
502 notify: Option<Arc<dyn Fn() + Send + Sync>>,
503 ) -> Result<Self, String> {
504 let mut headers = Vec::new();
505 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
506 for (k, v) in map {
507 if let Some(s) = v.as_str() {
508 headers.push((k.clone(), s.to_owned()));
509 }
510 }
511 }
512 let ws_url = config
513 .get("wsUrl")
514 .and_then(|v| v.as_str())
515 .map(str::to_owned)
516 .unwrap_or_else(|| derive_ws_url(base_url));
517 Ok(NativeTransport {
518 base_url: base_url.trim_end_matches('/').to_owned(),
519 ws_url,
520 headers,
521 agent: ureq::AgentBuilder::new().build(),
522 signed_urls: false,
523 inbound: Arc::new(match notify {
524 Some(notify) => InboundBuffer::with_notify(notify),
525 None => InboundBuffer::default(),
526 }),
527 socket: None,
528 reader: None,
529 reader_stop: Arc::new(AtomicBool::new(false)),
530 round: Arc::new(RoundChannel::default()),
531 realtime_client_id: None,
532 })
533 }
534
535 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
536 let url = format!("{}{}", self.base_url, path);
537 let mut req = self
540 .agent
541 .post(&url)
542 .set("content-type", "application/vnd.syncular.sync.v2");
543 for (k, v) in &self.headers {
544 req = req.set(k, v);
545 }
546 let resp = req.send_bytes(body).map_err(|e| http_err("POST", e))?;
547 read_body(resp)
548 }
549
550 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
551 let mut req = self.agent.get(url);
552 if with_headers {
553 for (k, v) in &self.headers {
554 req = req.set(k, v);
555 }
556 }
557 let resp = req.call().map_err(|e| http_err("GET", e))?;
558 read_body(resp)
559 }
560
561 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
562 self.headers = headers;
563 }
564
565 pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
566 self.realtime_client_id = client_id.map(str::to_owned);
567 }
568
569 pub fn shutdown(&mut self) {
570 self.reader_stop.store(true, Ordering::SeqCst);
571 self.round.fail_in_flight(TransportError::new(
574 "sync.transport_failed",
575 "realtime disconnected mid-round (§8.7)",
576 ));
577 if let Some(socket) = &self.socket {
578 if let Ok(mut ws) = socket.lock() {
579 let _ = ws.close(None);
580 let _ = ws.flush();
581 }
582 }
583 if let Some(handle) = self.reader.take() {
584 let _ = handle.join();
585 }
586 self.socket = None;
587 }
588 }
589
590 fn read_body(resp: ureq::Response) -> Result<Vec<u8>, TransportError> {
591 let mut buf = Vec::new();
592 resp.into_reader()
593 .read_to_end(&mut buf)
594 .map_err(|e| http_err("read", e))?;
595 Ok(buf)
596 }
597
598 impl Transport for NativeTransport {
599 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
600 self.post_sync("/sync", request)
601 }
602
603 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
604 let Some(socket) = self.socket.clone() else {
614 return self.post_sync("/sync", request);
615 };
616 let framed = self.round.begin(request)?;
617 let send = {
620 let mut ws = socket
621 .lock()
622 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
623 ws.send(Message::Binary(framed))
624 .map_err(|e| http_err("ws round send", &e))
625 .and_then(|()| ws.flush().map_err(|e| http_err("ws round flush", &e)))
626 };
627 if let Err(e) = send {
628 self.round.fail_in_flight(e);
631 }
632 self.round.wait()
633 }
634
635 fn download_segment(
636 &mut self,
637 request: &SegmentRequest,
638 ) -> Result<Vec<u8>, TransportError> {
639 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
646 let mut req = self
647 .agent
648 .get(&url)
649 .set("x-syncular-scopes", &request.requested_scopes_json);
650 for (k, v) in &self.headers {
651 req = req.set(k, v);
652 }
653 let resp = req.call().map_err(|e| http_err("GET segment", e))?;
654 read_body(resp)
655 }
656
657 fn supports_url_fetch(&self) -> bool {
658 self.signed_urls
659 }
660
661 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
662 self.get_bytes(url, false)
664 }
665
666 fn blob_upload(
667 &mut self,
668 blob_id: &str,
669 bytes: &[u8],
670 media_type: Option<&str>,
671 ) -> Result<(), TransportError> {
672 let url = format!("{}/blobs/{}", self.base_url, blob_id);
675 let mut req = self.agent.put(&url).set(
676 "content-type",
677 media_type.unwrap_or("application/octet-stream"),
678 );
679 for (k, v) in &self.headers {
680 req = req.set(k, v);
681 }
682 req.send_bytes(bytes).map_err(|e| http_err("PUT blob", e))?;
683 Ok(())
684 }
685
686 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
687 let url = format!("{}/blobs/{}", self.base_url, blob_id);
690 let mut req = self.agent.get(&url);
691 for (k, v) in &self.headers {
692 req = req.set(k, v);
693 }
694 let resp = req.call().map_err(|e| {
695 let e = http_err("GET blob", e);
696 if e.code == "transport.failed" {
698 TransportError::new("blob.not_found", e.message)
699 } else {
700 e
701 }
702 })?;
703 let is_json = resp
706 .header("content-type")
707 .is_some_and(|ct| ct.contains("application/json"));
708 let body = read_body(resp)?;
709 if is_json {
710 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
711 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
712 return Ok(BlobDownload::Url {
713 url: u.to_owned(),
714 url_expires_at_ms: parsed
715 .get("urlExpiresAtMs")
716 .and_then(|v| v.as_i64()),
717 });
718 }
719 }
720 }
721 Ok(BlobDownload::Bytes(body))
722 }
723
724 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
725 self.get_bytes(url, false)
727 }
728
729 fn blob_upload_grant(
730 &mut self,
731 blob_id: &str,
732 byte_length: u64,
733 media_type: Option<&str>,
734 ) -> Result<BlobUploadGrant, TransportError> {
735 let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
736 let mut req = self
737 .agent
738 .post(&url)
739 .set("content-type", "application/json");
740 for (k, v) in &self.headers {
741 req = req.set(k, v);
742 }
743 let body = serde_json::json!({
744 "byteLength": byte_length,
745 "mediaType": media_type,
746 });
747 let resp = req
748 .send_string(&body.to_string())
749 .map_err(|e| http_err("POST upload-grant", e))?;
750 let grant_body = read_body(resp)?;
751 let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
752 .map_err(|e| TransportError::new("transport.failed", format!("read grant: {e}")))?;
753 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
754 return Ok(BlobUploadGrant::Url {
755 url: u.to_owned(),
756 url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
757 });
758 }
759 if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
760 return Ok(BlobUploadGrant::Present);
761 }
762 Ok(BlobUploadGrant::None)
763 }
764
765 fn blob_put_url(
766 &mut self,
767 url: &str,
768 bytes: &[u8],
769 media_type: Option<&str>,
770 ) -> Result<(), TransportError> {
771 let req = self.agent.put(url).set(
773 "content-type",
774 media_type.unwrap_or("application/octet-stream"),
775 );
776 req.send_bytes(bytes)
777 .map_err(|e| http_err("PUT blob url", e))?;
778 Ok(())
779 }
780
781 fn realtime_connect(&mut self) -> Result<(), TransportError> {
782 if self.socket.is_some() {
783 return Ok(());
784 }
785 use tungstenite::client::IntoClientRequest;
791 let mut url = url::Url::parse(&self.ws_url)
792 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
793 if let Some(client_id) = self.realtime_client_id.as_deref() {
794 let retained_query: Vec<(String, String)> = url
795 .query_pairs()
796 .filter(|(key, _)| key != "clientId")
797 .map(|(key, value)| (key.into_owned(), value.into_owned()))
798 .collect();
799 url.set_query(None);
800 url.query_pairs_mut()
801 .extend_pairs(retained_query)
802 .append_pair("clientId", client_id);
803 }
804 let mut request = url
805 .as_str()
806 .into_client_request()
807 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
808 {
809 let out = request.headers_mut();
810 for (k, v) in &self.headers {
811 if let (Ok(name), Ok(value)) = (
812 tungstenite::http::HeaderName::try_from(k.as_str()),
813 tungstenite::http::HeaderValue::try_from(v.as_str()),
814 ) {
815 out.insert(name, value);
816 }
817 }
818 }
819 let (mut ws, _resp) = tungstenite::connect(request)
820 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
821 set_read_timeout(&mut ws, Some(READ_TIMEOUT));
825 let socket = Arc::new(Mutex::new(ws));
826 self.socket = Some(Arc::clone(&socket));
827 self.reader_stop.store(false, Ordering::SeqCst);
832 let inbound = Arc::clone(&self.inbound);
833 let stop = Arc::clone(&self.reader_stop);
834 let reader_socket = Arc::clone(&socket);
835 let round = Arc::clone(&self.round);
836 self.reader = Some(std::thread::spawn(move || loop {
837 if stop.load(Ordering::SeqCst) {
838 break;
839 }
840 let msg = {
841 let mut ws = match reader_socket.lock() {
842 Ok(ws) => ws,
843 Err(_) => break,
844 };
845 ws.read()
846 };
847 match msg {
848 Ok(Message::Text(text)) => inbound.push(Inbound::Text(text)),
849 Ok(Message::Binary(bytes)) => {
850 if let Some(delta) = round.route_binary(&bytes) {
854 inbound.push(Inbound::Binary(delta));
855 }
856 }
857 Err(e) if is_would_block(&e) => {
862 std::thread::sleep(READ_YIELD);
863 continue;
864 }
865 Ok(Message::Close(_)) | Err(_) => {
866 round.fail_in_flight(TransportError::new(
869 "sync.transport_failed",
870 "realtime disconnected mid-round (§8.7)",
871 ));
872 break;
873 }
874 Ok(_) => {}
875 }
876 }));
877 Ok(())
878 }
879
880 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
881 let Some(socket) = &self.socket else {
882 return Err(TransportError::new(
883 "transport.failed",
884 "realtime not connected",
885 ));
886 };
887 let mut ws = socket
888 .lock()
889 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
890 ws.send(Message::Text(text.to_owned()))
891 .map_err(|e| http_err("ws send", e))?;
892 ws.flush().map_err(|e| http_err("ws flush", e))?;
893 Ok(())
894 }
895
896 fn realtime_close(&mut self) -> Result<(), TransportError> {
897 self.shutdown();
898 Ok(())
899 }
900 }
901}