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::net::TcpStream;
319 use std::sync::atomic::{AtomicBool, Ordering};
320 use std::sync::{Arc, Condvar, Mutex};
321 use std::thread::JoinHandle;
322 use std::time::Duration;
323
324 use tungstenite::stream::MaybeTlsStream;
325 use tungstenite::{Message, WebSocket};
326
327 use super::{Inbound, InboundBuffer};
328 use crate::{
329 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
330 TransportError,
331 };
332
333 type Ws = WebSocket<MaybeTlsStream<TcpStream>>;
334
335 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
339 const READ_TIMEOUT: Duration = Duration::from_millis(5);
346 const READ_YIELD: Duration = Duration::from_micros(500);
352
353 #[derive(Default)]
360 pub(super) struct RoundChannel {
361 state: Mutex<RoundState>,
362 ready: Condvar,
363 }
364
365 #[derive(Default)]
366 struct RoundState {
367 round: RealtimeRound,
368 outcome: Option<Result<Vec<u8>, TransportError>>,
370 }
371
372 impl RoundChannel {
373 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
377 let mut state = self.state.lock().expect("round lock");
378 state.outcome = None;
379 state.round.begin(request)
380 }
381
382 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
386 let mut state = self.state.lock().expect("round lock");
387 match state.round.route_binary(frame) {
388 Ok(RoundInbound::Delta(body)) => Some(body),
389 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
390 Ok(RoundInbound::RoundComplete(bytes)) => {
391 state.outcome = Some(Ok(bytes));
392 self.ready.notify_all();
393 None
394 }
395 Err(error) => {
396 state.outcome = Some(Err(error));
397 self.ready.notify_all();
398 None
399 }
400 }
401 }
402
403 fn fail_in_flight(&self, error: TransportError) {
405 let mut state = self.state.lock().expect("round lock");
406 if state.round.in_flight() && state.outcome.is_none() {
407 state.round.abort();
408 state.outcome = Some(Err(error));
409 self.ready.notify_all();
410 }
411 }
412
413 fn wait(&self) -> Result<Vec<u8>, TransportError> {
415 let mut state = self.state.lock().expect("round lock");
416 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
417 while state.outcome.is_none() {
418 let now = std::time::Instant::now();
419 if now >= deadline {
420 state.round.abort();
421 return Err(TransportError::new(
422 "sync.transport_failed",
423 "realtime sync round timed out (§8.7)",
424 ));
425 }
426 let (guard, _timeout) = self
427 .ready
428 .wait_timeout(state, deadline - now)
429 .expect("round wait");
430 state = guard;
431 }
432 state.outcome.take().expect("outcome present")
433 }
434 }
435
436 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
437 TransportError::new("transport.failed", format!("{op}: {e}"))
438 }
439
440 fn is_would_block(e: &tungstenite::Error) -> bool {
443 matches!(
444 e,
445 tungstenite::Error::Io(io) if matches!(
446 io.kind(),
447 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
448 )
449 )
450 }
451
452 fn set_read_timeout(ws: &mut Ws, timeout: Option<Duration>) {
455 match ws.get_mut() {
456 MaybeTlsStream::Plain(s) => {
457 let _ = s.set_read_timeout(timeout);
458 }
459 MaybeTlsStream::Rustls(s) => {
460 let _ = s.get_ref().set_read_timeout(timeout);
461 }
462 _ => {}
463 }
464 }
465
466 pub struct NativeTransport {
467 base_url: String,
468 ws_url: String,
469 headers: Vec<(String, String)>,
471 agent: ureq::Agent,
472 pub signed_urls: bool,
473 pub inbound: Arc<InboundBuffer>,
474 socket: Option<Arc<Mutex<Ws>>>,
476 reader: Option<JoinHandle<()>>,
477 reader_stop: Arc<AtomicBool>,
478 round: Arc<RoundChannel>,
480 realtime_client_id: Option<String>,
481 }
482
483 fn derive_ws_url(base_url: &str) -> String {
484 let ws = if let Some(rest) = base_url.strip_prefix("https://") {
487 format!("wss://{rest}")
488 } else if let Some(rest) = base_url.strip_prefix("http://") {
489 format!("ws://{rest}")
490 } else {
491 base_url.to_owned()
492 };
493 let trimmed = ws.trim_end_matches('/');
494 format!("{trimmed}/realtime")
495 }
496
497 impl NativeTransport {
498 pub fn new(
499 base_url: &str,
500 config: &serde_json::Value,
501 notify: Option<Arc<dyn Fn() + Send + Sync>>,
502 ) -> Result<Self, String> {
503 let mut headers = Vec::new();
504 if let Some(map) = config.get("headers").and_then(|v| v.as_object()) {
505 for (k, v) in map {
506 if let Some(s) = v.as_str() {
507 headers.push((k.clone(), s.to_owned()));
508 }
509 }
510 }
511 let ws_url = config
512 .get("wsUrl")
513 .and_then(|v| v.as_str())
514 .map(str::to_owned)
515 .unwrap_or_else(|| derive_ws_url(base_url));
516 Ok(NativeTransport {
517 base_url: base_url.trim_end_matches('/').to_owned(),
518 ws_url,
519 headers,
520 agent: ureq::Agent::new_with_defaults(),
521 signed_urls: false,
522 inbound: Arc::new(match notify {
523 Some(notify) => InboundBuffer::with_notify(notify),
524 None => InboundBuffer::default(),
525 }),
526 socket: None,
527 reader: None,
528 reader_stop: Arc::new(AtomicBool::new(false)),
529 round: Arc::new(RoundChannel::default()),
530 realtime_client_id: None,
531 })
532 }
533
534 fn post_sync(&self, path: &str, body: &[u8]) -> Result<Vec<u8>, TransportError> {
535 let url = format!("{}{}", self.base_url, path);
536 let mut req = self
539 .agent
540 .post(&url)
541 .header("content-type", "application/vnd.syncular.sync.v2");
542 for (k, v) in &self.headers {
543 req = req.header(k.as_str(), v.as_str());
544 }
545 let resp = req.send(body).map_err(|e| http_err("POST", e))?;
546 read_body(resp)
547 }
548
549 fn get_bytes(&self, url: &str, with_headers: bool) -> Result<Vec<u8>, TransportError> {
550 let mut req = self.agent.get(url);
551 if with_headers {
552 for (k, v) in &self.headers {
553 req = req.header(k.as_str(), v.as_str());
554 }
555 }
556 let resp = req.call().map_err(|e| http_err("GET", e))?;
557 read_body(resp)
558 }
559
560 pub fn set_headers(&mut self, headers: Vec<(String, String)>) {
561 self.headers = headers;
562 }
563
564 pub fn set_realtime_client_id(&mut self, client_id: Option<&str>) {
565 self.realtime_client_id = client_id.map(str::to_owned);
566 }
567
568 pub fn shutdown(&mut self) {
569 self.reader_stop.store(true, Ordering::SeqCst);
570 self.round.fail_in_flight(TransportError::new(
573 "sync.transport_failed",
574 "realtime disconnected mid-round (§8.7)",
575 ));
576 if let Some(socket) = &self.socket {
577 if let Ok(mut ws) = socket.lock() {
578 let _ = ws.close(None);
579 let _ = ws.flush();
580 }
581 }
582 if let Some(handle) = self.reader.take() {
583 let _ = handle.join();
584 }
585 self.socket = None;
586 }
587 }
588
589 fn read_body(resp: ureq::http::Response<ureq::Body>) -> Result<Vec<u8>, TransportError> {
590 resp.into_body()
591 .into_with_config()
592 .read_to_vec()
593 .map_err(|e| http_err("read", e))
594 }
595
596 impl Transport for NativeTransport {
597 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
598 self.post_sync("/sync", request)
599 }
600
601 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
602 let Some(socket) = self.socket.clone() else {
612 return self.post_sync("/sync", request);
613 };
614 let framed = self.round.begin(request)?;
615 let send = {
618 let mut ws = socket
619 .lock()
620 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
621 ws.send(Message::Binary(framed.into()))
622 .map_err(|e| http_err("ws round send", &e))
623 .and_then(|()| ws.flush().map_err(|e| http_err("ws round flush", &e)))
624 };
625 if let Err(e) = send {
626 self.round.fail_in_flight(e);
629 }
630 self.round.wait()
631 }
632
633 fn download_segment(
634 &mut self,
635 request: &SegmentRequest,
636 ) -> Result<Vec<u8>, TransportError> {
637 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
644 let mut req = self
645 .agent
646 .get(&url)
647 .header("x-syncular-scopes", &request.requested_scopes_json);
648 for (k, v) in &self.headers {
649 req = req.header(k.as_str(), v.as_str());
650 }
651 let resp = req.call().map_err(|e| http_err("GET segment", e))?;
652 read_body(resp)
653 }
654
655 fn supports_url_fetch(&self) -> bool {
656 self.signed_urls
657 }
658
659 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
660 self.get_bytes(url, false)
662 }
663
664 fn blob_upload(
665 &mut self,
666 blob_id: &str,
667 bytes: &[u8],
668 media_type: Option<&str>,
669 ) -> Result<(), TransportError> {
670 let url = format!("{}/blobs/{}", self.base_url, blob_id);
673 let mut req = self.agent.put(&url).header(
674 "content-type",
675 media_type.unwrap_or("application/octet-stream"),
676 );
677 for (k, v) in &self.headers {
678 req = req.header(k.as_str(), v.as_str());
679 }
680 req.send(bytes).map_err(|e| http_err("PUT blob", e))?;
681 Ok(())
682 }
683
684 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
685 let url = format!("{}/blobs/{}", self.base_url, blob_id);
688 let mut req = self.agent.get(&url);
689 for (k, v) in &self.headers {
690 req = req.header(k.as_str(), v.as_str());
691 }
692 let resp = req.call().map_err(|e| {
693 let e = http_err("GET blob", e);
694 if e.code == "transport.failed" {
696 TransportError::new("blob.not_found", e.message)
697 } else {
698 e
699 }
700 })?;
701 let is_json = resp
704 .headers()
705 .get(ureq::http::header::CONTENT_TYPE)
706 .and_then(|value| value.to_str().ok())
707 .is_some_and(|value| value.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 .header("content-type", "application/json");
740 for (k, v) in &self.headers {
741 req = req.header(k.as_str(), v.as_str());
742 }
743 let body = serde_json::json!({
744 "byteLength": byte_length,
745 "mediaType": media_type,
746 });
747 let resp = req
748 .send(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).header(
773 "content-type",
774 media_type.unwrap_or("application/octet-stream"),
775 );
776 req.send(bytes).map_err(|e| http_err("PUT blob url", e))?;
777 Ok(())
778 }
779
780 fn realtime_connect(&mut self) -> Result<(), TransportError> {
781 if self.socket.is_some() {
782 return Ok(());
783 }
784 use tungstenite::client::IntoClientRequest;
790 let mut url = url::Url::parse(&self.ws_url)
791 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
792 if let Some(client_id) = self.realtime_client_id.as_deref() {
793 let retained_query: Vec<(String, String)> = url
794 .query_pairs()
795 .filter(|(key, _)| key != "clientId")
796 .map(|(key, value)| (key.into_owned(), value.into_owned()))
797 .collect();
798 url.set_query(None);
799 url.query_pairs_mut()
800 .extend_pairs(retained_query)
801 .append_pair("clientId", client_id);
802 }
803 let mut request = url
804 .as_str()
805 .into_client_request()
806 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
807 {
808 let out = request.headers_mut();
809 for (k, v) in &self.headers {
810 if let (Ok(name), Ok(value)) = (
811 tungstenite::http::HeaderName::try_from(k.as_str()),
812 tungstenite::http::HeaderValue::try_from(v.as_str()),
813 ) {
814 out.insert(name, value);
815 }
816 }
817 }
818 let (mut ws, _resp) = tungstenite::connect(request)
819 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
820 set_read_timeout(&mut ws, Some(READ_TIMEOUT));
824 let socket = Arc::new(Mutex::new(ws));
825 self.socket = Some(Arc::clone(&socket));
826 self.reader_stop.store(false, Ordering::SeqCst);
831 let inbound = Arc::clone(&self.inbound);
832 let stop = Arc::clone(&self.reader_stop);
833 let reader_socket = Arc::clone(&socket);
834 let round = Arc::clone(&self.round);
835 self.reader = Some(std::thread::spawn(move || loop {
836 if stop.load(Ordering::SeqCst) {
837 break;
838 }
839 let msg = {
840 let mut ws = match reader_socket.lock() {
841 Ok(ws) => ws,
842 Err(_) => break,
843 };
844 ws.read()
845 };
846 match msg {
847 Ok(Message::Text(text)) => inbound.push(Inbound::Text(text.to_string())),
848 Ok(Message::Binary(bytes)) => {
849 if let Some(delta) = round.route_binary(&bytes) {
853 inbound.push(Inbound::Binary(delta));
854 }
855 }
856 Err(e) if is_would_block(&e) => {
861 std::thread::sleep(READ_YIELD);
862 continue;
863 }
864 Ok(Message::Close(_)) | Err(_) => {
865 round.fail_in_flight(TransportError::new(
868 "sync.transport_failed",
869 "realtime disconnected mid-round (§8.7)",
870 ));
871 break;
872 }
873 Ok(_) => {}
874 }
875 }));
876 Ok(())
877 }
878
879 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
880 let Some(socket) = &self.socket else {
881 return Err(TransportError::new(
882 "transport.failed",
883 "realtime not connected",
884 ));
885 };
886 let mut ws = socket
887 .lock()
888 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
889 ws.send(Message::Text(text.to_owned().into()))
890 .map_err(|e| http_err("ws send", e))?;
891 ws.flush().map_err(|e| http_err("ws flush", e))?;
892 Ok(())
893 }
894
895 fn realtime_close(&mut self) -> Result<(), TransportError> {
896 self.shutdown();
897 Ok(())
898 }
899 }
900}