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}