Skip to main content

inline_client/
runtime.rs

1//! Async client facade and runner.
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::{
9    sync::{
10        Arc, Mutex as StdMutex,
11        atomic::{AtomicBool, Ordering},
12    },
13    time::{Duration, SystemTime, UNIX_EPOCH},
14};
15
16use tokio::sync::{broadcast, mpsc, oneshot, watch};
17use tokio::task::{JoinHandle, JoinSet};
18
19use crate::backend::retry_after_seconds_from_message;
20use crate::{
21    AccountStateSnapshot, AddChatParticipantRequest, AuthStartRequest, AuthStartResult,
22    AuthVerifyRequest, AuthVerifyResult, BackendError, BackendResult, ChatParticipantsPage,
23    ChatParticipantsRequest, ChatStateSnapshot, ClientBackend, ClientErrorCategory, ClientEvent,
24    ClientEventDelivery, ClientFailure, ClientStatus, ClientStatusSnapshot, ConnectRequest,
25    CreateDmRequest, CreateReplyThreadRequest, CreateThreadRequest, CreatedChat, DeleteChatRequest,
26    DeleteMessageRequest, DialogsPage, DialogsRequest, EditMessageRequest, HistoryPage,
27    HistoryRequest, InMemoryBackend, InlineId, MessageMutation, OperationOutcome, ReactRequest,
28    ReadRequest, RemoveChatParticipantRequest, SendTextOutcome, SendTextRequest,
29    SetMarkedUnreadRequest, TypingRequest, UpdateChatInfoRequest, UpdateDialogNotificationsRequest,
30    UploadRequest,
31};
32
33/// Default bounded command queue capacity.
34pub const DEFAULT_COMMAND_QUEUE_CAPACITY: usize = 128;
35/// Default maximum number of concurrent backend requests.
36pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 32;
37
38/// Default broadcast event queue capacity.
39pub const DEFAULT_EVENT_QUEUE_CAPACITY: usize = 1024;
40/// Default bounded queue capacity for the optional single lossless consumer.
41pub const DEFAULT_LOSSLESS_EVENT_QUEUE_CAPACITY: usize = 4096;
42
43/// Reconnect/backoff policy for the long-lived backend event receiver.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct ReconnectPolicy {
46    /// Delay before the first network/timeout retry.
47    pub initial_delay: Duration,
48    /// Maximum network/timeout retry delay.
49    pub max_delay: Duration,
50    /// Delay before the first rate-limit retry when no server hint is present.
51    pub rate_limit_initial_delay: Duration,
52    /// Maximum rate-limit retry delay.
53    pub rate_limit_max_delay: Duration,
54    /// Symmetric percentage of jitter applied to retry delays.
55    pub jitter_percent: u8,
56}
57
58impl Default for ReconnectPolicy {
59    fn default() -> Self {
60        Self {
61            initial_delay: Duration::from_secs(1),
62            max_delay: Duration::from_secs(60),
63            rate_limit_initial_delay: Duration::from_secs(30),
64            rate_limit_max_delay: Duration::from_secs(5 * 60),
65            jitter_percent: 20,
66        }
67    }
68}
69
70/// Errors returned by the async client handle before an operation reaches the backend.
71#[derive(Debug, thiserror::Error)]
72#[non_exhaustive]
73pub enum ClientCommandError {
74    /// The client runner has stopped accepting commands.
75    #[error("inline client runner is closed")]
76    Closed,
77
78    /// The client runner dropped a command response before completing it.
79    #[error("inline client runner dropped command response")]
80    ResponseDropped,
81}
82
83/// Errors returned by typed client operations.
84#[derive(Debug, thiserror::Error)]
85#[non_exhaustive]
86pub enum ClientRequestError {
87    /// The operation could not be delivered to the runner.
88    #[error(transparent)]
89    Command(#[from] ClientCommandError),
90
91    /// The backend rejected or failed the operation.
92    #[error(transparent)]
93    Backend(#[from] BackendError),
94}
95
96/// Builder for an [`InlineClient`] runtime.
97#[derive(Clone, Debug)]
98pub struct InlineClientBuilder {
99    command_queue_capacity: usize,
100    max_concurrent_requests: usize,
101    event_queue_capacity: usize,
102    lossless_event_queue_capacity: usize,
103    reconnect_policy: ReconnectPolicy,
104    initial_status: ClientStatus,
105    backend: Arc<dyn ClientBackend>,
106}
107
108impl InlineClientBuilder {
109    /// Sets the backend used by the runner.
110    pub fn backend(mut self, backend: impl ClientBackend) -> Self {
111        self.backend = Arc::new(backend);
112        self
113    }
114
115    /// Sets the shared backend used by the runner.
116    pub fn shared_backend(mut self, backend: Arc<dyn ClientBackend>) -> Self {
117        self.backend = backend;
118        self
119    }
120
121    /// Sets the bounded command queue capacity.
122    pub fn command_queue_capacity(mut self, capacity: usize) -> Self {
123        self.command_queue_capacity = capacity.max(1);
124        self
125    }
126
127    /// Sets the maximum number of backend requests allowed in flight.
128    pub fn max_concurrent_requests(mut self, maximum: usize) -> Self {
129        self.max_concurrent_requests = maximum.max(1);
130        self
131    }
132
133    /// Sets the broadcast event queue capacity.
134    pub fn event_queue_capacity(mut self, capacity: usize) -> Self {
135        self.event_queue_capacity = capacity.max(1);
136        self
137    }
138
139    /// Sets the bounded queue capacity for the optional lossless event consumer.
140    pub fn lossless_event_queue_capacity(mut self, capacity: usize) -> Self {
141        self.lossless_event_queue_capacity = capacity.max(1);
142        self
143    }
144
145    /// Sets event-stream reconnect and rate-limit backoff policy.
146    pub fn reconnect_policy(mut self, policy: ReconnectPolicy) -> Self {
147        self.reconnect_policy = policy;
148        self
149    }
150
151    /// Sets the initial client status.
152    pub fn initial_status(mut self, status: ClientStatus) -> Self {
153        self.initial_status = status;
154        self
155    }
156
157    /// Builds a client handle and runner pair.
158    pub fn build(self) -> InlineClientRuntime {
159        let (command_tx, command_rx) = mpsc::channel(self.command_queue_capacity);
160        let (event_tx, _) = broadcast::channel(self.event_queue_capacity);
161        let (lossless_event_tx, lossless_event_rx) =
162            mpsc::channel(self.lossless_event_queue_capacity);
163        let lossless_event_active = Arc::new(AtomicBool::new(false));
164        let (status_tx, status_rx) = watch::channel(self.initial_status);
165        let (failure_tx, failure_rx) = watch::channel(None);
166        let (signal_tx, signal_rx) = mpsc::channel(self.command_queue_capacity);
167
168        let client = InlineClient {
169            command_tx,
170            event_tx: event_tx.clone(),
171            lossless_event_rx: Arc::new(StdMutex::new(Some(lossless_event_rx))),
172            lossless_event_active: lossless_event_active.clone(),
173            status_rx,
174            failure_rx,
175        };
176        let event_emitter = ClientEventEmitter {
177            broadcast: event_tx,
178            lossless: lossless_event_tx,
179            lossless_active: lossless_event_active,
180            backend: self.backend.clone(),
181        };
182        let runner = ClientRunner {
183            command_rx,
184            event_emitter,
185            status_tx,
186            failure_tx,
187            status: self.initial_status,
188            failure: None,
189            backend: self.backend,
190            signal_tx,
191            signal_rx,
192            event_task: None,
193            request_tasks: JoinSet::new(),
194            max_concurrent_requests: self.max_concurrent_requests,
195            reconnect_policy: self.reconnect_policy,
196        };
197
198        InlineClientRuntime { client, runner }
199    }
200}
201
202impl Default for InlineClientBuilder {
203    fn default() -> Self {
204        Self {
205            command_queue_capacity: DEFAULT_COMMAND_QUEUE_CAPACITY,
206            max_concurrent_requests: DEFAULT_MAX_CONCURRENT_REQUESTS,
207            event_queue_capacity: DEFAULT_EVENT_QUEUE_CAPACITY,
208            lossless_event_queue_capacity: DEFAULT_LOSSLESS_EVENT_QUEUE_CAPACITY,
209            reconnect_policy: ReconnectPolicy::default(),
210            initial_status: ClientStatus::Disconnected,
211            backend: Arc::new(InMemoryBackend::default()),
212        }
213    }
214}
215
216/// Built client runtime before the runner is hosted.
217#[derive(Debug)]
218pub struct InlineClientRuntime {
219    /// Cloneable client handle.
220    pub client: InlineClient,
221    /// Single-owner client runner.
222    pub runner: ClientRunner,
223}
224
225/// Single-consumer bounded stream for hosts that must not silently lose
226/// committed lossless client events.
227#[derive(Debug)]
228pub struct LosslessEventReceiver {
229    receiver: mpsc::Receiver<LosslessEventDelivery>,
230}
231
232impl LosslessEventReceiver {
233    /// Receives the next lossless delivery without acknowledging it. Durable
234    /// hosts should commit the event and then call [`LosslessEventDelivery::ack`].
235    pub async fn recv_delivery(&mut self) -> Option<LosslessEventDelivery> {
236        self.receiver.recv().await
237    }
238
239    /// Receives the next lossless event, or `None` after the client runner
240    /// closes. This compatibility helper acknowledges the delivery before
241    /// returning; crash-safe hosts should use [`Self::recv_delivery`] instead.
242    pub async fn recv(&mut self) -> Option<ClientEvent> {
243        let delivery = self.recv_delivery().await?;
244        let event = delivery.event.clone();
245        if let Err(error) = delivery.ack().await {
246            log::error!("failed to acknowledge consumed Inline client event: {error}");
247        }
248        Some(event)
249    }
250}
251
252/// One lossless event held durably until its single consumer acknowledges it.
253#[derive(Debug)]
254pub struct LosslessEventDelivery {
255    delivery_id: Option<u64>,
256    event: ClientEvent,
257    backend: Arc<dyn ClientBackend>,
258    acknowledged: AtomicBool,
259}
260
261impl LosslessEventDelivery {
262    /// Returns the store-local durable delivery ID when the backend supports it.
263    pub const fn delivery_id(&self) -> Option<u64> {
264        self.delivery_id
265    }
266
267    /// Returns the committed client event.
268    pub const fn event(&self) -> &ClientEvent {
269        &self.event
270    }
271
272    /// Acknowledges the event after consumer-side handling is durably committed.
273    pub async fn ack(&self) -> BackendResult<()> {
274        if let Some(delivery_id) = self.delivery_id {
275            self.backend.acknowledge_event_delivery(delivery_id).await?;
276        }
277        self.acknowledged.store(true, Ordering::Release);
278        Ok(())
279    }
280}
281
282impl Drop for LosslessEventDelivery {
283    fn drop(&mut self) {
284        if let Some(delivery_id) = self.delivery_id
285            && !self.acknowledged.load(Ordering::Acquire)
286        {
287            self.backend.release_event_delivery(delivery_id);
288        }
289    }
290}
291
292impl InlineClientRuntime {
293    /// Splits the runtime into handle and runner.
294    pub fn split(self) -> (InlineClient, ClientRunner) {
295        (self.client, self.runner)
296    }
297
298    /// Spawns the runner on the current Tokio runtime and returns the handle.
299    pub fn spawn(self) -> InlineClient {
300        let (client, runner) = self.split();
301        tokio::spawn(runner.run());
302        client
303    }
304}
305
306/// Cheap cloneable handle for apps, bridges, agents, and tests.
307#[derive(Clone, Debug)]
308pub struct InlineClient {
309    command_tx: mpsc::Sender<ClientCommand>,
310    event_tx: broadcast::Sender<ClientEvent>,
311    lossless_event_rx: Arc<StdMutex<Option<mpsc::Receiver<LosslessEventDelivery>>>>,
312    lossless_event_active: Arc<AtomicBool>,
313    status_rx: watch::Receiver<ClientStatus>,
314    failure_rx: watch::Receiver<Option<ClientFailure>>,
315}
316
317impl InlineClient {
318    /// Creates a default client runtime builder.
319    pub fn builder() -> InlineClientBuilder {
320        InlineClientBuilder::default()
321    }
322
323    /// Returns the latest observed client status.
324    pub fn status(&self) -> ClientStatus {
325        *self.status_rx.borrow()
326    }
327
328    /// Returns the latest observed status snapshot.
329    pub fn status_snapshot(&self) -> ClientStatusSnapshot {
330        ClientStatusSnapshot {
331            status: *self.status_rx.borrow(),
332            failure: self.failure_rx.borrow().clone(),
333        }
334    }
335
336    /// Subscribes to committed client events.
337    pub fn subscribe(&self) -> broadcast::Receiver<ClientEvent> {
338        self.event_tx.subscribe()
339    }
340
341    /// Claims the single bounded lossless event stream.
342    ///
343    /// Once claimed, the runner applies backpressure before accepting more
344    /// work rather than dropping lossless events. Broadcast subscribers remain
345    /// independent and may still observe lag errors. Returns `None` after the
346    /// lossless stream has already been claimed.
347    pub fn take_lossless_events(&self) -> Option<LosslessEventReceiver> {
348        let receiver = self
349            .lossless_event_rx
350            .lock()
351            .expect("lossless event receiver poisoned")
352            .take()?;
353        self.lossless_event_active.store(true, Ordering::Release);
354        Some(LosslessEventReceiver { receiver })
355    }
356
357    /// Sends an Inline login code.
358    pub async fn auth_start(
359        &self,
360        request: AuthStartRequest,
361    ) -> Result<AuthStartResult, ClientRequestError> {
362        match self.request(ClientRequest::AuthStart(request)).await? {
363            ClientResponse::AuthStart(result) => Ok(result),
364            other => unreachable!("auth_start returned {other:?}"),
365        }
366    }
367
368    /// Verifies an Inline login code and persists the resulting session.
369    pub async fn auth_verify(
370        &self,
371        request: AuthVerifyRequest,
372    ) -> Result<AuthVerifyResult, ClientRequestError> {
373        match self.request(ClientRequest::AuthVerify(request)).await? {
374            ClientResponse::AuthVerify(result) => Ok(result),
375            other => unreachable!("auth_verify returned {other:?}"),
376        }
377    }
378
379    /// Resumes a previously stored session, if available.
380    pub async fn resume_session(&self) -> Result<ClientStatusSnapshot, ClientRequestError> {
381        match self.request(ClientRequest::Resume).await? {
382            ClientResponse::Status(status) => Ok(status),
383            other => unreachable!("resume_session returned {other:?}"),
384        }
385    }
386
387    /// Connects or reconnects the client.
388    pub async fn connect(
389        &self,
390        request: ConnectRequest,
391    ) -> Result<ClientStatusSnapshot, ClientRequestError> {
392        match self.request(ClientRequest::Connect(request)).await? {
393            ClientResponse::Status(status) => Ok(status),
394            other => unreachable!("connect returned {other:?}"),
395        }
396    }
397
398    /// Logs out the current account.
399    pub async fn logout(&self) -> Result<(), ClientRequestError> {
400        match self.request(ClientRequest::Logout).await? {
401            ClientResponse::Empty => Ok(()),
402            other => unreachable!("logout returned {other:?}"),
403        }
404    }
405
406    /// Lists dialogs.
407    pub async fn dialogs(
408        &self,
409        request: DialogsRequest,
410    ) -> Result<DialogsPage, ClientRequestError> {
411        match self.request(ClientRequest::Dialogs(request)).await? {
412            ClientResponse::Dialogs(page) => Ok(page),
413            other => unreachable!("dialogs returned {other:?}"),
414        }
415    }
416
417    /// Lists only durable cached dialogs without making a network request.
418    pub async fn cached_dialogs(
419        &self,
420        request: DialogsRequest,
421    ) -> Result<DialogsPage, ClientRequestError> {
422        match self.request(ClientRequest::CachedDialogs(request)).await? {
423            ClientResponse::Dialogs(page) => Ok(page),
424            other => unreachable!("cached_dialogs returned {other:?}"),
425        }
426    }
427
428    /// Loads account-level durable state for recovery after a consumer event
429    /// stream retention gap.
430    pub async fn account_state(&self) -> Result<AccountStateSnapshot, ClientRequestError> {
431        match self.request(ClientRequest::AccountState).await? {
432            ClientResponse::AccountState(snapshot) => Ok(snapshot),
433            other => unreachable!("account_state returned {other:?}"),
434        }
435    }
436
437    /// Loads durable state for one chat. Current messages remain paged through
438    /// [`Self::history`] so large accounts do not require an unbounded snapshot.
439    pub async fn chat_state(
440        &self,
441        chat_id: InlineId,
442    ) -> Result<ChatStateSnapshot, ClientRequestError> {
443        match self.request(ClientRequest::ChatState(chat_id)).await? {
444            ClientResponse::ChatState(snapshot) => Ok(*snapshot),
445            other => unreachable!("chat_state returned {other:?}"),
446        }
447    }
448
449    /// Fetches chat history.
450    pub async fn history(
451        &self,
452        request: HistoryRequest,
453    ) -> Result<HistoryPage, ClientRequestError> {
454        match self.request(ClientRequest::History(request)).await? {
455            ClientResponse::History(page) => Ok(page),
456            other => unreachable!("history returned {other:?}"),
457        }
458    }
459
460    /// Fetches only durable locally cached history without a network request.
461    pub async fn cached_history(
462        &self,
463        request: HistoryRequest,
464    ) -> Result<HistoryPage, ClientRequestError> {
465        match self.request(ClientRequest::CachedHistory(request)).await? {
466            ClientResponse::History(page) => Ok(page),
467            other => unreachable!("cached_history returned {other:?}"),
468        }
469    }
470
471    /// Fetches chat participants.
472    pub async fn chat_participants(
473        &self,
474        request: ChatParticipantsRequest,
475    ) -> Result<ChatParticipantsPage, ClientRequestError> {
476        match self
477            .request(ClientRequest::ChatParticipants(request))
478            .await?
479        {
480            ClientResponse::ChatParticipants(page) => Ok(page),
481            other => unreachable!("chat_participants returned {other:?}"),
482        }
483    }
484
485    /// Adds a user to an Inline chat.
486    pub async fn add_chat_participant(
487        &self,
488        request: AddChatParticipantRequest,
489    ) -> Result<(), ClientRequestError> {
490        match self
491            .request(ClientRequest::AddChatParticipant(request))
492            .await?
493        {
494            ClientResponse::Empty => Ok(()),
495            other => unreachable!("add_chat_participant returned {other:?}"),
496        }
497    }
498
499    /// Removes a user from an Inline chat.
500    pub async fn remove_chat_participant(
501        &self,
502        request: RemoveChatParticipantRequest,
503    ) -> Result<(), ClientRequestError> {
504        match self
505            .request(ClientRequest::RemoveChatParticipant(request))
506            .await?
507        {
508            ClientResponse::Empty => Ok(()),
509            other => unreachable!("remove_chat_participant returned {other:?}"),
510        }
511    }
512
513    /// Updates mutable Inline chat metadata.
514    pub async fn update_chat_info(
515        &self,
516        request: UpdateChatInfoRequest,
517    ) -> Result<(), ClientRequestError> {
518        match self.request(ClientRequest::UpdateChatInfo(request)).await? {
519            ClientResponse::Empty => Ok(()),
520            other => unreachable!("update_chat_info returned {other:?}"),
521        }
522    }
523
524    /// Deletes an Inline chat when permitted by the service.
525    pub async fn delete_chat(&self, request: DeleteChatRequest) -> Result<(), ClientRequestError> {
526        match self.request(ClientRequest::DeleteChat(request)).await? {
527            ClientResponse::Empty => Ok(()),
528            other => unreachable!("delete_chat returned {other:?}"),
529        }
530    }
531
532    /// Creates or opens a direct message chat.
533    pub async fn create_dm(
534        &self,
535        request: CreateDmRequest,
536    ) -> Result<CreatedChat, ClientRequestError> {
537        match self.request(ClientRequest::CreateDm(request)).await? {
538            ClientResponse::CreatedChat(chat) => Ok(chat),
539            other => unreachable!("create_dm returned {other:?}"),
540        }
541    }
542
543    /// Creates a regular Inline thread chat.
544    pub async fn create_thread(
545        &self,
546        request: CreateThreadRequest,
547    ) -> Result<CreatedChat, ClientRequestError> {
548        match self.request(ClientRequest::CreateThread(request)).await? {
549            ClientResponse::CreatedChat(chat) => Ok(chat),
550            other => unreachable!("create_thread returned {other:?}"),
551        }
552    }
553
554    /// Creates a child/reply Inline thread chat.
555    pub async fn create_reply_thread(
556        &self,
557        request: CreateReplyThreadRequest,
558    ) -> Result<CreatedChat, ClientRequestError> {
559        match self
560            .request(ClientRequest::CreateReplyThread(request))
561            .await?
562        {
563            ClientResponse::CreatedChat(chat) => Ok(chat),
564            other => unreachable!("create_reply_thread returned {other:?}"),
565        }
566    }
567
568    /// Sends a text message.
569    pub async fn send_text(
570        &self,
571        request: SendTextRequest,
572    ) -> Result<MessageMutation, ClientRequestError> {
573        match self.request(ClientRequest::SendText(request)).await? {
574            ClientResponse::Message(mutation) => Ok(mutation),
575            other => unreachable!("send_text returned {other:?}"),
576        }
577    }
578
579    /// Uploads and sends a media message.
580    pub async fn send_media(
581        &self,
582        request: UploadRequest,
583        bytes: Vec<u8>,
584    ) -> Result<MessageMutation, ClientRequestError> {
585        match self
586            .request(ClientRequest::SendMedia { request, bytes })
587            .await?
588        {
589            ClientResponse::Message(mutation) => Ok(mutation),
590            other => unreachable!("send_media returned {other:?}"),
591        }
592    }
593
594    /// Edits a text message.
595    pub async fn edit_message(
596        &self,
597        request: EditMessageRequest,
598    ) -> Result<(), ClientRequestError> {
599        match self.request(ClientRequest::EditMessage(request)).await? {
600            ClientResponse::Empty => Ok(()),
601            other => unreachable!("edit_message returned {other:?}"),
602        }
603    }
604
605    /// Deletes or unsends a message.
606    pub async fn delete_message(
607        &self,
608        request: DeleteMessageRequest,
609    ) -> Result<(), ClientRequestError> {
610        match self.request(ClientRequest::DeleteMessage(request)).await? {
611            ClientResponse::Empty => Ok(()),
612            other => unreachable!("delete_message returned {other:?}"),
613        }
614    }
615
616    /// Adds or removes a reaction.
617    pub async fn react(&self, request: ReactRequest) -> Result<(), ClientRequestError> {
618        match self.request(ClientRequest::React(request)).await? {
619            ClientResponse::Empty => Ok(()),
620            other => unreachable!("react returned {other:?}"),
621        }
622    }
623
624    /// Marks messages read.
625    pub async fn read(&self, request: ReadRequest) -> Result<(), ClientRequestError> {
626        match self.request(ClientRequest::Read(request)).await? {
627            ClientResponse::Empty => Ok(()),
628            other => unreachable!("read returned {other:?}"),
629        }
630    }
631
632    /// Sets the explicit marked-unread state for a chat.
633    pub async fn set_marked_unread(
634        &self,
635        request: SetMarkedUnreadRequest,
636    ) -> Result<(), ClientRequestError> {
637        match self
638            .request(ClientRequest::SetMarkedUnread(request))
639            .await?
640        {
641            ClientResponse::Empty => Ok(()),
642            other => unreachable!("set_marked_unread returned {other:?}"),
643        }
644    }
645
646    /// Sets or clears a per-dialog notification override.
647    pub async fn update_dialog_notifications(
648        &self,
649        request: UpdateDialogNotificationsRequest,
650    ) -> Result<(), ClientRequestError> {
651        match self
652            .request(ClientRequest::UpdateDialogNotifications(request))
653            .await?
654        {
655            ClientResponse::Empty => Ok(()),
656            other => unreachable!("update_dialog_notifications returned {other:?}"),
657        }
658    }
659
660    /// Sends a typing state.
661    pub async fn typing(&self, request: TypingRequest) -> Result<(), ClientRequestError> {
662        match self.request(ClientRequest::Typing(request)).await? {
663            ClientResponse::Empty => Ok(()),
664            other => unreachable!("typing returned {other:?}"),
665        }
666    }
667
668    /// Updates status through the runner.
669    ///
670    /// This is public so early hosts and tests can exercise the event/status
671    /// pipeline before the real transport manager is wired in. Future transport
672    /// code should call the same internal command path.
673    pub async fn set_status(
674        &self,
675        status: ClientStatus,
676        failure: Option<ClientFailure>,
677    ) -> Result<(), ClientCommandError> {
678        let (respond_to, response) = oneshot::channel();
679        self.command_tx
680            .send(ClientCommand::SetStatus {
681                status,
682                failure,
683                respond_to,
684            })
685            .await
686            .map_err(|_| ClientCommandError::Closed)?;
687        response
688            .await
689            .map_err(|_| ClientCommandError::ResponseDropped)
690    }
691
692    /// Requests runner shutdown.
693    pub async fn shutdown(&self) -> Result<(), ClientCommandError> {
694        let (respond_to, response) = oneshot::channel();
695        self.command_tx
696            .send(ClientCommand::Shutdown { respond_to })
697            .await
698            .map_err(|_| ClientCommandError::Closed)?;
699        response
700            .await
701            .map_err(|_| ClientCommandError::ResponseDropped)
702    }
703
704    async fn request(&self, request: ClientRequest) -> Result<ClientResponse, ClientRequestError> {
705        let (respond_to, response) = oneshot::channel();
706        self.command_tx
707            .send(ClientCommand::Request {
708                request: Box::new(request),
709                respond_to,
710            })
711            .await
712            .map_err(|_| ClientCommandError::Closed)?;
713        response
714            .await
715            .map_err(|_| ClientCommandError::ResponseDropped)?
716            .map_err(ClientRequestError::Backend)
717    }
718}
719
720/// Single-owner async client runner.
721#[derive(Debug)]
722pub struct ClientRunner {
723    command_rx: mpsc::Receiver<ClientCommand>,
724    event_emitter: ClientEventEmitter,
725    status_tx: watch::Sender<ClientStatus>,
726    failure_tx: watch::Sender<Option<ClientFailure>>,
727    status: ClientStatus,
728    failure: Option<ClientFailure>,
729    backend: Arc<dyn ClientBackend>,
730    signal_tx: mpsc::Sender<RunnerSignal>,
731    signal_rx: mpsc::Receiver<RunnerSignal>,
732    event_task: Option<JoinHandle<()>>,
733    request_tasks: JoinSet<()>,
734    max_concurrent_requests: usize,
735    reconnect_policy: ReconnectPolicy,
736}
737
738#[derive(Clone, Debug)]
739struct ClientEventEmitter {
740    broadcast: broadcast::Sender<ClientEvent>,
741    lossless: mpsc::Sender<LosslessEventDelivery>,
742    lossless_active: Arc<AtomicBool>,
743    backend: Arc<dyn ClientBackend>,
744}
745
746impl ClientEventEmitter {
747    async fn emit_delivery(&self, delivery: ClientEventDelivery) -> BackendResult<()> {
748        let _ = self.broadcast.send(delivery.event.clone());
749        if delivery.event.reliability() != crate::EventReliability::Lossless {
750            return Ok(());
751        }
752        let delivery = LosslessEventDelivery {
753            delivery_id: delivery.delivery_id,
754            event: delivery.event,
755            backend: self.backend.clone(),
756            acknowledged: AtomicBool::new(false),
757        };
758        if self.lossless_active.load(Ordering::Acquire) {
759            if let Err(error) = self.lossless.send(delivery).await {
760                self.lossless_active.store(false, Ordering::Release);
761                error.0.ack().await?;
762            }
763        } else {
764            delivery.ack().await?;
765        }
766        Ok(())
767    }
768
769    async fn emit_events(&self, events: Vec<ClientEvent>) -> BackendResult<()> {
770        let deliveries = self.backend.stage_client_events(events).await?;
771        self.emit_deliveries(deliveries).await
772    }
773
774    async fn emit_deliveries(&self, deliveries: Vec<ClientEventDelivery>) -> BackendResult<()> {
775        for delivery in deliveries {
776            self.emit_delivery(delivery).await?;
777        }
778        Ok(())
779    }
780
781    async fn emit_operation_events(&self, outcome: OperationOutcome) -> BackendResult<()> {
782        self.emit_events(outcome.events).await
783    }
784
785    async fn emit_send_outcome(&self, outcome: SendTextOutcome) -> BackendResult<MessageMutation> {
786        let transaction = outcome.transaction_event();
787        let message_id = outcome.message_id;
788        let chat_id = outcome.chat_id;
789        let message = outcome.message;
790        let mut mutation = outcome.mutation;
791        mutation.state = Some(outcome.state);
792        mutation.failure = outcome.failure.clone();
793        let mut events = vec![ClientEvent::TransactionChanged(transaction)];
794        if let Some(message_id) = message_id {
795            events.push(ClientEvent::MessageUpserted {
796                chat_id,
797                message_id,
798            });
799        }
800        if let Some(message) = message {
801            events.push(ClientEvent::MessageStored { message });
802        }
803        self.emit_events(events).await?;
804        Ok(mutation)
805    }
806}
807
808impl ClientRunner {
809    /// Runs the client command loop until shutdown or all handles are dropped.
810    pub async fn run(mut self) {
811        log::debug!("inline client runner started");
812        loop {
813            tokio::select! {
814                command = self.command_rx.recv(), if self.request_tasks.len() < self.max_concurrent_requests => {
815                    if !self.handle_optional_command(command).await {
816                        break;
817                    }
818                }
819                signal = self.signal_rx.recv() => {
820                    if let Some(signal) = signal {
821                        self.handle_signal(signal).await;
822                    }
823                }
824                completed = self.request_tasks.join_next(), if !self.request_tasks.is_empty() => {
825                    if let Some(Err(error)) = completed {
826                        log::error!("inline client request task failed: {error}");
827                    }
828                }
829            }
830        }
831        self.stop_event_receiver().await;
832        log::debug!("inline client runner stopped");
833    }
834
835    async fn handle_optional_command(&mut self, command: Option<ClientCommand>) -> bool {
836        let Some(command) = command else {
837            return false;
838        };
839        self.handle_command(command).await
840    }
841
842    async fn handle_command(&mut self, command: ClientCommand) -> bool {
843        match command {
844            ClientCommand::Request {
845                request,
846                respond_to,
847            } => {
848                if request.can_run_concurrently() {
849                    self.spawn_concurrent_request(*request, respond_to);
850                    return true;
851                }
852                self.finish_concurrent_requests().await;
853                let response = self.handle_request(*request).await;
854                let _ = respond_to.send(response);
855                true
856            }
857            ClientCommand::SetStatus {
858                status,
859                failure,
860                respond_to,
861            } => {
862                self.update_status(status, failure).await;
863                let _ = respond_to.send(());
864                true
865            }
866            ClientCommand::Shutdown { respond_to } => {
867                log::debug!("inline client runner shutdown requested");
868                self.request_tasks.abort_all();
869                self.update_status(ClientStatus::ShuttingDown, None).await;
870                let _ = respond_to.send(());
871                false
872            }
873        }
874    }
875
876    const fn should_receive_events(&self) -> bool {
877        matches!(
878            self.status,
879            ClientStatus::Connected | ClientStatus::Reconnecting
880        )
881    }
882
883    async fn emit_received_events(&self, events: Vec<ClientEventDelivery>) -> BackendResult<()> {
884        self.event_emitter.emit_deliveries(events).await
885    }
886
887    async fn handle_signal(&mut self, signal: RunnerSignal) {
888        match signal {
889            RunnerSignal::Events(events) => {
890                if self.status == ClientStatus::Reconnecting {
891                    self.update_status(ClientStatus::Connected, None).await;
892                }
893                if let Err(error) = self.emit_received_events(events).await {
894                    log::error!("failed to emit durable Inline client events: {error}");
895                    self.update_status_for_backend_error(&error).await;
896                }
897            }
898            RunnerSignal::ReceiveError(error) => {
899                self.update_status_for_backend_error(&error).await;
900            }
901        }
902    }
903
904    async fn handle_request(&mut self, request: ClientRequest) -> BackendResult<ClientResponse> {
905        log::debug!("handling inline client request: {}", request.kind());
906        match request {
907            ClientRequest::AuthStart(auth) => self
908                .backend
909                .auth_start(auth)
910                .await
911                .map(ClientResponse::AuthStart),
912            ClientRequest::AuthVerify(auth) => {
913                self.update_status(ClientStatus::Connecting, None).await;
914                match self.backend.auth_verify(auth).await {
915                    Ok(result) => {
916                        let status = result.status.clone();
917                        self.update_status(status.status, status.failure.clone())
918                            .await;
919                        Ok(ClientResponse::AuthVerify(result))
920                    }
921                    Err(error) => {
922                        self.update_status_for_backend_error(&error).await;
923                        Err(error)
924                    }
925                }
926            }
927            ClientRequest::Resume => {
928                self.update_status(ClientStatus::Connecting, None).await;
929                match self.backend.resume_session().await {
930                    Ok(status) => {
931                        self.update_status(status.status, status.failure.clone())
932                            .await;
933                        Ok(ClientResponse::Status(status))
934                    }
935                    Err(error) => {
936                        self.update_status_for_backend_error(&error).await;
937                        Err(error)
938                    }
939                }
940            }
941            ClientRequest::Connect(connect) => {
942                self.update_status(ClientStatus::Connecting, None).await;
943                match self.backend.connect(connect).await {
944                    Ok(status) => {
945                        self.update_status(status.status, status.failure.clone())
946                            .await;
947                        Ok(ClientResponse::Status(status))
948                    }
949                    Err(error) => {
950                        self.update_status_for_backend_error(&error).await;
951                        Err(error)
952                    }
953                }
954            }
955            ClientRequest::Logout => {
956                self.backend.logout().await?;
957                self.update_status(ClientStatus::LoggedOut, None).await;
958                Ok(ClientResponse::Empty)
959            }
960            ClientRequest::Dialogs(dialogs) => self
961                .backend
962                .dialogs(dialogs)
963                .await
964                .map(ClientResponse::Dialogs),
965            ClientRequest::CachedDialogs(dialogs) => self
966                .backend
967                .cached_dialogs(dialogs)
968                .await
969                .map(ClientResponse::Dialogs),
970            ClientRequest::AccountState => self
971                .backend
972                .account_state()
973                .await
974                .map(ClientResponse::AccountState),
975            ClientRequest::ChatState(chat_id) => self
976                .backend
977                .chat_state(chat_id)
978                .await
979                .map(Box::new)
980                .map(ClientResponse::ChatState),
981            ClientRequest::History(history) => self
982                .backend
983                .history(history)
984                .await
985                .map(ClientResponse::History),
986            ClientRequest::CachedHistory(history) => self
987                .backend
988                .cached_history(history)
989                .await
990                .map(ClientResponse::History),
991            ClientRequest::ChatParticipants(participants) => self
992                .backend
993                .chat_participants(participants)
994                .await
995                .map(ClientResponse::ChatParticipants),
996            ClientRequest::AddChatParticipant(request) => {
997                let outcome = self.backend.add_chat_participant(request).await?;
998                self.event_emitter.emit_operation_events(outcome).await?;
999                Ok(ClientResponse::Empty)
1000            }
1001            ClientRequest::RemoveChatParticipant(request) => {
1002                let outcome = self.backend.remove_chat_participant(request).await?;
1003                self.event_emitter.emit_operation_events(outcome).await?;
1004                Ok(ClientResponse::Empty)
1005            }
1006            ClientRequest::UpdateChatInfo(request) => {
1007                let outcome = self.backend.update_chat_info(request).await?;
1008                self.event_emitter.emit_operation_events(outcome).await?;
1009                Ok(ClientResponse::Empty)
1010            }
1011            ClientRequest::DeleteChat(request) => {
1012                let outcome = self.backend.delete_chat(request).await?;
1013                self.event_emitter.emit_operation_events(outcome).await?;
1014                Ok(ClientResponse::Empty)
1015            }
1016            ClientRequest::CreateDm(request) => self
1017                .backend
1018                .create_dm(request)
1019                .await
1020                .map(ClientResponse::CreatedChat),
1021            ClientRequest::CreateThread(request) => self
1022                .backend
1023                .create_thread(request)
1024                .await
1025                .map(ClientResponse::CreatedChat),
1026            ClientRequest::CreateReplyThread(request) => self
1027                .backend
1028                .create_reply_thread(request)
1029                .await
1030                .map(ClientResponse::CreatedChat),
1031            ClientRequest::SendText(send) => {
1032                let outcome = self.backend.send_text(send).await?;
1033                Ok(ClientResponse::Message(
1034                    self.event_emitter.emit_send_outcome(outcome).await?,
1035                ))
1036            }
1037            ClientRequest::SendMedia { request, bytes } => {
1038                let outcome = self.backend.send_media(request, bytes).await?;
1039                Ok(ClientResponse::Message(
1040                    self.event_emitter.emit_send_outcome(outcome).await?,
1041                ))
1042            }
1043            ClientRequest::EditMessage(edit) => {
1044                let outcome = self.backend.edit_message(edit).await?;
1045                self.event_emitter.emit_operation_events(outcome).await?;
1046                Ok(ClientResponse::Empty)
1047            }
1048            ClientRequest::DeleteMessage(delete) => {
1049                let outcome = self.backend.delete_message(delete).await?;
1050                self.event_emitter.emit_operation_events(outcome).await?;
1051                Ok(ClientResponse::Empty)
1052            }
1053            ClientRequest::React(react) => {
1054                let outcome = self.backend.react(react).await?;
1055                self.event_emitter.emit_operation_events(outcome).await?;
1056                Ok(ClientResponse::Empty)
1057            }
1058            ClientRequest::Read(read) => {
1059                let outcome = self.backend.read(read).await?;
1060                self.event_emitter.emit_operation_events(outcome).await?;
1061                Ok(ClientResponse::Empty)
1062            }
1063            ClientRequest::SetMarkedUnread(request) => {
1064                let outcome = self.backend.set_marked_unread(request).await?;
1065                self.event_emitter.emit_operation_events(outcome).await?;
1066                Ok(ClientResponse::Empty)
1067            }
1068            ClientRequest::UpdateDialogNotifications(request) => {
1069                let outcome = self.backend.update_dialog_notifications(request).await?;
1070                self.event_emitter.emit_operation_events(outcome).await?;
1071                Ok(ClientResponse::Empty)
1072            }
1073            ClientRequest::Typing(typing) => {
1074                let outcome = self.backend.typing(typing).await?;
1075                self.event_emitter.emit_operation_events(outcome).await?;
1076                Ok(ClientResponse::Empty)
1077            }
1078        }
1079    }
1080
1081    async fn update_status_for_backend_error(&mut self, error: &BackendError) {
1082        let status = match error.category {
1083            ClientErrorCategory::AuthRequired => ClientStatus::AuthRequired,
1084            ClientErrorCategory::AuthExpired => ClientStatus::AuthExpired,
1085            ClientErrorCategory::Network
1086            | ClientErrorCategory::Timeout
1087            | ClientErrorCategory::RateLimited => ClientStatus::Reconnecting,
1088            _ => ClientStatus::Disconnected,
1089        };
1090        self.update_status(
1091            status,
1092            Some(ClientFailure::new(error.category, error.message.clone())),
1093        )
1094        .await;
1095    }
1096
1097    async fn update_status(&mut self, status: ClientStatus, failure: Option<ClientFailure>) {
1098        log::debug!("inline client status changed: {status:?}");
1099        self.status = status;
1100        self.failure = failure.clone();
1101        let _ = self.status_tx.send(status);
1102        let _ = self.failure_tx.send(failure.clone());
1103        if let Err(error) = self
1104            .event_emitter
1105            .emit_events(vec![ClientEvent::StatusChanged { status, failure }])
1106            .await
1107        {
1108            log::error!("failed to persist Inline status event: {error}");
1109        }
1110        self.sync_event_receiver_to_status().await;
1111    }
1112
1113    fn spawn_concurrent_request(
1114        &mut self,
1115        request: ClientRequest,
1116        respond_to: oneshot::Sender<BackendResult<ClientResponse>>,
1117    ) {
1118        let backend = self.backend.clone();
1119        let event_emitter = self.event_emitter.clone();
1120        self.request_tasks.spawn(async move {
1121            let response = handle_concurrent_request(backend, event_emitter, request).await;
1122            let _ = respond_to.send(response);
1123        });
1124    }
1125
1126    async fn finish_concurrent_requests(&mut self) {
1127        while let Some(completed) = self.request_tasks.join_next().await {
1128            if let Err(error) = completed {
1129                log::error!("inline client request task failed: {error}");
1130            }
1131        }
1132    }
1133
1134    async fn sync_event_receiver_to_status(&mut self) {
1135        if self.should_receive_events() {
1136            self.start_event_receiver();
1137        } else {
1138            self.stop_event_receiver().await;
1139        }
1140    }
1141
1142    fn start_event_receiver(&mut self) {
1143        if self.event_task.is_some() {
1144            return;
1145        }
1146        let backend = self.backend.clone();
1147        let signal_tx = self.signal_tx.clone();
1148        let reconnect_policy = self.reconnect_policy;
1149        self.event_task = Some(tokio::spawn(run_backend_event_receiver(
1150            backend,
1151            signal_tx,
1152            reconnect_policy,
1153        )));
1154    }
1155
1156    async fn stop_event_receiver(&mut self) {
1157        if let Some(task) = self.event_task.take() {
1158            task.abort();
1159            let _ = task.await;
1160        }
1161        if let Err(error) = self.backend.reset_event_delivery_claims().await {
1162            log::error!("failed to release Inline client event delivery claims: {error}");
1163        }
1164    }
1165}
1166
1167async fn handle_concurrent_request(
1168    backend: Arc<dyn ClientBackend>,
1169    events: ClientEventEmitter,
1170    request: ClientRequest,
1171) -> BackendResult<ClientResponse> {
1172    log::debug!(
1173        "handling concurrent inline client request: {}",
1174        request.kind()
1175    );
1176    match request {
1177        ClientRequest::Dialogs(request) => {
1178            backend.dialogs(request).await.map(ClientResponse::Dialogs)
1179        }
1180        ClientRequest::CachedDialogs(request) => backend
1181            .cached_dialogs(request)
1182            .await
1183            .map(ClientResponse::Dialogs),
1184        ClientRequest::AccountState => backend
1185            .account_state()
1186            .await
1187            .map(ClientResponse::AccountState),
1188        ClientRequest::ChatState(chat_id) => backend
1189            .chat_state(chat_id)
1190            .await
1191            .map(Box::new)
1192            .map(ClientResponse::ChatState),
1193        ClientRequest::History(request) => {
1194            backend.history(request).await.map(ClientResponse::History)
1195        }
1196        ClientRequest::CachedHistory(request) => backend
1197            .cached_history(request)
1198            .await
1199            .map(ClientResponse::History),
1200        ClientRequest::ChatParticipants(request) => backend
1201            .chat_participants(request)
1202            .await
1203            .map(ClientResponse::ChatParticipants),
1204        ClientRequest::AddChatParticipant(request) => {
1205            events
1206                .emit_operation_events(backend.add_chat_participant(request).await?)
1207                .await?;
1208            Ok(ClientResponse::Empty)
1209        }
1210        ClientRequest::RemoveChatParticipant(request) => {
1211            events
1212                .emit_operation_events(backend.remove_chat_participant(request).await?)
1213                .await?;
1214            Ok(ClientResponse::Empty)
1215        }
1216        ClientRequest::UpdateChatInfo(request) => {
1217            events
1218                .emit_operation_events(backend.update_chat_info(request).await?)
1219                .await?;
1220            Ok(ClientResponse::Empty)
1221        }
1222        ClientRequest::DeleteChat(request) => {
1223            events
1224                .emit_operation_events(backend.delete_chat(request).await?)
1225                .await?;
1226            Ok(ClientResponse::Empty)
1227        }
1228        ClientRequest::CreateDm(request) => backend
1229            .create_dm(request)
1230            .await
1231            .map(ClientResponse::CreatedChat),
1232        ClientRequest::CreateThread(request) => backend
1233            .create_thread(request)
1234            .await
1235            .map(ClientResponse::CreatedChat),
1236        ClientRequest::CreateReplyThread(request) => backend
1237            .create_reply_thread(request)
1238            .await
1239            .map(ClientResponse::CreatedChat),
1240        ClientRequest::SendText(request) => {
1241            let outcome = backend.send_text(request).await?;
1242            Ok(ClientResponse::Message(
1243                events.emit_send_outcome(outcome).await?,
1244            ))
1245        }
1246        ClientRequest::SendMedia { request, bytes } => {
1247            let outcome = backend.send_media(request, bytes).await?;
1248            Ok(ClientResponse::Message(
1249                events.emit_send_outcome(outcome).await?,
1250            ))
1251        }
1252        ClientRequest::EditMessage(request) => {
1253            events
1254                .emit_operation_events(backend.edit_message(request).await?)
1255                .await?;
1256            Ok(ClientResponse::Empty)
1257        }
1258        ClientRequest::DeleteMessage(request) => {
1259            events
1260                .emit_operation_events(backend.delete_message(request).await?)
1261                .await?;
1262            Ok(ClientResponse::Empty)
1263        }
1264        ClientRequest::React(request) => {
1265            events
1266                .emit_operation_events(backend.react(request).await?)
1267                .await?;
1268            Ok(ClientResponse::Empty)
1269        }
1270        ClientRequest::Read(request) => {
1271            events
1272                .emit_operation_events(backend.read(request).await?)
1273                .await?;
1274            Ok(ClientResponse::Empty)
1275        }
1276        ClientRequest::SetMarkedUnread(request) => {
1277            events
1278                .emit_operation_events(backend.set_marked_unread(request).await?)
1279                .await?;
1280            Ok(ClientResponse::Empty)
1281        }
1282        ClientRequest::UpdateDialogNotifications(request) => {
1283            events
1284                .emit_operation_events(backend.update_dialog_notifications(request).await?)
1285                .await?;
1286            Ok(ClientResponse::Empty)
1287        }
1288        ClientRequest::Typing(request) => {
1289            events
1290                .emit_operation_events(backend.typing(request).await?)
1291                .await?;
1292            Ok(ClientResponse::Empty)
1293        }
1294        ClientRequest::AuthStart(_)
1295        | ClientRequest::AuthVerify(_)
1296        | ClientRequest::Resume
1297        | ClientRequest::Connect(_)
1298        | ClientRequest::Logout => unreachable!("session request was spawned concurrently"),
1299    }
1300}
1301
1302async fn run_backend_event_receiver(
1303    backend: Arc<dyn ClientBackend>,
1304    signal_tx: mpsc::Sender<RunnerSignal>,
1305    reconnect_policy: ReconnectPolicy,
1306) {
1307    let mut retry_attempt = 0_u32;
1308    loop {
1309        match backend.receive_event_deliveries().await {
1310            Ok(events) => {
1311                retry_attempt = 0;
1312                if signal_tx.send(RunnerSignal::Events(events)).await.is_err() {
1313                    break;
1314                }
1315            }
1316            Err(error) => {
1317                let retry = should_retry_event_receive(&error);
1318                let delay = event_retry_delay(&error, retry_attempt, reconnect_policy);
1319                if signal_tx
1320                    .send(RunnerSignal::ReceiveError(error))
1321                    .await
1322                    .is_err()
1323                {
1324                    break;
1325                }
1326                if !retry {
1327                    break;
1328                }
1329                retry_attempt = retry_attempt.saturating_add(1);
1330                tokio::time::sleep(delay).await;
1331            }
1332        }
1333    }
1334    if let Err(error) = backend.reset_event_delivery_claims().await {
1335        log::error!("failed to release stopped Inline event receiver claims: {error}");
1336    }
1337}
1338
1339enum ClientCommand {
1340    Request {
1341        request: Box<ClientRequest>,
1342        respond_to: oneshot::Sender<BackendResult<ClientResponse>>,
1343    },
1344    SetStatus {
1345        status: ClientStatus,
1346        failure: Option<ClientFailure>,
1347        respond_to: oneshot::Sender<()>,
1348    },
1349    Shutdown {
1350        respond_to: oneshot::Sender<()>,
1351    },
1352}
1353
1354#[derive(Debug)]
1355enum RunnerSignal {
1356    Events(Vec<ClientEventDelivery>),
1357    ReceiveError(BackendError),
1358}
1359
1360impl std::fmt::Debug for ClientCommand {
1361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1362        match self {
1363            Self::Request { request, .. } => f
1364                .debug_struct("Request")
1365                .field("request", request)
1366                .finish_non_exhaustive(),
1367            Self::SetStatus {
1368                status, failure, ..
1369            } => f
1370                .debug_struct("SetStatus")
1371                .field("status", status)
1372                .field("failure", failure)
1373                .finish_non_exhaustive(),
1374            Self::Shutdown { .. } => f.debug_struct("Shutdown").finish_non_exhaustive(),
1375        }
1376    }
1377}
1378
1379#[derive(Debug)]
1380enum ClientRequest {
1381    AuthStart(AuthStartRequest),
1382    AuthVerify(AuthVerifyRequest),
1383    Resume,
1384    Connect(ConnectRequest),
1385    Logout,
1386    Dialogs(DialogsRequest),
1387    CachedDialogs(DialogsRequest),
1388    AccountState,
1389    ChatState(InlineId),
1390    History(HistoryRequest),
1391    CachedHistory(HistoryRequest),
1392    ChatParticipants(ChatParticipantsRequest),
1393    AddChatParticipant(AddChatParticipantRequest),
1394    RemoveChatParticipant(RemoveChatParticipantRequest),
1395    UpdateChatInfo(UpdateChatInfoRequest),
1396    DeleteChat(DeleteChatRequest),
1397    CreateDm(CreateDmRequest),
1398    CreateThread(CreateThreadRequest),
1399    CreateReplyThread(CreateReplyThreadRequest),
1400    SendText(SendTextRequest),
1401    SendMedia {
1402        request: UploadRequest,
1403        bytes: Vec<u8>,
1404    },
1405    EditMessage(EditMessageRequest),
1406    DeleteMessage(DeleteMessageRequest),
1407    React(ReactRequest),
1408    Read(ReadRequest),
1409    SetMarkedUnread(SetMarkedUnreadRequest),
1410    UpdateDialogNotifications(UpdateDialogNotificationsRequest),
1411    Typing(TypingRequest),
1412}
1413
1414impl ClientRequest {
1415    const fn can_run_concurrently(&self) -> bool {
1416        !matches!(
1417            self,
1418            Self::AuthStart(_)
1419                | Self::AuthVerify(_)
1420                | Self::Resume
1421                | Self::Connect(_)
1422                | Self::Logout
1423        )
1424    }
1425
1426    const fn kind(&self) -> &'static str {
1427        match self {
1428            Self::AuthStart(_) => "auth_start",
1429            Self::AuthVerify(_) => "auth_verify",
1430            Self::Resume => "resume",
1431            Self::Connect(_) => "connect",
1432            Self::Logout => "logout",
1433            Self::Dialogs(_) => "dialogs",
1434            Self::CachedDialogs(_) => "cached_dialogs",
1435            Self::AccountState => "account_state",
1436            Self::ChatState(_) => "chat_state",
1437            Self::History(_) => "history",
1438            Self::CachedHistory(_) => "cached_history",
1439            Self::ChatParticipants(_) => "chat_participants",
1440            Self::AddChatParticipant(_) => "add_chat_participant",
1441            Self::RemoveChatParticipant(_) => "remove_chat_participant",
1442            Self::UpdateChatInfo(_) => "update_chat_info",
1443            Self::DeleteChat(_) => "delete_chat",
1444            Self::CreateDm(_) => "create_dm",
1445            Self::CreateThread(_) => "create_thread",
1446            Self::CreateReplyThread(_) => "create_reply_thread",
1447            Self::SendText(_) => "send_text",
1448            Self::SendMedia { .. } => "send_media",
1449            Self::EditMessage(_) => "edit_message",
1450            Self::DeleteMessage(_) => "delete_message",
1451            Self::React(_) => "react",
1452            Self::Read(_) => "read",
1453            Self::SetMarkedUnread(_) => "set_marked_unread",
1454            Self::UpdateDialogNotifications(_) => "update_dialog_notifications",
1455            Self::Typing(_) => "typing",
1456        }
1457    }
1458}
1459
1460fn event_retry_delay(error: &BackendError, attempt: u32, policy: ReconnectPolicy) -> Duration {
1461    if error.category == ClientErrorCategory::RateLimited
1462        && let Some(hint) = error
1463            .retry_after_seconds
1464            .map(Duration::from_secs)
1465            .or_else(|| retry_after_hint(&error.message))
1466    {
1467        return apply_retry_jitter(hint.min(policy.rate_limit_max_delay), policy.jitter_percent);
1468    }
1469    let (initial, maximum) = match error.category {
1470        ClientErrorCategory::RateLimited => {
1471            (policy.rate_limit_initial_delay, policy.rate_limit_max_delay)
1472        }
1473        _ => (policy.initial_delay, policy.max_delay),
1474    };
1475    let multiplier = 1_u32.checked_shl(attempt.min(16)).unwrap_or(u32::MAX);
1476    apply_retry_jitter(
1477        initial.saturating_mul(multiplier).min(maximum),
1478        policy.jitter_percent,
1479    )
1480}
1481
1482fn retry_after_hint(message: &str) -> Option<Duration> {
1483    retry_after_seconds_from_message(message).map(Duration::from_secs)
1484}
1485
1486fn apply_retry_jitter(delay: Duration, jitter_percent: u8) -> Duration {
1487    let jitter = u64::from(jitter_percent.min(100));
1488    if jitter == 0 || delay.is_zero() {
1489        return delay;
1490    }
1491    let seed = SystemTime::now()
1492        .duration_since(UNIX_EPOCH)
1493        .unwrap_or_default()
1494        .subsec_nanos() as u64;
1495    let spread = jitter * 2 + 1;
1496    let factor = 100_u64.saturating_sub(jitter) + seed % spread;
1497    Duration::from_millis(
1498        u64::try_from(delay.as_millis().saturating_mul(u128::from(factor)) / 100)
1499            .unwrap_or(u64::MAX),
1500    )
1501}
1502
1503const fn should_retry_event_receive(error: &BackendError) -> bool {
1504    matches!(
1505        error.category,
1506        ClientErrorCategory::Network
1507            | ClientErrorCategory::Timeout
1508            | ClientErrorCategory::RateLimited
1509    )
1510}
1511
1512#[derive(Debug)]
1513enum ClientResponse {
1514    Empty,
1515    Status(ClientStatusSnapshot),
1516    AuthStart(AuthStartResult),
1517    AuthVerify(AuthVerifyResult),
1518    Dialogs(DialogsPage),
1519    AccountState(AccountStateSnapshot),
1520    ChatState(Box<ChatStateSnapshot>),
1521    History(HistoryPage),
1522    ChatParticipants(ChatParticipantsPage),
1523    CreatedChat(CreatedChat),
1524    Message(MessageMutation),
1525}
1526
1527#[cfg(test)]
1528mod tests {
1529    use crate::{
1530        AuthContactKind, AuthCredential, AuthToken, ClientStore, DialogRecord, HistoryRequest,
1531        InlineId, MediaKind, MessageContent, PeerRef,
1532    };
1533
1534    #[tokio::test]
1535    async fn lossless_subscriber_receives_events_that_overflow_broadcast_history() {
1536        let client = InlineClient::builder()
1537            .event_queue_capacity(1)
1538            .lossless_event_queue_capacity(3)
1539            .build()
1540            .spawn();
1541        let mut broadcast = client.subscribe();
1542        let mut lossless = client.take_lossless_events().unwrap();
1543        assert!(client.take_lossless_events().is_none());
1544
1545        for status in [
1546            ClientStatus::AuthRequired,
1547            ClientStatus::AuthExpired,
1548            ClientStatus::LoggedOut,
1549        ] {
1550            client.set_status(status, None).await.unwrap();
1551        }
1552
1553        let mut received = Vec::new();
1554        for _ in 0..3 {
1555            received.push(lossless.recv().await.unwrap());
1556        }
1557        let statuses = received
1558            .into_iter()
1559            .map(|event| match event {
1560                ClientEvent::StatusChanged { status, .. } => status,
1561                other => panic!("expected status event, got {other:?}"),
1562            })
1563            .collect::<Vec<_>>();
1564        assert_eq!(
1565            statuses,
1566            vec![
1567                ClientStatus::AuthRequired,
1568                ClientStatus::AuthExpired,
1569                ClientStatus::LoggedOut
1570            ]
1571        );
1572        assert!(matches!(
1573            broadcast.recv().await,
1574            Err(broadcast::error::RecvError::Lagged(2))
1575        ));
1576    }
1577
1578    #[tokio::test]
1579    async fn durable_lossless_delivery_remains_pending_until_consumer_ack() {
1580        let store = crate::InMemoryStore::new();
1581        let backend = crate::SdkBackend::builder()
1582            .store(store.clone())
1583            .build()
1584            .unwrap();
1585        let client = InlineClient::builder().backend(backend).build().spawn();
1586        let mut lossless = client.take_lossless_events().unwrap();
1587
1588        client
1589            .set_status(ClientStatus::AuthRequired, None)
1590            .await
1591            .unwrap();
1592        let delivery = lossless.recv_delivery().await.unwrap();
1593        assert!(delivery.delivery_id().is_some());
1594        assert_eq!(store.pending_client_events().await.unwrap().len(), 1);
1595        assert!(matches!(
1596            delivery.event(),
1597            ClientEvent::StatusChanged {
1598                status: ClientStatus::AuthRequired,
1599                ..
1600            }
1601        ));
1602
1603        delivery.ack().await.unwrap();
1604        assert!(store.pending_client_events().await.unwrap().is_empty());
1605        client.shutdown().await.unwrap();
1606    }
1607
1608    #[test]
1609    fn reconnect_backoff_is_bounded_and_uses_rate_limit_hints() {
1610        let policy = ReconnectPolicy {
1611            initial_delay: Duration::from_secs(1),
1612            max_delay: Duration::from_secs(8),
1613            rate_limit_initial_delay: Duration::from_secs(30),
1614            rate_limit_max_delay: Duration::from_secs(120),
1615            jitter_percent: 0,
1616        };
1617        let network = BackendError::new(ClientErrorCategory::Network, "offline");
1618        assert_eq!(
1619            event_retry_delay(&network, 0, policy),
1620            Duration::from_secs(1)
1621        );
1622        assert_eq!(
1623            event_retry_delay(&network, 1, policy),
1624            Duration::from_secs(2)
1625        );
1626        assert_eq!(
1627            event_retry_delay(&network, 20, policy),
1628            Duration::from_secs(8)
1629        );
1630
1631        let limited = BackendError::new(
1632            ClientErrorCategory::RateLimited,
1633            "FLOOD_WAIT_45: retry later",
1634        );
1635        assert_eq!(
1636            event_retry_delay(&limited, 0, policy),
1637            Duration::from_secs(45)
1638        );
1639        let typed_limited = BackendError::new(ClientErrorCategory::RateLimited, "slow down")
1640            .with_retry_after_seconds(75);
1641        assert_eq!(
1642            event_retry_delay(&typed_limited, 0, policy),
1643            Duration::from_secs(75)
1644        );
1645        assert_eq!(
1646            retry_after_hint("please retry after 12 seconds"),
1647            Some(Duration::from_secs(12))
1648        );
1649        assert_eq!(
1650            retry_after_hint("retry_after=19"),
1651            Some(Duration::from_secs(19))
1652        );
1653    }
1654
1655    use super::*;
1656
1657    fn token_connect() -> ConnectRequest {
1658        ConnectRequest::new(AuthCredential::AccessToken {
1659            token: AuthToken::try_new("token").unwrap(),
1660        })
1661    }
1662
1663    fn auth_verify_request() -> AuthVerifyRequest {
1664        AuthVerifyRequest {
1665            contact: "mo@example.com".to_owned(),
1666            kind: AuthContactKind::Email,
1667            code: "123456".to_owned(),
1668            challenge_token: None,
1669            device_name: Some("inline-client test".to_owned()),
1670            account_namespace: None,
1671        }
1672    }
1673
1674    #[tokio::test]
1675    async fn status_snapshot_returns_current_status() {
1676        let client = InlineClient::builder()
1677            .initial_status(ClientStatus::Connected)
1678            .build()
1679            .spawn();
1680
1681        let status = client.status_snapshot();
1682
1683        assert_eq!(status.status, ClientStatus::Connected);
1684        assert_eq!(status.failure, None);
1685    }
1686
1687    #[tokio::test]
1688    async fn set_status_emits_lossless_event() {
1689        let client = InlineClient::builder().build().spawn();
1690        let mut events = client.subscribe();
1691
1692        client
1693            .set_status(
1694                ClientStatus::AuthExpired,
1695                Some(ClientFailure::new(
1696                    ClientErrorCategory::AuthExpired,
1697                    "relogin required",
1698                )),
1699            )
1700            .await
1701            .unwrap();
1702
1703        let event = recv_until_event(&mut events, |event| {
1704            matches!(
1705                event,
1706                ClientEvent::StatusChanged {
1707                    status: ClientStatus::AuthExpired,
1708                    ..
1709                }
1710            )
1711        })
1712        .await;
1713        assert_eq!(event.reliability(), crate::EventReliability::Lossless);
1714        assert!(matches!(
1715            event,
1716            ClientEvent::StatusChanged {
1717                status: ClientStatus::AuthExpired,
1718                ..
1719            }
1720        ));
1721        assert_eq!(client.status(), ClientStatus::AuthExpired);
1722        assert_eq!(
1723            client.status_snapshot().failure.unwrap().category,
1724            ClientErrorCategory::AuthExpired
1725        );
1726    }
1727
1728    #[tokio::test]
1729    async fn connect_updates_status_and_emits_event() {
1730        let client = InlineClient::builder().build().spawn();
1731        let mut events = client.subscribe();
1732
1733        let status = client.connect(token_connect()).await.unwrap();
1734
1735        assert_eq!(status.status, ClientStatus::Connected);
1736        assert_eq!(client.status(), ClientStatus::Connected);
1737
1738        let event = recv_until_event(&mut events, |event| {
1739            matches!(
1740                event,
1741                ClientEvent::StatusChanged {
1742                    status: ClientStatus::Connected,
1743                    ..
1744                }
1745            )
1746        })
1747        .await;
1748        assert!(matches!(
1749            event,
1750            ClientEvent::StatusChanged {
1751                status: ClientStatus::Connected,
1752                ..
1753            }
1754        ));
1755    }
1756
1757    #[tokio::test]
1758    async fn runner_emits_backend_pushed_events_while_connected() {
1759        let backend = InMemoryBackend::new();
1760        let client = InlineClient::builder()
1761            .backend(backend.clone())
1762            .build()
1763            .spawn();
1764        let mut events = client.subscribe();
1765
1766        client.connect(token_connect()).await.unwrap();
1767        backend.push_event_batch(vec![ClientEvent::MessageDeleted {
1768            chat_id: InlineId::new(7),
1769            message_id: InlineId::new(99),
1770        }]);
1771
1772        let event = recv_until_event(&mut events, |event| {
1773            matches!(
1774                event,
1775                ClientEvent::MessageDeleted {
1776                    chat_id,
1777                    message_id,
1778                } if *chat_id == InlineId::new(7) && *message_id == InlineId::new(99)
1779            )
1780        })
1781        .await;
1782        assert_eq!(
1783            event,
1784            ClientEvent::MessageDeleted {
1785                chat_id: InlineId::new(7),
1786                message_id: InlineId::new(99),
1787            }
1788        );
1789    }
1790
1791    #[tokio::test]
1792    async fn runner_marks_reconnecting_when_event_receive_is_rate_limited() {
1793        let backend = InMemoryBackend::new();
1794        let client = InlineClient::builder()
1795            .backend(backend.clone())
1796            .build()
1797            .spawn();
1798        let mut events = client.subscribe();
1799
1800        client.connect(token_connect()).await.unwrap();
1801        backend.push_event_error(BackendError::new(
1802            ClientErrorCategory::RateLimited,
1803            "rate limited",
1804        ));
1805
1806        let event = recv_until_event(&mut events, |event| {
1807            matches!(
1808                event,
1809                ClientEvent::StatusChanged {
1810                    status: ClientStatus::Reconnecting,
1811                    ..
1812                }
1813            )
1814        })
1815        .await;
1816        assert!(matches!(
1817            event,
1818            ClientEvent::StatusChanged {
1819                status: ClientStatus::Reconnecting,
1820                ..
1821            }
1822        ));
1823        assert_eq!(client.status(), ClientStatus::Reconnecting);
1824    }
1825
1826    #[tokio::test]
1827    async fn auth_start_and_verify_flow_through_backend() {
1828        let client = InlineClient::builder().build().spawn();
1829        let mut events = client.subscribe();
1830
1831        let started = client
1832            .auth_start(AuthStartRequest {
1833                contact: "mo@example.com".to_owned(),
1834                kind: AuthContactKind::Email,
1835                device_name: Some("inline-client test".to_owned()),
1836            })
1837            .await
1838            .unwrap();
1839        assert!(started.existing_user);
1840        assert!(!started.needs_invite_code);
1841
1842        let verified = client.auth_verify(auth_verify_request()).await.unwrap();
1843        assert_eq!(verified.user_id, InlineId::new(1));
1844        assert_eq!(verified.account_namespace, "1");
1845        assert_eq!(verified.status.status, ClientStatus::Connected);
1846        assert_eq!(client.status(), ClientStatus::Connected);
1847
1848        let event = recv_until_event(&mut events, |event| {
1849            matches!(
1850                event,
1851                ClientEvent::StatusChanged {
1852                    status: ClientStatus::Connected,
1853                    ..
1854                }
1855            )
1856        })
1857        .await;
1858        assert!(matches!(
1859            event,
1860            ClientEvent::StatusChanged {
1861                status: ClientStatus::Connected,
1862                ..
1863            }
1864        ));
1865    }
1866
1867    #[tokio::test]
1868    async fn resume_without_session_reports_auth_required() {
1869        let client = InlineClient::builder().build().spawn();
1870        let mut events = client.subscribe();
1871
1872        let status = client.resume_session().await.unwrap();
1873
1874        assert_eq!(status.status, ClientStatus::AuthRequired);
1875        assert_eq!(client.status(), ClientStatus::AuthRequired);
1876
1877        let event = recv_until_event(&mut events, |event| {
1878            matches!(
1879                event,
1880                ClientEvent::StatusChanged {
1881                    status: ClientStatus::AuthRequired,
1882                    ..
1883                }
1884            )
1885        })
1886        .await;
1887        assert!(matches!(
1888            event,
1889            ClientEvent::StatusChanged {
1890                status: ClientStatus::AuthRequired,
1891                ..
1892            }
1893        ));
1894    }
1895
1896    #[tokio::test]
1897    async fn dialogs_and_history_flow_through_backend() {
1898        let backend = InMemoryBackend::new();
1899        backend.upsert_dialog(DialogRecord {
1900            chat_id: InlineId::new(7),
1901            peer_user_id: None,
1902            title: Some("general".to_owned()),
1903            last_message_id: None,
1904            synced_through_message_id: None,
1905            unread_count: Some(0),
1906            ..DialogRecord::new(InlineId::new(7))
1907        });
1908        backend.insert_message(crate::MessageRecord {
1909            chat_id: InlineId::new(7),
1910            message_id: InlineId::new(1),
1911            sender_id: InlineId::new(2),
1912            timestamp: 1,
1913            is_outgoing: false,
1914            content: MessageContent::Text {
1915                text: "hello".to_owned(),
1916            },
1917            reply_to_message_id: None,
1918            transaction: None,
1919        });
1920        let client = InlineClient::builder().backend(backend).build().spawn();
1921        client.connect(token_connect()).await.unwrap();
1922
1923        let dialogs = client.dialogs(DialogsRequest::default()).await.unwrap();
1924        assert_eq!(dialogs.dialogs.len(), 1);
1925        assert_eq!(dialogs.dialogs[0].chat_id, InlineId::new(7));
1926
1927        let history = client
1928            .history(HistoryRequest {
1929                chat_id: InlineId::new(7),
1930                limit: Some(10),
1931                before_message_id: None,
1932                after_message_id: None,
1933            })
1934            .await
1935            .unwrap();
1936        assert_eq!(history.messages.len(), 1);
1937        assert_eq!(history.messages[0].message_id, InlineId::new(1));
1938    }
1939
1940    #[tokio::test]
1941    async fn send_text_returns_mutation_and_emits_events() {
1942        let client = InlineClient::builder().build().spawn();
1943        client.connect(token_connect()).await.unwrap();
1944        let mut events = client.subscribe();
1945
1946        let mutation = client
1947            .send_text(SendTextRequest::new(
1948                PeerRef::Chat {
1949                    chat_id: InlineId::new(7),
1950                },
1951                "hello",
1952            ))
1953            .await
1954            .unwrap();
1955
1956        assert_eq!(mutation.message_id, Some(InlineId::new(1)));
1957        assert_eq!(mutation.state, Some(crate::TransactionState::Completed));
1958        assert!(mutation.failure.is_none());
1959        assert_eq!(
1960            mutation.transaction.final_message_id,
1961            Some(InlineId::new(1))
1962        );
1963
1964        let events = [
1965            events.recv().await.unwrap(),
1966            events.recv().await.unwrap(),
1967            events.recv().await.unwrap(),
1968        ];
1969        assert!(
1970            events
1971                .iter()
1972                .any(|event| matches!(event, ClientEvent::TransactionChanged(_)))
1973        );
1974        assert!(events.iter().any(|event| matches!(
1975            event,
1976            ClientEvent::MessageUpserted {
1977                chat_id: InlineId(7),
1978                message_id: InlineId(1)
1979            }
1980        )));
1981        assert!(events.iter().any(|event| matches!(
1982            event,
1983            ClientEvent::MessageStored { message }
1984                if message.chat_id == InlineId::new(7)
1985                    && message.message_id == InlineId::new(1)
1986        )));
1987    }
1988
1989    #[tokio::test]
1990    async fn edit_message_emits_stored_upsert_event() {
1991        let backend = InMemoryBackend::new();
1992        backend.insert_message(crate::MessageRecord {
1993            chat_id: InlineId::new(7),
1994            message_id: InlineId::new(1),
1995            sender_id: InlineId::new(2),
1996            timestamp: 1,
1997            is_outgoing: false,
1998            content: MessageContent::Text {
1999                text: "old".to_owned(),
2000            },
2001            reply_to_message_id: None,
2002            transaction: None,
2003        });
2004        let client = InlineClient::builder().backend(backend).build().spawn();
2005        client.connect(token_connect()).await.unwrap();
2006        let mut events = client.subscribe();
2007
2008        client
2009            .edit_message(EditMessageRequest {
2010                chat_id: InlineId::new(7),
2011                message_id: InlineId::new(1),
2012                text: "edited".to_owned(),
2013                external_id: None,
2014            })
2015            .await
2016            .unwrap();
2017
2018        match events.recv().await.unwrap() {
2019            ClientEvent::MessageStored { message } => {
2020                assert_eq!(message.chat_id, InlineId::new(7));
2021                assert_eq!(message.message_id, InlineId::new(1));
2022                assert_eq!(
2023                    message.content,
2024                    MessageContent::Text {
2025                        text: "edited".to_owned()
2026                    }
2027                );
2028            }
2029            other => panic!("unexpected event: {other:?}"),
2030        }
2031    }
2032
2033    #[tokio::test]
2034    async fn send_media_emits_message_events() {
2035        let client = InlineClient::builder().build().spawn();
2036        client.connect(token_connect()).await.unwrap();
2037        let mut events = client.subscribe();
2038
2039        let mutation = client
2040            .send_media(
2041                UploadRequest {
2042                    peer: PeerRef::Chat {
2043                        chat_id: InlineId::new(7),
2044                    },
2045                    kind: MediaKind::Photo,
2046                    file_name: Some("image.png".to_owned()),
2047                    mime_type: Some("image/png".to_owned()),
2048                    size_bytes: Some(4),
2049                    caption: Some("caption".to_owned()),
2050                    width: Some(10),
2051                    height: Some(10),
2052                    duration_ms: None,
2053                    external_id: None,
2054                    random_id: None,
2055                    reply_to_message_id: None,
2056                },
2057                vec![1, 2, 3, 4],
2058            )
2059            .await
2060            .unwrap();
2061
2062        assert_eq!(mutation.message_id, Some(InlineId::new(1)));
2063
2064        let _transaction = events.recv().await.unwrap();
2065        let _upsert = events.recv().await.unwrap();
2066        match events.recv().await.unwrap() {
2067            ClientEvent::MessageStored { message } => {
2068                assert_eq!(message.chat_id, InlineId::new(7));
2069                assert_eq!(message.message_id, InlineId::new(1));
2070                match message.content {
2071                    MessageContent::Media {
2072                        kind,
2073                        file_name,
2074                        caption,
2075                        ..
2076                    } => {
2077                        assert_eq!(kind, MediaKind::Photo);
2078                        assert_eq!(file_name.as_deref(), Some("image.png"));
2079                        assert_eq!(caption.as_deref(), Some("caption"));
2080                    }
2081                    other => panic!("unexpected content: {other:?}"),
2082                }
2083            }
2084            other => panic!("unexpected event: {other:?}"),
2085        }
2086    }
2087
2088    #[tokio::test]
2089    async fn shutdown_stops_runner() {
2090        let client = InlineClient::builder().build().spawn();
2091
2092        client.shutdown().await.unwrap();
2093        let err = client
2094            .dialogs(DialogsRequest::default())
2095            .await
2096            .expect_err("runner should reject commands after shutdown");
2097
2098        assert!(matches!(
2099            err,
2100            ClientRequestError::Command(
2101                ClientCommandError::Closed | ClientCommandError::ResponseDropped
2102            )
2103        ));
2104    }
2105
2106    async fn recv_until_event(
2107        events: &mut broadcast::Receiver<ClientEvent>,
2108        matches: impl Fn(&ClientEvent) -> bool,
2109    ) -> ClientEvent {
2110        tokio::time::timeout(Duration::from_secs(3), async {
2111            loop {
2112                let event = events.recv().await.unwrap();
2113                if matches(&event) {
2114                    return event;
2115                }
2116            }
2117        })
2118        .await
2119        .expect("expected matching client event")
2120    }
2121}