Skip to main content

matrix_sdk/sliding_sync/
mod.rs

1// Copyright 2022-2023 Benjamin Kampmann
2// Copyright 2022 The Matrix.org Foundation C.I.C.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for that specific language governing permissions and
14// limitations under the License.
15
16#![doc = include_str!("README.md")]
17
18mod builder;
19mod cache;
20mod client;
21mod error;
22mod list;
23
24use std::{
25    collections::{BTreeMap, btree_map::Entry},
26    fmt::Debug,
27    future::Future,
28    sync::{Arc, RwLock as StdRwLock, RwLockWriteGuard as StdRwLockWriteGuard},
29    time::Duration,
30};
31
32use async_stream::stream;
33pub use client::{Version, VersionBuilder};
34use futures_core::stream::Stream;
35use matrix_sdk_base::RequestedRequiredStates;
36#[cfg(feature = "e2e-encryption")]
37use matrix_sdk_common::executor::JoinHandleExt as _;
38use matrix_sdk_common::{executor::spawn, timer};
39use ruma::{
40    OwnedRoomId, RoomId,
41    api::{client::sync::sync_events::v5 as http, error::ErrorKind},
42    assign,
43};
44use tokio::{
45    select,
46    sync::{Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock, broadcast::Sender},
47};
48use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn};
49
50pub use self::{builder::*, client::VersionBuilderError, error::*, list::*};
51use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor};
52use crate::{Client, Result, config::RequestConfig};
53
54/// The Sliding Sync instance.
55///
56/// It is OK to clone this type as much as you need: cloning it is cheap.
57#[derive(Clone, Debug)]
58pub struct SlidingSync {
59    /// The Sliding Sync data.
60    inner: Arc<SlidingSyncInner>,
61}
62
63#[derive(Debug)]
64pub(super) struct SlidingSyncInner {
65    /// A unique identifier for this instance of sliding sync.
66    ///
67    /// Used to distinguish different connections to sliding sync.
68    id: String,
69
70    /// The HTTP Matrix client.
71    client: Client,
72
73    /// Long-polling timeout that appears in sliding sync request.
74    poll_timeout: Duration,
75
76    /// Extra duration for the sliding sync request to timeout. This is added to
77    /// the [`Self::poll_timeout`].
78    network_timeout: Duration,
79
80    /// The storage key to keep this cache at and load it from.
81    storage_key: String,
82
83    /// Should this sliding sync instance try to restore its sync position
84    /// from the database?
85    ///
86    /// Note: in non-cfg(e2e-encryption) builds, it's always set to false. We
87    /// keep it even so, to avoid sparkling cfg statements everywhere
88    /// throughout this file.
89    share_pos: bool,
90
91    /// Position markers.
92    ///
93    /// The `pos` marker represents a progression when exchanging requests and
94    /// responses with the server: the server acknowledges the request by
95    /// responding with a new `pos`. If the client sends two non-necessarily
96    /// consecutive requests with the same `pos`, the server has to reply with
97    /// the same identical response.
98    ///
99    /// `position` is behind a mutex so that a new request starts after the
100    /// previous request trip has fully ended (successfully or not). This
101    /// mechanism exists to wait for the response to be handled and to see the
102    /// `position` being updated, before sending a new request.
103    position: Arc<AsyncMutex<SlidingSyncPositionMarkers>>,
104
105    /// The lists of this Sliding Sync instance.
106    lists: AsyncRwLock<BTreeMap<String, SlidingSyncList>>,
107
108    /// Room subscriptions, i.e. rooms that may be out-of-scope of all lists
109    /// but one wants to receive updates.
110    room_subscriptions: StdRwLock<BTreeMap<OwnedRoomId, http::request::RoomSubscription>>,
111
112    /// The intended state of the extensions being supplied to sliding /sync
113    /// calls.
114    extensions: http::request::Extensions,
115
116    /// Internal channel used to pass messages between Sliding Sync and other
117    /// types.
118    internal_channel: Sender<SlidingSyncInternalMessage>,
119}
120
121impl SlidingSync {
122    pub(super) fn new(inner: SlidingSyncInner) -> Self {
123        Self { inner: Arc::new(inner) }
124    }
125
126    async fn cache_to_storage(&self, position: &SlidingSyncPositionMarkers) -> Result<()> {
127        cache::store_sliding_sync_state(self, position).await
128    }
129
130    /// Create a new [`SlidingSyncBuilder`].
131    pub fn builder(id: String, client: Client) -> Result<SlidingSyncBuilder, Error> {
132        SlidingSyncBuilder::new(id, client)
133    }
134
135    /// Add subscriptions to many rooms.
136    ///
137    /// If the associated `Room`s exist, they will be marked as members are
138    /// missing, so that it ensures to re-fetch all members.
139    ///
140    /// A subscription to an already subscribed room is ignored.
141    pub fn subscribe_to_rooms(
142        &self,
143        room_ids: &[&RoomId],
144        settings: Option<http::request::RoomSubscription>,
145        cancel_in_flight_request: bool,
146    ) {
147        if subscribe_to_rooms(
148            self.inner.room_subscriptions.write().unwrap(),
149            &self.inner.client,
150            room_ids,
151            settings,
152            cancel_in_flight_request,
153        ) {
154            self.inner.internal_channel_send_if_possible(
155                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
156            );
157        }
158    }
159
160    /// Remove subscriptions to many rooms.
161    pub fn unsubscribe_to_rooms(&self, room_ids: &[&RoomId], cancel_in_flight_request: bool) {
162        let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
163        let mut skip_over_current_sync_loop_iteration = false;
164
165        for room_id in room_ids {
166            if room_subscriptions.remove(*room_id).is_some() {
167                skip_over_current_sync_loop_iteration = true;
168            }
169        }
170
171        if cancel_in_flight_request && skip_over_current_sync_loop_iteration {
172            self.inner.internal_channel_send_if_possible(
173                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
174            );
175        }
176    }
177
178    /// Add subscriptions to the specified rooms if they don't already exist and
179    /// remove any existing subscriptions to other rooms.
180    ///
181    /// This is similar to [`Self::clear_and_subscribe_to_rooms`] but doesn't
182    /// clear and then recreate all subscriptions. Instead, it will perform
183    /// a delta-like update which involves:
184    ///
185    /// - leaving existing subscriptions untouched if the room is contained  in
186    ///   `room_ids`
187    /// - adding new subscriptions for rooms in `room_ids` that are currently
188    ///   unsubscribed
189    /// - removing existing subscriptions for rooms that are not contained in
190    ///   `room_ids`
191    ///
192    /// Note that unlike [`Self::clear_and_subscribe_to_rooms`], this method
193    /// will not mark members as unsynced (which would cause them to be
194    /// refetched) for subscriptions that already exist.
195    pub fn resubscribe_to_rooms(
196        &self,
197        room_ids: &[&RoomId],
198        settings: Option<http::request::RoomSubscription>,
199        cancel_in_flight_request: bool,
200    ) {
201        let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
202        let mut skip_over_current_sync_loop_iteration = false;
203
204        // Remove rooms that are no longer subscribed.
205        room_subscriptions.retain(|room_id, _| {
206            if room_ids.contains(&room_id.as_ref()) {
207                true
208            } else {
209                skip_over_current_sync_loop_iteration = true;
210                false
211            }
212        });
213
214        if subscribe_to_rooms(
215            room_subscriptions,
216            &self.inner.client,
217            room_ids,
218            settings,
219            cancel_in_flight_request && skip_over_current_sync_loop_iteration,
220        ) {
221            self.inner.internal_channel_send_if_possible(
222                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
223            );
224        }
225    }
226
227    /// Replace all subscriptions to rooms by other ones.
228    ///
229    /// If the associated `Room`s exist, they will be marked as members are
230    /// missing, so that it ensures to re-fetch all members.
231    pub fn clear_and_subscribe_to_rooms(
232        &self,
233        room_ids: &[&RoomId],
234        settings: Option<http::request::RoomSubscription>,
235        cancel_in_flight_request: bool,
236    ) {
237        let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
238        room_subscriptions.clear();
239
240        if subscribe_to_rooms(
241            room_subscriptions,
242            &self.inner.client,
243            room_ids,
244            settings,
245            cancel_in_flight_request,
246        ) {
247            self.inner.internal_channel_send_if_possible(
248                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
249            );
250        }
251    }
252
253    /// Find a list by its name, and do something on it if it exists.
254    pub async fn on_list<Function, FunctionOutput, R>(
255        &self,
256        list_name: &str,
257        function: Function,
258    ) -> Option<R>
259    where
260        Function: FnOnce(&SlidingSyncList) -> FunctionOutput,
261        FunctionOutput: Future<Output = R>,
262    {
263        let lists = self.inner.lists.read().await;
264
265        match lists.get(list_name) {
266            Some(list) => Some(function(list).await),
267            None => None,
268        }
269    }
270
271    /// Add the list to the list of lists.
272    ///
273    /// As lists need to have a unique `.name`, if a list with the same name
274    /// is found the new list will replace the old one and the return it or
275    /// `None`.
276    pub async fn add_list(
277        &self,
278        list_builder: SlidingSyncListBuilder,
279    ) -> Result<Option<SlidingSyncList>> {
280        let list = list_builder.build(self.inner.internal_channel.clone());
281
282        let old_list = self.inner.lists.write().await.insert(list.name().to_owned(), list);
283
284        self.inner.internal_channel_send_if_possible(
285            SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
286        );
287
288        Ok(old_list)
289    }
290
291    /// Add a list that will be cached and reloaded from the cache.
292    ///
293    /// This will raise an error if a storage key was not set, or if there
294    /// was a I/O error reading from the cache.
295    ///
296    /// The rest of the semantics is the same as [`Self::add_list`].
297    pub async fn add_cached_list(
298        &self,
299        mut list_builder: SlidingSyncListBuilder,
300    ) -> Result<Option<SlidingSyncList>> {
301        let _timer = timer!(format!("restoring (loading+processing) list {}", list_builder.name));
302
303        list_builder.set_cached_and_reload(&self.inner.client, &self.inner.storage_key).await?;
304
305        self.add_list(list_builder).await
306    }
307
308    /// Handle the HTTP response.
309    #[instrument(skip_all)]
310    async fn handle_response(
311        &self,
312        mut sliding_sync_response: http::Response,
313        position: &mut SlidingSyncPositionMarkers,
314        requested_required_states: RequestedRequiredStates,
315    ) -> Result<UpdateSummary, crate::Error> {
316        let pos = Some(sliding_sync_response.pos.clone());
317
318        let must_process_rooms_response = self.must_process_rooms_response().await;
319
320        trace!(yes = must_process_rooms_response, "Must process rooms response?");
321
322        // Transform a Sliding Sync Response to a `SyncResponse`.
323        //
324        // We may not need the `sync_response` in the future (once `SyncResponse` will
325        // move to Sliding Sync, i.e. to `http::Response`), but processing the
326        // `sliding_sync_response` is vital, so it must be done somewhere; for now it
327        // happens here.
328
329        let sync_response = {
330            let _timer = timer!("response processor");
331
332            let response_processor = {
333                // Take the lock to synchronise accesses to the state store, to avoid concurrent
334                // sliding syncs overwriting each other's room infos.
335                let state_store_guard = {
336                    let _timer = timer!("acquiring the `state_store_lock`");
337
338                    self.inner.client.base_client().state_store_lock().lock().await
339                };
340
341                let mut response_processor =
342                    SlidingSyncResponseProcessor::new(self.inner.client.clone());
343
344                // Process thread subscriptions if they're available.
345                //
346                // It's important to do this *before* handling the room responses, so that
347                // notifications can be properly generated based on the thread subscriptions,
348                // for the events in threads we've subscribed to.
349                if self.is_thread_subscriptions_enabled() {
350                    response_processor
351                        .handle_thread_subscriptions(
352                            position.pos.as_deref(),
353                            std::mem::take(
354                                &mut sliding_sync_response.extensions.thread_subscriptions,
355                            ),
356                        )
357                        .await?;
358                }
359
360                #[cfg(feature = "e2e-encryption")]
361                if self.is_e2ee_enabled() {
362                    response_processor
363                        .handle_encryption(&sliding_sync_response.extensions, &state_store_guard)
364                        .await?
365                }
366
367                // Only handle the room's subsection of the response, if this sliding sync was
368                // configured to do so.
369                if must_process_rooms_response {
370                    response_processor
371                        .handle_room_response(
372                            &sliding_sync_response,
373                            &requested_required_states,
374                            &state_store_guard,
375                        )
376                        .await?;
377                }
378
379                response_processor
380            };
381
382            // Release the lock before calling event handlers
383            response_processor.process_and_take_response().await?
384        };
385
386        debug!("Sliding Sync response has been handled by the client");
387        trace!(?sync_response);
388
389        let update_summary = {
390            // Update the rooms.
391            let updated_rooms = {
392                let mut updated_rooms = Vec::with_capacity(
393                    sliding_sync_response.rooms.len() + sync_response.rooms.joined.len(),
394                );
395
396                updated_rooms.extend(sliding_sync_response.rooms.keys().cloned());
397
398                // There might be other rooms that were only mentioned in the sliding sync
399                // extensions part of the response, and thus would result in rooms present in
400                // the `sync_response.joined`. Mark them as updated too.
401                //
402                // Since we've removed rooms that were in the room subsection from
403                // `sync_response.rooms.joined`, the remaining ones aren't already present in
404                // `updated_rooms` and wouldn't cause any duplicates.
405                updated_rooms.extend(sync_response.rooms.joined.keys().cloned());
406
407                updated_rooms
408            };
409
410            // Update the lists.
411            let updated_lists = {
412                debug!(
413                    lists = ?sliding_sync_response.lists,
414                    "Update lists"
415                );
416
417                let mut updated_lists = Vec::with_capacity(sliding_sync_response.lists.len());
418                let mut lists = self.inner.lists.write().await;
419
420                // Iterate on known lists, not on lists in the response. Rooms may have been
421                // updated that were not involved in any list update.
422                for (name, list) in lists.iter_mut() {
423                    if let Some(updates) = sliding_sync_response.lists.get(name) {
424                        let maximum_number_of_rooms: u32 =
425                            updates.count.try_into().expect("failed to convert `count` to `u32`");
426
427                        if list.update(Some(maximum_number_of_rooms))? {
428                            updated_lists.push(name.clone());
429                        }
430                    } else if list.update(None)? {
431                        updated_lists.push(name.clone());
432                    }
433                }
434
435                // Report about unknown lists.
436                for name in sliding_sync_response.lists.keys() {
437                    if !lists.contains_key(name) {
438                        error!("Response for list `{name}` - unknown to us; skipping");
439                    }
440                }
441
442                updated_lists
443            };
444
445            UpdateSummary { lists: updated_lists, rooms: updated_rooms }
446        };
447
448        // Everything went well, we can update the position markers.
449        //
450        // Save the new position markers.
451        debug!(previous_pos = position.pos, new_pos = pos, "Updating `pos`");
452
453        position.pos = pos;
454
455        Ok(update_summary)
456    }
457
458    async fn generate_sync_request(
459        &self,
460    ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard<SlidingSyncPositionMarkers>)> {
461        // Collect requests for lists.
462        let mut requests_lists = BTreeMap::new();
463
464        let timeout = {
465            let lists = self.inner.lists.read().await;
466
467            // Start at `Default` in case there is zero list.
468            let mut timeout = PollTimeout::Default;
469
470            for (name, list) in lists.iter() {
471                requests_lists.insert(name.clone(), list.next_request()?);
472                timeout = timeout.min(list.requires_timeout());
473            }
474
475            timeout
476        };
477
478        // Collect the `pos`.
479        //
480        // Wait on the `position` mutex to be available. It means no request nor
481        // response is running. The `position` mutex is released whether the response
482        // has been fully handled successfully, in this case the `pos` is updated, or
483        // the response handling has failed, in this case the `pos` hasn't been updated
484        // and the same `pos` will be used for this new request.
485        let mut position_guard = {
486            debug!("Waiting to acquire the `position` lock");
487
488            let _timer = timer!("acquiring the `position` lock");
489
490            self.inner.position.clone().lock_owned().await
491        };
492
493        debug!(pos = ?position_guard.pos, "Got a position");
494
495        let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
496
497        let restored_fields = if self.inner.share_pos || to_device_enabled {
498            restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
499        } else {
500            None
501        };
502
503        // Update pos: either the one restored from the database, if any and the sliding
504        // sync was configured so, or read it from the memory cache.
505        let pos = if self.inner.share_pos {
506            if let Some(fields) = &restored_fields {
507                // Override the memory one with the database one, for consistency.
508                if fields.pos != position_guard.pos {
509                    info!(
510                        "Pos from previous request ('{:?}') was different from \
511                         pos in database ('{:?}').",
512                        position_guard.pos, fields.pos
513                    );
514                    position_guard.pos = fields.pos.clone();
515                }
516                fields.pos.clone()
517            } else {
518                position_guard.pos.clone()
519            }
520        } else {
521            position_guard.pos.clone()
522        };
523
524        // When the client sends a request with no `pos`, MSC4186 returns no device
525        // lists updates, as it only returns changes since the provided `pos`
526        // (which is `null` in this case); this is in line with sync v2.
527        //
528        // Therefore, with MSC4186, the device list cache must be marked as to be
529        // re-downloaded if the `since` token is `None`, otherwise it's easy to miss
530        // device lists updates that happened between the previous request and the new
531        // “initial” request.
532        #[cfg(feature = "e2e-encryption")]
533        if pos.is_none() && self.is_e2ee_enabled() {
534            info!("Marking all tracked users as dirty");
535
536            let olm_machine = self.inner.client.olm_machine().await;
537            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
538            olm_machine.mark_all_tracked_users_as_dirty().await?;
539        }
540
541        // Configure the timeout.
542        //
543        // The `timeout` query is necessary when all lists require it. Please see
544        // [`SlidingSyncList::requires_timeout`].
545        let timeout = match timeout {
546            PollTimeout::None => None,
547            PollTimeout::Some(timeout) => Some(Duration::from_secs(timeout.into())),
548            PollTimeout::Default => Some(self.inner.poll_timeout),
549        };
550
551        Span::current()
552            .record("pos", &pos)
553            .record("timeout", timeout.map(|duration| duration.as_millis()));
554
555        let mut request = assign!(http::Request::new(), {
556            conn_id: Some(self.inner.id.clone()),
557            pos,
558            set_presence: self.inner.client.sync_presence(),
559            timeout,
560            lists: requests_lists,
561        });
562
563        // Add room subscriptions.
564        request.room_subscriptions = self.inner.room_subscriptions.read().unwrap().clone();
565
566        // Add extensions.
567        request.extensions = self.inner.extensions.clone();
568
569        // Override the to-device token if the extension is enabled.
570        if to_device_enabled {
571            request.extensions.to_device.since =
572                restored_fields.and_then(|fields| fields.to_device_token);
573        }
574
575        Ok((
576            // The request itself.
577            request,
578            // Configure long-polling. We need some time for the long-poll itself,
579            // and extra time for the network delays.
580            RequestConfig::default()
581                .timeout(self.inner.poll_timeout + self.inner.network_timeout)
582                .retry_limit(3),
583            position_guard,
584        ))
585    }
586
587    /// Send a sliding sync request.
588    ///
589    /// This method contains the sending logic.
590    async fn send_sync_request(
591        &self,
592        request: http::Request,
593        request_config: RequestConfig,
594        mut position_guard: OwnedMutexGuard<SlidingSyncPositionMarkers>,
595    ) -> Result<UpdateSummary> {
596        debug!("Sending request");
597
598        // Prepare the request.
599        let requested_required_states = RequestedRequiredStates::from(&request);
600        let request = self.inner.client.send(request).with_request_config(request_config);
601
602        // Send the request and get a response with end-to-end encryption support.
603        //
604        // Sending the `/sync` request out when end-to-end encryption is enabled means
605        // that we need to also send out any outgoing e2ee related request out
606        // coming from the `OlmMachine::outgoing_requests()` method.
607
608        #[cfg(feature = "e2e-encryption")]
609        let response = {
610            if self.is_e2ee_enabled() {
611                // Here, we need to run 2 things:
612                //
613                // 1. Send the sliding sync request and get a response,
614                // 2. Send the E2EE requests.
615                //
616                // We don't want to use a `join` or `try_join` because we want to fail if and
617                // only if sending the sliding sync request fails. Failing to send the E2EE
618                // requests should just result in a log.
619                //
620                // We also want to give the priority to sliding sync request. E2EE requests are
621                // sent concurrently to the sliding sync request, but the priority is on waiting
622                // a sliding sync response.
623                //
624                // If sending sliding sync request fails, the sending of E2EE requests must be
625                // aborted as soon as possible.
626
627                let client = self.inner.client.clone();
628                let e2ee_uploads = spawn(
629                    async move {
630                        if let Err(error) = client.send_outgoing_requests().await {
631                            error!(?error, "Error while sending outgoing E2EE requests");
632                        }
633                    }
634                    .instrument(Span::current()),
635                )
636                // Ensure that the task is not running in detached mode. It is aborted when it's
637                // dropped.
638                .abort_on_drop();
639
640                // Wait on the sliding sync request success or failure early.
641                let response = request.await?;
642
643                // At this point, if `request` has been resolved successfully, we wait on
644                // `e2ee_uploads`. It did run concurrently, so it should not be blocking for too
645                // long. Otherwise —if `request` has failed— `e2ee_uploads` has
646                // been dropped, so aborted.
647                e2ee_uploads.await.map_err(|error| Error::JoinError {
648                    task_description: "e2ee_uploads".to_owned(),
649                    error,
650                })?;
651
652                response
653            } else {
654                request.await?
655            }
656        };
657
658        // Send the request and get a response _without_ end-to-end encryption support.
659        #[cfg(not(feature = "e2e-encryption"))]
660        let response = request.await?;
661
662        debug!("Received response");
663
664        // At this point, the request has been sent, and a response has been received.
665        //
666        // We must ensure the handling of the response cannot be stopped/
667        // cancelled. It must be done entirely, otherwise we can have
668        // corrupted/incomplete states for Sliding Sync and other parts of
669        // the code.
670        //
671        // That's why we are running the handling of the response in a spawned
672        // future that cannot be cancelled by anything.
673        let this = self.clone();
674
675        // Spawn a new future to ensure that the code inside this future cannot be
676        // cancelled if this method is cancelled.
677        let future = async move {
678            debug!("Start handling response");
679
680            // In case the task running this future is detached, we must
681            // ensure responses are handled one at a time. At this point we still own
682            // `position_guard`, so we're fine.
683
684            // Handle the response.
685            let updates = this
686                .handle_response(response, &mut position_guard, requested_required_states)
687                .await?;
688
689            this.cache_to_storage(&position_guard).await?;
690
691            // Release the position guard lock.
692            // It means that other responses can be generated and then handled later.
693            drop(position_guard);
694
695            debug!("Done handling response");
696
697            Ok(updates)
698        };
699
700        spawn(future.instrument(Span::current())).await.map_err(|error| Error::JoinError {
701            task_description: "handle_response".to_owned(),
702            error,
703        })?
704    }
705
706    /// Is the e2ee extension enabled for this sliding sync instance?
707    #[cfg(feature = "e2e-encryption")]
708    fn is_e2ee_enabled(&self) -> bool {
709        self.inner.extensions.e2ee.enabled == Some(true)
710    }
711
712    /// Is the thread subscriptions extension enabled for this sliding sync
713    /// instance?
714    fn is_thread_subscriptions_enabled(&self) -> bool {
715        self.inner.extensions.thread_subscriptions.enabled == Some(true)
716    }
717
718    #[cfg(not(feature = "e2e-encryption"))]
719    fn is_e2ee_enabled(&self) -> bool {
720        false
721    }
722
723    /// Should we process the room's subpart of a response?
724    async fn must_process_rooms_response(&self) -> bool {
725        // We consider that we must, if there's any room subscription or there's any
726        // list.
727        !self.inner.room_subscriptions.read().unwrap().is_empty()
728            || !self.inner.lists.read().await.is_empty()
729    }
730
731    /// Send a single sliding sync request, and returns the response summary.
732    ///
733    /// Public for testing purposes only.
734    #[doc(hidden)]
735    #[instrument(skip_all, fields(conn_id = self.inner.id, pos, timeout))]
736    pub async fn sync_once(&self) -> Result<UpdateSummary> {
737        let (request, request_config, position_guard) = self.generate_sync_request().await?;
738
739        // Send the request.
740        let summaries = self.send_sync_request(request, request_config, position_guard).await?;
741
742        // Notify a new sync was received.
743        self.inner.client.inner.sync_beat.notify(usize::MAX);
744
745        Ok(summaries)
746    }
747
748    /// Create a _new_ Sliding Sync sync loop.
749    ///
750    /// This method returns a `Stream`, which will send requests and will handle
751    /// responses automatically. Lists and rooms are updated automatically.
752    ///
753    /// This function returns `Ok(…)` if everything went well, otherwise it will
754    /// return `Err(…)`. An `Err` will _always_ lead to the `Stream`
755    /// termination.
756    #[allow(unknown_lints, clippy::let_with_type_underscore)] // triggered by instrument macro
757    #[instrument(name = "sync_stream", skip_all, fields(conn_id = self.inner.id, with_e2ee = self.is_e2ee_enabled()))]
758    pub fn sync(&self) -> impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_ {
759        debug!("Starting sync stream");
760
761        let mut internal_channel_receiver = self.inner.internal_channel.subscribe();
762
763        stream! {
764            loop {
765                debug!("Sync stream is running");
766
767                select! {
768                    biased;
769
770                    internal_message = internal_channel_receiver.recv() => {
771                        use SlidingSyncInternalMessage::*;
772
773                        debug!(?internal_message, "Sync stream has received an internal message");
774
775                        match internal_message {
776                            Err(_) | Ok(SyncLoopStop) => {
777                                break;
778                            }
779
780                            Ok(SyncLoopSkipOverCurrentIteration) => {
781                                continue;
782                            }
783                        }
784                    }
785
786                    update_summary = self.sync_once() => {
787                        match update_summary {
788                            Ok(updates) => {
789                                yield Ok(updates);
790                            }
791
792                            // Here, errors we **cannot** ignore, and that must stop the sync loop.
793                            Err(error) => {
794                                if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) {
795                                    // The Sliding Sync session has expired. Let's reset `pos`.
796                                    self.expire_session().await;
797                                }
798
799                                yield Err(error);
800
801                                // Terminates the loop, and terminates the stream.
802                                break;
803                            }
804                        }
805                    }
806                }
807            }
808
809            debug!("Sync stream has exited.");
810        }
811    }
812
813    /// Force to stop the sync loop ([`Self::sync`]) if it's running.
814    ///
815    /// Usually, dropping the `Stream` returned by [`Self::sync`] should be
816    /// enough to “stop” it, but depending of how this `Stream` is used, it
817    /// might not be obvious to drop it immediately (thinking of using this API
818    /// over FFI; the foreign-language might not be able to drop a value
819    /// immediately). Thus, calling this method will ensure that the sync loop
820    /// stops gracefully and as soon as it returns.
821    pub fn stop_sync(&self) -> Result<()> {
822        Ok(self.inner.internal_channel_send(SlidingSyncInternalMessage::SyncLoopStop)?)
823    }
824
825    /// Expire the current Sliding Sync session on the client-side.
826    ///
827    /// Expiring a Sliding Sync session means: resetting `pos`.
828    ///
829    /// This should only be used when it's clear that this session was about to
830    /// expire anyways, and should be used only in very specific cases (e.g.
831    /// multiple sliding syncs being run in parallel, and one of them has
832    /// expired).
833    ///
834    /// This method **MUST** be called when the sync loop is stopped.
835    #[doc(hidden)]
836    pub async fn expire_session(&self) {
837        info!("Session expired; resetting `pos`");
838
839        {
840            let lists = self.inner.lists.read().await;
841
842            for list in lists.values() {
843                // Invalidate in-memory data that would be persisted on disk.
844                list.set_maximum_number_of_rooms(None);
845            }
846        }
847
848        // Remove the cached sliding sync state as well.
849        {
850            let mut position = self.inner.position.lock().await;
851
852            // Invalidate in memory.
853            position.pos = None;
854
855            // Propagate to disk.
856            // Note: this propagates both the sliding sync state and the cached lists'
857            // state to disk.
858            if let Err(err) = self.cache_to_storage(&position).await {
859                warn!("Failed to invalidate cached sliding sync state: {err}");
860            }
861        }
862
863        {
864            // Clear all room subscriptions: we don't want to resend all room subscriptions
865            // when the session will restart.
866            self.inner.room_subscriptions.write().unwrap().clear();
867        }
868    }
869}
870
871/// Private implementation for [`SlidingSync::subscribe_to_rooms`] and
872/// [`SlidingSync::clear_and_subscribe_to_rooms`].
873fn subscribe_to_rooms(
874    mut room_subscriptions: StdRwLockWriteGuard<
875        '_,
876        BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
877    >,
878    client: &Client,
879    room_ids: &[&RoomId],
880    settings: Option<http::request::RoomSubscription>,
881    cancel_in_flight_request: bool,
882) -> bool {
883    let settings = settings.unwrap_or_default();
884    let mut skip_over_current_sync_loop_iteration = false;
885
886    for room_id in room_ids {
887        if let Entry::Vacant(entry) = room_subscriptions.entry((*room_id).to_owned()) {
888            if let Some(room) = client.get_room(room_id) {
889                room.mark_members_missing();
890            }
891
892            entry.insert(settings.clone());
893
894            skip_over_current_sync_loop_iteration = true;
895        }
896    }
897
898    cancel_in_flight_request && skip_over_current_sync_loop_iteration
899}
900
901impl SlidingSyncInner {
902    /// Send a message over the internal channel.
903    #[instrument]
904    fn internal_channel_send(&self, message: SlidingSyncInternalMessage) -> Result<(), Error> {
905        self.internal_channel.send(message).map(|_| ()).map_err(|_| Error::InternalChannelIsBroken)
906    }
907
908    /// Send a message over the internal channel if there is a receiver, i.e. if
909    /// the sync loop is running.
910    #[instrument]
911    fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
912        // If there is no receiver, the send will fail, but that's OK here.
913        let _ = self.internal_channel.send(message);
914    }
915}
916
917#[derive(Copy, Clone, Debug, PartialEq)]
918enum SlidingSyncInternalMessage {
919    /// Instruct the sync loop to stop.
920    SyncLoopStop,
921
922    /// Instruct the sync loop to skip over any remaining work in its iteration,
923    /// and to jump to the next iteration.
924    SyncLoopSkipOverCurrentIteration,
925}
926
927#[cfg(any(test, feature = "testing"))]
928impl SlidingSync {
929    /// Set a new value for `pos`.
930    pub async fn set_pos(&self, new_pos: String) {
931        let mut position_lock = self.inner.position.lock().await;
932        position_lock.pos = Some(new_pos);
933    }
934}
935
936#[derive(Clone, Debug)]
937pub(super) struct SlidingSyncPositionMarkers {
938    /// An ephemeral position in the current stream, as received from the
939    /// previous `/sync` response, or `None` for the first request.
940    pos: Option<String>,
941}
942
943/// A summary of the updates received after a sync (like in
944/// [`SlidingSync::sync`]).
945#[derive(Debug, Clone)]
946pub struct UpdateSummary {
947    /// The names of the lists that have seen an update.
948    pub lists: Vec<String>,
949    /// The rooms that have seen updates
950    pub rooms: Vec<OwnedRoomId>,
951}
952
953/// Define what kind of poll timeout [`SlidingSync`] must use.
954///
955/// [The spec says about `timeout`][spec]:
956///
957/// > How long to wait for new events […] If omitted the response is always
958/// > returned immediately, even if there are no changes.
959///
960/// [spec]: https://github.com/matrix-org/matrix-spec-proposals/blob/erikj/sss/proposals/4186-simplified-sliding-sync.md#top-level
961#[derive(Debug)]
962pub enum PollTimeout {
963    /// No `timeout` must be present.
964    None,
965
966    /// A `timeout=X` must be present, where `X` is in seconds and
967    /// represents how long to wait for new events.
968    Some(u32),
969
970    /// A `timeout=X` must be present, where `X` is the default value passed to
971    /// [`SlidingSyncBuilder::poll_timeout`].
972    Default,
973}
974
975impl PollTimeout {
976    /// Computes the smallest `PollTimeout` between two of them.
977    ///
978    /// The rules are the following:
979    ///
980    /// * `None` < `Some`,
981    /// * `Some(x) < Some(y)` if and only if `x < y`,
982    /// * `Some < Default`.
983    ///
984    /// The `Default` value is unknown at this step but is assumed to be the
985    /// largest.
986    fn min(self, left: Self) -> Self {
987        match (self, left) {
988            (Self::None, _) => Self::None,
989
990            (Self::Some(_), Self::None) => Self::None,
991            (Self::Some(right), Self::Some(left)) => Self::Some(right.min(left)),
992            (Self::Some(right), Self::Default) => Self::Some(right),
993
994            (Self::Default, Self::None) => Self::None,
995            (Self::Default, Self::Some(left)) => Self::Some(left),
996            (Self::Default, Self::Default) => Self::Default,
997        }
998    }
999}
1000
1001#[cfg(all(test, not(target_family = "wasm")))]
1002#[allow(clippy::dbg_macro)]
1003mod tests {
1004    use std::{
1005        collections::BTreeMap,
1006        future::ready,
1007        ops::Not,
1008        sync::{Arc, Mutex},
1009        time::Duration,
1010    };
1011
1012    use assert_matches::assert_matches;
1013    use event_listener::Listener;
1014    use futures_util::{StreamExt, future::join_all, pin_mut};
1015    use matrix_sdk_base::{RequestedRequiredStates, RoomMemberships};
1016    use matrix_sdk_common::executor::spawn;
1017    use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1018    use ruma::{
1019        OwnedRoomId, assign,
1020        events::{direct::DirectEvent, room::member::MembershipState},
1021        owned_room_id,
1022        presence::PresenceState,
1023        room_id,
1024        serde::Raw,
1025        uint,
1026    };
1027    use serde::Deserialize;
1028    use serde_json::json;
1029    use wiremock::{
1030        Match, Mock, MockServer, Request, ResponseTemplate, http::Method, matchers::method,
1031    };
1032
1033    use super::{
1034        SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode,
1035        cache::restore_sliding_sync_state, http,
1036    };
1037    use crate::{
1038        Client, Result,
1039        test_utils::{logged_in_client, mocks::MatrixMockServer},
1040    };
1041
1042    #[derive(Copy, Clone)]
1043    struct SlidingSyncMatcher;
1044
1045    impl Match for SlidingSyncMatcher {
1046        fn matches(&self, request: &Request) -> bool {
1047            request.url.path() == "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
1048                && request.method == Method::POST
1049        }
1050    }
1051
1052    async fn new_sliding_sync(
1053        lists: Vec<SlidingSyncListBuilder>,
1054    ) -> Result<(MockServer, SlidingSync)> {
1055        let server = MockServer::start().await;
1056        let client = logged_in_client(Some(server.uri())).await;
1057
1058        let mut sliding_sync_builder = client.sliding_sync("test-slidingsync")?;
1059
1060        for list in lists {
1061            sliding_sync_builder = sliding_sync_builder.add_list(list);
1062        }
1063
1064        let sliding_sync = sliding_sync_builder.build().await?;
1065
1066        Ok((server, sliding_sync))
1067    }
1068
1069    #[async_test]
1070    async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> {
1071        let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
1072        let client = sliding_sync.inner.client.clone();
1073
1074        {
1075            let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1076
1077            assert_eq!(request.set_presence, PresenceState::Online);
1078        }
1079
1080        client.set_presence(PresenceState::Unavailable, None, false).await?;
1081
1082        {
1083            let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1084
1085            assert_eq!(request.set_presence, PresenceState::Unavailable);
1086        }
1087
1088        client.set_presence(PresenceState::Offline, None, false).await?;
1089
1090        {
1091            let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1092
1093            assert_eq!(request.set_presence, PresenceState::Offline);
1094        }
1095
1096        Ok(())
1097    }
1098
1099    #[async_test]
1100    async fn test_subscribe_to_rooms() -> Result<()> {
1101        let (server, sliding_sync) = new_sliding_sync(vec![
1102            SlidingSyncList::builder("foo")
1103                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1104        ])
1105        .await?;
1106
1107        let stream = sliding_sync.sync();
1108        pin_mut!(stream);
1109
1110        let room_id_0 = room_id!("!r0:bar.org");
1111        let room_id_1 = room_id!("!r1:bar.org");
1112        let room_id_2 = room_id!("!r2:bar.org");
1113
1114        {
1115            let _mock_guard = Mock::given(SlidingSyncMatcher)
1116                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1117                    "pos": "1",
1118                    "lists": {},
1119                    "rooms": {
1120                        room_id_0: {
1121                            "name": "Room #0",
1122                            "initial": true,
1123                        },
1124                        room_id_1: {
1125                            "name": "Room #1",
1126                            "initial": true,
1127                        },
1128                        room_id_2: {
1129                            "name": "Room #2",
1130                            "initial": true,
1131                        },
1132                    }
1133                })))
1134                .mount_as_scoped(&server)
1135                .await;
1136
1137            let _ = stream.next().await.unwrap()?;
1138        }
1139
1140        let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1141
1142        // Members aren't synced.
1143        // We need to make them synced, so that we can test that subscribing to a room
1144        // make members not synced. That's a desired feature.
1145        assert!(room0.are_members_synced().not());
1146
1147        {
1148            struct MemberMatcher(OwnedRoomId);
1149
1150            impl Match for MemberMatcher {
1151                fn matches(&self, request: &Request) -> bool {
1152                    request.url.path()
1153                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1154                        && request.method == Method::GET
1155                }
1156            }
1157
1158            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1159                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1160                    "chunk": [],
1161                })))
1162                .mount_as_scoped(&server)
1163                .await;
1164
1165            assert_matches!(room0.request_members().await, Ok(()));
1166        }
1167
1168        // Members are now synced! We can start subscribing and see how it goes.
1169        assert!(room0.are_members_synced());
1170
1171        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, true);
1172
1173        // OK, we have subscribed to some rooms. Let's check on `room0` if members are
1174        // now marked as not synced.
1175        assert!(room0.are_members_synced().not());
1176
1177        {
1178            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1179
1180            assert!(room_subscriptions.contains_key(room_id_0));
1181            assert!(room_subscriptions.contains_key(room_id_1));
1182            assert!(!room_subscriptions.contains_key(room_id_2));
1183        }
1184
1185        // Subscribing to the same room doesn't reset the member sync state.
1186
1187        {
1188            struct MemberMatcher(OwnedRoomId);
1189
1190            impl Match for MemberMatcher {
1191                fn matches(&self, request: &Request) -> bool {
1192                    request.url.path()
1193                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1194                        && request.method == Method::GET
1195                }
1196            }
1197
1198            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1199                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1200                    "chunk": [],
1201                })))
1202                .mount_as_scoped(&server)
1203                .await;
1204
1205            assert_matches!(room0.request_members().await, Ok(()));
1206        }
1207
1208        // Members are synced, good, good.
1209        assert!(room0.are_members_synced());
1210
1211        sliding_sync.subscribe_to_rooms(&[room_id_0], None, false);
1212
1213        // Members are still synced: because we have already subscribed to the
1214        // room, the members aren't marked as unsynced.
1215        assert!(room0.are_members_synced());
1216
1217        Ok(())
1218    }
1219
1220    #[async_test]
1221    async fn test_subscribe_unsubscribe_and_clear_and_subscribe_to_rooms() -> Result<()> {
1222        let (_server, sliding_sync) = new_sliding_sync(vec![
1223            SlidingSyncList::builder("foo")
1224                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1225        ])
1226        .await?;
1227
1228        let room_id_0 = room_id!("!r0:bar.org");
1229        let room_id_1 = room_id!("!r1:bar.org");
1230        let room_id_2 = room_id!("!r2:bar.org");
1231        let room_id_3 = room_id!("!r3:bar.org");
1232
1233        // Initially empty.
1234        {
1235            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1236
1237            assert!(room_subscriptions.is_empty());
1238        }
1239
1240        // Add 2 rooms.
1241        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], Default::default(), false);
1242
1243        {
1244            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1245
1246            assert_eq!(room_subscriptions.len(), 2);
1247            assert!(room_subscriptions.contains_key(room_id_0));
1248            assert!(room_subscriptions.contains_key(room_id_1));
1249        }
1250
1251        // Remove 1 room.
1252        sliding_sync.unsubscribe_to_rooms(&[room_id_0], false);
1253
1254        {
1255            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1256
1257            assert_eq!(room_subscriptions.len(), 1);
1258            assert!(room_subscriptions.contains_key(room_id_1));
1259        }
1260
1261        // Add 2 rooms, but one already exists.
1262        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], Default::default(), false);
1263
1264        {
1265            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1266
1267            assert_eq!(room_subscriptions.len(), 2);
1268            assert!(room_subscriptions.contains_key(room_id_0));
1269            assert!(room_subscriptions.contains_key(room_id_1));
1270        }
1271
1272        // Replace all rooms with 2 other rooms.
1273        sliding_sync.clear_and_subscribe_to_rooms(
1274            &[room_id_2, room_id_3],
1275            Default::default(),
1276            false,
1277        );
1278
1279        {
1280            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1281
1282            assert_eq!(room_subscriptions.len(), 2);
1283            assert!(room_subscriptions.contains_key(room_id_2));
1284            assert!(room_subscriptions.contains_key(room_id_3));
1285        }
1286
1287        Ok(())
1288    }
1289
1290    #[async_test]
1291    async fn test_resubscribe_to_rooms() -> Result<()> {
1292        let (server, sliding_sync) = new_sliding_sync(vec![
1293            SlidingSyncList::builder("foo")
1294                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1295        ])
1296        .await?;
1297
1298        let stream = sliding_sync.sync();
1299        pin_mut!(stream);
1300
1301        let room_id_0 = room_id!("!r0:bar.org");
1302        let room_id_1 = room_id!("!r1:bar.org");
1303        let room_id_2 = room_id!("!r2:bar.org");
1304
1305        {
1306            let _mock_guard = Mock::given(SlidingSyncMatcher)
1307                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1308                    "pos": "1",
1309                    "lists": {},
1310                    "rooms": {
1311                        room_id_0: {
1312                            "name": "Room #0",
1313                            "initial": true,
1314                        },
1315                        room_id_1: {
1316                            "name": "Room #1",
1317                            "initial": true,
1318                        },
1319                        room_id_2: {
1320                            "name": "Room #2",
1321                            "initial": true,
1322                        },
1323                    }
1324                })))
1325                .mount_as_scoped(&server)
1326                .await;
1327
1328            let _ = stream.next().await.unwrap()?;
1329        }
1330
1331        let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1332
1333        // Members aren't synced.
1334        // We need to make them synced, so that we can test that subscribing to a room
1335        // make members not synced. That's a desired feature.
1336        assert!(room0.are_members_synced().not());
1337
1338        {
1339            struct MemberMatcher(OwnedRoomId);
1340
1341            impl Match for MemberMatcher {
1342                fn matches(&self, request: &Request) -> bool {
1343                    request.url.path()
1344                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1345                        && request.method == Method::GET
1346                }
1347            }
1348
1349            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1350                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1351                    "chunk": [],
1352                })))
1353                .mount_as_scoped(&server)
1354                .await;
1355
1356            assert_matches!(room0.request_members().await, Ok(()));
1357        }
1358
1359        // Members are now synced! We can start subscribing and see how it goes.
1360        assert!(room0.are_members_synced());
1361
1362        sliding_sync.resubscribe_to_rooms(&[room_id_0, room_id_1], None, true);
1363
1364        // OK, we have subscribed to some rooms. Let's check on `room0` if members are
1365        // now marked as not synced.
1366        assert!(room0.are_members_synced().not());
1367
1368        // Both resubscribed rooms are subscribed.
1369        {
1370            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1371
1372            assert!(room_subscriptions.contains_key(room_id_0));
1373            assert!(room_subscriptions.contains_key(room_id_1));
1374            assert!(!room_subscriptions.contains_key(room_id_2));
1375        }
1376
1377        // Subscribing to the same room doesn't reset the member sync state.
1378
1379        {
1380            struct MemberMatcher(OwnedRoomId);
1381
1382            impl Match for MemberMatcher {
1383                fn matches(&self, request: &Request) -> bool {
1384                    request.url.path()
1385                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1386                        && request.method == Method::GET
1387                }
1388            }
1389
1390            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1391                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1392                    "chunk": [],
1393                })))
1394                .mount_as_scoped(&server)
1395                .await;
1396
1397            assert_matches!(room0.request_members().await, Ok(()));
1398        }
1399
1400        // Members are synced, good, good.
1401        assert!(room0.are_members_synced());
1402
1403        sliding_sync.resubscribe_to_rooms(&[room_id_0], None, false);
1404
1405        // Members are still synced: because we have already subscribed to the
1406        // room, the members aren't marked as unsynced.
1407        assert!(room0.are_members_synced());
1408
1409        // Only the resubscribed room is subscribed.
1410        {
1411            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1412
1413            assert!(room_subscriptions.contains_key(room_id_0));
1414            assert!(!room_subscriptions.contains_key(room_id_1));
1415            assert!(!room_subscriptions.contains_key(room_id_2));
1416        }
1417        Ok(())
1418    }
1419
1420    #[async_test]
1421    async fn test_room_subscriptions_are_reset_when_session_expires() -> Result<()> {
1422        let (_server, sliding_sync) = new_sliding_sync(vec![
1423            SlidingSyncList::builder("foo")
1424                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1425        ])
1426        .await?;
1427
1428        let room_id_0 = room_id!("!r0:bar.org");
1429        let room_id_1 = room_id!("!r1:bar.org");
1430        let room_id_2 = room_id!("!r2:bar.org");
1431
1432        // Subscribe to two rooms.
1433        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, false);
1434
1435        {
1436            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1437
1438            assert!(room_subscriptions.contains_key(room_id_0));
1439            assert!(room_subscriptions.contains_key(room_id_1));
1440            assert!(room_subscriptions.contains_key(room_id_2).not());
1441        }
1442
1443        // Subscribe to one more room.
1444        sliding_sync.subscribe_to_rooms(&[room_id_2], None, false);
1445
1446        {
1447            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1448
1449            assert!(room_subscriptions.contains_key(room_id_0));
1450            assert!(room_subscriptions.contains_key(room_id_1));
1451            assert!(room_subscriptions.contains_key(room_id_2));
1452        }
1453
1454        // Suddenly, the session expires!
1455        sliding_sync.expire_session().await;
1456
1457        {
1458            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1459
1460            assert!(room_subscriptions.is_empty());
1461        }
1462
1463        // Subscribe to one room again.
1464        sliding_sync.subscribe_to_rooms(&[room_id_2], None, false);
1465
1466        {
1467            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1468
1469            assert!(room_subscriptions.contains_key(room_id_0).not());
1470            assert!(room_subscriptions.contains_key(room_id_1).not());
1471            assert!(room_subscriptions.contains_key(room_id_2));
1472        }
1473
1474        Ok(())
1475    }
1476
1477    #[async_test]
1478    async fn test_add_list() -> Result<()> {
1479        let (_server, sliding_sync) = new_sliding_sync(vec![
1480            SlidingSyncList::builder("foo")
1481                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1482        ])
1483        .await?;
1484
1485        let _stream = sliding_sync.sync();
1486        pin_mut!(_stream);
1487
1488        sliding_sync
1489            .add_list(
1490                SlidingSyncList::builder("bar")
1491                    .sync_mode(SlidingSyncMode::new_selective().add_range(50..=60)),
1492            )
1493            .await?;
1494
1495        let lists = sliding_sync.inner.lists.read().await;
1496
1497        assert!(lists.contains_key("foo"));
1498        assert!(lists.contains_key("bar"));
1499
1500        // this test also ensures that Tokio is not panicking when calling `add_list`.
1501
1502        Ok(())
1503    }
1504
1505    #[cfg(feature = "e2e-encryption")]
1506    #[async_test]
1507    async fn test_extensions_to_device_since_is_set() {
1508        use matrix_sdk_base::crypto::store::types::Changes;
1509
1510        let client = logged_in_client(None).await;
1511        let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone())
1512            .unwrap()
1513            .with_to_device_extension(assign!(
1514                http::request::ToDevice::default(),
1515                {
1516                    enabled: Some(true),
1517                }
1518            ))
1519            .build()
1520            .await
1521            .unwrap();
1522
1523        // Test `SlidingSyncInner::extensions`.
1524        {
1525            let to_device = &sliding_sync.inner.extensions.to_device;
1526
1527            assert_eq!(to_device.enabled, Some(true));
1528            assert!(to_device.since.is_none());
1529        }
1530
1531        // Test `Request::extensions`.
1532        {
1533            let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1534
1535            let to_device = &request.extensions.to_device;
1536
1537            assert_eq!(to_device.enabled, Some(true));
1538            assert!(to_device.since.is_none());
1539        }
1540
1541        // Define a `since` token.
1542        let since_token = "depuis".to_owned();
1543
1544        {
1545            if let Some(olm_machine) = &*client.olm_machine().await {
1546                olm_machine
1547                    .store()
1548                    .save_changes(Changes {
1549                        next_batch_token: Some(since_token.clone()),
1550                        ..Default::default()
1551                    })
1552                    .await
1553                    .unwrap();
1554            } else {
1555                panic!("Where is the Olm machine?");
1556            }
1557        }
1558
1559        // Test `Request::extensions` again.
1560        {
1561            let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1562
1563            let to_device = &request.extensions.to_device;
1564
1565            assert_eq!(to_device.enabled, Some(true));
1566            assert_eq!(to_device.since, Some(since_token));
1567        }
1568    }
1569
1570    // With MSC4186, with the `e2ee` extension enabled, if a request has no `pos`,
1571    // all the tracked users by the `OlmMachine` must be marked as dirty, i.e.
1572    // `/key/query` requests must be sent. See the code to see the details.
1573    //
1574    // This test is asserting that.
1575    #[async_test]
1576    #[cfg(feature = "e2e-encryption")]
1577    async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> {
1578        use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest};
1579        use matrix_sdk_test::ruma_response_from_json;
1580        use ruma::user_id;
1581
1582        let server = MockServer::start().await;
1583        let client = logged_in_client(Some(server.uri())).await;
1584
1585        let alice = user_id!("@alice:localhost");
1586        let bob = user_id!("@bob:localhost");
1587        let me = user_id!("@example:localhost");
1588
1589        // Track and mark users are not dirty, so that we can check they are “dirty”
1590        // after that. Dirty here means that a `/key/query` must be sent.
1591        {
1592            let olm_machine = client.olm_machine().await;
1593            let olm_machine = olm_machine.as_ref().unwrap();
1594
1595            olm_machine.update_tracked_users([alice, bob]).await?;
1596
1597            // Assert requests.
1598            let outgoing_requests = olm_machine.outgoing_requests().await?;
1599
1600            assert_eq!(outgoing_requests.len(), 2);
1601            assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_));
1602            assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_));
1603
1604            // Fake responses.
1605            olm_machine
1606                .mark_request_as_sent(
1607                    outgoing_requests[0].request_id(),
1608                    AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({
1609                        "one_time_key_counts": {}
1610                    }))),
1611                )
1612                .await?;
1613
1614            olm_machine
1615                .mark_request_as_sent(
1616                    outgoing_requests[1].request_id(),
1617                    AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1618                        "device_keys": {
1619                            alice: {},
1620                            bob: {},
1621                        }
1622                    }))),
1623                )
1624                .await?;
1625
1626            // Once more.
1627            let outgoing_requests = olm_machine.outgoing_requests().await?;
1628
1629            assert_eq!(outgoing_requests.len(), 1);
1630            assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_));
1631
1632            olm_machine
1633                .mark_request_as_sent(
1634                    outgoing_requests[0].request_id(),
1635                    AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1636                        "device_keys": {
1637                            me: {},
1638                        }
1639                    }))),
1640                )
1641                .await?;
1642
1643            // No more.
1644            let outgoing_requests = olm_machine.outgoing_requests().await?;
1645
1646            assert!(outgoing_requests.is_empty());
1647        }
1648
1649        let sync = client
1650            .sliding_sync("test-slidingsync")?
1651            .add_list(SlidingSyncList::builder("new_list"))
1652            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
1653            .build()
1654            .await?;
1655
1656        // First request: no `pos`.
1657        let (_request, _, _) = sync.generate_sync_request().await?;
1658
1659        // Now, tracked users must be dirty.
1660        {
1661            let olm_machine = client.olm_machine().await;
1662            let olm_machine = olm_machine.as_ref().unwrap();
1663
1664            // Assert requests.
1665            let outgoing_requests = olm_machine.outgoing_requests().await?;
1666
1667            assert_eq!(outgoing_requests.len(), 1);
1668            assert_matches!(
1669                outgoing_requests[0].request(),
1670                AnyOutgoingRequest::KeysQuery(request) => {
1671                    assert!(request.device_keys.contains_key(alice));
1672                    assert!(request.device_keys.contains_key(bob));
1673                    assert!(request.device_keys.contains_key(me));
1674                }
1675            );
1676
1677            // Fake responses.
1678            olm_machine
1679                .mark_request_as_sent(
1680                    outgoing_requests[0].request_id(),
1681                    AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1682                        "device_keys": {
1683                            alice: {},
1684                            bob: {},
1685                            me: {},
1686                        }
1687                    }))),
1688                )
1689                .await?;
1690        }
1691
1692        // Second request: with a `pos` this time.
1693        sync.set_pos("chocolat".to_owned()).await;
1694
1695        let (_request, _, _) = sync.generate_sync_request().await?;
1696
1697        // Tracked users are not marked as dirty.
1698        {
1699            let olm_machine = client.olm_machine().await;
1700            let olm_machine = olm_machine.as_ref().unwrap();
1701
1702            // Assert requests.
1703            let outgoing_requests = olm_machine.outgoing_requests().await?;
1704
1705            assert!(outgoing_requests.is_empty());
1706        }
1707
1708        Ok(())
1709    }
1710
1711    #[cfg(feature = "e2e-encryption")]
1712    #[async_test]
1713    async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> {
1714        let server = MockServer::start().await;
1715
1716        #[derive(Deserialize)]
1717        struct PartialRequest {
1718            txn_id: Option<String>,
1719        }
1720
1721        let server_pos = Arc::new(Mutex::new(0));
1722        let _mock_guard = Mock::given(SlidingSyncMatcher)
1723            .respond_with(move |request: &Request| {
1724                // Repeat the txn_id in the response, if set.
1725                let request: PartialRequest = request.body_json().unwrap();
1726                let pos = {
1727                    let mut pos = server_pos.lock().unwrap();
1728                    let prev = *pos;
1729                    *pos += 1;
1730                    prev
1731                };
1732
1733                ResponseTemplate::new(200).set_body_json(json!({
1734                    "txn_id": request.txn_id,
1735                    "pos": pos.to_string(),
1736                }))
1737            })
1738            .mount_as_scoped(&server)
1739            .await;
1740
1741        let client = logged_in_client(Some(server.uri())).await;
1742
1743        let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1744
1745        // `pos` is `None` to start with.
1746        {
1747            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1748
1749            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1750            assert!(request.pos.is_none());
1751        }
1752
1753        let sync = sliding_sync.sync();
1754        pin_mut!(sync);
1755
1756        // Sync goes well, and then the position is saved both into the internal memory
1757        // and the database.
1758        let next = sync.next().await;
1759        assert_matches!(next, Some(Ok(_update_summary)));
1760
1761        assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
1762
1763        let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1764            .await?
1765            .expect("must have restored fields");
1766
1767        // While it has been saved into the database, it's not necessarily going to be
1768        // used later!
1769        assert_eq!(restored_fields.pos.as_deref(), Some("0"));
1770
1771        // Now, even if we mess with the position stored in the database, the sliding
1772        // sync instance isn't configured to reload the stream position from the
1773        // database, so it won't be changed.
1774        {
1775            let other_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1776
1777            let mut position_guard = other_sync.inner.position.lock().await;
1778            position_guard.pos = Some("yolo".to_owned());
1779
1780            other_sync.cache_to_storage(&position_guard).await?;
1781        }
1782
1783        // It's still 0, not "yolo".
1784        {
1785            assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
1786            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1787            assert_eq!(request.pos.as_deref(), Some("0"));
1788        }
1789
1790        // Recreating a sliding sync with the same ID doesn't preload the pos, if not
1791        // asked to.
1792        {
1793            let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1794            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1795        }
1796
1797        Ok(())
1798    }
1799
1800    #[cfg(feature = "e2e-encryption")]
1801    #[async_test]
1802    async fn test_sliding_sync_does_remember_pos() -> Result<()> {
1803        let server = MockServer::start().await;
1804
1805        #[derive(Deserialize)]
1806        struct PartialRequest {
1807            txn_id: Option<String>,
1808        }
1809
1810        let server_pos = Arc::new(Mutex::new(0));
1811        let _mock_guard = Mock::given(SlidingSyncMatcher)
1812            .respond_with(move |request: &Request| {
1813                // Repeat the txn_id in the response, if set.
1814                let request: PartialRequest = request.body_json().unwrap();
1815                let pos = {
1816                    let mut pos = server_pos.lock().unwrap();
1817                    let prev = *pos;
1818                    *pos += 1;
1819                    prev
1820                };
1821
1822                ResponseTemplate::new(200).set_body_json(json!({
1823                    "txn_id": request.txn_id,
1824                    "pos": pos.to_string(),
1825                }))
1826            })
1827            .mount_as_scoped(&server)
1828            .await;
1829
1830        let client = logged_in_client(Some(server.uri())).await;
1831
1832        let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1833
1834        // `pos` is `None` to start with.
1835        {
1836            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1837
1838            assert!(request.pos.is_none());
1839            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1840        }
1841
1842        let sync = sliding_sync.sync();
1843        pin_mut!(sync);
1844
1845        // Sync goes well, and then the position is saved both into the internal memory
1846        // and the database.
1847        let next = sync.next().await;
1848        assert_matches!(next, Some(Ok(_update_summary)));
1849
1850        assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned()));
1851
1852        let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1853            .await?
1854            .expect("must have restored fields");
1855
1856        // While it has been saved into the database, it's not necessarily going to be
1857        // used later!
1858        assert_eq!(restored_fields.pos.as_deref(), Some("0"));
1859
1860        // Another process modifies the stream position under our feet...
1861        {
1862            let other_sync = client.sliding_sync("elephant-sync")?.build().await?;
1863
1864            let mut position_guard = other_sync.inner.position.lock().await;
1865            position_guard.pos = Some("42".to_owned());
1866
1867            other_sync.cache_to_storage(&position_guard).await?;
1868        }
1869
1870        // It's alright, the next request will load it from the database.
1871        {
1872            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1873            assert_eq!(request.pos.as_deref(), Some("42"));
1874            assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
1875        }
1876
1877        // Recreating a sliding sync with the same ID will reload it too.
1878        {
1879            let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1880            assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
1881
1882            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1883            assert_eq!(request.pos.as_deref(), Some("42"));
1884        }
1885
1886        // Invalidating the session will remove the in-memory value AND the database
1887        // value.
1888        sliding_sync.expire_session().await;
1889
1890        {
1891            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1892
1893            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1894            assert!(request.pos.is_none());
1895        }
1896
1897        // And new sliding syncs with the same ID won't find it either.
1898        {
1899            let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1900            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1901
1902            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1903            assert!(request.pos.is_none());
1904        }
1905
1906        Ok(())
1907    }
1908
1909    #[async_test]
1910    async fn test_stop_sync_loop() -> Result<()> {
1911        let (_server, sliding_sync) = new_sliding_sync(vec![
1912            SlidingSyncList::builder("foo")
1913                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1914        ])
1915        .await?;
1916
1917        // Start the sync loop.
1918        let stream = sliding_sync.sync();
1919        pin_mut!(stream);
1920
1921        // The sync loop is actually running.
1922        assert!(stream.next().await.is_some());
1923
1924        // Stop the sync loop.
1925        sliding_sync.stop_sync()?;
1926
1927        // The sync loop is actually stopped.
1928        assert!(stream.next().await.is_none());
1929
1930        // Start a new sync loop.
1931        let stream = sliding_sync.sync();
1932        pin_mut!(stream);
1933
1934        // The sync loop is actually running.
1935        assert!(stream.next().await.is_some());
1936
1937        Ok(())
1938    }
1939
1940    #[async_test]
1941    async fn test_process_read_receipts() -> Result<()> {
1942        let room = owned_room_id!("!pony:example.org");
1943
1944        let server = MockServer::start().await;
1945        let client = logged_in_client(Some(server.uri())).await;
1946        client.event_cache().subscribe().unwrap();
1947
1948        let sliding_sync = client
1949            .sliding_sync("test")?
1950            .with_receipt_extension(
1951                assign!(http::request::Receipts::default(), { enabled: Some(true) }),
1952            )
1953            .add_list(
1954                SlidingSyncList::builder("all")
1955                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
1956            )
1957            .build()
1958            .await?;
1959
1960        // Initial state.
1961        {
1962            let server_response = assign!(http::Response::new("0".to_owned()), {
1963                rooms: BTreeMap::from([(
1964                    room.clone(),
1965                    http::response::Room::default(),
1966                )])
1967            });
1968
1969            let _summary = {
1970                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
1971                sliding_sync
1972                    .handle_response(
1973                        server_response.clone(),
1974                        &mut pos_guard,
1975                        RequestedRequiredStates::default(),
1976                    )
1977                    .await?
1978            };
1979        }
1980
1981        let server_response = assign!(http::Response::new("1".to_owned()), {
1982            extensions: assign!(http::response::Extensions::default(), {
1983                receipts: assign!(http::response::Receipts::default(), {
1984                    rooms: BTreeMap::from([
1985                        (
1986                            room.clone(),
1987                            Raw::from_json_string(
1988                                json!({
1989                                    "room_id": room,
1990                                    "type": "m.receipt",
1991                                    "content": {
1992                                        "$event:bar.org": {
1993                                            "m.read": {
1994                                                client.user_id().unwrap(): {
1995                                                    "ts": 1436451550,
1996                                                }
1997                                            }
1998                                        }
1999                                    }
2000                                })
2001                                .to_string(),
2002                            ).unwrap()
2003                        )
2004                    ])
2005                })
2006            })
2007        });
2008
2009        let summary = {
2010            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2011            sliding_sync
2012                .handle_response(
2013                    server_response.clone(),
2014                    &mut pos_guard,
2015                    RequestedRequiredStates::default(),
2016                )
2017                .await?
2018        };
2019
2020        assert!(summary.rooms.contains(&room));
2021
2022        Ok(())
2023    }
2024
2025    #[async_test]
2026    async fn test_process_marked_unread_room_account_data() -> Result<()> {
2027        let room_id = owned_room_id!("!unicorn:example.org");
2028
2029        let server = MockServer::start().await;
2030        let client = logged_in_client(Some(server.uri())).await;
2031
2032        // Setup sliding sync with with one room and one list
2033
2034        let sliding_sync = client
2035            .sliding_sync("test")?
2036            .with_account_data_extension(
2037                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2038            )
2039            .add_list(
2040                SlidingSyncList::builder("all")
2041                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2042            )
2043            .build()
2044            .await?;
2045
2046        // Initial state.
2047        {
2048            let server_response = assign!(http::Response::new("0".to_owned()), {
2049                rooms: BTreeMap::from([(
2050                    room_id.clone(),
2051                    http::response::Room::default(),
2052                )])
2053            });
2054
2055            let _summary = {
2056                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2057                sliding_sync
2058                    .handle_response(
2059                        server_response.clone(),
2060                        &mut pos_guard,
2061                        RequestedRequiredStates::default(),
2062                    )
2063                    .await?
2064            };
2065        }
2066
2067        // Simulate a response that only changes the marked unread state of the room to
2068        // true
2069
2070        let server_response = make_mark_unread_response("1", room_id.clone(), true, false);
2071
2072        let update_summary = {
2073            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2074            sliding_sync
2075                .handle_response(
2076                    server_response.clone(),
2077                    &mut pos_guard,
2078                    RequestedRequiredStates::default(),
2079                )
2080                .await?
2081        };
2082
2083        // Check that the list list and entry received the update
2084
2085        assert!(update_summary.rooms.contains(&room_id));
2086
2087        let room = client.get_room(&room_id).unwrap();
2088
2089        // Check the actual room data, this powers RoomInfo
2090
2091        assert!(room.is_marked_unread());
2092
2093        // Change it back to false and check if it updates
2094
2095        let server_response = make_mark_unread_response("2", room_id.clone(), false, true);
2096
2097        let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2098        sliding_sync
2099            .handle_response(
2100                server_response.clone(),
2101                &mut pos_guard,
2102                RequestedRequiredStates::default(),
2103            )
2104            .await?;
2105
2106        let room = client.get_room(&room_id).unwrap();
2107
2108        assert!(!room.is_marked_unread());
2109
2110        Ok(())
2111    }
2112
2113    fn make_mark_unread_response(
2114        response_number: &str,
2115        room_id: OwnedRoomId,
2116        unread: bool,
2117        add_rooms_section: bool,
2118    ) -> http::Response {
2119        let rooms = if add_rooms_section {
2120            BTreeMap::from([(room_id.clone(), http::response::Room::default())])
2121        } else {
2122            BTreeMap::new()
2123        };
2124
2125        let extensions = assign!(http::response::Extensions::default(), {
2126            account_data: assign!(http::response::AccountData::default(), {
2127                rooms: BTreeMap::from([
2128                    (
2129                        room_id,
2130                        vec![
2131                            Raw::from_json_string(
2132                                json!({
2133                                    "content": {
2134                                        "unread": unread
2135                                    },
2136                                    "type": "m.marked_unread"
2137                                })
2138                                .to_string(),
2139                            ).unwrap()
2140                        ]
2141                    )
2142                ])
2143            })
2144        });
2145
2146        assign!(http::Response::new(response_number.to_owned()), { rooms: rooms, extensions: extensions })
2147    }
2148
2149    #[async_test]
2150    async fn test_process_rooms_account_data() -> Result<()> {
2151        let room = owned_room_id!("!pony:example.org");
2152
2153        let server = MockServer::start().await;
2154        let client = logged_in_client(Some(server.uri())).await;
2155
2156        let sliding_sync = client
2157            .sliding_sync("test")?
2158            .with_account_data_extension(
2159                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2160            )
2161            .add_list(
2162                SlidingSyncList::builder("all")
2163                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2164            )
2165            .build()
2166            .await?;
2167
2168        // Initial state.
2169        {
2170            let server_response = assign!(http::Response::new("0".to_owned()), {
2171                rooms: BTreeMap::from([(
2172                    room.clone(),
2173                    http::response::Room::default(),
2174                )])
2175            });
2176
2177            let _summary = {
2178                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2179                sliding_sync
2180                    .handle_response(
2181                        server_response.clone(),
2182                        &mut pos_guard,
2183                        RequestedRequiredStates::default(),
2184                    )
2185                    .await?
2186            };
2187        }
2188
2189        let server_response = assign!(http::Response::new("1".to_owned()), {
2190            extensions: assign!(http::response::Extensions::default(), {
2191                account_data: assign!(http::response::AccountData::default(), {
2192                    rooms: BTreeMap::from([
2193                        (
2194                            room.clone(),
2195                            vec![
2196                                Raw::from_json_string(
2197                                    json!({
2198                                        "content": {
2199                                            "tags": {
2200                                                "u.work": {
2201                                                    "order": 0.9
2202                                                }
2203                                            }
2204                                        },
2205                                        "type": "m.tag"
2206                                    })
2207                                    .to_string(),
2208                                ).unwrap()
2209                            ]
2210                        )
2211                    ])
2212                })
2213            })
2214        });
2215        let summary = {
2216            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2217            sliding_sync
2218                .handle_response(
2219                    server_response.clone(),
2220                    &mut pos_guard,
2221                    RequestedRequiredStates::default(),
2222                )
2223                .await?
2224        };
2225
2226        assert!(summary.rooms.contains(&room));
2227
2228        Ok(())
2229    }
2230
2231    #[async_test]
2232    #[cfg(feature = "e2e-encryption")]
2233    async fn test_process_only_encryption_events() -> Result<()> {
2234        use ruma::OneTimeKeyAlgorithm;
2235
2236        let room = owned_room_id!("!croissant:example.org");
2237
2238        let server = MockServer::start().await;
2239        let client = logged_in_client(Some(server.uri())).await;
2240
2241        let server_response = assign!(http::Response::new("0".to_owned()), {
2242            rooms: BTreeMap::from([(
2243                room.clone(),
2244                assign!(http::response::Room::default(), {
2245                    name: Some("Croissants lovers".to_owned()),
2246                    timeline: Vec::new(),
2247                }),
2248            )]),
2249
2250            extensions: assign!(http::response::Extensions::default(), {
2251                e2ee: assign!(http::response::E2EE::default(), {
2252                    device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))])
2253                }),
2254                to_device: Some(assign!(http::response::ToDevice::default(), {
2255                    next_batch: "to-device-token".to_owned(),
2256                })),
2257            })
2258        });
2259
2260        // Don't process non-encryption events if the sliding sync is configured for
2261        // encryption only.
2262
2263        let sliding_sync = client
2264            .sliding_sync("test")?
2265            .with_to_device_extension(
2266                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2267            )
2268            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2269            .build()
2270            .await?;
2271
2272        {
2273            let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2274
2275            sliding_sync
2276                .handle_response(
2277                    server_response.clone(),
2278                    &mut position_guard,
2279                    RequestedRequiredStates::default(),
2280                )
2281                .await?;
2282        }
2283
2284        // E2EE has been properly handled.
2285        let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2286        assert_eq!(uploaded_key_count, 42);
2287
2288        {
2289            let olm_machine = &*client.olm_machine_for_testing().await;
2290            assert_eq!(
2291                olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2292                Some("to-device-token")
2293            );
2294        }
2295
2296        // Room events haven't.
2297        assert!(client.get_room(&room).is_none());
2298
2299        // Conversely, only process room lists events if the sliding sync was configured
2300        // as so.
2301        let client = logged_in_client(Some(server.uri())).await;
2302
2303        let sliding_sync = client
2304            .sliding_sync("test")?
2305            .add_list(SlidingSyncList::builder("thelist"))
2306            .build()
2307            .await?;
2308
2309        {
2310            let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2311
2312            sliding_sync
2313                .handle_response(
2314                    server_response.clone(),
2315                    &mut position_guard,
2316                    RequestedRequiredStates::default(),
2317                )
2318                .await?;
2319        }
2320
2321        // E2EE response has been ignored.
2322        let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2323        assert_eq!(uploaded_key_count, 0);
2324
2325        {
2326            let olm_machine = &*client.olm_machine_for_testing().await;
2327            assert_eq!(
2328                olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2329                None
2330            );
2331        }
2332
2333        // The room is now known.
2334        assert!(client.get_room(&room).is_some());
2335
2336        // And it's also possible to set up both.
2337        let client = logged_in_client(Some(server.uri())).await;
2338
2339        let sliding_sync = client
2340            .sliding_sync("test")?
2341            .add_list(SlidingSyncList::builder("thelist"))
2342            .with_to_device_extension(
2343                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2344            )
2345            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2346            .build()
2347            .await?;
2348
2349        {
2350            let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2351
2352            sliding_sync
2353                .handle_response(
2354                    server_response.clone(),
2355                    &mut position_guard,
2356                    RequestedRequiredStates::default(),
2357                )
2358                .await?;
2359        }
2360
2361        // E2EE has been properly handled.
2362        let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2363        assert_eq!(uploaded_key_count, 42);
2364
2365        {
2366            let olm_machine = &*client.olm_machine_for_testing().await;
2367            assert_eq!(
2368                olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2369                Some("to-device-token")
2370            );
2371        }
2372
2373        // The room is now known.
2374        assert!(client.get_room(&room).is_some());
2375
2376        Ok(())
2377    }
2378
2379    #[async_test]
2380    async fn test_lock_multiple_requests() -> Result<()> {
2381        let server = MockServer::start().await;
2382        let client = logged_in_client(Some(server.uri())).await;
2383
2384        let pos = Arc::new(Mutex::new(0));
2385        let _mock_guard = Mock::given(SlidingSyncMatcher)
2386            .respond_with(move |_: &Request| {
2387                let mut pos = pos.lock().unwrap();
2388                *pos += 1;
2389                ResponseTemplate::new(200).set_body_json(json!({
2390                    "pos": pos.to_string(),
2391                    "lists": {},
2392                    "rooms": {}
2393                }))
2394            })
2395            .mount_as_scoped(&server)
2396            .await;
2397
2398        let sliding_sync = client
2399            .sliding_sync("test")?
2400            .with_to_device_extension(
2401                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2402            )
2403            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2404            .build()
2405            .await?;
2406
2407        // Spawn two requests in parallel. Before #2430, this lead to a deadlock and the
2408        // test would never terminate.
2409        let requests = join_all([sliding_sync.sync_once(), sliding_sync.sync_once()]);
2410
2411        for result in requests.await {
2412            result?;
2413        }
2414
2415        Ok(())
2416    }
2417
2418    #[async_test]
2419    async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> {
2420        let server = MockServer::start().await;
2421        let client = logged_in_client(Some(server.uri())).await;
2422
2423        let pos = Arc::new(Mutex::new(0));
2424        let _mock_guard = Mock::given(SlidingSyncMatcher)
2425            .respond_with(move |_: &Request| {
2426                let mut pos = pos.lock().unwrap();
2427                *pos += 1;
2428                // Respond slowly enough that we can skip one iteration.
2429                ResponseTemplate::new(200)
2430                    .set_body_json(json!({
2431                        "pos": pos.to_string(),
2432                        "lists": {},
2433                        "rooms": {}
2434                    }))
2435                    .set_delay(Duration::from_secs(2))
2436            })
2437            .mount_as_scoped(&server)
2438            .await;
2439
2440        let sliding_sync =
2441            client
2442                .sliding_sync("test")?
2443                .add_list(SlidingSyncList::builder("room-list").sync_mode(
2444                    SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(100),
2445                ))
2446                .add_list(
2447                    SlidingSyncList::builder("another-list")
2448                        .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2449                )
2450                .build()
2451                .await?;
2452
2453        let stream = sliding_sync.sync();
2454        pin_mut!(stream);
2455
2456        let cloned_sync = sliding_sync.clone();
2457        spawn(async move {
2458            tokio::time::sleep(Duration::from_millis(100)).await;
2459
2460            cloned_sync
2461                .on_list("another-list", |list| {
2462                    list.set_sync_mode(SlidingSyncMode::new_selective().add_range(10..=20));
2463                    ready(())
2464                })
2465                .await;
2466        });
2467
2468        assert_matches!(stream.next().await, Some(Ok(_)));
2469
2470        sliding_sync.stop_sync().unwrap();
2471
2472        assert_matches!(stream.next().await, None);
2473
2474        let mut num_requests = 0;
2475
2476        for request in server.received_requests().await.unwrap() {
2477            if !SlidingSyncMatcher.matches(&request) {
2478                continue;
2479            }
2480
2481            let another_list_ranges = if num_requests == 0 {
2482                // First request
2483                json!([[0, 10]])
2484            } else {
2485                // Second request
2486                json!([[10, 20]])
2487            };
2488
2489            num_requests += 1;
2490            assert!(num_requests <= 2, "more than one request hit the server");
2491
2492            let json_value = serde_json::from_slice::<serde_json::Value>(&request.body).unwrap();
2493
2494            if let Err(err) = assert_json_diff::assert_json_matches_no_panic(
2495                &json_value,
2496                &json!({
2497                    "conn_id": "test",
2498                    "lists": {
2499                        "room-list": {
2500                            "ranges": [[0, 9]],
2501                            "required_state": [
2502                                ["m.room.encryption", ""],
2503                                ["m.room.tombstone", ""]
2504                            ],
2505                        },
2506                        "another-list": {
2507                            "ranges": another_list_ranges,
2508                            "required_state": [
2509                                ["m.room.encryption", ""],
2510                                ["m.room.tombstone", ""]
2511                            ],
2512                        },
2513                    }
2514                }),
2515                assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive),
2516            ) {
2517                dbg!(json_value);
2518                panic!("json differ: {err}");
2519            }
2520        }
2521
2522        assert_eq!(num_requests, 2);
2523
2524        Ok(())
2525    }
2526
2527    #[async_test]
2528    async fn test_timeout_zero_list() -> Result<()> {
2529        let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
2530
2531        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2532
2533        // Zero list means sliding sync is fully loaded, so there is a timeout to wait
2534        // on new update to pop.
2535        assert!(request.timeout.is_some());
2536
2537        Ok(())
2538    }
2539
2540    #[async_test]
2541    async fn test_timeout_one_list() -> Result<()> {
2542        let (_server, sliding_sync) = new_sliding_sync(vec![
2543            SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2544        ])
2545        .await?;
2546
2547        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2548
2549        // The list does not require a timeout.
2550        assert!(request.timeout.is_none());
2551
2552        // Simulate a response.
2553        {
2554            let server_response = assign!(http::Response::new("0".to_owned()), {
2555                lists: BTreeMap::from([(
2556                    "foo".to_owned(),
2557                    assign!(http::response::List::default(), {
2558                        count: uint!(7),
2559                    })
2560                 )])
2561            });
2562
2563            let _summary = {
2564                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2565                sliding_sync
2566                    .handle_response(
2567                        server_response.clone(),
2568                        &mut pos_guard,
2569                        RequestedRequiredStates::default(),
2570                    )
2571                    .await?
2572            };
2573        }
2574
2575        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2576
2577        // The list is now fully loaded, so it requires a timeout.
2578        assert!(request.timeout.is_some());
2579
2580        Ok(())
2581    }
2582
2583    #[async_test]
2584    async fn test_timeout_three_lists() -> Result<()> {
2585        let (_server, sliding_sync) = new_sliding_sync(vec![
2586            SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2587            SlidingSyncList::builder("bar").sync_mode(SlidingSyncMode::new_paging(10)),
2588            SlidingSyncList::builder("baz")
2589                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2590        ])
2591        .await?;
2592
2593        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2594
2595        // Two lists don't require a timeout.
2596        assert!(request.timeout.is_none());
2597
2598        // Simulate a response.
2599        {
2600            let server_response = assign!(http::Response::new("0".to_owned()), {
2601                lists: BTreeMap::from([(
2602                    "foo".to_owned(),
2603                    assign!(http::response::List::default(), {
2604                        count: uint!(7),
2605                    })
2606                 )])
2607            });
2608
2609            let _summary = {
2610                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2611                sliding_sync
2612                    .handle_response(
2613                        server_response.clone(),
2614                        &mut pos_guard,
2615                        RequestedRequiredStates::default(),
2616                    )
2617                    .await?
2618            };
2619        }
2620
2621        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2622
2623        // One don't require a timeout.
2624        assert!(request.timeout.is_none());
2625
2626        // Simulate a response.
2627        {
2628            let server_response = assign!(http::Response::new("1".to_owned()), {
2629                lists: BTreeMap::from([(
2630                    "bar".to_owned(),
2631                    assign!(http::response::List::default(), {
2632                        count: uint!(7),
2633                    })
2634                 )])
2635            });
2636
2637            let _summary = {
2638                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2639                sliding_sync
2640                    .handle_response(
2641                        server_response.clone(),
2642                        &mut pos_guard,
2643                        RequestedRequiredStates::default(),
2644                    )
2645                    .await?
2646            };
2647        }
2648
2649        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2650
2651        // All lists require a timeout.
2652        assert!(request.timeout.is_some());
2653
2654        Ok(())
2655    }
2656
2657    #[async_test]
2658    async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> {
2659        let server = MockServer::start().await;
2660        let client = logged_in_client(Some(server.uri())).await;
2661
2662        let _mock_guard = Mock::given(SlidingSyncMatcher)
2663            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2664                "pos": "0",
2665                "lists": {},
2666                "rooms": {}
2667            })))
2668            .mount_as_scoped(&server)
2669            .await;
2670
2671        let sliding_sync = client
2672            .sliding_sync("test")?
2673            .with_to_device_extension(
2674                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2675            )
2676            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2677            .build()
2678            .await?;
2679
2680        let sliding_sync = Arc::new(sliding_sync);
2681
2682        // Create the listener and perform a sync request
2683        let sync_beat_listener = client.inner.sync_beat.listen();
2684        sliding_sync.sync_once().await?;
2685
2686        // The sync beat listener should be notified shortly after
2687        assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some());
2688        Ok(())
2689    }
2690
2691    #[async_test]
2692    async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> {
2693        let server = MockServer::start().await;
2694        let client = logged_in_client(Some(server.uri())).await;
2695
2696        let _mock_guard = Mock::given(SlidingSyncMatcher)
2697            .respond_with(ResponseTemplate::new(404))
2698            .mount_as_scoped(&server)
2699            .await;
2700
2701        let sliding_sync = client
2702            .sliding_sync("test")?
2703            .with_to_device_extension(
2704                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2705            )
2706            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2707            .build()
2708            .await?;
2709
2710        let sliding_sync = Arc::new(sliding_sync);
2711
2712        // Create the listener and perform a sync request
2713        let sync_beat_listener = client.inner.sync_beat.listen();
2714        let sync_result = sliding_sync.sync_once().await;
2715        assert!(sync_result.is_err());
2716
2717        // The sync beat listener won't be notified in this case
2718        assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none());
2719
2720        Ok(())
2721    }
2722
2723    #[async_test]
2724    async fn test_state_store_lock_is_released_before_calling_handlers() -> Result<()> {
2725        let server = MatrixMockServer::new().await;
2726        let client = server.client_builder().build().await;
2727        let room_id = room_id!("!mu5hr00m:example.org");
2728
2729        let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
2730            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2731                "pos": "0",
2732                "lists": {},
2733                "extensions": {
2734                    "account_data": {
2735                        "global": [
2736                            {
2737                                "type": "m.direct",
2738                                "content": {
2739                                    "@de4dlockh0lmes:example.org": [
2740                                        "!mu5hr00m:example.org"
2741                                    ]
2742                                }
2743                            }
2744                        ]
2745                    }
2746                },
2747                "rooms": {
2748                    room_id: {
2749                        "name": "Mario Bros Fanbase Room",
2750                        "initial": true,
2751                    },
2752                }
2753            })))
2754            .mount_as_scoped(server.server())
2755            .await;
2756
2757        let f = EventFactory::new().room(room_id);
2758
2759        Mock::given(method("GET"))
2760            .and(wiremock::matchers::path_regex(r"/_matrix/client/v3/rooms/.*/members"))
2761            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2762                "chunk": [
2763                    f.member(&ALICE).membership(MembershipState::Join).into_raw_timeline(),
2764                ]
2765            })))
2766            .mount(server.server())
2767            .await;
2768
2769        let (tx, rx) = tokio::sync::oneshot::channel();
2770
2771        let tx = Arc::new(Mutex::new(Some(tx)));
2772        client.add_event_handler(move |_: DirectEvent, client: Client| async move {
2773            // Try to run a /members query while in a event handler.
2774            let members =
2775                client.get_room(room_id).unwrap().members(RoomMemberships::JOIN).await.unwrap();
2776            assert_eq!(members.len(), 1);
2777            tx.lock().unwrap().take().expect("sender consumed multiple times").send(()).unwrap();
2778        });
2779
2780        let sliding_sync = client
2781            .sliding_sync("test")?
2782            .add_list(SlidingSyncList::builder("thelist"))
2783            .with_account_data_extension(
2784                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2785            )
2786            .build()
2787            .await?;
2788
2789        tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
2790            .await
2791            .expect("Sync did not complete in time")
2792            .expect("Sync failed");
2793
2794        // Wait for the event handler to complete.
2795        tokio::time::timeout(Duration::from_secs(5), rx)
2796            .await
2797            .expect("Event handler did not complete in time")
2798            .expect("Event handler failed");
2799
2800        Ok(())
2801    }
2802}