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(&mut self, request: &SegmentRequest) -> Result<Vec<u8>, TransportError> {
194 match self {
195 HostTransport::Null { .. } => Err(unavailable("downloadSegment")),
196 #[cfg(feature = "native-transport")]
197 HostTransport::Native(t) => t.download_segment(request),
198 }
199 }
200
201 fn supports_url_fetch(&self) -> bool {
202 match self {
203 HostTransport::Null { signed_urls, .. } => *signed_urls,
204 #[cfg(feature = "native-transport")]
205 HostTransport::Native(t) => t.signed_urls,
206 }
207 }
208
209 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
210 match self {
211 HostTransport::Null { .. } => Err(unavailable("fetchUrl")),
212 #[cfg(feature = "native-transport")]
213 HostTransport::Native(t) => t.fetch_url(url),
214 }
215 }
216
217 fn blob_upload(
218 &mut self,
219 blob_id: &str,
220 bytes: &[u8],
221 media_type: Option<&str>,
222 ) -> Result<(), TransportError> {
223 match self {
224 HostTransport::Null { .. } => Err(unavailable("blobUpload")),
225 #[cfg(feature = "native-transport")]
226 HostTransport::Native(t) => t.blob_upload(blob_id, bytes, media_type),
227 }
228 }
229
230 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
231 match self {
232 HostTransport::Null { .. } => Err(unavailable("blobDownload")),
233 #[cfg(feature = "native-transport")]
234 HostTransport::Native(t) => t.blob_download(blob_id),
235 }
236 }
237
238 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
239 match self {
240 HostTransport::Null { .. } => Err(unavailable("fetchBlobUrl")),
241 #[cfg(feature = "native-transport")]
242 HostTransport::Native(t) => t.fetch_blob_url(url),
243 }
244 }
245
246 fn blob_upload_grant(
247 &mut self,
248 blob_id: &str,
249 byte_length: u64,
250 media_type: Option<&str>,
251 ) -> Result<BlobUploadGrant, TransportError> {
252 match self {
253 HostTransport::Null { .. } => Ok(BlobUploadGrant::None),
256 #[cfg(feature = "native-transport")]
257 HostTransport::Native(t) => t.blob_upload_grant(blob_id, byte_length, media_type),
258 }
259 }
260
261 fn blob_put_url(
262 &mut self,
263 url: &str,
264 bytes: &[u8],
265 media_type: Option<&str>,
266 ) -> Result<(), TransportError> {
267 match self {
268 HostTransport::Null { .. } => Err(unavailable("blobPutUrl")),
269 #[cfg(feature = "native-transport")]
270 HostTransport::Native(t) => t.blob_put_url(url, bytes, media_type),
271 }
272 }
273
274 fn realtime_connect(&mut self) -> Result<(), TransportError> {
275 match self {
276 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
277 #[cfg(feature = "native-transport")]
278 HostTransport::Native(t) => {
279 t.set_realtime_client_id(None);
280 t.realtime_connect()
281 }
282 }
283 }
284
285 fn realtime_connect_for_client(&mut self, client_id: &str) -> Result<(), TransportError> {
286 match self {
287 HostTransport::Null { .. } => Err(unavailable("realtimeConnect")),
288 #[cfg(feature = "native-transport")]
289 HostTransport::Native(t) => {
290 t.set_realtime_client_id(Some(client_id));
291 t.realtime_connect()
292 }
293 }
294 }
295
296 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
297 match self {
298 HostTransport::Null { .. } => Err(unavailable("realtimeSend")),
299 #[cfg(feature = "native-transport")]
300 HostTransport::Native(t) => t.realtime_send(text),
301 }
302 }
303
304 fn realtime_close(&mut self) -> Result<(), TransportError> {
305 match self {
306 HostTransport::Null { .. } => Ok(()),
307 #[cfg(feature = "native-transport")]
308 HostTransport::Native(t) => t.realtime_close(),
309 }
310 }
311}
312
313#[cfg(feature = "native-transport")]
314mod native {
315 use std::net::TcpStream;
327 use std::sync::atomic::{AtomicBool, Ordering};
328 use std::sync::{Arc, Condvar, Mutex};
329 use std::thread::JoinHandle;
330 use std::time::Duration;
331
332 use tungstenite::stream::MaybeTlsStream;
333 use tungstenite::{Message, WebSocket};
334
335 use super::{Inbound, InboundBuffer};
336 use crate::{
337 BlobDownload, BlobUploadGrant, RealtimeRound, RoundInbound, SegmentRequest, Transport,
338 TransportError,
339 };
340
341 type Ws = WebSocket<MaybeTlsStream<TcpStream>>;
342
343 const ROUND_TIMEOUT: Duration = Duration::from_secs(30);
347 const READ_TIMEOUT: Duration = Duration::from_millis(5);
354 const READ_YIELD: Duration = Duration::from_micros(500);
360
361 #[derive(Default)]
368 pub(super) struct RoundChannel {
369 state: Mutex<RoundState>,
370 ready: Condvar,
371 }
372
373 #[derive(Default)]
374 struct RoundState {
375 round: RealtimeRound,
376 outcome: Option<Result<Vec<u8>, TransportError>>,
378 }
379
380 impl RoundChannel {
381 fn begin(&self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
385 let mut state = self.state.lock().expect("round lock");
386 state.outcome = None;
387 state.round.begin(request)
388 }
389
390 fn route_binary(&self, frame: &[u8]) -> Option<Vec<u8>> {
394 let mut state = self.state.lock().expect("round lock");
395 match state.round.route_binary(frame) {
396 Ok(RoundInbound::Delta(body)) => Some(body),
397 Ok(RoundInbound::RoundProgress) | Ok(RoundInbound::Ignored) => None,
398 Ok(RoundInbound::RoundComplete(bytes)) => {
399 state.outcome = Some(Ok(bytes));
400 self.ready.notify_all();
401 None
402 }
403 Err(error) => {
404 state.outcome = Some(Err(error));
405 self.ready.notify_all();
406 None
407 }
408 }
409 }
410
411 fn fail_in_flight(&self, error: TransportError) {
413 let mut state = self.state.lock().expect("round lock");
414 if state.round.in_flight() && state.outcome.is_none() {
415 state.round.abort();
416 state.outcome = Some(Err(error));
417 self.ready.notify_all();
418 }
419 }
420
421 fn wait(&self) -> Result<Vec<u8>, TransportError> {
423 let mut state = self.state.lock().expect("round lock");
424 let deadline = std::time::Instant::now() + ROUND_TIMEOUT;
425 while state.outcome.is_none() {
426 let now = std::time::Instant::now();
427 if now >= deadline {
428 state.round.abort();
429 return Err(TransportError::new(
430 "sync.transport_failed",
431 "realtime sync round timed out (§8.7)",
432 ));
433 }
434 let (guard, _timeout) = self
435 .ready
436 .wait_timeout(state, deadline - now)
437 .expect("round wait");
438 state = guard;
439 }
440 state.outcome.take().expect("outcome present")
441 }
442 }
443
444 fn http_err(op: &str, e: impl std::fmt::Display) -> TransportError {
445 TransportError::new("transport.failed", format!("{op}: {e}"))
446 }
447
448 fn is_would_block(e: &tungstenite::Error) -> bool {
451 matches!(
452 e,
453 tungstenite::Error::Io(io) if matches!(
454 io.kind(),
455 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
456 )
457 )
458 }
459
460 fn set_read_timeout(ws: &mut Ws, timeout: Option<Duration>) {
463 match ws.get_mut() {
464 MaybeTlsStream::Plain(s) => {
465 let _ = s.set_read_timeout(timeout);
466 }
467 MaybeTlsStream::Rustls(s) => {
468 let _ = s.get_ref().set_read_timeout(timeout);
469 }
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 socket: Option<Arc<Mutex<Ws>>>,
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 socket: 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)
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)
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)
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(socket) = &self.socket {
600 if let Ok(mut ws) = socket.lock() {
601 let _ = ws.close(None);
602 let _ = ws.flush();
603 }
604 }
605 if let Some(handle) = self.reader.take() {
606 let _ = handle.join();
607 }
608 self.socket = None;
609 }
610 }
611
612 fn read_body(resp: ureq::http::Response<ureq::Body>) -> Result<Vec<u8>, TransportError> {
613 resp.into_body()
614 .into_with_config()
615 .read_to_vec()
616 .map_err(|e| http_err("read", e))
617 }
618
619 impl Transport for NativeTransport {
620 fn sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
621 self.post_sync("/sync", request)
622 }
623
624 fn remote_operation(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
625 self.post_operation(request)
626 }
627
628 fn realtime_sync(&mut self, request: &[u8]) -> Result<Vec<u8>, TransportError> {
629 let Some(socket) = self.socket.clone() else {
639 return self.post_sync("/sync", request);
640 };
641 let framed = self.round.begin(request)?;
642 let send = {
645 let mut ws = socket
646 .lock()
647 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
648 ws.send(Message::Binary(framed.into()))
649 .map_err(|e| http_err("ws round send", &e))
650 .and_then(|()| ws.flush().map_err(|e| http_err("ws round flush", &e)))
651 };
652 if let Err(e) = send {
653 self.round.fail_in_flight(e);
656 }
657 self.round.wait()
658 }
659
660 fn download_segment(
661 &mut self,
662 request: &SegmentRequest,
663 ) -> Result<Vec<u8>, TransportError> {
664 let url = format!("{}/segments/{}", self.base_url, request.segment_id);
671 let mut req = self
672 .agent
673 .get(&url)
674 .header("x-syncular-scopes", &request.requested_scopes_json);
675 for (k, v) in &self.headers {
676 req = req.header(k.as_str(), v.as_str());
677 }
678 let resp = req.call().map_err(|e| http_err("GET segment", e))?;
679 read_body(resp)
680 }
681
682 fn supports_url_fetch(&self) -> bool {
683 self.signed_urls
684 }
685
686 fn fetch_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
687 self.get_bytes(url, false)
689 }
690
691 fn blob_upload(
692 &mut self,
693 blob_id: &str,
694 bytes: &[u8],
695 media_type: Option<&str>,
696 ) -> Result<(), TransportError> {
697 let url = format!("{}/blobs/{}", self.base_url, blob_id);
700 let mut req = self.agent.put(&url).header(
701 "content-type",
702 media_type.unwrap_or("application/octet-stream"),
703 );
704 for (k, v) in &self.headers {
705 req = req.header(k.as_str(), v.as_str());
706 }
707 req.send(bytes).map_err(|e| http_err("PUT blob", e))?;
708 Ok(())
709 }
710
711 fn blob_download(&mut self, blob_id: &str) -> Result<BlobDownload, TransportError> {
712 let url = format!("{}/blobs/{}", self.base_url, blob_id);
715 let mut req = self.agent.get(&url);
716 for (k, v) in &self.headers {
717 req = req.header(k.as_str(), v.as_str());
718 }
719 let resp = req.call().map_err(|e| {
720 let e = http_err("GET blob", e);
721 if e.code == "transport.failed" {
723 TransportError::new("blob.not_found", e.message)
724 } else {
725 e
726 }
727 })?;
728 let is_json = resp
731 .headers()
732 .get(ureq::http::header::CONTENT_TYPE)
733 .and_then(|value| value.to_str().ok())
734 .is_some_and(|value| value.contains("application/json"));
735 let body = read_body(resp)?;
736 if is_json {
737 if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&body) {
738 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
739 return Ok(BlobDownload::Url {
740 url: u.to_owned(),
741 url_expires_at_ms: parsed
742 .get("urlExpiresAtMs")
743 .and_then(|v| v.as_i64()),
744 });
745 }
746 }
747 }
748 Ok(BlobDownload::Bytes(body))
749 }
750
751 fn fetch_blob_url(&mut self, url: &str) -> Result<Vec<u8>, TransportError> {
752 self.get_bytes(url, false)
754 }
755
756 fn blob_upload_grant(
757 &mut self,
758 blob_id: &str,
759 byte_length: u64,
760 media_type: Option<&str>,
761 ) -> Result<BlobUploadGrant, TransportError> {
762 let url = format!("{}/blobs/{}/upload-grant", self.base_url, blob_id);
763 let mut req = self
764 .agent
765 .post(&url)
766 .header("content-type", "application/json");
767 for (k, v) in &self.headers {
768 req = req.header(k.as_str(), v.as_str());
769 }
770 let body = serde_json::json!({
771 "byteLength": byte_length,
772 "mediaType": media_type,
773 });
774 let resp = req
775 .send(body.to_string())
776 .map_err(|e| http_err("POST upload-grant", e))?;
777 let grant_body = read_body(resp)?;
778 let parsed: serde_json::Value = serde_json::from_slice(&grant_body)
779 .map_err(|e| TransportError::new("transport.failed", format!("read grant: {e}")))?;
780 if let Some(u) = parsed.get("url").and_then(|v| v.as_str()) {
781 return Ok(BlobUploadGrant::Url {
782 url: u.to_owned(),
783 url_expires_at_ms: parsed.get("urlExpiresAtMs").and_then(|v| v.as_i64()),
784 });
785 }
786 if parsed.get("present").and_then(|v| v.as_bool()) == Some(true) {
787 return Ok(BlobUploadGrant::Present);
788 }
789 Ok(BlobUploadGrant::None)
790 }
791
792 fn blob_put_url(
793 &mut self,
794 url: &str,
795 bytes: &[u8],
796 media_type: Option<&str>,
797 ) -> Result<(), TransportError> {
798 let req = self.agent.put(url).header(
800 "content-type",
801 media_type.unwrap_or("application/octet-stream"),
802 );
803 req.send(bytes).map_err(|e| http_err("PUT blob url", e))?;
804 Ok(())
805 }
806
807 fn realtime_connect(&mut self) -> Result<(), TransportError> {
808 if self.socket.is_some() {
809 return Ok(());
810 }
811 use tungstenite::client::IntoClientRequest;
817 let mut url = url::Url::parse(&self.ws_url)
818 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
819 if let Some(client_id) = self.realtime_client_id.as_deref() {
820 let retained_query: Vec<(String, String)> = url
821 .query_pairs()
822 .filter(|(key, _)| key != "clientId")
823 .map(|(key, value)| (key.into_owned(), value.into_owned()))
824 .collect();
825 url.set_query(None);
826 url.query_pairs_mut()
827 .extend_pairs(retained_query)
828 .append_pair("clientId", client_id);
829 }
830 let mut request = url
831 .as_str()
832 .into_client_request()
833 .map_err(|e| TransportError::new("transport.failed", format!("ws url: {e}")))?;
834 {
835 let out = request.headers_mut();
836 for (k, v) in &self.headers {
837 if let (Ok(name), Ok(value)) = (
838 tungstenite::http::HeaderName::try_from(k.as_str()),
839 tungstenite::http::HeaderValue::try_from(v.as_str()),
840 ) {
841 out.insert(name, value);
842 }
843 }
844 }
845 let (mut ws, _resp) = tungstenite::connect(request)
846 .map_err(|e| TransportError::new("transport.failed", format!("ws connect: {e}")))?;
847 set_read_timeout(&mut ws, Some(READ_TIMEOUT));
851 let socket = Arc::new(Mutex::new(ws));
852 self.socket = Some(Arc::clone(&socket));
853 self.reader_stop.store(false, Ordering::SeqCst);
858 let inbound = Arc::clone(&self.inbound);
859 let stop = Arc::clone(&self.reader_stop);
860 let reader_socket = Arc::clone(&socket);
861 let round = Arc::clone(&self.round);
862 self.reader = Some(std::thread::spawn(move || loop {
863 if stop.load(Ordering::SeqCst) {
864 break;
865 }
866 let msg = {
867 let mut ws = match reader_socket.lock() {
868 Ok(ws) => ws,
869 Err(_) => break,
870 };
871 ws.read()
872 };
873 match msg {
874 Ok(Message::Text(text)) => inbound.push(Inbound::Text(text.to_string())),
875 Ok(Message::Binary(bytes)) => {
876 if let Some(delta) = round.route_binary(&bytes) {
880 inbound.push(Inbound::Binary(delta));
881 }
882 }
883 Err(e) if is_would_block(&e) => {
888 std::thread::sleep(READ_YIELD);
889 continue;
890 }
891 Ok(Message::Close(_)) | Err(_) => {
892 round.fail_in_flight(TransportError::new(
895 "sync.transport_failed",
896 "realtime disconnected mid-round (§8.7)",
897 ));
898 break;
899 }
900 Ok(_) => {}
901 }
902 }));
903 Ok(())
904 }
905
906 fn realtime_send(&mut self, text: &str) -> Result<(), TransportError> {
907 let Some(socket) = &self.socket else {
908 return Err(TransportError::new(
909 "transport.failed",
910 "realtime not connected",
911 ));
912 };
913 let mut ws = socket
914 .lock()
915 .map_err(|_| TransportError::new("transport.failed", "ws lock poisoned"))?;
916 ws.send(Message::Text(text.to_owned().into()))
917 .map_err(|e| http_err("ws send", e))?;
918 ws.flush().map_err(|e| http_err("ws flush", e))?;
919 Ok(())
920 }
921
922 fn realtime_close(&mut self) -> Result<(), TransportError> {
923 self.shutdown();
924 Ok(())
925 }
926 }
927}