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                let request_config = RequestConfig::default().retry_limit(5);
196
197                // We're in an infinite loop, but our request sending already
198                // has an exponential backoff set up. This will kick in for any
199                // request errors that we consider to be transient. Common
200                // network errors (timeouts, DNS failures) or any server error
201                // in the 5xx range of HTTP errors are considered to be
202                // transient.
203                //
204                // Still, as a precaution, we're going to sleep here for a while
205                // in the Error case.
206                match client.fetch_server_versions(Some(request_config)).await {
207                    Ok(_) => break,
208                    Err(_) => sleep(Duration::from_millis(100)).await,
209                }
210            }
211        };
212
213        let wait_for_termination_report = pin!(wait_for_termination_report);
214        let wait_to_be_online = pin!(wait_to_be_online);
215
216        let maybe_termination_report = select(wait_for_termination_report, wait_to_be_online).await;
217
218        let report = match maybe_termination_report {
219            Either::Left((termination_report, _)) => Some(termination_report),
220            Either::Right((_, _)) => None,
221        };
222
223        info!("Exiting offline mode: {report:?}");
224
225        report
226    }
227
228    /// The role of the supervisor task is to wait for a termination message
229    /// ([`TerminationReport`]), sent either because we wanted to stop both
230    /// syncs, or because one of the syncs failed (in which case we'll stop the
231    /// other one too).
232    async fn spawn_supervisor_task(
233        inner: &SyncServiceInner,
234        room_list_service: Arc<RoomListService>,
235        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
236    ) -> (JoinHandle<()>, Sender<TerminationReport>) {
237        let (sender, mut receiver) = tokio::sync::mpsc::channel(16);
238
239        let encryption_sync = inner.encryption_sync_service.clone();
240        let state = inner.state.clone();
241        let termination_sender = sender.clone();
242
243        // When we first start, and don't use offline mode, we want to acquire
244        // the sync permit before we enter a future that might be polled at a
245        // later time, this means that the permit will be acquired as soon as
246        // this future, the one the `spawn_supervisor_task` function creates, is
247        // awaited.
248        //
249        // In other words, once `sync_service.start().await` is finished, the
250        // permit will be in the acquired state.
251        let mut sync_permit_guard =
252            MaybeAcquiredPermit::Acquired(encryption_sync_permit.clone().lock_owned().await);
253
254        let offline_mode = inner.with_offline_mode;
255        let parent_span = inner.parent_span.clone();
256
257        let future = async move {
258            loop {
259                let (room_list_task, encryption_sync_task) = Self::spawn_child_tasks(
260                    room_list_service.clone(),
261                    encryption_sync.clone(),
262                    sync_permit_guard,
263                    sender.clone(),
264                    parent_span.clone(),
265                )
266                .await;
267
268                sync_permit_guard = MaybeAcquiredPermit::Unacquired(encryption_sync_permit.clone());
269
270                let report = if let Some(report) = receiver.recv().await {
271                    report
272                } else {
273                    info!("internal channel has been closed?");
274                    // We should still stop the child tasks in the unlikely
275                    // scenario that our receiver died.
276                    TerminationReport::supervisor_error()
277                };
278
279                // If one service failed, make sure to request stopping the
280                // other one.
281                let (stop_room_list, stop_encryption) = match &report.origin {
282                    TerminationOrigin::EncryptionSync => (true, false),
283                    TerminationOrigin::RoomList => (false, true),
284                    TerminationOrigin::Supervisor => (true, true),
285                };
286
287                // Stop both services, and wait for the streams to properly
288                // finish: at some point they'll return `None` and will exit
289                // their infinite loops, and their tasks will gracefully
290                // terminate.
291
292                if stop_room_list {
293                    if let Err(err) = room_list_service.stop_sync() {
294                        warn!(?report, "unable to stop room list service: {err:#}");
295                    }
296
297                    if report.has_expired() {
298                        room_list_service.expire_sync_session().await;
299                    }
300                }
301
302                if let Err(err) = room_list_task.await {
303                    error!("when awaiting room list service: {err:#}");
304                }
305
306                if stop_encryption {
307                    if let Err(err) = encryption_sync.stop_sync() {
308                        warn!(?report, "unable to stop encryption sync: {err:#}");
309                    }
310
311                    if report.has_expired() {
312                        encryption_sync.expire_sync_session().await;
313                    }
314                }
315
316                if let Err(err) = encryption_sync_task.await {
317                    error!("when awaiting encryption sync: {err:#}");
318                }
319
320                if let Some(error) = report.error {
321                    if offline_mode {
322                        state.set(State::Offline);
323
324                        let client = room_list_service.client();
325
326                        if let Some(report) = Self::offline_check(client, &mut receiver).await {
327                            if let Some(error) = report.error {
328                                state.set(State::Error(Arc::new(error)));
329                            } else {
330                                state.set(State::Idle);
331                            }
332                            break;
333                        }
334
335                        state.set(State::Running);
336                    } else {
337                        state.set(State::Error(Arc::new(error)));
338                        break;
339                    }
340                } else if matches!(report.origin, TerminationOrigin::Supervisor) {
341                    state.set(State::Idle);
342                    break;
343                } else {
344                    state.set(State::Terminated);
345                    break;
346                }
347            }
348        }
349        .instrument(tracing::span!(Level::WARN, "supervisor task"));
350
351        let task = spawn(future);
352
353        (task, termination_sender)
354    }
355
356    async fn spawn_child_tasks(
357        room_list_service: Arc<RoomListService>,
358        encryption_sync_service: Arc<EncryptionSyncService>,
359        sync_permit_guard: MaybeAcquiredPermit,
360        sender: Sender<TerminationReport>,
361        parent_span: Span,
362    ) -> (JoinHandle<()>, JoinHandle<()>) {
363        // First, take care of the room list.
364        let room_list_task = spawn(
365            Self::room_list_sync_task(room_list_service, sender.clone())
366                .instrument(parent_span.clone()),
367        );
368
369        // Then, take care of the encryption sync.
370        let encryption_sync_task = spawn(
371            Self::encryption_sync_task(
372                encryption_sync_service,
373                sender.clone(),
374                sync_permit_guard.acquire().await,
375            )
376            .instrument(parent_span),
377        );
378
379        (room_list_task, encryption_sync_task)
380    }
381
382    async fn encryption_sync_task(
383        encryption_sync: Arc<EncryptionSyncService>,
384        sender: Sender<TerminationReport>,
385        sync_permit_guard: OwnedMutexGuard<EncryptionSyncPermit>,
386    ) {
387        let encryption_sync_stream = encryption_sync.sync(sync_permit_guard);
388        let mut encryption_sync_stream = pin!(encryption_sync_stream);
389
390        let termination_report = loop {
391            match encryption_sync_stream.next().await {
392                Some(Ok(())) => {
393                    // Carry on.
394                }
395                Some(Err(error)) => {
396                    let termination_report = TerminationReport::encryption_sync(Some(error));
397
398                    if !termination_report.has_expired() {
399                        error!(
400                            "Error while processing encryption in sync service: {:#?}",
401                            termination_report.error
402                        );
403                    }
404
405                    break termination_report;
406                }
407                None => {
408                    // The stream has ended.
409                    break TerminationReport::encryption_sync(None);
410                }
411            }
412        };
413
414        if let Err(err) = sender.send(termination_report).await {
415            error!("Error while sending termination report: {err:#}");
416        }
417    }
418
419    async fn room_list_sync_task(
420        room_list_service: Arc<RoomListService>,
421        sender: Sender<TerminationReport>,
422    ) {
423        let room_list_stream = room_list_service.sync();
424        let mut room_list_stream = pin!(room_list_stream);
425
426        let termination_report = loop {
427            match room_list_stream.next().await {
428                Some(Ok(())) => {
429                    // Carry on.
430                }
431                Some(Err(error)) => {
432                    let termination_report = TerminationReport::room_list(Some(error));
433
434                    if !termination_report.has_expired() {
435                        error!(
436                            "Error while processing room list in sync service: {:#?}",
437                            termination_report.error
438                        );
439                    }
440
441                    break termination_report;
442                }
443                None => {
444                    // The stream has ended.
445                    break TerminationReport::room_list(None);
446                }
447            }
448        };
449
450        if let Err(err) = sender.send(termination_report).await {
451            error!("Error while sending termination report: {err:#}");
452        }
453    }
454
455    async fn shutdown(self) {
456        match self.termination_sender.send(TerminationReport::supervisor()).await {
457            Ok(_) => {
458                let _ = self.task.await.inspect_err(|err| {
459                    // A `JoinError` indicates that the task was already dead,
460                    // either because it got cancelled or because it panicked.
461                    // We only cancel the task in the Err branch below and the
462                    // task shouldn't be able to panic.
463                    //
464                    // So let's log an error and return.
465                    error!("The supervisor task has stopped unexpectedly: {err:?}");
466                });
467            }
468            Err(err) => {
469                error!("Couldn't send the termination report to the supervisor task: {err}");
470                // Let's abort the task if it won't shut down properly,
471                // otherwise we would have left it as a detached task.
472                self.task.abort();
473            }
474        }
475    }
476}
477
478struct SyncServiceInner {
479    encryption_sync_service: Arc<EncryptionSyncService>,
480
481    /// Is the offline mode for the [`SyncService`] enabled?
482    ///
483    /// The offline mode is described in the [`State::Offline`] enum variant.
484    with_offline_mode: bool,
485
486    /// What's the state of this sync service?
487    state: SharedObservable<State>,
488
489    /// The parent tracing span to use for the tasks within this service.
490    ///
491    /// Normally this will be [`Span::none`], but it may be useful to assign a
492    /// defined span, for example if there is more than one active sync service.
493    parent_span: Span,
494
495    /// Supervisor task ensuring proper termination.
496    ///
497    /// This task is waiting for a [`TerminationReport`] from any of the other
498    /// two tasks, or from a user request via [`SyncService::stop()`]. It makes
499    /// sure that the two services are properly shut up and just interrupted.
500    ///
501    /// This is set at the same time as the other two tasks.
502    supervisor: Option<SyncTaskSupervisor>,
503}
504
505impl SyncServiceInner {
506    async fn start(
507        &mut self,
508        room_list_service: Arc<RoomListService>,
509        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
510    ) {
511        trace!("starting sync service");
512
513        self.supervisor =
514            Some(SyncTaskSupervisor::new(self, room_list_service, encryption_sync_permit).await);
515        self.state.set(State::Running);
516    }
517
518    async fn stop(&mut self) {
519        trace!("pausing sync service");
520
521        // Remove the supervisor from our state and request the tasks to be
522        // shutdown.
523        if let Some(supervisor) = self.supervisor.take() {
524            supervisor.shutdown().await;
525        } else {
526            error!("The sync service was not properly started, the supervisor task doesn't exist");
527        }
528    }
529
530    async fn restart(
531        &mut self,
532        room_list_service: Arc<RoomListService>,
533        encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
534    ) {
535        self.stop().await;
536        self.start(room_list_service, encryption_sync_permit).await;
537    }
538}
539
540/// A high level manager for your Matrix syncing needs.
541///
542/// The [`SyncService`] is responsible for managing real-time synchronization
543/// with a Matrix server. It can initiate and maintain the necessary
544/// synchronization tasks for you.
545///
546/// **Note**: The [`SyncService`] requires a server with support for [MSC4186],
547/// otherwise it will fail with an 404 `M_UNRECOGNIZED` request error.
548///
549/// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186/
550///
551/// # Example
552///
553/// ```no_run
554/// use matrix_sdk::Client;
555/// use matrix_sdk_ui::sync_service::{State, SyncService};
556/// # use url::Url;
557/// # async {
558/// let homeserver = Url::parse("http://example.com")?;
559/// let client = Client::new(homeserver).await?;
560///
561/// client
562///     .matrix_auth()
563///     .login_username("example", "wordpass")
564///     .initial_device_display_name("My bot")
565///     .await?;
566///
567/// let sync_service = SyncService::builder(client).build().await?;
568/// let mut state = sync_service.state();
569///
570/// while let Some(state) = state.next().await {
571///     match state {
572///         State::Idle => eprintln!("The sync service is idle."),
573///         State::Running => eprintln!("The sync has started to run."),
574///         State::Offline => eprintln!(
575///             "We have entered the offline mode, the server seems to be
576///              unavailable"
577///         ),
578///         State::Terminated => {
579///             eprintln!("The sync service has been gracefully terminated");
580///             break;
581///         }
582///         State::Error(_) => {
583///             eprintln!("The sync service has run into an error");
584///             break;
585///         }
586///     }
587/// }
588/// # anyhow::Ok(()) };
589/// ```
590pub struct SyncService {
591    inner: Arc<AsyncMutex<SyncServiceInner>>,
592
593    /// Room list service used to synchronize the rooms state.
594    room_list_service: Arc<RoomListService>,
595
596    /// What's the state of this sync service? This field is replicated from the
597    /// [`SyncServiceInner`] struct, but it should not be modified in this
598    /// struct. It's re-exposed here so we can subscribe to the state without
599    /// taking the lock on the `inner` field.
600    state: SharedObservable<State>,
601
602    /// Global lock to allow using at most one [`EncryptionSyncService`] at all
603    /// times.
604    ///
605    /// This ensures that there's only one ever existing in the application's
606    /// lifetime (under the assumption that there is at most one [`SyncService`]
607    /// per application).
608    encryption_sync_permit: Arc<AsyncMutex<EncryptionSyncPermit>>,
609}
610
611impl SyncService {
612    /// Create a new builder for configuring an `SyncService`.
613    pub fn builder(client: Client) -> SyncServiceBuilder {
614        SyncServiceBuilder::new(client)
615    }
616
617    /// Get the underlying `RoomListService` instance for easier access to its
618    /// methods.
619    pub fn room_list_service(&self) -> Arc<RoomListService> {
620        self.room_list_service.clone()
621    }
622
623    /// Returns the state of the sync service.
624    pub fn state(&self) -> Subscriber<State> {
625        self.state.subscribe()
626    }
627
628    /// Start (or restart) the underlying sliding syncs.
629    ///
630    /// This can be called multiple times safely:
631    ///
632    /// - if the stream is still properly running, it won't be restarted.
633    /// - if the [`SyncService`] is in the offline mode we will exit the offline
634    ///   mode and immediately attempt to sync again.
635    /// - if the stream has been aborted before, it will be properly cleaned up
636    ///   and restarted.
637    pub async fn start(&self) {
638        let mut inner = self.inner.lock().await;
639
640        // Only (re)start the tasks if it's stopped or if we're in the offline
641        // mode.
642        match inner.state.get() {
643            // If we're already running, there's nothing to do.
644            State::Running => {}
645            // If we're in the offline mode, first stop the service and then start it again.
646            State::Offline => {
647                inner
648                    .restart(self.room_list_service.clone(), self.encryption_sync_permit.clone())
649                    .await
650            }
651            // Otherwise just start.
652            State::Idle | State::Terminated | State::Error(_) => {
653                inner
654                    .start(self.room_list_service.clone(), self.encryption_sync_permit.clone())
655                    .await
656            }
657        }
658    }
659
660    /// Stop the underlying sliding syncs.
661    ///
662    /// This must be called when the app goes into the background. It's better
663    /// to call this API when the application exits, although not strictly
664    /// necessary.
665    #[instrument(skip_all)]
666    pub async fn stop(&self) {
667        let mut inner = self.inner.lock().await;
668
669        match inner.state.get() {
670            State::Idle | State::Terminated | State::Error(_) => {
671                // No need to stop if we were not running.
672                return;
673            }
674            State::Running | State::Offline => {}
675        }
676
677        inner.stop().await;
678    }
679
680    /// Force expiring both sessions.
681    ///
682    /// This ensures that the sync service is stopped before expiring both
683    /// sessions. It should be used sparingly, as it will cause a restart of the
684    /// sessions on the server as well.
685    #[instrument(skip_all)]
686    pub async fn expire_sessions(&self) {
687        // First, stop the sync service if it was running; it's a no-op if it
688        // was already stopped.
689        self.stop().await;
690
691        // Expire the room list sync session.
692        self.room_list_service.expire_sync_session().await;
693
694        // Expire the encryption sync session.
695        self.inner.lock().await.encryption_sync_service.expire_sync_session().await;
696    }
697
698    /// Attempt to get a permit to use an `EncryptionSyncService` at a given
699    /// time.
700    ///
701    /// This ensures there is at most one [`EncryptionSyncService`] active at
702    /// any time, per application.
703    pub fn try_get_encryption_sync_permit(&self) -> Option<OwnedMutexGuard<EncryptionSyncPermit>> {
704        self.encryption_sync_permit.clone().try_lock_owned().ok()
705    }
706}
707
708#[derive(Debug)]
709enum TerminationOrigin {
710    EncryptionSync,
711    RoomList,
712    Supervisor,
713}
714
715#[derive(Debug)]
716struct TerminationReport {
717    /// The origin of the termination.
718    origin: TerminationOrigin,
719
720    /// If the termination is due to an error, this is the cause.
721    error: Option<Error>,
722}
723
724impl TerminationReport {
725    /// Create a new [`TerminationReport`] with `origin` set to
726    /// [`TerminationOrigin::EncryptionSync`] and `error` set to
727    /// [`Error::EncryptionSync`].
728    fn encryption_sync(error: Option<encryption_sync_service::Error>) -> Self {
729        Self { origin: TerminationOrigin::EncryptionSync, error: error.map(Error::EncryptionSync) }
730    }
731
732    /// Create a new [`TerminationReport`] with `origin` set to
733    /// [`TerminationOrigin::RoomList`] and `error` set to [`Error::RoomList`].
734    fn room_list(error: Option<room_list_service::Error>) -> Self {
735        Self { origin: TerminationOrigin::RoomList, error: error.map(Error::RoomList) }
736    }
737
738    /// Create a new [`TerminationReport`] with `origin` set to
739    /// [`TerminationOrigin::Supervisor`] and `error` set to
740    /// [`Error::Supervisor`].
741    fn supervisor_error() -> Self {
742        Self { origin: TerminationOrigin::Supervisor, error: Some(Error::Supervisor) }
743    }
744
745    /// Create a new [`TerminationReport`] with `origin` set to
746    /// [`TerminationOrigin::Supervisor`] and `error` set to `None`.
747    fn supervisor() -> Self {
748        Self { origin: TerminationOrigin::Supervisor, error: None }
749    }
750
751    /// Check whether the termination is due to an expired sliding sync session.
752    fn has_expired(&self) -> bool {
753        match &self.error {
754            Some(Error::RoomList(room_list_service::Error::SlidingSync(error)))
755            | Some(Error::EncryptionSync(encryption_sync_service::Error::SlidingSync(error))) => {
756                error.client_api_error_kind() == Some(&ruma::api::error::ErrorKind::UnknownPos)
757            }
758            _ => false,
759        }
760    }
761}
762
763// Testing helpers, mostly.
764#[doc(hidden)]
765impl SyncService {
766    /// Is the task supervisor running?
767    pub async fn is_supervisor_running(&self) -> bool {
768        self.inner.lock().await.supervisor.is_some()
769    }
770}
771
772#[derive(Clone)]
773pub struct SyncServiceBuilder {
774    /// SDK client.
775    client: Client,
776
777    /// Is the offline mode for the [`SyncService`] enabled?
778    ///
779    /// The offline mode is described in the [`State::Offline`] enum variant.
780    with_offline_mode: bool,
781
782    /// Whether to turn [`SlidingSyncBuilder::share_pos`] on or off.
783    ///
784    /// [`SlidingSyncBuilder::share_pos`]: matrix_sdk::sliding_sync::SlidingSyncBuilder::share_pos
785    with_share_pos: bool,
786
787    /// Custom connection ID for the room list service. Defaults to
788    /// [`room_list_service::DEFAULT_CONNECTION_ID`]. Use a different value for
789    /// secondary processes such as iOS share extensions that are not meant to
790    /// reuse the main app's connection.
791    room_list_conn_id: String,
792
793    /// Custom timeline limit for the room list service. Defaults to
794    /// [`room_list_service::DEFAULT_LIST_TIMELINE_LIMIT`].
795    room_list_timeline_limit: u32,
796
797    /// The parent tracing span to use for the tasks within this service.
798    ///
799    /// Normally this will be [`Span::none`], but it may be useful to assign a
800    /// defined span, for example if there is more than one active sync service.
801    parent_span: Span,
802}
803
804impl SyncServiceBuilder {
805    fn new(client: Client) -> Self {
806        Self {
807            client,
808            with_offline_mode: false,
809            with_share_pos: true,
810            room_list_conn_id: DEFAULT_CONNECTION_ID.to_owned(),
811            room_list_timeline_limit: DEFAULT_LIST_TIMELINE_LIMIT,
812            parent_span: Span::none(),
813        }
814    }
815
816    /// Enable the "offline" mode for the [`SyncService`].
817    ///
818    /// To learn more about the "offline" mode read the documentation for the
819    /// [`State::Offline`] enum variant.
820    pub fn with_offline_mode(mut self) -> Self {
821        self.with_offline_mode = true;
822        self
823    }
824
825    /// Whether to turn [`SlidingSyncBuilder::share_pos`] on or off.
826    ///
827    /// [`SlidingSyncBuilder::share_pos`]: matrix_sdk::sliding_sync::SlidingSyncBuilder::share_pos
828    pub fn with_share_pos(mut self, enable: bool) -> Self {
829        self.with_share_pos = enable;
830        self
831    }
832
833    /// Set a custom conn_id for the room list sliding sync connection.
834    pub fn with_room_list_conn_id(mut self, conn_id: String) -> Self {
835        self.room_list_conn_id = conn_id;
836        self
837    }
838
839    /// Set a custom timeline limit for the room list service.
840    pub fn with_room_list_timeline_limit(mut self, limit: u32) -> Self {
841        self.room_list_timeline_limit = limit;
842        self
843    }
844
845    /// Set the parent tracing span to be used for the tasks within this
846    /// service.
847    pub fn with_parent_span(mut self, parent_span: Span) -> Self {
848        self.parent_span = parent_span;
849        self
850    }
851
852    /// Finish setting up the [`SyncService`].
853    ///
854    /// This creates the underlying sliding syncs, and will _not_ start them in
855    /// the background. The resulting [`SyncService`] must be kept alive as long
856    /// as the sliding syncs are supposed to run.
857    pub async fn build(self) -> Result<SyncService, Error> {
858        let Self {
859            client,
860            with_offline_mode,
861            with_share_pos,
862            room_list_conn_id,
863            room_list_timeline_limit,
864            parent_span,
865        } = self;
866
867        let encryption_sync_permit = Arc::new(AsyncMutex::new(EncryptionSyncPermit::new()));
868
869        let room_list = RoomListService::new_with(
870            client.clone(),
871            with_share_pos,
872            &room_list_conn_id,
873            room_list_timeline_limit,
874        )
875        .await?;
876
877        let encryption_sync = Arc::new(EncryptionSyncService::new(client, None).await?);
878
879        let room_list_service = Arc::new(room_list);
880        let state = SharedObservable::new(State::Idle);
881
882        Ok(SyncService {
883            state: state.clone(),
884            room_list_service,
885            encryption_sync_permit,
886            inner: Arc::new(AsyncMutex::new(SyncServiceInner {
887                supervisor: None,
888                encryption_sync_service: encryption_sync,
889                state,
890                with_offline_mode,
891                parent_span,
892            })),
893        })
894    }
895}
896
897/// Errors for the [`SyncService`] API.
898#[derive(Debug, Error)]
899pub enum Error {
900    /// An error received from the `RoomListService` API.
901    #[error(transparent)]
902    RoomList(#[from] room_list_service::Error),
903
904    /// An error received from the `EncryptionSyncService` API.
905    #[error(transparent)]
906    EncryptionSync(#[from] encryption_sync_service::Error),
907
908    /// An error had occurred in the sync task supervisor, likely due to a bug.
909    #[error("the supervisor channel has run into an unexpected error")]
910    Supervisor,
911}