1use futures_util::{SinkExt, StreamExt};
4use prost::Message;
5use std::collections::HashMap;
6use std::fmt;
7use std::future::Future;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::sync::{Semaphore, broadcast, mpsc, oneshot, watch};
11use tokio_tungstenite::connect_async;
12use tokio_tungstenite::tungstenite::Message as WsMessage;
13use tokio_tungstenite::tungstenite::client::IntoClientRequest;
14use tokio_tungstenite::tungstenite::http::HeaderValue;
15use url::Url;
16
17use crate::client_info::{self, ClientIdentity};
18use inline_protocol::proto;
19
20pub const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
22pub const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(60);
24pub const DEFAULT_SESSION_COMMAND_CAPACITY: usize = 64;
26pub const DEFAULT_SESSION_MAX_IN_FLIGHT_RPCS: usize = 64;
28pub const DEFAULT_SESSION_EVENT_CAPACITY: usize = 256;
30pub const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
32pub const DEFAULT_HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(12);
34
35#[derive(thiserror::Error)]
37#[non_exhaustive]
38pub enum RealtimeError {
39 #[error("invalid realtime URL: {message}")]
41 InvalidUrl {
42 url: String,
44 message: String,
46 },
47 #[error("websocket error: {0}")]
49 WebSocket(Box<tokio_tungstenite::tungstenite::Error>),
50 #[error("{field} contains characters that are invalid in realtime headers")]
52 InvalidHeaderValue {
53 field: &'static str,
55 },
56 #[error("protocol error: {0}")]
58 Protocol(#[from] prost::DecodeError),
59 #[error("missing rpc result")]
61 MissingResult,
62 #[error("unexpected rpc result for {method}: expected {expected}, got {actual}")]
64 UnexpectedResult {
65 method: &'static str,
67 expected: &'static str,
69 actual: &'static str,
71 },
72 #[error("realtime {operation} timed out after {timeout:?}")]
74 Timeout {
75 operation: &'static str,
77 timeout: Duration,
79 },
80 #[error("{friendly}")]
82 ConnectionError {
83 reason: i32,
85 reason_name: String,
87 friendly: String,
89 },
90 #[error("realtime connection closed")]
92 ConnectionClosed,
93 #[error("realtime event subscriber lagged and skipped {skipped} events")]
95 EventLagged {
96 skipped: u64,
98 },
99 #[error("{friendly}")]
101 RpcError {
102 code: i32,
104 error_code: i32,
106 error_name: String,
108 message: String,
110 friendly: String,
112 },
113}
114
115impl fmt::Debug for RealtimeError {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self {
118 RealtimeError::InvalidUrl { url, message } => f
119 .debug_struct("InvalidUrl")
120 .field("url", &realtime_url_for_debug(url))
121 .field("message", message)
122 .finish(),
123 RealtimeError::WebSocket(error) => f.debug_tuple("WebSocket").field(error).finish(),
124 RealtimeError::InvalidHeaderValue { field } => f
125 .debug_struct("InvalidHeaderValue")
126 .field("field", field)
127 .finish(),
128 RealtimeError::Protocol(error) => f.debug_tuple("Protocol").field(error).finish(),
129 RealtimeError::MissingResult => f.debug_struct("MissingResult").finish(),
130 RealtimeError::UnexpectedResult {
131 method,
132 expected,
133 actual,
134 } => f
135 .debug_struct("UnexpectedResult")
136 .field("method", method)
137 .field("expected", expected)
138 .field("actual", actual)
139 .finish(),
140 RealtimeError::Timeout { operation, timeout } => f
141 .debug_struct("Timeout")
142 .field("operation", operation)
143 .field("timeout", timeout)
144 .finish(),
145 RealtimeError::ConnectionError {
146 reason,
147 reason_name,
148 friendly,
149 } => f
150 .debug_struct("ConnectionError")
151 .field("reason", reason)
152 .field("reason_name", reason_name)
153 .field("friendly", friendly)
154 .finish(),
155 RealtimeError::ConnectionClosed => f.debug_struct("ConnectionClosed").finish(),
156 RealtimeError::EventLagged { skipped } => f
157 .debug_struct("EventLagged")
158 .field("skipped", skipped)
159 .finish(),
160 RealtimeError::RpcError {
161 code,
162 error_code,
163 error_name,
164 message,
165 friendly,
166 } => f
167 .debug_struct("RpcError")
168 .field("code", code)
169 .field("error_code", error_code)
170 .field("error_name", error_name)
171 .field("message", message)
172 .field("friendly", friendly)
173 .finish(),
174 }
175 }
176}
177
178impl From<tokio_tungstenite::tungstenite::Error> for RealtimeError {
179 fn from(error: tokio_tungstenite::tungstenite::Error) -> Self {
180 Self::WebSocket(Box::new(error))
181 }
182}
183
184pub struct RealtimeClient {
186 ws: tokio_tungstenite::WebSocketStream<
187 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
188 >,
189 seq: u32,
190 id_gen: IdGenerator,
191 rpc_timeout: Option<Duration>,
192 heartbeat_interval: Option<Duration>,
193 heartbeat_timeout: Duration,
194 max_in_flight_rpcs: usize,
195}
196
197#[derive(Clone, Debug, PartialEq)]
199#[non_exhaustive]
200pub enum RealtimeEvent {
201 Updates(Vec<proto::Update>),
203 Ack {
205 msg_id: u64,
207 },
208 Pong {
210 nonce: u64,
212 },
213}
214
215#[derive(Clone)]
221pub struct RealtimeSession {
222 commands: mpsc::Sender<SessionCommand>,
223 events: broadcast::Sender<RealtimeEvent>,
224 closed: watch::Receiver<bool>,
225 rpc_timeout: Option<Duration>,
226 heartbeat_interval: Option<Duration>,
227 heartbeat_timeout: Duration,
228 rpc_permits: Arc<Semaphore>,
229}
230
231impl fmt::Debug for RealtimeSession {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 f.debug_struct("RealtimeSession")
234 .field("rpc_timeout", &self.rpc_timeout)
235 .field("heartbeat_interval", &self.heartbeat_interval)
236 .field("heartbeat_timeout", &self.heartbeat_timeout)
237 .field(
238 "available_rpc_permits",
239 &self.rpc_permits.available_permits(),
240 )
241 .field("closed", &*self.closed.borrow())
242 .finish_non_exhaustive()
243 }
244}
245
246pub struct RealtimeEventReceiver {
248 events: broadcast::Receiver<RealtimeEvent>,
249 closed: watch::Receiver<bool>,
250}
251
252impl fmt::Debug for RealtimeEventReceiver {
253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254 f.debug_struct("RealtimeEventReceiver")
255 .field("closed", &*self.closed.borrow())
256 .finish_non_exhaustive()
257 }
258}
259
260enum SessionCommand {
261 Invoke {
262 method: proto::Method,
263 input: proto::rpc_call::Input,
264 response: oneshot::Sender<Result<proto::rpc_result::Result, RealtimeError>>,
265 },
266}
267
268#[must_use]
270#[derive(Clone)]
271pub struct RealtimeClientBuilder {
272 url: String,
273 token: String,
274 identity: ClientIdentity,
275 connect_timeout: Option<Duration>,
276 rpc_timeout: Option<Duration>,
277 heartbeat_interval: Option<Duration>,
278 heartbeat_timeout: Duration,
279 max_in_flight_rpcs: usize,
280}
281
282impl fmt::Debug for RealtimeClientBuilder {
283 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284 f.debug_struct("RealtimeClientBuilder")
285 .field("url", &realtime_url_for_debug(&self.url))
286 .field("token", &"<redacted>")
287 .field("identity", &self.identity)
288 .field("connect_timeout", &self.connect_timeout)
289 .field("rpc_timeout", &self.rpc_timeout)
290 .field("heartbeat_interval", &self.heartbeat_interval)
291 .field("heartbeat_timeout", &self.heartbeat_timeout)
292 .field("max_in_flight_rpcs", &self.max_in_flight_rpcs)
293 .finish()
294 }
295}
296
297pub trait RpcRequest: Sized {
303 type Response;
305
306 const METHOD: proto::Method;
308
309 fn into_rpc_input(self) -> proto::rpc_call::Input;
311
312 fn response_from_rpc_result(
314 result: proto::rpc_result::Result,
315 ) -> Result<Self::Response, RealtimeError>;
316}
317
318impl RealtimeClient {
319 pub fn builder(url: impl Into<String>, token: impl Into<String>) -> RealtimeClientBuilder {
321 RealtimeClientBuilder::new(url, token)
322 }
323
324 pub async fn connect(url: &str, token: &str) -> Result<Self, RealtimeError> {
326 Self::builder(url, token).connect().await
327 }
328
329 pub async fn connect_with_identity(
331 url: &str,
332 token: &str,
333 identity: ClientIdentity,
334 ) -> Result<Self, RealtimeError> {
335 Self::builder(url, token).identity(identity).connect().await
336 }
337}
338
339impl RealtimeSession {
340 pub async fn connect(url: &str, token: &str) -> Result<Self, RealtimeError> {
342 RealtimeClient::builder(url, token).connect_session().await
343 }
344
345 pub async fn connect_with_identity(
347 url: &str,
348 token: &str,
349 identity: ClientIdentity,
350 ) -> Result<Self, RealtimeError> {
351 RealtimeClient::builder(url, token)
352 .identity(identity)
353 .connect_session()
354 .await
355 }
356
357 pub fn subscribe(&self) -> RealtimeEventReceiver {
359 RealtimeEventReceiver {
360 events: self.events.subscribe(),
361 closed: self.closed.clone(),
362 }
363 }
364
365 pub fn is_closed(&self) -> bool {
367 *self.closed.borrow()
368 }
369
370 pub async fn call<R>(&self, request: R) -> Result<R::Response, RealtimeError>
373 where
374 R: RpcRequest,
375 {
376 let result = self.invoke(R::METHOD, request.into_rpc_input()).await?;
377 R::response_from_rpc_result(result)
378 }
379
380 pub async fn invoke(
383 &self,
384 method: proto::Method,
385 input: proto::rpc_call::Input,
386 ) -> Result<proto::rpc_result::Result, RealtimeError> {
387 if self.is_closed() {
388 return Err(RealtimeError::ConnectionClosed);
389 }
390 let (response_tx, response_rx) = oneshot::channel();
391 let commands = self.commands.clone();
392 let permits = self.rpc_permits.clone();
393 let response = async move {
394 let _permit = permits
395 .acquire_owned()
396 .await
397 .map_err(|_| RealtimeError::ConnectionClosed)?;
398 commands
399 .send(SessionCommand::Invoke {
400 method,
401 input,
402 response: response_tx,
403 })
404 .await
405 .map_err(|_| RealtimeError::ConnectionClosed)?;
406 response_rx
407 .await
408 .map_err(|_| RealtimeError::ConnectionClosed)?
409 };
410 with_optional_timeout("rpc", self.rpc_timeout, response).await
411 }
412
413 fn from_client(client: RealtimeClient) -> Self {
414 let rpc_timeout = client.rpc_timeout;
415 let heartbeat_interval = client.heartbeat_interval;
416 let heartbeat_timeout = client.heartbeat_timeout;
417 let max_in_flight_rpcs = client.max_in_flight_rpcs;
418 let (command_tx, command_rx) = mpsc::channel(DEFAULT_SESSION_COMMAND_CAPACITY);
419 let (event_tx, _) = broadcast::channel(DEFAULT_SESSION_EVENT_CAPACITY);
420 let (closed_tx, closed_rx) = watch::channel(false);
421 tokio::spawn(run_realtime_session(
422 client,
423 command_rx,
424 event_tx.clone(),
425 closed_tx,
426 ));
427 Self {
428 commands: command_tx,
429 events: event_tx,
430 closed: closed_rx,
431 rpc_timeout,
432 heartbeat_interval,
433 heartbeat_timeout,
434 rpc_permits: Arc::new(Semaphore::new(max_in_flight_rpcs)),
435 }
436 }
437}
438
439impl RealtimeEventReceiver {
440 pub async fn recv(&mut self) -> Result<RealtimeEvent, RealtimeError> {
442 loop {
443 if *self.closed.borrow() && self.events.is_empty() {
444 return Err(RealtimeError::ConnectionClosed);
445 }
446
447 tokio::select! {
448 result = self.events.recv() => return match result {
449 Ok(event) => Ok(event),
450 Err(broadcast::error::RecvError::Lagged(skipped)) => {
451 Err(RealtimeError::EventLagged { skipped })
452 }
453 Err(broadcast::error::RecvError::Closed) => {
454 Err(RealtimeError::ConnectionClosed)
455 }
456 },
457 changed = self.closed.changed() => {
458 match changed {
459 Ok(()) => continue,
460 Err(_) => return Err(RealtimeError::ConnectionClosed),
461 }
462 }
463 }
464 }
465 }
466}
467
468impl RealtimeClientBuilder {
469 pub fn new(url: impl Into<String>, token: impl Into<String>) -> Self {
471 Self {
472 url: url.into(),
473 token: token.into(),
474 identity: ClientIdentity::sdk(),
475 connect_timeout: Some(DEFAULT_CONNECT_TIMEOUT),
476 rpc_timeout: Some(DEFAULT_RPC_TIMEOUT),
477 heartbeat_interval: Some(DEFAULT_HEARTBEAT_INTERVAL),
478 heartbeat_timeout: DEFAULT_HEARTBEAT_TIMEOUT,
479 max_in_flight_rpcs: DEFAULT_SESSION_MAX_IN_FLIGHT_RPCS,
480 }
481 }
482
483 pub fn identity(mut self, identity: ClientIdentity) -> Self {
485 self.identity = identity;
486 self
487 }
488
489 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
491 self.connect_timeout = Some(timeout);
492 self
493 }
494
495 pub fn without_connect_timeout(mut self) -> Self {
497 self.connect_timeout = None;
498 self
499 }
500
501 pub fn rpc_timeout(mut self, timeout: Duration) -> Self {
503 self.rpc_timeout = Some(timeout);
504 self
505 }
506
507 pub fn without_rpc_timeout(mut self) -> Self {
509 self.rpc_timeout = None;
510 self
511 }
512
513 pub fn max_in_flight_rpcs(mut self, maximum: usize) -> Self {
515 self.max_in_flight_rpcs = maximum.max(1);
516 self
517 }
518
519 pub fn heartbeat(mut self, interval: Duration, timeout: Duration) -> Self {
521 self.heartbeat_interval = Some(interval.max(Duration::from_millis(1)));
522 self.heartbeat_timeout = timeout.max(Duration::from_millis(1));
523 self
524 }
525
526 pub fn without_heartbeat(mut self) -> Self {
528 self.heartbeat_interval = None;
529 self
530 }
531
532 pub async fn connect(self) -> Result<RealtimeClient, RealtimeError> {
534 let url = normalize_realtime_url(self.url)?;
535 log::debug!(
536 target: "inline_sdk::realtime",
537 "opening realtime websocket url={} identity_type={} connect_timeout={:?} rpc_timeout={:?}",
538 realtime_url_for_log(&url),
539 self.identity.client_type(),
540 self.connect_timeout,
541 self.rpc_timeout
542 );
543 let mut request = url.as_str().into_client_request()?;
544 request.headers_mut().insert(
545 client_info::CLIENT_TYPE_HEADER,
546 realtime_header_value("client_type", self.identity.client_type())?,
547 );
548 request.headers_mut().insert(
549 client_info::CLIENT_VERSION_HEADER,
550 realtime_header_value("client_version", self.identity.client_version())?,
551 );
552 request.headers_mut().insert(
553 "user-agent",
554 realtime_header_value("user_agent", &client_info::user_agent_for(&self.identity))?,
555 );
556
557 let (ws, _) =
558 with_optional_timeout("connect", self.connect_timeout, connect_async(request)).await?;
559 log::debug!(target: "inline_sdk::realtime", "websocket connected");
560 let mut client = RealtimeClient {
561 ws,
562 seq: 0,
563 id_gen: IdGenerator::new(),
564 rpc_timeout: self.rpc_timeout,
565 heartbeat_interval: self.heartbeat_interval,
566 heartbeat_timeout: self.heartbeat_timeout,
567 max_in_flight_rpcs: self.max_in_flight_rpcs,
568 };
569
570 with_optional_timeout(
571 "connection_init",
572 self.connect_timeout,
573 client.send_connection_init(&self.token, &self.identity),
574 )
575 .await?;
576 log::trace!(target: "inline_sdk::realtime", "connection init sent");
577 with_optional_timeout(
578 "connection_open",
579 self.connect_timeout,
580 client.wait_for_connection_open(),
581 )
582 .await?;
583 log::debug!(target: "inline_sdk::realtime", "realtime protocol open");
584 Ok(client)
585 }
586
587 pub async fn connect_session(self) -> Result<RealtimeSession, RealtimeError> {
590 self.connect().await.map(RealtimeSession::from_client)
591 }
592}
593
594impl RealtimeClient {
595 pub async fn call<R>(&mut self, request: R) -> Result<R::Response, RealtimeError>
597 where
598 R: RpcRequest,
599 {
600 log::trace!(
601 target: "inline_sdk::realtime",
602 "calling typed rpc method={}",
603 R::METHOD.as_str_name()
604 );
605 let result = self.invoke(R::METHOD, request.into_rpc_input()).await?;
606 R::response_from_rpc_result(result)
607 }
608
609 pub async fn invoke(
611 &mut self,
612 method: proto::Method,
613 input: proto::rpc_call::Input,
614 ) -> Result<proto::rpc_result::Result, RealtimeError> {
615 let message_id = self.send_rpc_call(method, input).await?;
616
617 with_optional_timeout(
618 "rpc",
619 self.rpc_timeout,
620 self.wait_for_rpc_result(message_id),
621 )
622 .await
623 }
624
625 async fn send_rpc_call(
626 &mut self,
627 method: proto::Method,
628 input: proto::rpc_call::Input,
629 ) -> Result<u64, RealtimeError> {
630 let message_id = self.next_id();
631 log::trace!(
632 target: "inline_sdk::realtime",
633 "sending rpc method={} msg_id={message_id}",
634 method.as_str_name()
635 );
636 let message = proto::ClientMessage {
637 id: message_id,
638 seq: self.next_seq(),
639 body: Some(proto::client_message::Body::RpcCall(proto::RpcCall {
640 method: method as i32,
641 input: Some(input),
642 })),
643 };
644 self.send_client_message(message).await?;
645 Ok(message_id)
646 }
647
648 pub fn rpc_timeout(&self) -> Option<Duration> {
650 self.rpc_timeout
651 }
652
653 pub async fn next_event(&mut self) -> Result<RealtimeEvent, RealtimeError> {
660 loop {
661 let message = self.read_server_message().await?;
662 if let Some(event) = self.event_from_server_message(message).await? {
663 return Ok(event);
664 }
665 }
666 }
667
668 async fn send_connection_init(
669 &mut self,
670 token: &str,
671 identity: &ClientIdentity,
672 ) -> Result<(), RealtimeError> {
673 let init = connection_init_for_token(token, identity);
674 let message = proto::ClientMessage {
675 id: self.next_id(),
676 seq: self.next_seq(),
677 body: Some(proto::client_message::Body::ConnectionInit(init)),
678 };
679
680 self.send_client_message(message).await
681 }
682
683 async fn wait_for_connection_open(&mut self) -> Result<(), RealtimeError> {
684 loop {
685 let message = self.read_server_message().await?;
686 let message_id = message.id;
687 match message.body {
688 Some(proto::server_protocol_message::Body::ConnectionOpen(_)) => return Ok(()),
689 Some(proto::server_protocol_message::Body::ConnectionError(error)) => {
690 log::warn!(
691 target: "inline_sdk::realtime",
692 "connection open rejected reason={}",
693 proto::connection_error::Reason::try_from(error.reason)
694 .map(|reason| reason.as_str_name())
695 .unwrap_or("UNKNOWN")
696 );
697 return Err(connection_error_from_proto(error));
698 }
699 Some(proto::server_protocol_message::Body::Message(message)) => {
700 let _ = self.server_payload_event(message_id, message).await?;
701 }
702 _ => {}
703 }
704 }
705 }
706
707 async fn wait_for_rpc_result(
708 &mut self,
709 message_id: u64,
710 ) -> Result<proto::rpc_result::Result, RealtimeError> {
711 loop {
712 let message = self.read_server_message().await?;
713 match message.body {
714 Some(proto::server_protocol_message::Body::RpcResult(result))
715 if result.req_msg_id == message_id =>
716 {
717 log::trace!(
718 target: "inline_sdk::realtime",
719 "received rpc result msg_id={message_id}"
720 );
721 return result.result.ok_or(RealtimeError::MissingResult);
722 }
723 Some(proto::server_protocol_message::Body::RpcError(error))
724 if error.req_msg_id == message_id =>
725 {
726 log::warn!(
727 target: "inline_sdk::realtime",
728 "received rpc error msg_id={message_id} error={} status={}",
729 rpc_error_code_name(error.error_code),
730 error.code,
731 );
732 return Err(rpc_error_from_proto(error));
733 }
734 Some(proto::server_protocol_message::Body::ConnectionError(error)) => {
735 log::warn!(
736 target: "inline_sdk::realtime",
737 "connection error while waiting for rpc msg_id={message_id} reason={}",
738 proto::connection_error::Reason::try_from(error.reason)
739 .map(|reason| reason.as_str_name())
740 .unwrap_or("UNKNOWN")
741 );
742 return Err(connection_error_from_proto(error));
743 }
744 Some(proto::server_protocol_message::Body::Message(server_message)) => {
745 if let Some(event) = self
746 .server_payload_event(message.id, server_message)
747 .await?
748 {
749 log::trace!(
750 target: "inline_sdk::realtime",
751 "received pushed realtime event while waiting for rpc msg_id={message_id}: {}",
752 realtime_event_kind(&event)
753 );
754 }
755 }
756 _ => {}
757 }
758 }
759 }
760
761 async fn event_from_server_message(
762 &mut self,
763 message: proto::ServerProtocolMessage,
764 ) -> Result<Option<RealtimeEvent>, RealtimeError> {
765 match message.body {
766 Some(proto::server_protocol_message::Body::Message(server_message)) => {
767 self.server_payload_event(message.id, server_message).await
768 }
769 Some(proto::server_protocol_message::Body::Ack(ack)) => {
770 Ok(Some(RealtimeEvent::Ack { msg_id: ack.msg_id }))
771 }
772 Some(proto::server_protocol_message::Body::Pong(pong)) => {
773 Ok(Some(RealtimeEvent::Pong { nonce: pong.nonce }))
774 }
775 Some(proto::server_protocol_message::Body::ConnectionError(error)) => {
776 Err(connection_error_from_proto(error))
777 }
778 Some(proto::server_protocol_message::Body::ConnectionOpen(_))
779 | Some(proto::server_protocol_message::Body::RpcResult(_))
780 | Some(proto::server_protocol_message::Body::RpcError(_))
781 | None => Ok(None),
782 }
783 }
784
785 async fn server_payload_event(
786 &mut self,
787 message_id: u64,
788 message: proto::ServerMessage,
789 ) -> Result<Option<RealtimeEvent>, RealtimeError> {
790 match message.payload {
791 Some(proto::server_message::Payload::Update(payload)) => {
792 self.send_ack(message_id).await?;
793 Ok(Some(RealtimeEvent::Updates(payload.updates)))
794 }
795 None => Ok(None),
796 }
797 }
798
799 async fn send_ack(&mut self, msg_id: u64) -> Result<(), RealtimeError> {
800 let message = proto::ClientMessage {
801 id: self.next_id(),
802 seq: self.next_seq(),
803 body: Some(proto::client_message::Body::Ack(proto::Ack { msg_id })),
804 };
805 self.send_client_message(message).await
806 }
807
808 async fn send_ping(&mut self, nonce: u64) -> Result<(), RealtimeError> {
809 let message = proto::ClientMessage {
810 id: self.next_id(),
811 seq: self.next_seq(),
812 body: Some(proto::client_message::Body::Ping(proto::Ping { nonce })),
813 };
814 self.send_client_message(message).await
815 }
816
817 async fn send_client_message(
818 &mut self,
819 message: proto::ClientMessage,
820 ) -> Result<(), RealtimeError> {
821 let bytes = message.encode_to_vec();
822 self.ws.send(WsMessage::Binary(bytes.into())).await?;
823 Ok(())
824 }
825
826 async fn read_server_message(&mut self) -> Result<proto::ServerProtocolMessage, RealtimeError> {
827 loop {
828 let message = self
829 .ws
830 .next()
831 .await
832 .ok_or(RealtimeError::ConnectionClosed)??;
833 match message {
834 WsMessage::Binary(data) => {
835 return Ok(proto::ServerProtocolMessage::decode(&*data)?);
836 }
837 WsMessage::Text(_) => continue,
838 WsMessage::Close(_) => return Err(RealtimeError::ConnectionClosed),
839 WsMessage::Ping(_) | WsMessage::Pong(_) => continue,
840 _ => continue,
841 }
842 }
843 }
844
845 fn next_seq(&mut self) -> u32 {
846 self.seq = self.seq.wrapping_add(1);
847 self.seq
848 }
849
850 fn next_id(&mut self) -> u64 {
851 self.id_gen.next_id()
852 }
853}
854
855async fn run_realtime_session(
856 mut client: RealtimeClient,
857 mut commands: mpsc::Receiver<SessionCommand>,
858 events: broadcast::Sender<RealtimeEvent>,
859 closed: watch::Sender<bool>,
860) {
861 let heartbeat_interval = client.heartbeat_interval;
862 let heartbeat_timeout = client.heartbeat_timeout;
863 let mut heartbeat =
864 tokio::time::interval(heartbeat_interval.unwrap_or(Duration::from_secs(24 * 60 * 60)));
865 heartbeat.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
866 heartbeat.tick().await;
867 let mut pending_ping: Option<(u64, tokio::time::Instant)> = None;
868 let mut pending =
869 HashMap::<u64, oneshot::Sender<Result<proto::rpc_result::Result, RealtimeError>>>::new();
870
871 loop {
872 pending.retain(|_, response| !response.is_closed());
876 let heartbeat_deadline = pending_ping
877 .map(|(_, deadline)| deadline)
878 .unwrap_or_else(tokio::time::Instant::now);
879 tokio::select! {
880 _ = heartbeat.tick(), if heartbeat_interval.is_some() => {
881 if pending_ping.is_none() {
882 let nonce = client.next_id();
883 if let Err(error) = client.send_ping(nonce).await {
884 log::warn!(
885 target: "inline_sdk::realtime",
886 "failed to send realtime heartbeat: {error}"
887 );
888 break;
889 }
890 pending_ping = Some((nonce, tokio::time::Instant::now() + heartbeat_timeout));
891 }
892 }
893 _ = tokio::time::sleep_until(heartbeat_deadline), if pending_ping.is_some() => {
894 let nonce = pending_ping.map(|(nonce, _)| nonce).unwrap_or_default();
895 log::warn!(
896 target: "inline_sdk::realtime",
897 "realtime heartbeat timed out nonce={nonce} timeout={heartbeat_timeout:?}"
898 );
899 break;
900 }
901 command = commands.recv() => {
902 let Some(command) = command else {
903 break;
904 };
905 match command {
906 SessionCommand::Invoke { method, input, response } => {
907 match client.send_rpc_call(method, input).await {
908 Ok(message_id) => {
909 pending.insert(message_id, response);
910 }
911 Err(error) => {
912 let _ = response.send(Err(error));
913 break;
914 }
915 }
916 }
917 }
918 }
919 message = client.read_server_message() => {
920 let message = match message {
921 Ok(message) => message,
922 Err(error) => {
923 log::warn!(
924 target: "inline_sdk::realtime",
925 "multiplexed realtime session stopped: {error}"
926 );
927 break;
928 }
929 };
930 match message.body {
931 Some(proto::server_protocol_message::Body::RpcResult(result)) => {
932 if let Some(response) = pending.remove(&result.req_msg_id) {
933 let value = result.result.ok_or(RealtimeError::MissingResult);
934 let _ = response.send(value);
935 } else {
936 log::debug!(
937 target: "inline_sdk::realtime",
938 "received result for unknown or timed-out rpc msg_id={}",
939 result.req_msg_id
940 );
941 }
942 }
943 Some(proto::server_protocol_message::Body::RpcError(error)) => {
944 if let Some(response) = pending.remove(&error.req_msg_id) {
945 let _ = response.send(Err(rpc_error_from_proto(error)));
946 } else {
947 log::debug!(
948 target: "inline_sdk::realtime",
949 "received error for unknown or timed-out rpc msg_id={}",
950 error.req_msg_id
951 );
952 }
953 }
954 Some(proto::server_protocol_message::Body::Message(server_message)) => {
955 match client.server_payload_event(message.id, server_message).await {
956 Ok(Some(event)) => {
957 let _ = events.send(event);
958 }
959 Ok(None) => {}
960 Err(error) => {
961 log::warn!(
962 target: "inline_sdk::realtime",
963 "failed to route pushed realtime event: {error}"
964 );
965 break;
966 }
967 }
968 }
969 Some(proto::server_protocol_message::Body::Ack(ack)) => {
970 let _ = events.send(RealtimeEvent::Ack { msg_id: ack.msg_id });
971 }
972 Some(proto::server_protocol_message::Body::Pong(pong)) => {
973 if pending_ping.is_some_and(|(nonce, _)| nonce == pong.nonce) {
974 pending_ping = None;
975 }
976 let _ = events.send(RealtimeEvent::Pong { nonce: pong.nonce });
977 }
978 Some(proto::server_protocol_message::Body::ConnectionError(error)) => {
979 let error = connection_error_from_proto(error);
980 log::warn!(
981 target: "inline_sdk::realtime",
982 "multiplexed realtime session rejected: {error}"
983 );
984 break;
985 }
986 Some(proto::server_protocol_message::Body::ConnectionOpen(_)) | None => {}
987 }
988 }
989 }
990 }
991
992 for (_, response) in pending {
993 let _ = response.send(Err(RealtimeError::ConnectionClosed));
994 }
995 let _ = closed.send(true);
996}
997
998macro_rules! rpc_requests {
999 ($(($input_ty:ident, $method:ident, $input_variant:ident, $result_ty:ident, $result_variant:ident)),+ $(,)?) => {
1000 $(
1001 impl RpcRequest for proto::$input_ty {
1002 type Response = proto::$result_ty;
1003
1004 const METHOD: proto::Method = proto::Method::$method;
1005
1006 fn into_rpc_input(self) -> proto::rpc_call::Input {
1007 proto::rpc_call::Input::$input_variant(self)
1008 }
1009
1010 fn response_from_rpc_result(
1011 result: proto::rpc_result::Result,
1012 ) -> Result<Self::Response, RealtimeError> {
1013 match result {
1014 proto::rpc_result::Result::$result_variant(response) => Ok(response),
1015 other => {
1016 let actual = rpc_result_variant_name(&other);
1017 log::warn!(
1018 target: "inline_sdk::realtime",
1019 "unexpected rpc result method={} expected={} actual={actual}",
1020 Self::METHOD.as_str_name(),
1021 stringify!($result_variant)
1022 );
1023 Err(RealtimeError::UnexpectedResult {
1024 method: Self::METHOD.as_str_name(),
1025 expected: stringify!($result_variant),
1026 actual,
1027 })
1028 }
1029 }
1030 }
1031 }
1032 )+
1033
1034 fn rpc_result_variant_name(result: &proto::rpc_result::Result) -> &'static str {
1035 match result {
1036 $(proto::rpc_result::Result::$result_variant(_) => stringify!($result_variant),)+
1037 }
1038 }
1039 };
1040}
1041
1042rpc_requests!(
1043 (GetMeInput, GetMe, GetMe, GetMeResult, GetMe),
1044 (
1045 GetPeerPhotoInput,
1046 GetPeerPhoto,
1047 GetPeerPhoto,
1048 GetPeerPhotoResult,
1049 GetPeerPhoto
1050 ),
1051 (
1052 DeleteMessagesInput,
1053 DeleteMessages,
1054 DeleteMessages,
1055 DeleteMessagesResult,
1056 DeleteMessages
1057 ),
1058 (
1059 SendMessageInput,
1060 SendMessage,
1061 SendMessage,
1062 SendMessageResult,
1063 SendMessage
1064 ),
1065 (
1066 GetChatHistoryInput,
1067 GetChatHistory,
1068 GetChatHistory,
1069 GetChatHistoryResult,
1070 GetChatHistory
1071 ),
1072 (
1073 AddReactionInput,
1074 AddReaction,
1075 AddReaction,
1076 AddReactionResult,
1077 AddReaction
1078 ),
1079 (
1080 DeleteReactionInput,
1081 DeleteReaction,
1082 DeleteReaction,
1083 DeleteReactionResult,
1084 DeleteReaction
1085 ),
1086 (
1087 EditMessageInput,
1088 EditMessage,
1089 EditMessage,
1090 EditMessageResult,
1091 EditMessage
1092 ),
1093 (
1094 CreateChatInput,
1095 CreateChat,
1096 CreateChat,
1097 CreateChatResult,
1098 CreateChat
1099 ),
1100 (
1101 GetSpaceMembersInput,
1102 GetSpaceMembers,
1103 GetSpaceMembers,
1104 GetSpaceMembersResult,
1105 GetSpaceMembers
1106 ),
1107 (
1108 DeleteChatInput,
1109 DeleteChat,
1110 DeleteChat,
1111 DeleteChatResult,
1112 DeleteChat
1113 ),
1114 (
1115 InviteToSpaceInput,
1116 InviteToSpace,
1117 InviteToSpace,
1118 InviteToSpaceResult,
1119 InviteToSpace
1120 ),
1121 (
1122 GetChatParticipantsInput,
1123 GetChatParticipants,
1124 GetChatParticipants,
1125 GetChatParticipantsResult,
1126 GetChatParticipants
1127 ),
1128 (
1129 AddChatParticipantInput,
1130 AddChatParticipant,
1131 AddChatParticipant,
1132 AddChatParticipantResult,
1133 AddChatParticipant
1134 ),
1135 (
1136 RemoveChatParticipantInput,
1137 RemoveChatParticipant,
1138 RemoveChatParticipant,
1139 RemoveChatParticipantResult,
1140 RemoveChatParticipant
1141 ),
1142 (
1143 TranslateMessagesInput,
1144 TranslateMessages,
1145 TranslateMessages,
1146 TranslateMessagesResult,
1147 TranslateMessages
1148 ),
1149 (GetChatsInput, GetChats, GetChats, GetChatsResult, GetChats),
1150 (
1151 UpdateUserSettingsInput,
1152 UpdateUserSettings,
1153 UpdateUserSettings,
1154 UpdateUserSettingsResult,
1155 UpdateUserSettings
1156 ),
1157 (
1158 GetUserSettingsInput,
1159 GetUserSettings,
1160 GetUserSettings,
1161 GetUserSettingsResult,
1162 GetUserSettings
1163 ),
1164 (
1165 SendComposeActionInput,
1166 SendComposeAction,
1167 SendComposeAction,
1168 SendComposeActionResult,
1169 SendComposeAction
1170 ),
1171 (
1172 CreateBotInput,
1173 CreateBot,
1174 CreateBot,
1175 CreateBotResult,
1176 CreateBot
1177 ),
1178 (
1179 DeleteMemberInput,
1180 DeleteMember,
1181 DeleteMember,
1182 DeleteMemberResult,
1183 DeleteMember
1184 ),
1185 (
1186 MarkAsUnreadInput,
1187 MarkAsUnread,
1188 MarkAsUnread,
1189 MarkAsUnreadResult,
1190 MarkAsUnread
1191 ),
1192 (
1193 GetUpdatesStateInput,
1194 GetUpdatesState,
1195 GetUpdatesState,
1196 GetUpdatesStateResult,
1197 GetUpdatesState
1198 ),
1199 (GetChatInput, GetChat, GetChat, GetChatResult, GetChat),
1200 (
1201 GetUpdatesInput,
1202 GetUpdates,
1203 GetUpdates,
1204 GetUpdatesResult,
1205 GetUpdates
1206 ),
1207 (
1208 UpdateMemberAccessInput,
1209 UpdateMemberAccess,
1210 UpdateMemberAccess,
1211 UpdateMemberAccessResult,
1212 UpdateMemberAccess
1213 ),
1214 (
1215 SearchMessagesInput,
1216 SearchMessages,
1217 SearchMessages,
1218 SearchMessagesResult,
1219 SearchMessages
1220 ),
1221 (
1222 ForwardMessagesInput,
1223 ForwardMessages,
1224 ForwardMessages,
1225 ForwardMessagesResult,
1226 ForwardMessages
1227 ),
1228 (
1229 UpdateChatVisibilityInput,
1230 UpdateChatVisibility,
1231 UpdateChatVisibility,
1232 UpdateChatVisibilityResult,
1233 UpdateChatVisibility
1234 ),
1235 (
1236 PinMessageInput,
1237 PinMessage,
1238 PinMessage,
1239 PinMessageResult,
1240 PinMessage
1241 ),
1242 (
1243 UpdateChatInfoInput,
1244 UpdateChatInfo,
1245 UpdateChatInfo,
1246 UpdateChatInfoResult,
1247 UpdateChatInfo
1248 ),
1249 (ListBotsInput, ListBots, ListBots, ListBotsResult, ListBots),
1250 (
1251 RevealBotTokenInput,
1252 RevealBotToken,
1253 RevealBotToken,
1254 RevealBotTokenResult,
1255 RevealBotToken
1256 ),
1257 (
1258 MoveThreadInput,
1259 MoveThread,
1260 MoveThread,
1261 MoveThreadResult,
1262 MoveThread
1263 ),
1264 (
1265 RotateBotTokenInput,
1266 RotateBotToken,
1267 RotateBotToken,
1268 RotateBotTokenResult,
1269 RotateBotToken
1270 ),
1271 (
1272 UpdateBotProfileInput,
1273 UpdateBotProfile,
1274 UpdateBotProfile,
1275 UpdateBotProfileResult,
1276 UpdateBotProfile
1277 ),
1278 (
1279 GetMessagesInput,
1280 GetMessages,
1281 GetMessages,
1282 GetMessagesResult,
1283 GetMessages
1284 ),
1285 (
1286 UpdateDialogNotificationSettingsInput,
1287 UpdateDialogNotificationSettings,
1288 UpdateDialogNotificationSettings,
1289 UpdateDialogNotificationSettingsResult,
1290 UpdateDialogNotificationSettings
1291 ),
1292 (
1293 ReadMessagesInput,
1294 ReadMessages,
1295 ReadMessages,
1296 ReadMessagesResult,
1297 ReadMessages
1298 ),
1299 (
1300 UpdatePushNotificationDetailsInput,
1301 UpdatePushNotificationDetails,
1302 UpdatePushNotificationDetails,
1303 UpdatePushNotificationDetailsResult,
1304 UpdatePushNotificationDetails
1305 ),
1306 (
1307 CreateSubthreadInput,
1308 CreateSubthread,
1309 CreateSubthread,
1310 CreateSubthreadResult,
1311 CreateSubthread
1312 ),
1313 (
1314 GetBotCommandsInput,
1315 GetBotCommands,
1316 GetBotCommands,
1317 GetBotCommandsResult,
1318 GetBotCommands
1319 ),
1320 (
1321 SetBotCommandsInput,
1322 SetBotCommands,
1323 SetBotCommands,
1324 SetBotCommandsResult,
1325 SetBotCommands
1326 ),
1327 (
1328 GetPeerBotCommandsInput,
1329 GetPeerBotCommands,
1330 GetPeerBotCommands,
1331 GetPeerBotCommandsResult,
1332 GetPeerBotCommands
1333 ),
1334 (
1335 ShowInChatListInput,
1336 ShowInChatList,
1337 ShowInChatList,
1338 ShowInChatListResult,
1339 ShowInChatList
1340 ),
1341 (
1342 ReserveChatIdsInput,
1343 ReserveChatIds,
1344 ReserveChatIds,
1345 ReserveChatIdsResult,
1346 ReserveChatIds
1347 ),
1348 (
1349 InvokeMessageActionInput,
1350 InvokeMessageAction,
1351 InvokeMessageAction,
1352 InvokeMessageActionResult,
1353 InvokeMessageAction
1354 ),
1355 (
1356 AnswerMessageActionInput,
1357 AnswerMessageAction,
1358 AnswerMessageAction,
1359 AnswerMessageActionResult,
1360 AnswerMessageAction
1361 ),
1362 (
1363 RevokeSessionInput,
1364 RevokeSession,
1365 RevokeSession,
1366 RevokeSessionResult,
1367 RevokeSession
1368 ),
1369 (
1370 UpdateDialogOpenInput,
1371 UpdateDialogOpen,
1372 UpdateDialogOpen,
1373 UpdateDialogOpenResult,
1374 UpdateDialogOpen
1375 ),
1376 (
1377 UpdateDialogOrderInput,
1378 UpdateDialogOrder,
1379 UpdateDialogOrder,
1380 UpdateDialogOrderResult,
1381 UpdateDialogOrder
1382 ),
1383 (
1384 ClearChatHistoryInput,
1385 ClearChatHistory,
1386 ClearChatHistory,
1387 ClearChatHistoryResult,
1388 ClearChatHistory
1389 ),
1390 (
1391 DeleteBotInput,
1392 DeleteBot,
1393 DeleteBot,
1394 DeleteBotResult,
1395 DeleteBot
1396 ),
1397 (
1398 DeleteMessageAttachmentInput,
1399 DeleteMessageAttachment,
1400 DeleteMessageAttachment,
1401 DeleteMessageAttachmentResult,
1402 DeleteMessageAttachment
1403 ),
1404 (
1405 SetBotAvatarInput,
1406 SetBotAvatar,
1407 SetBotAvatar,
1408 SetBotAvatarResult,
1409 SetBotAvatar
1410 ),
1411 (
1412 ClearBotAvatarInput,
1413 ClearBotAvatar,
1414 ClearBotAvatar,
1415 ClearBotAvatarResult,
1416 ClearBotAvatar
1417 ),
1418 (
1419 GetBotPresenceInput,
1420 GetBotPresence,
1421 GetBotPresence,
1422 GetBotPresenceResult,
1423 GetBotPresence
1424 ),
1425 (
1426 SetBotPresenceStateInput,
1427 SetBotPresenceState,
1428 SetBotPresenceState,
1429 SetBotPresenceStateResult,
1430 SetBotPresenceState
1431 ),
1432 (
1433 UpdateDialogFollowModeInput,
1434 UpdateDialogFollowMode,
1435 UpdateDialogFollowMode,
1436 UpdateDialogFollowModeResult,
1437 UpdateDialogFollowMode
1438 ),
1439 (
1440 GetSessionsInput,
1441 GetSessions,
1442 GetSessions,
1443 GetSessionsResult,
1444 GetSessions
1445 ),
1446 (
1447 CheckUsernameInput,
1448 CheckUsername,
1449 CheckUsername,
1450 CheckUsernameResult,
1451 CheckUsername
1452 ),
1453 (
1454 ChangeUsernameInput,
1455 ChangeUsername,
1456 ChangeUsername,
1457 ChangeUsernameResult,
1458 ChangeUsername
1459 ),
1460 (
1461 UpdateProfileInput,
1462 UpdateProfile,
1463 UpdateProfile,
1464 UpdateProfileResult,
1465 UpdateProfile
1466 ),
1467 (
1468 GetSpaceUrlPreviewExclusionsInput,
1469 GetSpaceUrlPreviewExclusions,
1470 GetSpaceUrlPreviewExclusions,
1471 GetSpaceUrlPreviewExclusionsResult,
1472 GetSpaceUrlPreviewExclusions
1473 ),
1474 (
1475 AddSpaceUrlPreviewExclusionInput,
1476 AddSpaceUrlPreviewExclusion,
1477 AddSpaceUrlPreviewExclusion,
1478 AddSpaceUrlPreviewExclusionResult,
1479 AddSpaceUrlPreviewExclusion
1480 ),
1481 (
1482 RemoveSpaceUrlPreviewExclusionInput,
1483 RemoveSpaceUrlPreviewExclusion,
1484 RemoveSpaceUrlPreviewExclusion,
1485 RemoveSpaceUrlPreviewExclusionResult,
1486 RemoveSpaceUrlPreviewExclusion
1487 ),
1488 (
1489 GetUserGroupsInput,
1490 GetUserGroups,
1491 GetUserGroups,
1492 GetUserGroupsResult,
1493 GetUserGroups
1494 ),
1495 (
1496 CreateUserGroupInput,
1497 CreateUserGroup,
1498 CreateUserGroup,
1499 CreateUserGroupResult,
1500 CreateUserGroup
1501 ),
1502 (
1503 UpdateUserGroupInput,
1504 UpdateUserGroup,
1505 UpdateUserGroup,
1506 UpdateUserGroupResult,
1507 UpdateUserGroup
1508 ),
1509 (
1510 DeleteUserGroupInput,
1511 DeleteUserGroup,
1512 DeleteUserGroup,
1513 DeleteUserGroupResult,
1514 DeleteUserGroup
1515 ),
1516 (
1517 GetSpaceSettingsInput,
1518 GetSpaceSettings,
1519 GetSpaceSettings,
1520 GetSpaceSettingsResult,
1521 GetSpaceSettings
1522 ),
1523 (
1524 ToggleSpaceGridInput,
1525 ToggleSpaceGrid,
1526 ToggleSpaceGrid,
1527 ToggleSpaceGridResult,
1528 ToggleSpaceGrid
1529 ),
1530 (
1531 GetThreadReferencesInput,
1532 GetThreadReferences,
1533 GetThreadReferences,
1534 GetThreadReferencesResult,
1535 GetThreadReferences
1536 ),
1537 (
1538 GetThreadSubthreadsInput,
1539 GetThreadSubthreads,
1540 GetThreadSubthreads,
1541 GetThreadSubthreadsResult,
1542 GetThreadSubthreads
1543 ),
1544);
1545
1546fn connection_init_for_token(token: &str, identity: &ClientIdentity) -> proto::ConnectionInit {
1547 proto::ConnectionInit {
1548 token: token.to_string(),
1549 build_number: None,
1550 layer: None,
1551 client_version: Some(identity.client_version().to_string()),
1552 os_version: client_info::current_os_version(),
1553 }
1554}
1555
1556fn rpc_error_code_name(error_code: i32) -> String {
1557 proto::rpc_error::Code::try_from(error_code)
1558 .map(|code| code.as_str_name())
1559 .unwrap_or("UNKNOWN")
1560 .to_string()
1561}
1562
1563fn rpc_error_from_proto(error: proto::RpcError) -> RealtimeError {
1564 let error_name = rpc_error_code_name(error.error_code);
1565 let friendly = format_rpc_error(error.error_code, &error_name, &error.message, error.code);
1566 RealtimeError::RpcError {
1567 code: error.code,
1568 error_code: error.error_code,
1569 error_name,
1570 message: error.message,
1571 friendly,
1572 }
1573}
1574
1575fn format_rpc_error(error_code: i32, error_name: &str, message: &str, status_code: i32) -> String {
1576 let label = match error_name {
1577 "UNKNOWN" => "Unknown RPC error",
1578 "BAD_REQUEST" => "Bad request",
1579 "UNAUTHENTICATED" => "Not authenticated",
1580 "RATE_LIMIT" => "Rate limited",
1581 "INTERNAL_ERROR" => "Internal server error",
1582 "PEER_ID_INVALID" => "Invalid peer (chat/user id)",
1583 "MESSAGE_ID_INVALID" => "Invalid message id",
1584 "USER_ID_INVALID" => "Invalid user id",
1585 "USER_ALREADY_MEMBER" => "User already in chat/space",
1586 "SPACE_ID_INVALID" => "Invalid space id",
1587 "CHAT_ID_INVALID" => "Invalid chat id",
1588 "EMAIL_INVALID" => "Invalid email address",
1589 "PHONE_NUMBER_INVALID" => "Invalid phone number",
1590 "SPACE_ADMIN_REQUIRED" => "Space admin required",
1591 "SPACE_OWNER_REQUIRED" => "Space owner required",
1592 "USERNAME_INVALID" => "Invalid username",
1593 "USERNAME_TAKEN" => "Username already taken",
1594 "FIRST_NAME_INVALID" => "Invalid first name",
1595 _ => "Unknown RPC error",
1596 };
1597
1598 let mut formatted = String::from(label);
1599 if error_name == "UNKNOWN" && error_code != 0 {
1600 formatted.push_str(&format!(" {error_code}"));
1601 }
1602 if !message.is_empty() && !message.eq_ignore_ascii_case(label) {
1603 formatted.push_str(": ");
1604 formatted.push_str(message);
1605 }
1606 if status_code != 0 {
1607 formatted.push_str(&format!(" (HTTP {status_code})"));
1608 }
1609 formatted
1610}
1611
1612fn connection_error_from_proto(error: proto::ConnectionError) -> RealtimeError {
1613 let reason = error.reason;
1614 let reason_name = proto::connection_error::Reason::try_from(reason)
1615 .map(|reason| reason.as_str_name())
1616 .unwrap_or("UNKNOWN")
1617 .to_string();
1618 let friendly = format_connection_error(reason, &reason_name);
1619 RealtimeError::ConnectionError {
1620 reason,
1621 reason_name,
1622 friendly,
1623 }
1624}
1625
1626fn format_connection_error(reason: i32, reason_name: &str) -> String {
1627 match reason_name {
1628 "UNAUTHORIZED" => "Realtime connection unauthorized".to_string(),
1629 "INVALID_AUTH" => "Realtime auth token is invalid".to_string(),
1630 "SESSION_REVOKED" => "Realtime session was revoked".to_string(),
1631 "REASON_UNSPECIFIED" => "Realtime connection rejected".to_string(),
1632 _ => format!("Realtime connection rejected: unknown reason {reason}"),
1633 }
1634}
1635
1636fn normalize_realtime_url(url: impl Into<String>) -> Result<Url, RealtimeError> {
1637 let original = url.into();
1638 let normalized = original.trim().to_string();
1639 if normalized.is_empty() {
1640 return Err(RealtimeError::InvalidUrl {
1641 url: original,
1642 message: "realtime URL cannot be empty".to_string(),
1643 });
1644 }
1645
1646 let parsed = Url::parse(&normalized).map_err(|err| RealtimeError::InvalidUrl {
1647 url: normalized.clone(),
1648 message: err.to_string(),
1649 })?;
1650
1651 if !matches!(parsed.scheme(), "ws" | "wss") {
1652 return Err(RealtimeError::InvalidUrl {
1653 url: normalized,
1654 message: "scheme must be ws or wss".to_string(),
1655 });
1656 }
1657
1658 if parsed.host_str().is_none() {
1659 return Err(RealtimeError::InvalidUrl {
1660 url: normalized,
1661 message: "host is required".to_string(),
1662 });
1663 }
1664
1665 if !parsed.username().is_empty() || parsed.password().is_some() {
1666 return Err(RealtimeError::InvalidUrl {
1667 url: normalized,
1668 message: "credentials are not valid in the realtime URL".to_string(),
1669 });
1670 }
1671
1672 if parsed.fragment().is_some() {
1673 return Err(RealtimeError::InvalidUrl {
1674 url: normalized,
1675 message: "fragments are not valid in the realtime URL".to_string(),
1676 });
1677 }
1678
1679 Ok(parsed)
1680}
1681
1682fn realtime_url_for_log(url: &Url) -> String {
1683 let host = url.host_str().unwrap_or("<missing-host>");
1684 let port = url
1685 .port()
1686 .map(|port| format!(":{port}"))
1687 .unwrap_or_default();
1688 format!("{}://{}{}{}", url.scheme(), host, port, url.path())
1689}
1690
1691fn realtime_url_for_debug(raw_url: &str) -> String {
1692 Url::parse(raw_url.trim())
1693 .map(|url| realtime_url_for_log(&url))
1694 .unwrap_or_else(|_| "<invalid>".to_string())
1695}
1696
1697fn realtime_header_value(field: &'static str, value: &str) -> Result<HeaderValue, RealtimeError> {
1698 HeaderValue::from_str(value).map_err(|_| RealtimeError::InvalidHeaderValue { field })
1699}
1700
1701fn realtime_event_kind(event: &RealtimeEvent) -> &'static str {
1702 match event {
1703 RealtimeEvent::Updates(_) => "updates",
1704 RealtimeEvent::Ack { .. } => "ack",
1705 RealtimeEvent::Pong { .. } => "pong",
1706 }
1707}
1708
1709async fn with_optional_timeout<T, E, Fut>(
1710 operation: &'static str,
1711 timeout: Option<Duration>,
1712 future: Fut,
1713) -> Result<T, RealtimeError>
1714where
1715 Fut: Future<Output = Result<T, E>>,
1716 RealtimeError: From<E>,
1717{
1718 match timeout {
1719 Some(timeout) => tokio::time::timeout(timeout, future)
1720 .await
1721 .map_err(|_| RealtimeError::Timeout { operation, timeout })?
1722 .map_err(RealtimeError::from),
1723 None => future.await.map_err(RealtimeError::from),
1724 }
1725}
1726
1727struct IdGenerator {
1728 last_timestamp: u64,
1729 sequence: u32,
1730}
1731
1732impl IdGenerator {
1733 fn new() -> Self {
1734 Self {
1735 last_timestamp: 0,
1736 sequence: 0,
1737 }
1738 }
1739
1740 fn next_id(&mut self) -> u64 {
1741 let timestamp = current_epoch_seconds().saturating_sub(EPOCH_SECONDS);
1742 if timestamp == self.last_timestamp {
1743 self.sequence = self.sequence.wrapping_add(1);
1744 } else {
1745 self.sequence = 0;
1746 self.last_timestamp = timestamp;
1747 }
1748
1749 (timestamp << 32) | self.sequence as u64
1750 }
1751}
1752
1753const EPOCH_SECONDS: u64 = 1_735_689_600; fn current_epoch_seconds() -> u64 {
1756 use std::time::{SystemTime, UNIX_EPOCH};
1757 SystemTime::now()
1758 .duration_since(UNIX_EPOCH)
1759 .unwrap_or_default()
1760 .as_secs()
1761}
1762
1763#[cfg(test)]
1764mod tests {
1765 use super::*;
1766 use tokio::net::{TcpListener, TcpStream};
1767 use tokio_tungstenite::WebSocketStream;
1768 use tokio_tungstenite::tungstenite::handshake::server::{Request, Response};
1769 use tokio_tungstenite::{accept_async, accept_hdr_async};
1770
1771 #[test]
1772 fn rpc_error_code_name_uses_stable_proto_name() {
1773 assert_eq!(
1774 rpc_error_code_name(proto::rpc_error::Code::PeerIdInvalid as i32),
1775 "PEER_ID_INVALID"
1776 );
1777 assert_eq!(rpc_error_code_name(999), "UNKNOWN");
1778 }
1779
1780 #[test]
1781 fn rpc_error_formatter_covers_new_profile_codes() {
1782 assert_eq!(
1783 format_rpc_error(
1784 proto::rpc_error::Code::UsernameInvalid as i32,
1785 "USERNAME_INVALID",
1786 "",
1787 400
1788 ),
1789 "Invalid username (HTTP 400)"
1790 );
1791 assert_eq!(
1792 format_rpc_error(
1793 proto::rpc_error::Code::UsernameTaken as i32,
1794 "USERNAME_TAKEN",
1795 "handle exists",
1796 409
1797 ),
1798 "Username already taken: handle exists (HTTP 409)"
1799 );
1800 assert_eq!(
1801 format_rpc_error(
1802 proto::rpc_error::Code::FirstNameInvalid as i32,
1803 "FIRST_NAME_INVALID",
1804 "",
1805 400
1806 ),
1807 "Invalid first name (HTTP 400)"
1808 );
1809 }
1810
1811 #[test]
1812 fn connection_error_preserves_proto_reason() {
1813 let err = connection_error_from_proto(proto::ConnectionError {
1814 reason: proto::connection_error::Reason::InvalidAuth as i32,
1815 });
1816
1817 match err {
1818 RealtimeError::ConnectionError {
1819 reason,
1820 reason_name,
1821 friendly,
1822 } => {
1823 assert_eq!(reason, 2);
1824 assert_eq!(reason_name, "INVALID_AUTH");
1825 assert_eq!(friendly, "Realtime auth token is invalid");
1826 }
1827 other => panic!("expected connection error, got {other:?}"),
1828 }
1829 }
1830
1831 #[test]
1832 fn unknown_connection_error_reason_is_preserved() {
1833 let err = connection_error_from_proto(proto::ConnectionError { reason: 999 });
1834
1835 match err {
1836 RealtimeError::ConnectionError {
1837 reason,
1838 reason_name,
1839 friendly,
1840 } => {
1841 assert_eq!(reason, 999);
1842 assert_eq!(reason_name, "UNKNOWN");
1843 assert_eq!(friendly, "Realtime connection rejected: unknown reason 999");
1844 }
1845 other => panic!("expected connection error, got {other:?}"),
1846 }
1847 }
1848
1849 #[test]
1850 fn connection_init_uses_custom_client_identity() {
1851 let init =
1852 connection_init_for_token("token-1", &ClientIdentity::new("integration-test", "9.9.9"));
1853
1854 assert_eq!(init.token, "token-1");
1855 assert_eq!(init.client_version.as_deref(), Some("9.9.9"));
1856 assert!(init.build_number.is_none());
1857 assert!(init.layer.is_none());
1858 }
1859
1860 #[tokio::test]
1861 #[allow(clippy::result_large_err)]
1862 async fn realtime_client_connects_and_calls_get_me_against_local_server() {
1863 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1864 let addr = listener.local_addr().unwrap();
1865 let server = tokio::spawn(async move {
1866 let (stream, _) = listener.accept().await.unwrap();
1867 let mut ws = accept_hdr_async(stream, |request: &Request, response: Response| {
1868 assert_eq!(
1869 request
1870 .headers()
1871 .get(client_info::CLIENT_TYPE_HEADER)
1872 .and_then(|value| value.to_str().ok()),
1873 Some("transport-test")
1874 );
1875 assert_eq!(
1876 request
1877 .headers()
1878 .get(client_info::CLIENT_VERSION_HEADER)
1879 .and_then(|value| value.to_str().ok()),
1880 Some("1.2.3")
1881 );
1882 Ok(response)
1883 })
1884 .await
1885 .unwrap();
1886
1887 let init = read_test_client_message(&mut ws).await;
1888 match init.body {
1889 Some(proto::client_message::Body::ConnectionInit(init)) => {
1890 assert_eq!(init.token, "token-1");
1891 assert_eq!(init.client_version.as_deref(), Some("1.2.3"));
1892 assert!(init.os_version.is_some());
1893 }
1894 other => panic!("expected connection init, got {other:?}"),
1895 }
1896 send_test_server_message(
1897 &mut ws,
1898 proto::ServerProtocolMessage {
1899 id: 1,
1900 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
1901 proto::ConnectionOpen {},
1902 )),
1903 },
1904 )
1905 .await;
1906
1907 let rpc = read_test_client_message(&mut ws).await;
1908 match &rpc.body {
1909 Some(proto::client_message::Body::RpcCall(call)) => {
1910 assert_eq!(call.method, proto::Method::GetMe as i32);
1911 assert!(matches!(call.input, Some(proto::rpc_call::Input::GetMe(_))));
1912 }
1913 other => panic!("expected getMe rpc call, got {other:?}"),
1914 }
1915 send_test_server_message(
1916 &mut ws,
1917 proto::ServerProtocolMessage {
1918 id: 2,
1919 body: Some(proto::server_protocol_message::Body::RpcResult(
1920 proto::RpcResult {
1921 req_msg_id: rpc.id,
1922 result: Some(proto::rpc_result::Result::GetMe(proto::GetMeResult {
1923 user: Some(proto::User {
1924 id: 42,
1925 first_name: Some("Ada".to_string()),
1926 ..Default::default()
1927 }),
1928 })),
1929 },
1930 )),
1931 },
1932 )
1933 .await;
1934 });
1935
1936 let mut client = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
1937 .identity(ClientIdentity::new("transport-test", "1.2.3"))
1938 .without_connect_timeout()
1939 .without_rpc_timeout()
1940 .connect()
1941 .await
1942 .unwrap();
1943 let result = client.call(proto::GetMeInput {}).await.unwrap();
1944
1945 assert_eq!(result.user.unwrap().id, 42);
1946 server.await.unwrap();
1947 }
1948
1949 #[tokio::test]
1950 async fn realtime_client_receives_update_events_and_acks_them() {
1951 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1952 let addr = listener.local_addr().unwrap();
1953 let server = tokio::spawn(async move {
1954 let (stream, _) = listener.accept().await.unwrap();
1955 let mut ws = accept_async(stream).await.unwrap();
1956
1957 let init = read_test_client_message(&mut ws).await;
1958 assert!(matches!(
1959 init.body,
1960 Some(proto::client_message::Body::ConnectionInit(_))
1961 ));
1962 send_test_server_message(
1963 &mut ws,
1964 proto::ServerProtocolMessage {
1965 id: 1,
1966 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
1967 proto::ConnectionOpen {},
1968 )),
1969 },
1970 )
1971 .await;
1972 send_test_server_message(&mut ws, test_update_server_message(9, 77)).await;
1973
1974 let ack = read_test_client_message(&mut ws).await;
1975 match ack.body {
1976 Some(proto::client_message::Body::Ack(ack)) => {
1977 assert_eq!(ack.msg_id, 9);
1978 }
1979 other => panic!("expected ack for pushed update, got {other:?}"),
1980 }
1981 });
1982
1983 let mut client = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
1984 .without_connect_timeout()
1985 .without_rpc_timeout()
1986 .connect()
1987 .await
1988 .unwrap();
1989 let event = client.next_event().await.unwrap();
1990
1991 match event {
1992 RealtimeEvent::Updates(updates) => {
1993 assert_eq!(updates.len(), 1);
1994 match updates[0].update.as_ref() {
1995 Some(proto::update::Update::NewMessage(update)) => {
1996 assert_eq!(update.message.as_ref().unwrap().id, 77);
1997 }
1998 other => panic!("expected new message update, got {other:?}"),
1999 }
2000 }
2001 other => panic!("expected updates event, got {other:?}"),
2002 }
2003 server.await.unwrap();
2004 }
2005
2006 #[tokio::test]
2007 async fn realtime_rpc_wait_acks_pushed_updates_before_matching_result() {
2008 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2009 let addr = listener.local_addr().unwrap();
2010 let server = tokio::spawn(async move {
2011 let (stream, _) = listener.accept().await.unwrap();
2012 let mut ws = accept_async(stream).await.unwrap();
2013
2014 let init = read_test_client_message(&mut ws).await;
2015 assert!(matches!(
2016 init.body,
2017 Some(proto::client_message::Body::ConnectionInit(_))
2018 ));
2019 send_test_server_message(
2020 &mut ws,
2021 proto::ServerProtocolMessage {
2022 id: 1,
2023 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
2024 proto::ConnectionOpen {},
2025 )),
2026 },
2027 )
2028 .await;
2029
2030 let rpc = read_test_client_message(&mut ws).await;
2031 send_test_server_message(&mut ws, test_update_server_message(10, 88)).await;
2032
2033 let ack = read_test_client_message(&mut ws).await;
2034 match ack.body {
2035 Some(proto::client_message::Body::Ack(ack)) => {
2036 assert_eq!(ack.msg_id, 10);
2037 }
2038 other => panic!("expected ack while waiting for rpc result, got {other:?}"),
2039 }
2040
2041 send_test_server_message(
2042 &mut ws,
2043 proto::ServerProtocolMessage {
2044 id: 2,
2045 body: Some(proto::server_protocol_message::Body::RpcResult(
2046 proto::RpcResult {
2047 req_msg_id: rpc.id,
2048 result: Some(proto::rpc_result::Result::GetMe(proto::GetMeResult {
2049 user: Some(proto::User {
2050 id: 42,
2051 ..Default::default()
2052 }),
2053 })),
2054 },
2055 )),
2056 },
2057 )
2058 .await;
2059 });
2060
2061 let mut client = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
2062 .without_connect_timeout()
2063 .without_rpc_timeout()
2064 .connect()
2065 .await
2066 .unwrap();
2067 let result = client.call(proto::GetMeInput {}).await.unwrap();
2068
2069 assert_eq!(result.user.unwrap().id, 42);
2070 server.await.unwrap();
2071 }
2072
2073 #[tokio::test]
2074 async fn realtime_session_routes_pushed_updates_during_rpc() {
2075 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2076 let addr = listener.local_addr().unwrap();
2077 let server = tokio::spawn(async move {
2078 let (stream, _) = listener.accept().await.unwrap();
2079 let mut ws = accept_async(stream).await.unwrap();
2080
2081 let init = read_test_client_message(&mut ws).await;
2082 assert!(matches!(
2083 init.body,
2084 Some(proto::client_message::Body::ConnectionInit(_))
2085 ));
2086 send_test_server_message(
2087 &mut ws,
2088 proto::ServerProtocolMessage {
2089 id: 1,
2090 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
2091 proto::ConnectionOpen {},
2092 )),
2093 },
2094 )
2095 .await;
2096
2097 let rpc = read_test_client_message(&mut ws).await;
2098 send_test_server_message(&mut ws, test_update_server_message(10, 88)).await;
2099 let ack = read_test_client_message(&mut ws).await;
2100 assert!(matches!(
2101 ack.body,
2102 Some(proto::client_message::Body::Ack(proto::Ack { msg_id: 10 }))
2103 ));
2104 send_test_server_message(
2105 &mut ws,
2106 proto::ServerProtocolMessage {
2107 id: 2,
2108 body: Some(proto::server_protocol_message::Body::RpcResult(
2109 proto::RpcResult {
2110 req_msg_id: rpc.id,
2111 result: Some(proto::rpc_result::Result::GetMe(proto::GetMeResult {
2112 user: Some(proto::User {
2113 id: 42,
2114 ..Default::default()
2115 }),
2116 })),
2117 },
2118 )),
2119 },
2120 )
2121 .await;
2122 });
2123
2124 let session = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
2125 .without_connect_timeout()
2126 .without_rpc_timeout()
2127 .connect_session()
2128 .await
2129 .unwrap();
2130 let mut events = session.subscribe();
2131 let (result, event) = tokio::join!(session.call(proto::GetMeInput {}), events.recv());
2132
2133 assert_eq!(result.unwrap().user.unwrap().id, 42);
2134 match event.unwrap() {
2135 RealtimeEvent::Updates(updates) => match updates[0].update.as_ref() {
2136 Some(proto::update::Update::NewMessage(update)) => {
2137 assert_eq!(update.message.as_ref().unwrap().id, 88);
2138 }
2139 other => panic!("expected new message update, got {other:?}"),
2140 },
2141 other => panic!("expected updates event, got {other:?}"),
2142 }
2143 server.await.unwrap();
2144 }
2145
2146 #[tokio::test]
2147 async fn realtime_session_matches_concurrent_rpc_results_by_request_id() {
2148 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2149 let addr = listener.local_addr().unwrap();
2150 let server = tokio::spawn(async move {
2151 let (stream, _) = listener.accept().await.unwrap();
2152 let mut ws = accept_async(stream).await.unwrap();
2153 let _ = read_test_client_message(&mut ws).await;
2154 send_test_server_message(
2155 &mut ws,
2156 proto::ServerProtocolMessage {
2157 id: 1,
2158 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
2159 proto::ConnectionOpen {},
2160 )),
2161 },
2162 )
2163 .await;
2164
2165 let first = read_test_client_message(&mut ws).await;
2166 let second = read_test_client_message(&mut ws).await;
2167 for (id, request, user_id) in [(2, &second, 2), (3, &first, 1)] {
2168 send_test_server_message(
2169 &mut ws,
2170 proto::ServerProtocolMessage {
2171 id,
2172 body: Some(proto::server_protocol_message::Body::RpcResult(
2173 proto::RpcResult {
2174 req_msg_id: request.id,
2175 result: Some(proto::rpc_result::Result::GetMe(
2176 proto::GetMeResult {
2177 user: Some(proto::User {
2178 id: user_id,
2179 ..Default::default()
2180 }),
2181 },
2182 )),
2183 },
2184 )),
2185 },
2186 )
2187 .await;
2188 }
2189 });
2190
2191 let session = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
2192 .without_connect_timeout()
2193 .without_rpc_timeout()
2194 .connect_session()
2195 .await
2196 .unwrap();
2197 let (first, second) = tokio::join!(
2198 session.call(proto::GetMeInput {}),
2199 session.call(proto::GetMeInput {})
2200 );
2201
2202 assert_eq!(first.unwrap().user.unwrap().id, 1);
2203 assert_eq!(second.unwrap().user.unwrap().id, 2);
2204 server.await.unwrap();
2205 }
2206
2207 #[tokio::test]
2208 async fn realtime_session_bounds_in_flight_rpcs() {
2209 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2210 let addr = listener.local_addr().unwrap();
2211 let server = tokio::spawn(async move {
2212 let (stream, _) = listener.accept().await.unwrap();
2213 let mut ws = accept_async(stream).await.unwrap();
2214 let _ = read_test_client_message(&mut ws).await;
2215 send_test_server_message(
2216 &mut ws,
2217 proto::ServerProtocolMessage {
2218 id: 1,
2219 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
2220 proto::ConnectionOpen {},
2221 )),
2222 },
2223 )
2224 .await;
2225
2226 let first = read_test_client_message(&mut ws).await;
2227 assert!(
2228 tokio::time::timeout(Duration::from_millis(50), read_test_client_message(&mut ws),)
2229 .await
2230 .is_err(),
2231 "session issued a second RPC above its configured in-flight limit"
2232 );
2233 send_test_server_message(&mut ws, get_me_result_message(2, first.id, 1)).await;
2234 let second = read_test_client_message(&mut ws).await;
2235 send_test_server_message(&mut ws, get_me_result_message(3, second.id, 2)).await;
2236 });
2237
2238 let session = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
2239 .without_connect_timeout()
2240 .without_rpc_timeout()
2241 .max_in_flight_rpcs(1)
2242 .connect_session()
2243 .await
2244 .unwrap();
2245 let (first, second) = tokio::join!(
2246 session.call(proto::GetMeInput {}),
2247 session.call(proto::GetMeInput {})
2248 );
2249
2250 assert_eq!(first.unwrap().user.unwrap().id, 1);
2251 assert_eq!(second.unwrap().user.unwrap().id, 2);
2252 server.await.unwrap();
2253 }
2254
2255 #[tokio::test]
2256 async fn realtime_session_drops_timed_out_rpc_and_remains_usable() {
2257 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2258 let addr = listener.local_addr().unwrap();
2259 let server = tokio::spawn(async move {
2260 let (stream, _) = listener.accept().await.unwrap();
2261 let mut ws = accept_async(stream).await.unwrap();
2262 let _ = read_test_client_message(&mut ws).await;
2263 send_test_server_message(
2264 &mut ws,
2265 proto::ServerProtocolMessage {
2266 id: 1,
2267 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
2268 proto::ConnectionOpen {},
2269 )),
2270 },
2271 )
2272 .await;
2273
2274 let timed_out = read_test_client_message(&mut ws).await;
2275 tokio::time::sleep(Duration::from_millis(40)).await;
2276 send_test_server_message(
2277 &mut ws,
2278 proto::ServerProtocolMessage {
2279 id: 2,
2280 body: Some(proto::server_protocol_message::Body::RpcResult(
2281 proto::RpcResult {
2282 req_msg_id: timed_out.id,
2283 result: Some(proto::rpc_result::Result::GetMe(proto::GetMeResult {
2284 user: Some(proto::User {
2285 id: 1,
2286 ..Default::default()
2287 }),
2288 })),
2289 },
2290 )),
2291 },
2292 )
2293 .await;
2294
2295 let recovered = read_test_client_message(&mut ws).await;
2296 send_test_server_message(
2297 &mut ws,
2298 proto::ServerProtocolMessage {
2299 id: 3,
2300 body: Some(proto::server_protocol_message::Body::RpcResult(
2301 proto::RpcResult {
2302 req_msg_id: recovered.id,
2303 result: Some(proto::rpc_result::Result::GetMe(proto::GetMeResult {
2304 user: Some(proto::User {
2305 id: 2,
2306 ..Default::default()
2307 }),
2308 })),
2309 },
2310 )),
2311 },
2312 )
2313 .await;
2314 });
2315
2316 let session = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
2317 .without_connect_timeout()
2318 .rpc_timeout(Duration::from_millis(25))
2319 .connect_session()
2320 .await
2321 .unwrap();
2322
2323 assert!(matches!(
2324 session.call(proto::GetMeInput {}).await,
2325 Err(RealtimeError::Timeout {
2326 operation: "rpc",
2327 ..
2328 })
2329 ));
2330 let recovered = session.call(proto::GetMeInput {}).await.unwrap();
2331 assert_eq!(recovered.user.unwrap().id, 2);
2332 server.await.unwrap();
2333 }
2334
2335 #[tokio::test]
2336 async fn realtime_session_closes_when_heartbeat_pong_is_missing() {
2337 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2338 let addr = listener.local_addr().unwrap();
2339 let server = tokio::spawn(async move {
2340 let (stream, _) = listener.accept().await.unwrap();
2341 let mut ws = accept_async(stream).await.unwrap();
2342 let _ = read_test_client_message(&mut ws).await;
2343 send_test_server_message(
2344 &mut ws,
2345 proto::ServerProtocolMessage {
2346 id: 1,
2347 body: Some(proto::server_protocol_message::Body::ConnectionOpen(
2348 proto::ConnectionOpen {},
2349 )),
2350 },
2351 )
2352 .await;
2353 let ping = read_test_client_message(&mut ws).await;
2354 assert!(matches!(
2355 ping.body,
2356 Some(proto::client_message::Body::Ping(_))
2357 ));
2358 tokio::time::sleep(Duration::from_millis(100)).await;
2359 });
2360
2361 let session = RealtimeClient::builder(format!("ws://{addr}/realtime"), "token-1")
2362 .without_connect_timeout()
2363 .heartbeat(Duration::from_millis(20), Duration::from_millis(30))
2364 .connect_session()
2365 .await
2366 .unwrap();
2367 let mut events = session.subscribe();
2368
2369 let error = tokio::time::timeout(Duration::from_millis(150), events.recv())
2370 .await
2371 .expect("heartbeat deadline should close the session")
2372 .unwrap_err();
2373 assert!(matches!(error, RealtimeError::ConnectionClosed));
2374 assert!(session.is_closed());
2375 server.await.unwrap();
2376 }
2377
2378 #[test]
2379 fn realtime_url_validation_accepts_ws_urls() {
2380 let url = normalize_realtime_url(" wss://api.inline.chat/realtime?edge=iad ").unwrap();
2381 assert_eq!(url.as_str(), "wss://api.inline.chat/realtime?edge=iad");
2382 }
2383
2384 #[test]
2385 fn realtime_url_validation_rejects_invalid_urls() {
2386 let err = normalize_realtime_url("inline.test").unwrap_err();
2387 match err {
2388 RealtimeError::InvalidUrl { url, message } => {
2389 assert_eq!(url, "inline.test");
2390 assert!(message.contains("relative URL without a base"));
2391 }
2392 other => panic!("expected invalid URL, got {other:?}"),
2393 }
2394
2395 let err = normalize_realtime_url("https://api.inline.chat/realtime").unwrap_err();
2396 match err {
2397 RealtimeError::InvalidUrl { message, .. } => {
2398 assert_eq!(message, "scheme must be ws or wss");
2399 }
2400 other => panic!("expected invalid URL, got {other:?}"),
2401 }
2402
2403 let err = normalize_realtime_url("wss://user:secret@api.inline.chat/realtime").unwrap_err();
2404 match &err {
2405 RealtimeError::InvalidUrl { message, .. } => {
2406 assert_eq!(message, "credentials are not valid in the realtime URL");
2407 assert!(!err.to_string().contains("secret"));
2408 }
2409 other => panic!("expected invalid URL, got {other:?}"),
2410 }
2411
2412 let err = normalize_realtime_url("wss://api.inline.chat/realtime#token").unwrap_err();
2413 match err {
2414 RealtimeError::InvalidUrl { message, .. } => {
2415 assert_eq!(message, "fragments are not valid in the realtime URL");
2416 }
2417 other => panic!("expected invalid URL, got {other:?}"),
2418 }
2419 }
2420
2421 #[test]
2422 fn realtime_invalid_url_debug_redacts_unsafe_url_parts() {
2423 let err = normalize_realtime_url(
2424 "wss://user:url-secret@api.inline.chat/realtime?token=query-secret#frag",
2425 )
2426 .unwrap_err();
2427 let debug = format!("{err:?}");
2428
2429 assert!(debug.contains("wss://api.inline.chat/realtime"));
2430 assert!(!debug.contains("url-secret"));
2431 assert!(!debug.contains("query-secret"));
2432 }
2433
2434 #[test]
2435 fn realtime_url_for_log_omits_query_and_fragment() {
2436 let url = Url::parse("wss://api.inline.chat/realtime?token=secret#frag").unwrap();
2437 assert_eq!(realtime_url_for_log(&url), "wss://api.inline.chat/realtime");
2438 }
2439
2440 #[test]
2441 fn realtime_header_value_rejects_invalid_values() {
2442 let err = realtime_header_value("user_agent", "bad\nvalue").unwrap_err();
2443 match err {
2444 RealtimeError::InvalidHeaderValue { field } => {
2445 assert_eq!(field, "user_agent");
2446 }
2447 other => panic!("expected invalid header value, got {other:?}"),
2448 }
2449 }
2450
2451 #[test]
2452 fn realtime_builder_keeps_custom_identity_and_default_timeouts() {
2453 let builder = RealtimeClient::builder("wss://api.inline.chat/realtime", "token-1")
2454 .identity(ClientIdentity::new("agent", "0.2.0"));
2455
2456 assert_eq!(builder.url, "wss://api.inline.chat/realtime");
2457 assert_eq!(builder.token, "token-1");
2458 assert_eq!(builder.identity.client_type(), "agent");
2459 assert_eq!(builder.identity.client_version(), "0.2.0");
2460 assert_eq!(builder.connect_timeout, Some(DEFAULT_CONNECT_TIMEOUT));
2461 assert_eq!(builder.rpc_timeout, Some(DEFAULT_RPC_TIMEOUT));
2462 assert_eq!(builder.heartbeat_interval, Some(DEFAULT_HEARTBEAT_INTERVAL));
2463 assert_eq!(builder.heartbeat_timeout, DEFAULT_HEARTBEAT_TIMEOUT);
2464 assert_eq!(
2465 builder.max_in_flight_rpcs,
2466 DEFAULT_SESSION_MAX_IN_FLIGHT_RPCS
2467 );
2468 }
2469
2470 #[test]
2471 fn realtime_builder_debug_redacts_token() {
2472 let builder = RealtimeClient::builder(
2473 "wss://user:url-secret@api.inline.chat/realtime?token=query-secret",
2474 "secret-token-1",
2475 );
2476 let debug = format!("{builder:?}");
2477
2478 assert!(debug.contains("RealtimeClientBuilder"));
2479 assert!(debug.contains("wss://api.inline.chat/realtime"));
2480 assert!(debug.contains("<redacted>"));
2481 assert!(!debug.contains("secret-token-1"));
2482 assert!(!debug.contains("url-secret"));
2483 assert!(!debug.contains("query-secret"));
2484 }
2485
2486 #[test]
2487 fn realtime_builder_can_override_or_disable_timeouts() {
2488 let builder = RealtimeClient::builder("wss://api.inline.chat/realtime", "token-1")
2489 .connect_timeout(Duration::from_secs(5))
2490 .rpc_timeout(Duration::from_secs(10))
2491 .max_in_flight_rpcs(7);
2492
2493 assert_eq!(builder.connect_timeout, Some(Duration::from_secs(5)));
2494 assert_eq!(builder.rpc_timeout, Some(Duration::from_secs(10)));
2495 assert_eq!(builder.max_in_flight_rpcs, 7);
2496
2497 let builder = builder
2498 .without_connect_timeout()
2499 .without_rpc_timeout()
2500 .without_heartbeat();
2501 assert_eq!(builder.connect_timeout, None);
2502 assert_eq!(builder.rpc_timeout, None);
2503 assert_eq!(builder.heartbeat_interval, None);
2504 }
2505
2506 #[test]
2507 fn typed_rpc_request_maps_method_input_and_result() {
2508 assert_eq!(
2509 <proto::GetChatsInput as RpcRequest>::METHOD,
2510 proto::Method::GetChats
2511 );
2512
2513 let input = proto::GetChatsInput {};
2514 match input.into_rpc_input() {
2515 proto::rpc_call::Input::GetChats(_) => {}
2516 other => panic!("expected GetChats input, got {other:?}"),
2517 }
2518
2519 let response = <proto::GetChatsInput as RpcRequest>::response_from_rpc_result(
2520 proto::rpc_result::Result::GetChats(proto::GetChatsResult::default()),
2521 )
2522 .unwrap();
2523 assert!(response.dialogs.is_empty());
2524 }
2525
2526 #[test]
2527 fn typed_rpc_request_rejects_unexpected_result_variant() {
2528 let err = <proto::GetChatsInput as RpcRequest>::response_from_rpc_result(
2529 proto::rpc_result::Result::GetMe(proto::GetMeResult::default()),
2530 )
2531 .unwrap_err();
2532
2533 match err {
2534 RealtimeError::UnexpectedResult {
2535 method,
2536 expected,
2537 actual,
2538 } => {
2539 assert_eq!(method, "GET_CHATS");
2540 assert_eq!(expected, "GetChats");
2541 assert_eq!(actual, "GetMe");
2542 }
2543 other => panic!("expected unexpected result error, got {other:?}"),
2544 }
2545 }
2546
2547 #[tokio::test]
2548 async fn optional_timeout_reports_elapsed_operation() {
2549 let err = with_optional_timeout("test", Some(Duration::from_millis(1)), async {
2550 tokio::time::sleep(Duration::from_millis(20)).await;
2551 Ok::<(), RealtimeError>(())
2552 })
2553 .await
2554 .unwrap_err();
2555
2556 match err {
2557 RealtimeError::Timeout { operation, timeout } => {
2558 assert_eq!(operation, "test");
2559 assert_eq!(timeout, Duration::from_millis(1));
2560 }
2561 other => panic!("expected timeout, got {other:?}"),
2562 }
2563 }
2564
2565 #[tokio::test]
2566 async fn optional_timeout_can_be_disabled() {
2567 with_optional_timeout("test", None, async { Ok::<_, RealtimeError>("done") })
2568 .await
2569 .unwrap();
2570 }
2571
2572 async fn read_test_client_message(ws: &mut WebSocketStream<TcpStream>) -> proto::ClientMessage {
2573 loop {
2574 match ws.next().await.unwrap().unwrap() {
2575 WsMessage::Binary(bytes) => {
2576 return proto::ClientMessage::decode(&*bytes).unwrap();
2577 }
2578 WsMessage::Ping(_) | WsMessage::Pong(_) | WsMessage::Text(_) => continue,
2579 other => panic!("unexpected websocket message: {other:?}"),
2580 }
2581 }
2582 }
2583
2584 async fn send_test_server_message(
2585 ws: &mut WebSocketStream<TcpStream>,
2586 message: proto::ServerProtocolMessage,
2587 ) {
2588 ws.send(WsMessage::Binary(message.encode_to_vec().into()))
2589 .await
2590 .unwrap();
2591 }
2592
2593 fn get_me_result_message(
2594 id: u64,
2595 request_id: u64,
2596 user_id: i64,
2597 ) -> proto::ServerProtocolMessage {
2598 proto::ServerProtocolMessage {
2599 id,
2600 body: Some(proto::server_protocol_message::Body::RpcResult(
2601 proto::RpcResult {
2602 req_msg_id: request_id,
2603 result: Some(proto::rpc_result::Result::GetMe(proto::GetMeResult {
2604 user: Some(proto::User {
2605 id: user_id,
2606 ..Default::default()
2607 }),
2608 })),
2609 },
2610 )),
2611 }
2612 }
2613
2614 fn test_update_server_message(
2615 message_id: u64,
2616 inline_message_id: i64,
2617 ) -> proto::ServerProtocolMessage {
2618 proto::ServerProtocolMessage {
2619 id: message_id,
2620 body: Some(proto::server_protocol_message::Body::Message(
2621 proto::ServerMessage {
2622 payload: Some(proto::server_message::Payload::Update(
2623 proto::UpdatesPayload {
2624 updates: vec![proto::Update {
2625 seq: Some(1),
2626 date: Some(1),
2627 update: Some(proto::update::Update::NewMessage(
2628 proto::UpdateNewMessage {
2629 message: Some(proto::Message {
2630 id: inline_message_id,
2631 from_id: 42,
2632 peer_id: Some(proto::Peer {
2633 r#type: Some(proto::peer::Type::Chat(
2634 proto::PeerChat { chat_id: 7 },
2635 )),
2636 }),
2637 chat_id: 7,
2638 message: Some("hello".to_owned()),
2639 date: 1,
2640 ..Default::default()
2641 }),
2642 },
2643 )),
2644 }],
2645 },
2646 )),
2647 },
2648 )),
2649 }
2650 }
2651}