Skip to main content

matrix_sdk_ui/
sync_service.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for that specific language governing permissions and
13// limitations under the License.
14
15//! Unified API for both the Room List API and the Encryption Sync API, that
16//! takes care of all the underlying details.
17//!
18//! This is an opiniated way to run both APIs, with high-level callbacks that
19//! should be called in reaction to user actions and/or system events.
20//!
21//! The sync service will signal errors via its [`state`](SyncService::state)
22//! that the user MUST observe. Whenever an error/termination is observed, the
23//! user should call [`SyncService::start()`] again to restart the room list
24//! sync, if that is not desirable, the offline support for the [`SyncService`]
25//! may be enabled using the [`SyncServiceBuilder::with_offline_mode`] setting.
26
27use std::{pin::pin, sync::Arc, time::Duration};
28
29use eyeball::{SharedObservable, Subscriber};
30use futures_util::{
31    StreamExt as _,
32    future::{Either, select},
33};
34use matrix_sdk::{
35    Client,
36    config::RequestConfig,
37    executor::{JoinHandle, spawn},
38    sleep::sleep,
39};
40use thiserror::Error;
41use tokio::sync::{
42    Mutex as AsyncMutex, OwnedMutexGuard,
43    mpsc::{Receiver, Sender},
44};
45use tracing::{Instrument, Level, Span, error, info, instrument, trace, warn};
46
47use crate::{
48    encryption_sync_service::{self, EncryptionSyncPermit, EncryptionSyncService},
49    room_list_service::{
50        self, DEFAULT_CONNECTION_ID, DEFAULT_LIST_TIMELINE_LIMIT, RoomListService,
51    },
52};
53
54/// Current state of the application.
55///
56/// This is a high-level state indicating what's the status of the underlying
57/// syncs. The application starts in [`State::Running`] mode, and then hits a
58/// terminal state [`State::Terminated`] (if it gracefully exited) or
59/// [`State::Error`] (in case any of the underlying syncs ran into an error).
60///
61/// This can be observed with [`SyncService::state`].
62#[derive(Clone, Debug)]
63pub enum State {
64    /// The service hasn't ever been started yet, or has been stopped.
65    Idle,
66
67    /// The underlying syncs are properly running in the background.
68    Running,
69
70    /// Any of the underlying syncs has terminated gracefully (i.e. be stopped).
71    Terminated,
72
73    /// Any of the underlying syncs has ran into an error.
74    ///
75    /// The associated [`enum@Error`] is inside an [`Arc`] to (i) make [`State`]
76    /// cloneable, and to (ii) not make it heavier.
77    Error(Arc<Error>),
78
79    /// The service has entered offline mode. This state will only be entered if
80    /// the [`SyncService`] has been built with the
81    /// [`SyncServiceBuilder::with_offline_mode`] setting.
82    ///
83    /// The [`SyncService`] will enter the offline mode if syncing with the
84    /// server fails, it will then periodically check if the server is available
85    /// using the `/_matrix/client/versions` endpoint.
86    ///
87    /// Once the [`SyncService`] receives a 200 response from the
88    /// `/_matrix/client/versions` endpoint, it will go back into the
89    /// [`State::Running`] mode and attempt to sync again.
90    ///
91    /// Calling [`SyncService::start()`] while in this state will abort the
92    /// `/_matrix/client/versions` checks and attempt to sync immediately.
93    ///
94    /// Calling [`SyncService::stop()`] will abort the offline mode and the
95    /// [`SyncService`] will go into the [`State::Idle`] mode.
96    Offline,
97}
98
99enum MaybeAcquiredPermit {
100    Acquired(OwnedMutexGuard<EncryptionSyncPermit>),
101    Unacquired(Arc<AsyncMutex<EncryptionSyncPermit>>),
102}
103
104impl MaybeAcquiredPermit {
105    async fn acquire(self) -> OwnedMutexGuard<EncryptionSyncPermit> {
106        match self {
107            MaybeAcquiredPermit::Acquired(owned_mutex_guard) => owned_mutex_guard,
108            MaybeAcquiredPermit::Unacquired(lock) => lock.lock_owned().await,
109        }
110    }
111}
112
113/// A supervisor responsible for managing two sync tasks: one for handling the
114/// room list and another for supporting end-to-end encryption.
115///
116/// The two sync tasks are spawned as child tasks and are contained within the
117/// supervising task, which is stored in the [`SyncTaskSupervisor::task`] field.
118///
119/// The supervisor ensures the two child tasks are managed as a single unit,
120/// allowing for them to be shutdown in unison.
121struct SyncTaskSupervisor {
122    /// The supervising task that manages and contains the two sync child tasks.
123    task: JoinHandle<()>,
124    /// [`TerminationReport`] sender for the [`SyncTaskSupervisor::shutdown()`]
125    /// function.
126    termination_sender: Sender<TerminationReport>,
127}
128
129impl SyncTaskSupervisor {
130    async fn new(
131        inner: &SyncServiceInner,
132        room_list_service: Arc<RoomListService>,
133        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
134    ) -> Self {
135        let (task, termination_sender) =
136            Self::spawn_supervisor_task(inner, room_list_service, encryption_sync_permit).await;
137
138        Self { task, termination_sender }
139    }
140
141    /// Check if a homeserver is reachable.
142    ///
143    /// This function handles the offline mode by waiting for either a
144    /// termination report or a successful `/_matrix/client/versions` response.
145    ///
146    /// This function waits for two conditions:
147    ///
148    /// 1. Waiting for a termination report: This ensures that the user can exit
149    ///    offline mode and attempt to restart the [`SyncService`] manually.
150    ///
151    /// 2. Waiting to come back online: This continuously checks server
152    ///    availability.
153    ///
154    /// If the `/_matrix/client/versions` request succeeds, the function exits
155    /// without a termination report. If we receive a [`TerminationReport`] from
156    /// the user, we exit immediately and return the termination report.
157    async fn offline_check(
158        client: &Client,
159        receiver: &mut Receiver<TerminationReport>,
160    ) -> Option<TerminationReport> {
161        info!("Entering the offline mode");
162
163        let wait_for_termination_report = async {
164            loop {
165                // Since we didn't empty the channel when entering the offline
166                // mode in fear that we might miss a report with the
167                // `TerminationOrigin::Supervisor` origin and the channel might
168                // contain stale reports from one of the sync services, in case
169                // both of them have sent a report, let's ignore all reports we
170                // receive from the sync services.
171                let report =
172                    receiver.recv().await.unwrap_or_else(TerminationReport::supervisor_error);
173
174                match report.origin {
175                    TerminationOrigin::EncryptionSync | TerminationOrigin::RoomList => {}
176                    // Since the sync service aren't running anymore, we can
177                    // only receive a report from the supervisor. It would have
178                    // probably made sense to have separate channels for reports
179                    // the sync services send and the user can send using the
180                    // `SyncService::stop()` method.
181                    TerminationOrigin::Supervisor => break report,
182                }
183            }
184        };
185
186        let wait_to_be_online = async move {
187            loop {
188                // Encountering network failures when sending a request which
189                // has with no retry limit set in the `RequestConfig` are
190                // treated as permanent failures and our exponential backoff
191                // doesn't kick in.
192                //
193                // Let's set a retry limit so network failures are retried as
194                // well.
195                //
196                // We only use /versions to check whether the server is
197                // reachable, we can skip authentication for this.
198                let request_config = RequestConfig::default().retry_limit(5).skip_auth();
199
200                // We're in an infinite loop, but our request sending already
201                // has an exponential backoff set up. This will kick in for any
202                // request errors that we consider to be transient. Common
203                // network errors (timeouts, DNS failures) or any server error
204                // in the 5xx range of HTTP errors are considered to be
205                // transient.
206                //
207                // Still, as a precaution, we're going to sleep here for a while
208                // in the Error case.
209                match client.fetch_server_versions(Some(request_config)).await {
210                    Ok(_) => break,
211                    Err(_) => sleep(Duration::from_millis(100)).await,
212                }
213            }
214        };
215
216        let wait_for_termination_report = pin!(wait_for_termination_report);
217        let wait_to_be_online = pin!(wait_to_be_online);
218
219        let maybe_termination_report = select(wait_for_termination_report, wait_to_be_online).await;
220
221        let report = match maybe_termination_report {
222            Either::Left((termination_report, _)) => Some(termination_report),
223            Either::Right((_, _)) => None,
224        };
225
226        info!("Exiting offline mode: {report:?}");
227
228        report
229    }
230
231    /// The role of the supervisor task is to wait for a termination message
232    /// ([`TerminationReport`]), sent either because we wanted to stop both
233    /// syncs, or because one of the syncs failed (in which case we'll stop the
234    /// other one too).
235    async fn spawn_supervisor_task(
236        inner: &SyncServiceInner,
237        room_list_service: Arc<RoomListService>,
238        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
239    ) -> (JoinHandle<()>, Sender<TerminationReport>) {
240        let (sender, mut receiver) = tokio::sync::mpsc::channel(16);
241
242        let encryption_sync = inner.encryption_sync_service.clone();
243        let state = inner.state.clone();
244        let termination_sender = sender.clone();
245
246        // When we first start, and don't use offline mode, we want to acquire
247        // the sync permit before we enter a future that might be polled at a
248        // later time, this means that the permit will be acquired as soon as
249        // this future, the one the `spawn_supervisor_task` function creates, is
250        // awaited.
251        //
252        // In other words, once `sync_service.start().await` is finished, the
253        // permit will be in the acquired state.
254        let mut sync_permit_guard =
255            MaybeAcquiredPermit::Acquired(encryption_sync_permit.clone().lock_owned().await);
256
257        let offline_mode = inner.with_offline_mode;
258        let parent_span = inner.parent_span.clone();
259
260        let future = async move {
261            loop {
262                let (room_list_task, encryption_sync_task) = Self::spawn_child_tasks(
263                    room_list_service.clone(),
264                    encryption_sync.clone(),
265                    sync_permit_guard,
266                    sender.clone(),
267                    parent_span.clone(),
268                )
269                .await;
270
271                sync_permit_guard = MaybeAcquiredPermit::Unacquired(encryption_sync_permit.clone());
272
273                let report = if let Some(report) = receiver.recv().await {
274                    report
275                } else {
276                    info!("internal channel has been closed?");
277                    // We should still stop the child tasks in the unlikely
278                    // scenario that our receiver died.
279                    TerminationReport::supervisor_error()
280                };
281
282                // If one service failed, make sure to request stopping the
283                // other one.
284                let (stop_room_list, stop_encryption) = match &report.origin {
285                    TerminationOrigin::EncryptionSync => (true, false),
286                    TerminationOrigin::RoomList => (false, true),
287                    TerminationOrigin::Supervisor => (true, true),
288                };
289
290                // Stop both services, and wait for the streams to properly
291                // finish: at some point they'll return `None` and will exit
292                // their infinite loops, and their tasks will gracefully
293                // terminate.
294
295                if stop_room_list {
296                    if let Err(err) = room_list_service.stop_sync() {
297                        warn!(?report, "unable to stop room list service: {err:#}");
298                    }
299
300                    if report.has_expired() {
301                        room_list_service.expire_sync_session().await;
302                    }
303                }
304
305                if let Err(err) = room_list_task.await {
306                    error!("when awaiting room list service: {err:#}");
307                }
308
309                if stop_encryption {
310                    if let Err(err) = encryption_sync.stop_sync() {
311                        warn!(?report, "unable to stop encryption sync: {err:#}");
312                    }
313
314                    if report.has_expired() {
315                        encryption_sync.expire_sync_session().await;
316                    }
317                }
318
319                if let Err(err) = encryption_sync_task.await {
320                    error!("when awaiting encryption sync: {err:#}");
321                }
322
323                let has_unknown_token = report.has_unknown_token();
324
325                if let Some(error) = report.error {
326                    if offline_mode & !has_unknown_token {
327                        state.set(State::Offline);
328
329                        let client = room_list_service.client();
330
331                        if let Some(report) = Self::offline_check(client, &mut receiver).await {
332                            if let Some(error) = report.error {
333                                state.set(State::Error(Arc::new(error)));
334                            } else {
335                                state.set(State::Idle);
336                            }
337                            break;
338                        }
339
340                        state.set(State::Running);
341                    } else {
342                        state.set(State::Error(Arc::new(error)));
343                        break;
344                    }
345                } else if matches!(report.origin, TerminationOrigin::Supervisor) {
346                    state.set(State::Idle);
347                    break;
348                } else {
349                    state.set(State::Terminated);
350                    break;
351                }
352            }
353        }
354        .instrument(tracing::span!(Level::WARN, "supervisor task"));
355
356        let task = spawn(future);
357
358        (task, termination_sender)
359    }
360
361    async fn spawn_child_tasks(
362        room_list_service: Arc<RoomListService>,
363        encryption_sync_service: Arc<EncryptionSyncService>,
364        sync_permit_guard: MaybeAcquiredPermit,
365        sender: Sender<TerminationReport>,
366        parent_span: Span,
367    ) -> (JoinHandle<()>, JoinHandle<()>) {
368        // First, take care of the room list.
369        let room_list_task = spawn(
370            Self::room_list_sync_task(room_list_service, sender.clone())
371                .instrument(parent_span.clone()),
372        );
373
374        // Then, take care of the encryption sync.
375        let encryption_sync_task = spawn(
376            Self::encryption_sync_task(
377                encryption_sync_service,
378                sender.clone(),
379                sync_permit_guard.acquire().await,
380            )
381            .instrument(parent_span),
382        );
383
384        (room_list_task, encryption_sync_task)
385    }
386
387    async fn encryption_sync_task(
388        encryption_sync: Arc<EncryptionSyncService>,
389        sender: Sender<TerminationReport>,
390        sync_permit_guard: OwnedMutexGuard<EncryptionSyncPermit>,
391    ) {
392        let encryption_sync_stream = encryption_sync.sync(sync_permit_guard);
393        let mut encryption_sync_stream = pin!(encryption_sync_stream);
394
395        let termination_report = loop {
396            match encryption_sync_stream.next().await {
397                Some(Ok(())) => {
398                    // Carry on.
399                }
400                Some(Err(error)) => {
401                    let termination_report = TerminationReport::encryption_sync(Some(error));
402
403                    if !termination_report.has_expired() {
404                        error!(
405                            "Error while processing encryption in sync service: {:#?}",
406                            termination_report.error
407                        );
408                    }
409
410                    break termination_report;
411                }
412                None => {
413                    // The stream has ended.
414                    break TerminationReport::encryption_sync(None);
415                }
416            }
417        };
418
419        if let Err(err) = sender.send(termination_report).await {
420            error!("Error while sending termination report: {err:#}");
421        }
422    }
423
424    async fn room_list_sync_task(
425        room_list_service: Arc<RoomListService>,
426        sender: Sender<TerminationReport>,
427    ) {
428        let room_list_stream = room_list_service.sync();
429        let mut room_list_stream = pin!(room_list_stream);
430
431        let termination_report = loop {
432            match room_list_stream.next().await {
433                Some(Ok(())) => {
434                    // Carry on.
435                }
436                Some(Err(error)) => {
437                    let termination_report = TerminationReport::room_list(Some(error));
438
439                    if !termination_report.has_expired() {
440                        error!(
441                            "Error while processing room list in sync service: {:#?}",
442                            termination_report.error
443                        );
444                    }
445
446                    break termination_report;
447                }
448                None => {
449                    // The stream has ended.
450                    break TerminationReport::room_list(None);
451                }
452            }
453        };
454
455        if let Err(err) = sender.send(termination_report).await {
456            error!("Error while sending termination report: {err:#}");
457        }
458    }
459
460    async fn shutdown(self) {
461        match self.termination_sender.send(TerminationReport::supervisor()).await {
462            Ok(_) => {
463                let _ = self.task.await.inspect_err(|err| {
464                    // A `JoinError` indicates that the task was already dead,
465                    // either because it got cancelled or because it panicked.
466                    // We only cancel the task in the Err branch below and the
467                    // task shouldn't be able to panic.
468                    //
469                    // So let's log an error and return.
470                    error!("The supervisor task has stopped unexpectedly: {err:?}");
471                });
472            }
473            Err(err) => {
474                error!("Couldn't send the termination report to the supervisor task: {err}");
475                // Let's abort the task if it won't shut down properly,
476                // otherwise we would have left it as a detached task.
477                self.task.abort();
478            }
479        }
480    }
481}
482
483struct SyncServiceInner {
484    encryption_sync_service: Arc<EncryptionSyncService>,
485
486    /// Is the offline mode for the [`SyncService`] enabled?
487    ///
488    /// The offline mode is described in the [`State::Offline`] enum variant.
489    with_offline_mode: bool,
490
491    /// What's the state of this sync service?
492    state: SharedObservable<State>,
493
494    /// The parent tracing span to use for the tasks within this service.
495    ///
496    /// Normally this will be [`Span::none`], but it may be useful to assign a
497    /// defined span, for example if there is more than one active sync service.
498    parent_span: Span,
499
500    /// Supervisor task ensuring proper termination.
501    ///
502    /// This task is waiting for a [`TerminationReport`] from any of the other
503    /// two tasks, or from a user request via [`SyncService::stop()`]. It makes
504    /// sure that the two services are properly shut up and just interrupted.
505    ///
506    /// This is set at the same time as the other two tasks.
507    supervisor: Option<SyncTaskSupervisor>,
508}
509
510impl SyncServiceInner {
511    async fn start(
512        &mut self,
513        room_list_service: Arc<RoomListService>,
514        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
515    ) {
516        trace!("starting sync service");
517
518        self.supervisor =
519            Some(SyncTaskSupervisor::new(self, room_list_service, encryption_sync_permit).await);
520        self.state.set(State::Running);
521    }
522
523    async fn stop(&mut self) {
524        trace!("pausing sync service");
525
526        // Remove the supervisor from our state and request the tasks to be
527        // shutdown.
528        if let Some(supervisor) = self.supervisor.take() {
529            supervisor.shutdown().await;
530        } else {
531            error!("The sync service was not properly started, the supervisor task doesn't exist");
532        }
533    }
534
535    async fn restart(
536        &mut self,
537        room_list_service: Arc<RoomListService>,
538        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
539    ) {
540        self.stop().await;
541        self.start(room_list_service, encryption_sync_permit).await;
542    }
543}
544
545/// A high level manager for your Matrix syncing needs.
546///
547/// The [`SyncService`] is responsible for managing real-time synchronization
548/// with a Matrix server. It can initiate and maintain the necessary
549/// synchronization tasks for you.
550///
551/// **Note**: The [`SyncService`] requires a server with support for [MSC4186],
552/// otherwise it will fail with an 404 `M_UNRECOGNIZED` request error.
553///
554/// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186/
555///
556/// # Example
557///
558/// ```no_run
559/// use matrix_sdk::Client;
560/// use matrix_sdk_ui::sync_service::{State, SyncService};
561/// # use url::Url;
562/// # async {
563/// let homeserver = Url::parse("http://example.com")?;
564/// let client = Client::new(homeserver).await?;
565///
566/// client
567///     .matrix_auth()
568///     .login_username("example", "wordpass")
569///     .initial_device_display_name("My bot")
570///     .await?;
571///
572/// let sync_service = SyncService::builder(client).build().await?;
573/// let mut state = sync_service.state();
574///
575/// while let Some(state) = state.next().await {
576///     match state {
577///         State::Idle => eprintln!("The sync service is idle."),
578///         State::Running => eprintln!("The sync has started to run."),
579///         State::Offline => eprintln!(
580///             "We have entered the offline mode, the server seems to be
581///              unavailable"
582///         ),
583///         State::Terminated => {
584///             eprintln!("The sync service has been gracefully terminated");
585///             break;
586///         }
587///         State::Error(_) => {
588///             eprintln!("The sync service has run into an error");
589///             break;
590///         }
591///     }
592/// }
593/// # anyhow::Ok(()) };
594/// ```
595pub struct SyncService {
596    inner: Arc<AsyncMutex<SyncServiceInner>>,
597
598    /// Room list service used to synchronize the rooms state.
599    room_list_service: Arc<RoomListService>,
600
601    /// What's the state of this sync service? This field is replicated from the
602    /// [`SyncServiceInner`] struct, but it should not be modified in this
603    /// struct. It's re-exposed here so we can subscribe to the state without
604    /// taking the lock on the `inner` field.
605    state: SharedObservable<State>,
606
607    /// Global lock to allow using at most one [`EncryptionSyncService`] at all
608    /// times.
609    ///
610    /// This ensures that there's only one ever existing in the application's
611    /// lifetime (under the assumption that there is at most one [`SyncService`]
612    /// per application).
613    encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
614}
615
616impl SyncService {
617    /// Create a new builder for configuring an `SyncService`.
618    pub fn builder(client: Client) -> SyncServiceBuilder {
619        SyncServiceBuilder::new(client)
620    }
621
622    /// Get the underlying `RoomListService` instance for easier access to its
623    /// methods.
624    pub fn room_list_service(&self) -> Arc<RoomListService> {
625        self.room_list_service.clone()
626    }
627
628    /// Returns the state of the sync service.
629    pub fn state(&self) -> Subscriber<State> {
630        self.state.subscribe()
631    }
632
633    /// Start (or restart) the underlying sliding syncs.
634    ///
635    /// This can be called multiple times safely:
636    ///
637    /// - if the stream is still properly running, it won't be restarted.
638    /// - if the [`SyncService`] is in the offline mode we will exit the offline
639    ///   mode and immediately attempt to sync again.
640    /// - if the stream has been aborted before, it will be properly cleaned up
641    ///   and restarted.
642    pub async fn start(&self) {
643        let mut inner = self.inner.lock().await;
644
645        // Only (re)start the tasks if it's stopped or if we're in the offline
646        // mode.
647        match inner.state.get() {
648            // If we're already running, there's nothing to do.
649            State::Running => {}
650            // If we're in the offline mode, first stop the service and then start it again.
651            State::Offline => {
652                inner
653                    .restart(self.room_list_service.clone(), self.encryption_sync_permit.clone())
654                    .await
655            }
656            // Otherwise just start.
657            State::Idle | State::Terminated | State::Error(_) => {
658                inner
659                    .start(self.room_list_service.clone(), self.encryption_sync_permit.clone())
660                    .await
661            }
662        }
663    }
664
665    /// Stop the underlying sliding syncs.
666    ///
667    /// This must be called when the app goes into the background. It's better
668    /// to call this API when the application exits, although not strictly
669    /// necessary.
670    #[instrument(skip_all)]
671    pub async fn stop(&self) {
672        let mut inner = self.inner.lock().await;
673
674        match inner.state.get() {
675            State::Idle | State::Terminated | State::Error(_) => {
676                // No need to stop if we were not running.
677                return;
678            }
679            State::Running | State::Offline => {}
680        }
681
682        inner.stop().await;
683    }
684
685    /// Force expiring both sessions.
686    ///
687    /// This ensures that the sync service is stopped before expiring both
688    /// sessions. It should be used sparingly, as it will cause a restart of the
689    /// sessions on the server as well.
690    #[instrument(skip_all)]
691    pub async fn expire_sessions(&self) {
692        // First, stop the sync service if it was running; it's a no-op if it
693        // was already stopped.
694        self.stop().await;
695
696        // Expire the room list sync session.
697        self.room_list_service.expire_sync_session().await;
698
699        // Expire the encryption sync session.
700        self.inner.lock().await.encryption_sync_service.expire_sync_session().await;
701    }
702
703    /// Attempt to get a permit to use an `EncryptionSyncService` at a given
704    /// time.
705    ///
706    /// This ensures there is at most one [`EncryptionSyncService`] active at
707    /// any time, per application.
708    pub fn try_get_encryption_sync_permit(&self) -> Option<OwnedMutexGuard<EncryptionSyncPermit>> {
709        self.encryption_sync_permit.clone().try_lock_owned().ok()
710    }
711}
712
713#[derive(Debug)]
714enum TerminationOrigin {
715    EncryptionSync,
716    RoomList,
717    Supervisor,
718}
719
720#[derive(Debug)]
721struct TerminationReport {
722    /// The origin of the termination.
723    origin: TerminationOrigin,
724
725    /// If the termination is due to an error, this is the cause.
726    error: Option<Error>,
727}
728
729impl TerminationReport {
730    /// Create a new [`TerminationReport`] with `origin` set to
731    /// [`TerminationOrigin::EncryptionSync`] and `error` set to
732    /// [`Error::EncryptionSync`].
733    fn encryption_sync(error: Option<encryption_sync_service::Error>) -> Self {
734        Self { origin: TerminationOrigin::EncryptionSync, error: error.map(Error::EncryptionSync) }
735    }
736
737    /// Create a new [`TerminationReport`] with `origin` set to
738    /// [`TerminationOrigin::RoomList`] and `error` set to [`Error::RoomList`].
739    fn room_list(error: Option<room_list_service::Error>) -> Self {
740        Self { origin: TerminationOrigin::RoomList, error: error.map(Error::RoomList) }
741    }
742
743    /// Create a new [`TerminationReport`] with `origin` set to
744    /// [`TerminationOrigin::Supervisor`] and `error` set to
745    /// [`Error::Supervisor`].
746    fn supervisor_error() -> Self {
747        Self { origin: TerminationOrigin::Supervisor, error: Some(Error::Supervisor) }
748    }
749
750    /// Create a new [`TerminationReport`] with `origin` set to
751    /// [`TerminationOrigin::Supervisor`] and `error` set to `None`.
752    fn supervisor() -> Self {
753        Self { origin: TerminationOrigin::Supervisor, error: None }
754    }
755
756    /// Check whether the termination is due to an expired sliding sync session.
757    fn has_expired(&self) -> bool {
758        match &self.error {
759            Some(Error::RoomList(room_list_service::Error::SlidingSync(error)))
760            | Some(Error::EncryptionSync(encryption_sync_service::Error::SlidingSync(error))) => {
761                error.client_api_error_kind() == Some(&ruma::api::error::ErrorKind::UnknownPos)
762            }
763            _ => false,
764        }
765    }
766
767    /// Check whether the termination is due to an unknown access token without
768    /// a soft logout.
769    fn has_unknown_token(&self) -> bool {
770        let (Some(Error::RoomList(room_list_service::Error::SlidingSync(error)))
771        | Some(Error::EncryptionSync(encryption_sync_service::Error::SlidingSync(error)))) =
772            &self.error
773        else {
774            return false;
775        };
776
777        matches!(
778            error.client_api_error_kind(),
779            Some(ruma::api::error::ErrorKind::UnknownToken(data))
780                if !data.soft_logout
781        )
782    }
783}
784
785// Testing helpers, mostly.
786#[doc(hidden)]
787impl SyncService {
788    /// Is the task supervisor running?
789    pub async fn is_supervisor_running(&self) -> bool {
790        self.inner.lock().await.supervisor.is_some()
791    }
792}
793
794#[derive(Clone)]
795pub struct SyncServiceBuilder {
796    /// SDK client.
797    client: Client,
798
799    /// Is the offline mode for the [`SyncService`] enabled?
800    ///
801    /// The offline mode is described in the [`State::Offline`] enum variant.
802    with_offline_mode: bool,
803
804    /// Whether to turn [`SlidingSyncBuilder::share_pos`] on or off.
805    ///
806    /// [`SlidingSyncBuilder::share_pos`]: matrix_sdk::sliding_sync::SlidingSyncBuilder::share_pos
807    with_share_pos: bool,
808
809    /// Custom connection ID for the room list service. Defaults to
810    /// [`room_list_service::DEFAULT_CONNECTION_ID`]. Use a different value for
811    /// secondary processes such as iOS share extensions that are not meant to
812    /// reuse the main app's connection.
813    room_list_conn_id: String,
814
815    /// Custom timeline limit for the room list service. Defaults to
816    /// [`room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`].
817    room_list_timeline_limit: u32,
818
819    /// The parent tracing span to use for the tasks within this service.
820    ///
821    /// Normally this will be [`Span::none`], but it may be useful to assign a
822    /// defined span, for example if there is more than one active sync service.
823    parent_span: Span,
824}
825
826impl SyncServiceBuilder {
827    fn new(client: Client) -> Self {
828        Self {
829            client,
830            with_offline_mode: false,
831            with_share_pos: true,
832            room_list_conn_id: DEFAULT_CONNECTION_ID.to_owned(),
833            room_list_timeline_limit: DEFAULT_LIST_TIMELINE_LIMIT,
834            parent_span: Span::none(),
835        }
836    }
837
838    /// Enable the "offline" mode for the [`SyncService`].
839    ///
840    /// To learn more about the "offline" mode read the documentation for the
841    /// [`State::Offline`] enum variant.
842    pub fn with_offline_mode(mut self) -> Self {
843        self.with_offline_mode = true;
844        self
845    }
846
847    /// Whether to turn [`SlidingSyncBuilder::share_pos`] on or off.
848    ///
849    /// [`SlidingSyncBuilder::share_pos`]: matrix_sdk::sliding_sync::SlidingSyncBuilder::share_pos
850    pub fn with_share_pos(mut self, enable: bool) -> Self {
851        self.with_share_pos = enable;
852        self
853    }
854
855    /// Set a custom conn_id for the room list sliding sync connection.
856    pub fn with_room_list_conn_id(mut self, conn_id: String) -> Self {
857        self.room_list_conn_id = conn_id;
858        self
859    }
860
861    /// Set a custom timeline limit for the room list service.
862    pub fn with_room_list_timeline_limit(mut self, limit: u32) -> Self {
863        self.room_list_timeline_limit = limit;
864        self
865    }
866
867    /// Set the parent tracing span to be used for the tasks within this
868    /// service.
869    pub fn with_parent_span(mut self, parent_span: Span) -> Self {
870        self.parent_span = parent_span;
871        self
872    }
873
874    /// Finish setting up the [`SyncService`].
875    ///
876    /// This creates the underlying sliding syncs, and will _not_ start them in
877    /// the background. The resulting [`SyncService`] must be kept alive as long
878    /// as the sliding syncs are supposed to run.
879    pub async fn build(self) -> Result<SyncService, Error> {
880        let Self {
881            client,
882            with_offline_mode,
883            with_share_pos,
884            room_list_conn_id,
885            room_list_timeline_limit,
886            parent_span,
887        } = self;
888
889        let encryption_sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new()));
890
891        let room_list = RoomListService::new_with(
892            client.clone(),
893            with_share_pos,
894            &room_list_conn_id,
895            room_list_timeline_limit,
896        )
897        .await?;
898
899        let encryption_sync = Arc::new(EncryptionSyncService::new(client, None).await?);
900
901        let room_list_service = Arc::new(room_list);
902        let state = SharedObservable::new(State::Idle);
903
904        Ok(SyncService {
905            state: state.clone(),
906            room_list_service,
907            encryption_sync_permit,
908            inner: Arc::new(AsyncMutex::new(SyncServiceInner {
909                supervisor: None,
910                encryption_sync_service: encryption_sync,
911                state,
912                with_offline_mode,
913                parent_span,
914            })),
915        })
916    }
917}
918
919/// Errors for the [`SyncService`] API.
920#[derive(Debug, Error)]
921pub enum Error {
922    /// An error received from the `RoomListService` API.
923    #[error(transparent)]
924    RoomList(#[from] room_list_service::Error),
925
926    /// An error received from the `EncryptionSyncService` API.
927    #[error(transparent)]
928    EncryptionSync(#[from] encryption_sync_service::Error),
929
930    /// An error had occurred in the sync task supervisor, likely due to a bug.
931    #[error("the supervisor channel has run into an unexpected error")]
932    Supervisor,
933}