Skip to main content

inline_client/
runtime.rs

1//! Async client facade and runner skeleton.
2//!
3//! This module establishes the TDLib-style host shape: cheap cloneable handle,
4//! single owner runner, bounded command queue, and broadcast committed events.
5//! Transport, sync, store, and transaction managers plug into this runner
6//! instead of being spread across bridges or agents.
7
8use std::{sync::Arc, time::Duration};
9
10use tokio::sync::{broadcast, mpsc, oneshot, watch};
11use tokio::task::JoinHandle;
12
13use crate::{
14    AuthStartRequest, AuthStartResult, AuthVerifyRequest, AuthVerifyResult, BackendError,
15    BackendResult, ChatParticipantsPage, ChatParticipantsRequest, ClientBackend,
16    ClientErrorCategory, ClientEvent, ClientFailure, ClientStatus, ClientStatusSnapshot,
17    ConnectRequest, CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat,
18    DeleteMessageRequest, DialogsPage, DialogsRequest, EditMessageRequest, HistoryPage,
19    HistoryRequest, InMemoryBackend, MessageMutation, OperationOutcome, ReactRequest, ReadRequest,
20    SendTextOutcome, SendTextRequest, TypingRequest, UploadRequest,
21};
22
23/// Default bounded command queue capacity.
24pub const DEFAULT_COMMAND_QUEUE_CAPACITY: usize = 128;
25
26/// Default broadcast event queue capacity.
27pub const DEFAULT_EVENT_QUEUE_CAPACITY: usize = 1024;
28const EVENT_RETRY_DELAY: Duration = Duration::from_secs(5);
29const EVENT_RATE_LIMIT_RETRY_DELAY: Duration = Duration::from_secs(30);
30
31/// Errors returned by the async client handle before an operation reaches the backend.
32#[derive(Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum ClientCommandError {
35    /// The client runner has stopped accepting commands.
36    #[error("inline client runner is closed")]
37    Closed,
38
39    /// The client runner dropped a command response before completing it.
40    #[error("inline client runner dropped command response")]
41    ResponseDropped,
42}
43
44/// Errors returned by typed client operations.
45#[derive(Debug, thiserror::Error)]
46#[non_exhaustive]
47pub enum ClientRequestError {
48    /// The operation could not be delivered to the runner.
49    #[error(transparent)]
50    Command(#[from] ClientCommandError),
51
52    /// The backend rejected or failed the operation.
53    #[error(transparent)]
54    Backend(#[from] BackendError),
55}
56
57/// Builder for an [`InlineClient`] runtime.
58#[derive(Clone, Debug)]
59pub struct InlineClientBuilder {
60    command_queue_capacity: usize,
61    event_queue_capacity: usize,
62    initial_status: ClientStatus,
63    backend: Arc<dyn ClientBackend>,
64}
65
66impl InlineClientBuilder {
67    /// Sets the backend used by the runner.
68    pub fn backend(mut self, backend: impl ClientBackend) -> Self {
69        self.backend = Arc::new(backend);
70        self
71    }
72
73    /// Sets the shared backend used by the runner.
74    pub fn shared_backend(mut self, backend: Arc<dyn ClientBackend>) -> Self {
75        self.backend = backend;
76        self
77    }
78
79    /// Sets the bounded command queue capacity.
80    pub fn command_queue_capacity(mut self, capacity: usize) -> Self {
81        self.command_queue_capacity = capacity.max(1);
82        self
83    }
84
85    /// Sets the broadcast event queue capacity.
86    pub fn event_queue_capacity(mut self, capacity: usize) -> Self {
87        self.event_queue_capacity = capacity.max(1);
88        self
89    }
90
91    /// Sets the initial client status.
92    pub fn initial_status(mut self, status: ClientStatus) -> Self {
93        self.initial_status = status;
94        self
95    }
96
97    /// Builds a client handle and runner pair.
98    pub fn build(self) -> InlineClientRuntime {
99        let (command_tx, command_rx) = mpsc::channel(self.command_queue_capacity);
100        let (event_tx, _) = broadcast::channel(self.event_queue_capacity);
101        let (status_tx, status_rx) = watch::channel(self.initial_status);
102        let (failure_tx, failure_rx) = watch::channel(None);
103        let (signal_tx, signal_rx) = mpsc::channel(self.command_queue_capacity);
104
105        let client = InlineClient {
106            command_tx,
107            event_tx: event_tx.clone(),
108            status_rx,
109            failure_rx,
110        };
111        let runner = ClientRunner {
112            command_rx,
113            event_tx,
114            status_tx,
115            failure_tx,
116            status: self.initial_status,
117            failure: None,
118            backend: self.backend,
119            signal_tx,
120            signal_rx,
121            event_task: None,
122        };
123
124        InlineClientRuntime { client, runner }
125    }
126}
127
128impl Default for InlineClientBuilder {
129    fn default() -> Self {
130        Self {
131            command_queue_capacity: DEFAULT_COMMAND_QUEUE_CAPACITY,
132            event_queue_capacity: DEFAULT_EVENT_QUEUE_CAPACITY,
133            initial_status: ClientStatus::Disconnected,
134            backend: Arc::new(InMemoryBackend::default()),
135        }
136    }
137}
138
139/// Built client runtime before the runner is hosted.
140#[derive(Debug)]
141pub struct InlineClientRuntime {
142    /// Cloneable client handle.
143    pub client: InlineClient,
144    /// Single-owner client runner.
145    pub runner: ClientRunner,
146}
147
148impl InlineClientRuntime {
149    /// Splits the runtime into handle and runner.
150    pub fn split(self) -> (InlineClient, ClientRunner) {
151        (self.client, self.runner)
152    }
153
154    /// Spawns the runner on the current Tokio runtime and returns the handle.
155    pub fn spawn(self) -> InlineClient {
156        let (client, runner) = self.split();
157        tokio::spawn(runner.run());
158        client
159    }
160}
161
162/// Cheap cloneable handle for apps, bridges, agents, and tests.
163#[derive(Clone, Debug)]
164pub struct InlineClient {
165    command_tx: mpsc::Sender<ClientCommand>,
166    event_tx: broadcast::Sender<ClientEvent>,
167    status_rx: watch::Receiver<ClientStatus>,
168    failure_rx: watch::Receiver<Option<ClientFailure>>,
169}
170
171impl InlineClient {
172    /// Creates a default client runtime builder.
173    pub fn builder() -> InlineClientBuilder {
174        InlineClientBuilder::default()
175    }
176
177    /// Returns the latest observed client status.
178    pub fn status(&self) -> ClientStatus {
179        *self.status_rx.borrow()
180    }
181
182    /// Returns the latest observed status snapshot.
183    pub fn status_snapshot(&self) -> ClientStatusSnapshot {
184        ClientStatusSnapshot {
185            status: *self.status_rx.borrow(),
186            failure: self.failure_rx.borrow().clone(),
187        }
188    }
189
190    /// Subscribes to committed client events.
191    pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
192        self.event_tx.subscribe()
193    }
194
195    /// Sends an Inline login code.
196    pub async fn auth_start(
197        &self,
198        request: AuthStartRequest,
199    ) -> Result<AuthStartResult, ClientRequestError> {
200        match self.request(ClientRequest::AuthStart(request)).await? {
201            ClientResponse::AuthStart(result) => Ok(result),
202            other => unreachable!("auth_start returned {other:?}"),
203        }
204    }
205
206    /// Verifies an Inline login code and persists the resulting session.
207    pub async fn auth_verify(
208        &self,
209        request: AuthVerifyRequest,
210    ) -> Result<AuthVerifyResult, ClientRequestError> {
211        match self.request(ClientRequest::AuthVerify(request)).await? {
212            ClientResponse::AuthVerify(result) => Ok(result),
213            other => unreachable!("auth_verify returned {other:?}"),
214        }
215    }
216
217    /// Resumes a previously stored session, if available.
218    pub async fn resume_session(&self) -> Result<ClientStatusSnapshot, ClientRequestError> {
219        match self.request(ClientRequest::Resume).await? {
220            ClientResponse::Status(status) => Ok(status),
221            other => unreachable!("resume_session returned {other:?}"),
222        }
223    }
224
225    /// Connects or reconnects the client.
226    pub async fn connect(
227        &self,
228        request: ConnectRequest,
229    ) -> Result<ClientStatusSnapshot, ClientRequestError> {
230        match self.request(ClientRequest::Connect(request)).await? {
231            ClientResponse::Status(status) => Ok(status),
232            other => unreachable!("connect returned {other:?}"),
233        }
234    }
235
236    /// Logs out the current account.
237    pub async fn logout(&self) -> Result<(), ClientRequestError> {
238        match self.request(ClientRequest::Logout).await? {
239            ClientResponse::Empty => Ok(()),
240            other => unreachable!("logout returned {other:?}"),
241        }
242    }
243
244    /// Lists dialogs.
245    pub async fn dialogs(
246        &self,
247        request: DialogsRequest,
248    ) -> Result<DialogsPage, ClientRequestError> {
249        match self.request(ClientRequest::Dialogs(request)).await? {
250            ClientResponse::Dialogs(page) => Ok(page),
251            other => unreachable!("dialogs returned {other:?}"),
252        }
253    }
254
255    /// Fetches chat history.
256    pub async fn history(
257        &self,
258        request: HistoryRequest,
259    ) -> Result<HistoryPage, ClientRequestError> {
260        match self.request(ClientRequest::History(request)).await? {
261            ClientResponse::History(page) => Ok(page),
262            other => unreachable!("history returned {other:?}"),
263        }
264    }
265
266    /// Fetches chat participants.
267    pub async fn chat_participants(
268        &self,
269        request: ChatParticipantsRequest,
270    ) -> Result<ChatParticipantsPage, ClientRequestError> {
271        match self
272            .request(ClientRequest::ChatParticipants(request))
273            .await?
274        {
275            ClientResponse::ChatParticipants(page) => Ok(page),
276            other => unreachable!("chat_participants returned {other:?}"),
277        }
278    }
279
280    /// Creates or opens a direct message chat.
281    pub async fn create_dm(
282        &self,
283        request: CreateDmRequest,
284    ) -> Result<CreatedChat, ClientRequestError> {
285        match self.request(ClientRequest::CreateDm(request)).await? {
286            ClientResponse::CreatedChat(chat) => Ok(chat),
287            other => unreachable!("create_dm returned {other:?}"),
288        }
289    }
290
291    /// Creates a regular Inline thread chat.
292    pub async fn create_thread(
293        &self,
294        request: CreateThreadRequest,
295    ) -> Result<CreatedChat, ClientRequestError> {
296        match self.request(ClientRequest::CreateThread(request)).await? {
297            ClientResponse::CreatedChat(chat) => Ok(chat),
298            other => unreachable!("create_thread returned {other:?}"),
299        }
300    }
301
302    /// Creates a child/reply Inline thread chat.
303    pub async fn create_reply_thread(
304        &self,
305        request: CreateReplyThreadRequest,
306    ) -> Result<CreatedChat, ClientRequestError> {
307        match self
308            .request(ClientRequest::CreateReplyThread(request))
309            .await?
310        {
311            ClientResponse::CreatedChat(chat) => Ok(chat),
312            other => unreachable!("create_reply_thread returned {other:?}"),
313        }
314    }
315
316    /// Sends a text message.
317    pub async fn send_text(
318        &self,
319        request: SendTextRequest,
320    ) -> Result<MessageMutation, ClientRequestError> {
321        match self.request(ClientRequest::SendText(request)).await? {
322            ClientResponse::Message(mutation) => Ok(mutation),
323            other => unreachable!("send_text returned {other:?}"),
324        }
325    }
326
327    /// Uploads and sends a media message.
328    pub async fn send_media(
329        &self,
330        request: UploadRequest,
331        bytes: Vec<u8>,
332    ) -> Result<MessageMutation, ClientRequestError> {
333        match self
334            .request(ClientRequest::SendMedia { request, bytes })
335            .await?
336        {
337            ClientResponse::Message(mutation) => Ok(mutation),
338            other => unreachable!("send_media returned {other:?}"),
339        }
340    }
341
342    /// Edits a text message.
343    pub async fn edit_message(
344        &self,
345        request: EditMessageRequest,
346    ) -> Result<(), ClientRequestError> {
347        match self.request(ClientRequest::EditMessage(request)).await? {
348            ClientResponse::Empty => Ok(()),
349            other => unreachable!("edit_message returned {other:?}"),
350        }
351    }
352
353    /// Deletes or unsends a message.
354    pub async fn delete_message(
355        &self,
356        request: DeleteMessageRequest,
357    ) -> Result<(), ClientRequestError> {
358        match self.request(ClientRequest::DeleteMessage(request)).await? {
359            ClientResponse::Empty => Ok(()),
360            other => unreachable!("delete_message returned {other:?}"),
361        }
362    }
363
364    /// Adds or removes a reaction.
365    pub async fn react(&self, request: ReactRequest) -> Result<(), ClientRequestError> {
366        match self.request(ClientRequest::React(request)).await? {
367            ClientResponse::Empty => Ok(()),
368            other => unreachable!("react returned {other:?}"),
369        }
370    }
371
372    /// Marks messages read.
373    pub async fn read(&self, request: ReadRequest) -> Result<(), ClientRequestError> {
374        match self.request(ClientRequest::Read(request)).await? {
375            ClientResponse::Empty => Ok(()),
376            other => unreachable!("read returned {other:?}"),
377        }
378    }
379
380    /// Sends a typing state.
381    pub async fn typing(&self, request: TypingRequest) -> Result<(), ClientRequestError> {
382        match self.request(ClientRequest::Typing(request)).await? {
383            ClientResponse::Empty => Ok(()),
384            other => unreachable!("typing returned {other:?}"),
385        }
386    }
387
388    /// Updates status through the runner.
389    ///
390    /// This is public so early hosts and tests can exercise the event/status
391    /// pipeline before the real transport manager is wired in. Future transport
392    /// code should call the same internal command path.
393    pub async fn set_status(
394        &self,
395        status: ClientStatus,
396        failure: Option<ClientFailure>,
397    ) -> Result<(), ClientCommandError> {
398        let (respond_to, response) = oneshot::channel();
399        self.command_tx
400            .send(ClientCommand::SetStatus {
401                status,
402                failure,
403                respond_to,
404            })
405            .await
406            .map_err(|_| ClientCommandError::Closed)?;
407        response
408            .await
409            .map_err(|_| ClientCommandError::ResponseDropped)
410    }
411
412    /// Requests runner shutdown.
413    pub async fn shutdown(&self) -> Result<(), ClientCommandError> {
414        let (respond_to, response) = oneshot::channel();
415        self.command_tx
416            .send(ClientCommand::Shutdown { respond_to })
417            .await
418            .map_err(|_| ClientCommandError::Closed)?;
419        response
420            .await
421            .map_err(|_| ClientCommandError::ResponseDropped)
422    }
423
424    async fn request(&self, request: ClientRequest) -> Result<ClientResponse, ClientRequestError> {
425        let (respond_to, response) = oneshot::channel();
426        self.command_tx
427            .send(ClientCommand::Request {
428                request: Box::new(request),
429                respond_to,
430            })
431            .await
432            .map_err(|_| ClientCommandError::Closed)?;
433        response
434            .await
435            .map_err(|_| ClientCommandError::ResponseDropped)?
436            .map_err(ClientRequestError::Backend)
437    }
438}
439
440/// Single-owner async client runner.
441#[derive(Debug)]
442pub struct ClientRunner {
443    command_rx: mpsc::Receiver<ClientCommand>,
444    event_tx: broadcast::Sender<ClientEvent>,
445    status_tx: watch::Sender<ClientStatus>,
446    failure_tx: watch::Sender<Option<ClientFailure>>,
447    status: ClientStatus,
448    failure: Option<ClientFailure>,
449    backend: Arc<dyn ClientBackend>,
450    signal_tx: mpsc::Sender<RunnerSignal>,
451    signal_rx: mpsc::Receiver<RunnerSignal>,
452    event_task: Option<JoinHandle<()>>,
453}
454
455impl ClientRunner {
456    /// Runs the client command loop until shutdown or all handles are dropped.
457    pub async fn run(mut self) {
458        log::debug!("inline client runner started");
459        loop {
460            tokio::select! {
461                command = self.command_rx.recv() => {
462                    if !self.handle_optional_command(command).await {
463                        break;
464                    }
465                }
466                signal = self.signal_rx.recv() => {
467                    if let Some(signal) = signal {
468                        self.handle_signal(signal);
469                    }
470                }
471            }
472        }
473        self.stop_event_receiver();
474        log::debug!("inline client runner stopped");
475    }
476
477    async fn handle_optional_command(&mut self, command: Option<ClientCommand>) -> bool {
478        let Some(command) = command else {
479            return false;
480        };
481        self.handle_command(command).await
482    }
483
484    async fn handle_command(&mut self, command: ClientCommand) -> bool {
485        match command {
486            ClientCommand::Request {
487                request,
488                respond_to,
489            } => {
490                let response = self.handle_request(*request).await;
491                let _ = respond_to.send(response);
492                true
493            }
494            ClientCommand::SetStatus {
495                status,
496                failure,
497                respond_to,
498            } => {
499                self.update_status(status, failure);
500                let _ = respond_to.send(());
501                true
502            }
503            ClientCommand::Shutdown { respond_to } => {
504                log::debug!("inline client runner shutdown requested");
505                self.update_status(ClientStatus::ShuttingDown, None);
506                let _ = respond_to.send(());
507                false
508            }
509        }
510    }
511
512    const fn should_receive_events(&self) -> bool {
513        matches!(
514            self.status,
515            ClientStatus::Connected | ClientStatus::Reconnecting
516        )
517    }
518
519    fn emit_received_events(&self, events: Vec<ClientEvent>) {
520        for event in events {
521            let _ = self.event_tx.send(event);
522        }
523    }
524
525    fn handle_signal(&mut self, signal: RunnerSignal) {
526        match signal {
527            RunnerSignal::Events(events) => {
528                if self.status == ClientStatus::Reconnecting {
529                    self.update_status(ClientStatus::Connected, None);
530                }
531                self.emit_received_events(events);
532            }
533            RunnerSignal::ReceiveError(error) => {
534                self.update_status_for_backend_error(&error);
535            }
536        }
537    }
538
539    async fn handle_request(&mut self, request: ClientRequest) -> BackendResult<ClientResponse> {
540        log::debug!("handling inline client request: {}", request.kind());
541        match request {
542            ClientRequest::AuthStart(auth) => self
543                .backend
544                .auth_start(auth)
545                .await
546                .map(ClientResponse::AuthStart),
547            ClientRequest::AuthVerify(auth) => {
548                let result = self.backend.auth_verify(auth).await?;
549                let status = result.status.clone();
550                self.update_status(status.status, status.failure.clone());
551                Ok(ClientResponse::AuthVerify(result))
552            }
553            ClientRequest::Resume => match self.backend.resume_session().await {
554                Ok(status) => {
555                    self.update_status(status.status, status.failure.clone());
556                    Ok(ClientResponse::Status(status))
557                }
558                Err(error) => {
559                    self.update_status_for_backend_error(&error);
560                    Err(error)
561                }
562            },
563            ClientRequest::Connect(connect) => {
564                let status = self.backend.connect(connect).await?;
565                self.update_status(status.status, status.failure.clone());
566                Ok(ClientResponse::Status(status))
567            }
568            ClientRequest::Logout => {
569                self.backend.logout().await?;
570                self.update_status(ClientStatus::Disconnected, None);
571                Ok(ClientResponse::Empty)
572            }
573            ClientRequest::Dialogs(dialogs) => self
574                .backend
575                .dialogs(dialogs)
576                .await
577                .map(ClientResponse::Dialogs),
578            ClientRequest::History(history) => self
579                .backend
580                .history(history)
581                .await
582                .map(ClientResponse::History),
583            ClientRequest::ChatParticipants(participants) => self
584                .backend
585                .chat_participants(participants)
586                .await
587                .map(ClientResponse::ChatParticipants),
588            ClientRequest::CreateDm(request) => self
589                .backend
590                .create_dm(request)
591                .await
592                .map(ClientResponse::CreatedChat),
593            ClientRequest::CreateThread(request) => self
594                .backend
595                .create_thread(request)
596                .await
597                .map(ClientResponse::CreatedChat),
598            ClientRequest::CreateReplyThread(request) => self
599                .backend
600                .create_reply_thread(request)
601                .await
602                .map(ClientResponse::CreatedChat),
603            ClientRequest::SendText(send) => {
604                let outcome = self.backend.send_text(send).await?;
605                Ok(ClientResponse::Message(self.emit_send_outcome(outcome)))
606            }
607            ClientRequest::SendMedia { request, bytes } => {
608                let outcome = self.backend.send_media(request, bytes).await?;
609                Ok(ClientResponse::Message(self.emit_send_outcome(outcome)))
610            }
611            ClientRequest::EditMessage(edit) => {
612                let outcome = self.backend.edit_message(edit).await?;
613                self.emit_operation_events(outcome);
614                Ok(ClientResponse::Empty)
615            }
616            ClientRequest::DeleteMessage(delete) => {
617                let outcome = self.backend.delete_message(delete).await?;
618                self.emit_operation_events(outcome);
619                Ok(ClientResponse::Empty)
620            }
621            ClientRequest::React(react) => {
622                let outcome = self.backend.react(react).await?;
623                self.emit_operation_events(outcome);
624                Ok(ClientResponse::Empty)
625            }
626            ClientRequest::Read(read) => {
627                let outcome = self.backend.read(read).await?;
628                self.emit_operation_events(outcome);
629                Ok(ClientResponse::Empty)
630            }
631            ClientRequest::Typing(typing) => {
632                let outcome = self.backend.typing(typing).await?;
633                self.emit_operation_events(outcome);
634                Ok(ClientResponse::Empty)
635            }
636        }
637    }
638
639    fn emit_operation_events(&self, outcome: OperationOutcome) {
640        for event in outcome.events {
641            let _ = self.event_tx.send(event);
642        }
643    }
644
645    fn emit_send_outcome(&self, outcome: SendTextOutcome) -> MessageMutation {
646        let transaction = outcome.transaction_event();
647        let message_id = outcome.message_id;
648        let chat_id = outcome.chat_id;
649        let message = outcome.message;
650        let mutation = outcome.mutation;
651        let _ = self
652            .event_tx
653            .send(ClientEvent::TransactionChanged(transaction));
654        if let Some(message_id) = message_id {
655            let _ = self.event_tx.send(ClientEvent::MessageUpserted {
656                chat_id,
657                message_id,
658            });
659        }
660        if let Some(message) = message {
661            let _ = self.event_tx.send(ClientEvent::MessageStored { message });
662        }
663        mutation
664    }
665
666    fn update_status_for_backend_error(&mut self, error: &BackendError) {
667        let status = match error.category {
668            ClientErrorCategory::AuthRequired => ClientStatus::AuthRequired,
669            ClientErrorCategory::AuthExpired => ClientStatus::AuthExpired,
670            ClientErrorCategory::Network
671            | ClientErrorCategory::Timeout
672            | ClientErrorCategory::RateLimited => ClientStatus::Reconnecting,
673            _ => ClientStatus::Disconnected,
674        };
675        self.update_status(
676            status,
677            Some(ClientFailure::new(error.category, error.message.clone())),
678        );
679    }
680
681    fn update_status(&mut self, status: ClientStatus, failure: Option<ClientFailure>) {
682        log::debug!("inline client status changed: {status:?}");
683        self.status = status;
684        self.failure = failure.clone();
685        let _ = self.status_tx.send(status);
686        let _ = self.failure_tx.send(failure.clone());
687        let _ = self
688            .event_tx
689            .send(ClientEvent::StatusChanged { status, failure });
690        self.sync_event_receiver_to_status();
691    }
692
693    fn sync_event_receiver_to_status(&mut self) {
694        if self.should_receive_events() {
695            self.start_event_receiver();
696        } else {
697            self.stop_event_receiver();
698        }
699    }
700
701    fn start_event_receiver(&mut self) {
702        if self.event_task.is_some() {
703            return;
704        }
705        let backend = self.backend.clone();
706        let signal_tx = self.signal_tx.clone();
707        self.event_task = Some(tokio::spawn(run_backend_event_receiver(backend, signal_tx)));
708    }
709
710    fn stop_event_receiver(&mut self) {
711        if let Some(task) = self.event_task.take() {
712            task.abort();
713        }
714    }
715}
716
717async fn run_backend_event_receiver(
718    backend: Arc<dyn ClientBackend>,
719    signal_tx: mpsc::Sender<RunnerSignal>,
720) {
721    loop {
722        match backend.receive_events().await {
723            Ok(events) => {
724                if signal_tx.send(RunnerSignal::Events(events)).await.is_err() {
725                    break;
726                }
727            }
728            Err(error) => {
729                let retry = should_retry_event_receive(&error);
730                let delay = event_retry_delay(&error);
731                if signal_tx
732                    .send(RunnerSignal::ReceiveError(error))
733                    .await
734                    .is_err()
735                {
736                    break;
737                }
738                if !retry {
739                    break;
740                }
741                tokio::time::sleep(delay).await;
742            }
743        }
744    }
745}
746
747enum ClientCommand {
748    Request {
749        request: Box<ClientRequest>,
750        respond_to: oneshot::Sender<BackendResult<ClientResponse>>,
751    },
752    SetStatus {
753        status: ClientStatus,
754        failure: Option<ClientFailure>,
755        respond_to: oneshot::Sender<()>,
756    },
757    Shutdown {
758        respond_to: oneshot::Sender<()>,
759    },
760}
761
762#[derive(Debug)]
763enum RunnerSignal {
764    Events(Vec<ClientEvent>),
765    ReceiveError(BackendError),
766}
767
768impl std::fmt::Debug for ClientCommand {
769    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
770        match self {
771            Self::Request { request, .. } => f
772                .debug_struct("Request")
773                .field("request", request)
774                .finish_non_exhaustive(),
775            Self::SetStatus {
776                status, failure, ..
777            } => f
778                .debug_struct("SetStatus")
779                .field("status", status)
780                .field("failure", failure)
781                .finish_non_exhaustive(),
782            Self::Shutdown { .. } => f.debug_struct("Shutdown").finish_non_exhaustive(),
783        }
784    }
785}
786
787#[derive(Debug)]
788enum ClientRequest {
789    AuthStart(AuthStartRequest),
790    AuthVerify(AuthVerifyRequest),
791    Resume,
792    Connect(ConnectRequest),
793    Logout,
794    Dialogs(DialogsRequest),
795    History(HistoryRequest),
796    ChatParticipants(ChatParticipantsRequest),
797    CreateDm(CreateDmRequest),
798    CreateThread(CreateThreadRequest),
799    CreateReplyThread(CreateReplyThreadRequest),
800    SendText(SendTextRequest),
801    SendMedia {
802        request: UploadRequest,
803        bytes: Vec<u8>,
804    },
805    EditMessage(EditMessageRequest),
806    DeleteMessage(DeleteMessageRequest),
807    React(ReactRequest),
808    Read(ReadRequest),
809    Typing(TypingRequest),
810}
811
812impl ClientRequest {
813    const fn kind(&self) -> &'static str {
814        match self {
815            Self::AuthStart(_) => "auth_start",
816            Self::AuthVerify(_) => "auth_verify",
817            Self::Resume => "resume",
818            Self::Connect(_) => "connect",
819            Self::Logout => "logout",
820            Self::Dialogs(_) => "dialogs",
821            Self::History(_) => "history",
822            Self::ChatParticipants(_) => "chat_participants",
823            Self::CreateDm(_) => "create_dm",
824            Self::CreateThread(_) => "create_thread",
825            Self::CreateReplyThread(_) => "create_reply_thread",
826            Self::SendText(_) => "send_text",
827            Self::SendMedia { .. } => "send_media",
828            Self::EditMessage(_) => "edit_message",
829            Self::DeleteMessage(_) => "delete_message",
830            Self::React(_) => "react",
831            Self::Read(_) => "read",
832            Self::Typing(_) => "typing",
833        }
834    }
835}
836
837const fn event_retry_delay(error: &BackendError) -> Duration {
838    match error.category {
839        ClientErrorCategory::RateLimited => EVENT_RATE_LIMIT_RETRY_DELAY,
840        _ => EVENT_RETRY_DELAY,
841    }
842}
843
844const fn should_retry_event_receive(error: &BackendError) -> bool {
845    matches!(
846        error.category,
847        ClientErrorCategory::Network
848            | ClientErrorCategory::Timeout
849            | ClientErrorCategory::RateLimited
850    )
851}
852
853#[derive(Debug)]
854enum ClientResponse {
855    Empty,
856    Status(ClientStatusSnapshot),
857    AuthStart(AuthStartResult),
858    AuthVerify(AuthVerifyResult),
859    Dialogs(DialogsPage),
860    History(HistoryPage),
861    ChatParticipants(ChatParticipantsPage),
862    CreatedChat(CreatedChat),
863    Message(MessageMutation),
864}
865
866#[cfg(test)]
867mod tests {
868    use crate::{
869        AuthContactKind, AuthCredential, AuthToken, DialogRecord, HistoryRequest, InlineId,
870        MediaKind, MessageContent, PeerRef,
871    };
872
873    use super::*;
874
875    fn token_connect() -> ConnectRequest {
876        ConnectRequest::new(AuthCredential::AccessToken {
877            token: AuthToken::try_new("token").unwrap(),
878        })
879    }
880
881    fn auth_verify_request() -> AuthVerifyRequest {
882        AuthVerifyRequest {
883            contact: "mo@example.com".to_owned(),
884            kind: AuthContactKind::Email,
885            code: "123456".to_owned(),
886            challenge_token: None,
887            device_name: Some("inline-client test".to_owned()),
888            account_namespace: None,
889        }
890    }
891
892    #[tokio::test]
893    async fn status_snapshot_returns_current_status() {
894        let client = InlineClient::builder()
895            .initial_status(ClientStatus::Connected)
896            .build()
897            .spawn();
898
899        let status = client.status_snapshot();
900
901        assert_eq!(status.status, ClientStatus::Connected);
902        assert_eq!(status.failure, None);
903    }
904
905    #[tokio::test]
906    async fn set_status_emits_lossless_event() {
907        let client = InlineClient::builder().build().spawn();
908        let mut events = client.subscribe();
909
910        client
911            .set_status(
912                ClientStatus::AuthExpired,
913                Some(ClientFailure::new(
914                    ClientErrorCategory::AuthExpired,
915                    "relogin required",
916                )),
917            )
918            .await
919            .unwrap();
920
921        let event = events.recv().await.unwrap();
922        assert_eq!(event.reliability(), crate::EventReliability::Lossless);
923        assert!(matches!(
924            event,
925            ClientEvent::StatusChanged {
926                status: ClientStatus::AuthExpired,
927                ..
928            }
929        ));
930        assert_eq!(client.status(), ClientStatus::AuthExpired);
931        assert_eq!(
932            client.status_snapshot().failure.unwrap().category,
933            ClientErrorCategory::AuthExpired
934        );
935    }
936
937    #[tokio::test]
938    async fn connect_updates_status_and_emits_event() {
939        let client = InlineClient::builder().build().spawn();
940        let mut events = client.subscribe();
941
942        let status = client.connect(token_connect()).await.unwrap();
943
944        assert_eq!(status.status, ClientStatus::Connected);
945        assert_eq!(client.status(), ClientStatus::Connected);
946
947        let event = events.recv().await.unwrap();
948        assert!(matches!(
949            event,
950            ClientEvent::StatusChanged {
951                status: ClientStatus::Connected,
952                ..
953            }
954        ));
955    }
956
957    #[tokio::test]
958    async fn runner_emits_backend_pushed_events_while_connected() {
959        let backend = InMemoryBackend::new();
960        let client = InlineClient::builder()
961            .backend(backend.clone())
962            .build()
963            .spawn();
964        let mut events = client.subscribe();
965
966        client.connect(token_connect()).await.unwrap();
967        backend.push_event_batch(vec![ClientEvent::MessageDeleted {
968            chat_id: InlineId::new(7),
969            message_id: InlineId::new(99),
970        }]);
971
972        let event = recv_until_event(&mut events, |event| {
973            matches!(
974                event,
975                ClientEvent::MessageDeleted {
976                    chat_id,
977                    message_id,
978                } if *chat_id == InlineId::new(7) && *message_id == InlineId::new(99)
979            )
980        })
981        .await;
982        assert_eq!(
983            event,
984            ClientEvent::MessageDeleted {
985                chat_id: InlineId::new(7),
986                message_id: InlineId::new(99),
987            }
988        );
989    }
990
991    #[tokio::test]
992    async fn runner_marks_reconnecting_when_event_receive_is_rate_limited() {
993        let backend = InMemoryBackend::new();
994        let client = InlineClient::builder()
995            .backend(backend.clone())
996            .build()
997            .spawn();
998        let mut events = client.subscribe();
999
1000        client.connect(token_connect()).await.unwrap();
1001        backend.push_event_error(BackendError::new(
1002            ClientErrorCategory::RateLimited,
1003            "rate limited",
1004        ));
1005
1006        let event = recv_until_event(&mut events, |event| {
1007            matches!(
1008                event,
1009                ClientEvent::StatusChanged {
1010                    status: ClientStatus::Reconnecting,
1011                    ..
1012                }
1013            )
1014        })
1015        .await;
1016        assert!(matches!(
1017            event,
1018            ClientEvent::StatusChanged {
1019                status: ClientStatus::Reconnecting,
1020                ..
1021            }
1022        ));
1023        assert_eq!(client.status(), ClientStatus::Reconnecting);
1024    }
1025
1026    #[tokio::test]
1027    async fn auth_start_and_verify_flow_through_backend() {
1028        let client = InlineClient::builder().build().spawn();
1029        let mut events = client.subscribe();
1030
1031        let started = client
1032            .auth_start(AuthStartRequest {
1033                contact: "mo@example.com".to_owned(),
1034                kind: AuthContactKind::Email,
1035                device_name: Some("inline-client test".to_owned()),
1036            })
1037            .await
1038            .unwrap();
1039        assert!(started.existing_user);
1040        assert!(!started.needs_invite_code);
1041
1042        let verified = client.auth_verify(auth_verify_request()).await.unwrap();
1043        assert_eq!(verified.user_id, InlineId::new(1));
1044        assert_eq!(verified.account_namespace, "1");
1045        assert_eq!(verified.status.status, ClientStatus::Connected);
1046        assert_eq!(client.status(), ClientStatus::Connected);
1047
1048        let event = events.recv().await.unwrap();
1049        assert!(matches!(
1050            event,
1051            ClientEvent::StatusChanged {
1052                status: ClientStatus::Connected,
1053                ..
1054            }
1055        ));
1056    }
1057
1058    #[tokio::test]
1059    async fn resume_without_session_reports_auth_required() {
1060        let client = InlineClient::builder().build().spawn();
1061        let mut events = client.subscribe();
1062
1063        let status = client.resume_session().await.unwrap();
1064
1065        assert_eq!(status.status, ClientStatus::AuthRequired);
1066        assert_eq!(client.status(), ClientStatus::AuthRequired);
1067
1068        let event = events.recv().await.unwrap();
1069        assert!(matches!(
1070            event,
1071            ClientEvent::StatusChanged {
1072                status: ClientStatus::AuthRequired,
1073                ..
1074            }
1075        ));
1076    }
1077
1078    #[tokio::test]
1079    async fn dialogs_and_history_flow_through_backend() {
1080        let backend = InMemoryBackend::new();
1081        backend.upsert_dialog(DialogRecord {
1082            chat_id: InlineId::new(7),
1083            peer_user_id: None,
1084            title: Some("general".to_owned()),
1085            last_message_id: None,
1086            synced_through_message_id: None,
1087            unread_count: Some(0),
1088        });
1089        backend.insert_message(crate::MessageRecord {
1090            chat_id: InlineId::new(7),
1091            message_id: InlineId::new(1),
1092            sender_id: InlineId::new(2),
1093            timestamp: 1,
1094            is_outgoing: false,
1095            content: MessageContent::Text {
1096                text: "hello".to_owned(),
1097            },
1098            reply_to_message_id: None,
1099            transaction: None,
1100        });
1101        let client = InlineClient::builder().backend(backend).build().spawn();
1102        client.connect(token_connect()).await.unwrap();
1103
1104        let dialogs = client.dialogs(DialogsRequest::default()).await.unwrap();
1105        assert_eq!(dialogs.dialogs.len(), 1);
1106        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
1107
1108        let history = client
1109            .history(HistoryRequest {
1110                chat_id: InlineId::new(7),
1111                limit: Some(10),
1112                before_message_id: None,
1113                after_message_id: None,
1114            })
1115            .await
1116            .unwrap();
1117        assert_eq!(history.messages.len(), 1);
1118        assert_eq!(history.messages[0].message_id, InlineId::new(1));
1119    }
1120
1121    #[tokio::test]
1122    async fn send_text_returns_mutation_and_emits_events() {
1123        let client = InlineClient::builder().build().spawn();
1124        client.connect(token_connect()).await.unwrap();
1125        let mut events = client.subscribe();
1126
1127        let mutation = client
1128            .send_text(SendTextRequest::new(
1129                PeerRef::Chat {
1130                    chat_id: InlineId::new(7),
1131                },
1132                "hello",
1133            ))
1134            .await
1135            .unwrap();
1136
1137        assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1138        assert_eq!(
1139            mutation.transaction.final_message_id,
1140            Some(InlineId::new(1))
1141        );
1142
1143        let events = [
1144            events.recv().await.unwrap(),
1145            events.recv().await.unwrap(),
1146            events.recv().await.unwrap(),
1147        ];
1148        assert!(
1149            events
1150                .iter()
1151                .any(|event| matches!(event, ClientEvent::TransactionChanged(_)))
1152        );
1153        assert!(events.iter().any(|event| matches!(
1154            event,
1155            ClientEvent::MessageUpserted {
1156                chat_id: InlineId(7),
1157                message_id: InlineId(1)
1158            }
1159        )));
1160        assert!(events.iter().any(|event| matches!(
1161            event,
1162            ClientEvent::MessageStored { message }
1163                if message.chat_id == InlineId::new(7)
1164                    && message.message_id == InlineId::new(1)
1165        )));
1166    }
1167
1168    #[tokio::test]
1169    async fn edit_message_emits_stored_upsert_event() {
1170        let backend = InMemoryBackend::new();
1171        backend.insert_message(crate::MessageRecord {
1172            chat_id: InlineId::new(7),
1173            message_id: InlineId::new(1),
1174            sender_id: InlineId::new(2),
1175            timestamp: 1,
1176            is_outgoing: false,
1177            content: MessageContent::Text {
1178                text: "old".to_owned(),
1179            },
1180            reply_to_message_id: None,
1181            transaction: None,
1182        });
1183        let client = InlineClient::builder().backend(backend).build().spawn();
1184        client.connect(token_connect()).await.unwrap();
1185        let mut events = client.subscribe();
1186
1187        client
1188            .edit_message(EditMessageRequest {
1189                chat_id: InlineId::new(7),
1190                message_id: InlineId::new(1),
1191                text: "edited".to_owned(),
1192                external_id: None,
1193            })
1194            .await
1195            .unwrap();
1196
1197        match events.recv().await.unwrap() {
1198            ClientEvent::MessageStored { message } => {
1199                assert_eq!(message.chat_id, InlineId::new(7));
1200                assert_eq!(message.message_id, InlineId::new(1));
1201                assert_eq!(
1202                    message.content,
1203                    MessageContent::Text {
1204                        text: "edited".to_owned()
1205                    }
1206                );
1207            }
1208            other => panic!("unexpected event: {other:?}"),
1209        }
1210    }
1211
1212    #[tokio::test]
1213    async fn send_media_emits_message_events() {
1214        let client = InlineClient::builder().build().spawn();
1215        client.connect(token_connect()).await.unwrap();
1216        let mut events = client.subscribe();
1217
1218        let mutation = client
1219            .send_media(
1220                UploadRequest {
1221                    peer: PeerRef::Chat {
1222                        chat_id: InlineId::new(7),
1223                    },
1224                    kind: MediaKind::Photo,
1225                    file_name: Some("image.png".to_owned()),
1226                    mime_type: Some("image/png".to_owned()),
1227                    size_bytes: Some(4),
1228                    caption: Some("caption".to_owned()),
1229                    width: Some(10),
1230                    height: Some(10),
1231                    duration_ms: None,
1232                    external_id: None,
1233                    random_id: None,
1234                    reply_to_message_id: None,
1235                },
1236                vec![1, 2, 3, 4],
1237            )
1238            .await
1239            .unwrap();
1240
1241        assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1242
1243        let _transaction = events.recv().await.unwrap();
1244        let _upsert = events.recv().await.unwrap();
1245        match events.recv().await.unwrap() {
1246            ClientEvent::MessageStored { message } => {
1247                assert_eq!(message.chat_id, InlineId::new(7));
1248                assert_eq!(message.message_id, InlineId::new(1));
1249                match message.content {
1250                    MessageContent::Media {
1251                        kind,
1252                        file_name,
1253                        caption,
1254                        ..
1255                    } => {
1256                        assert_eq!(kind, MediaKind::Photo);
1257                        assert_eq!(file_name.as_deref(), Some("image.png"));
1258                        assert_eq!(caption.as_deref(), Some("caption"));
1259                    }
1260                    other => panic!("unexpected content: {other:?}"),
1261                }
1262            }
1263            other => panic!("unexpected event: {other:?}"),
1264        }
1265    }
1266
1267    #[tokio::test]
1268    async fn shutdown_stops_runner() {
1269        let client = InlineClient::builder().build().spawn();
1270
1271        client.shutdown().await.unwrap();
1272        let err = client
1273            .dialogs(DialogsRequest::default())
1274            .await
1275            .expect_err("runner should reject commands after shutdown");
1276
1277        assert!(matches!(
1278            err,
1279            ClientRequestError::Command(
1280                ClientCommandError::Closed | ClientCommandError::ResponseDropped
1281            )
1282        ));
1283    }
1284
1285    async fn recv_until_event(
1286        events: &mut broadcast::Receiver<ClientEvent>,
1287        matches: impl Fn(&ClientEvent) -> bool,
1288    ) -> ClientEvent {
1289        tokio::time::timeout(Duration::from_secs(3), async {
1290            loop {
1291                let event = events.recv().await.unwrap();
1292                if matches(&event) {
1293                    return event;
1294                }
1295            }
1296        })
1297        .await
1298        .expect("expected matching client event")
1299    }
1300}