Commit 49a9cd1aff19
tarball41 files changed+2570-131
@@ -83,8 +83,10 @@ fn writing(c: &mut Criterion) {
8383
}8484
8585
{86
- operations87
- .push(Operation::PushGapBack(Gap { token: format!("gap{gap_nth}") }));86
+ operations.push(Operation::PushGapBack(Gap {87
+ token: format!("gap{gap_nth}"),88
+ history: None,89
+ }));8890
gap_nth += 1;8991
}9092
}@@ -187,7 +189,7 @@ fn reading(c: &mut Criterion) {
187189
}188190
189191
lc.push_items_back(events_chunk);190
- lc.push_gap_back(Gap { token: format!("gap{num_gaps}") });192
+ lc.push_gap_back(Gap { token: format!("gap{num_gaps}"), history: None });191193
192194
num_gaps += 1;193195
}@@ -914,6 +914,22 @@ impl TimelineItem {
914914
use matrix_sdk_ui::timeline::VirtualTimelineItem as VItem;915915
match self.0.as_virtual()? {916916
VItem::DateDivider(ts) => Some(VirtualTimelineItem::DateDivider { ts: (*ts).into() }),917
+ VItem::HistoryGap(gap) => Some(VirtualTimelineItem::HistoryGap {918
+ id: gap.id.to_string(),919
+ older_boundary: gap.older_boundary.as_ref().map(ToString::to_string),920
+ newer_boundary: gap.newer_boundary.as_ref().map(ToString::to_string),921
+ status: match &gap.status {922
+ matrix_sdk::event_cache::HistoryGapStatus::Idle => HistoryGapStatus::Idle,923
+ matrix_sdk::event_cache::HistoryGapStatus::Paused => HistoryGapStatus::Paused,924
+ matrix_sdk::event_cache::HistoryGapStatus::Failed { message } => {925
+ HistoryGapStatus::Failed { message: message.clone() }926
+ }927
+ matrix_sdk::event_cache::HistoryGapStatus::Unavailable => {928
+ HistoryGapStatus::Unavailable929
+ }930
+ matrix_sdk::event_cache::HistoryGapStatus::Joined => HistoryGapStatus::Joined,931
+ },932
+ }),917933
VItem::ReadMarker => Some(VirtualTimelineItem::ReadMarker),918934
VItem::TimelineStart => Some(VirtualTimelineItem::TimelineStart),919935
}@@ -1205,6 +1221,13 @@ pub enum VirtualTimelineItem {
12051221
ts: Timestamp,12061222
},12071223
1224
+ HistoryGap {1225
+ id: String,1226
+ older_boundary: Option<String>,1227
+ newer_boundary: Option<String>,1228
+ status: HistoryGapStatus,1229
+ },1230
+12081231
/// The user's own read marker.12091232
ReadMarker,12101233
@@ -1212,6 +1235,15 @@ pub enum VirtualTimelineItem {
12121235
TimelineStart,12131236
}12141237
1238
+#[derive(Clone, uniffi::Enum)]1239
+pub enum HistoryGapStatus {1240
+ Idle,1241
+ Paused,1242
+ Failed { message: String },1243
+ Unavailable,1244
+ Joined,1245
+}1246
+12151247
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.12161248
#[derive(uniffi::Enum)]12171249
pub enum ReceiptType {@@ -15,6 +15,8 @@
1515
//! Event cache store and common types shared with `matrix_sdk::event_cache`.1616
1717
use matrix_sdk_common::deserialized_responses::TimelineEvent;18
+use ruma::{OwnedEventId, OwnedTransactionId};19
+use serde::{Deserialize, Serialize};1820
1921
pub mod store;2022
@@ -22,11 +24,48 @@ pub mod store;
2224
pub type Event = TimelineEvent;2325
2426
/// The kind of gap the event storage holds.25
-#[derive(Clone, Debug)]27
+#[derive(Clone, Debug, Serialize, Deserialize)]2628
pub struct Gap {2729
/// The token to use in the pagination query as the `from` parameter,2830
/// extracted from a previous `start` / `end` field of a `/messages`2931
/// response, or from the `prev_batch` / `next_batch` field of a `/sync`3032
/// response.3133
pub token: String,34
+ #[serde(default, skip_serializing_if = "Option::is_none")]35
+ #[doc = "Present for an interval that must survive independently of ordinary oldest-history pagination."]36
+ pub history: Option<HistoryGap>,37
+}38
+39
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]40
+#[doc = "Durable identity and boundary evidence for an independently pageable interval."]41
+pub struct HistoryGap {42
+ #[doc = "Remains unchanged when pagination replaces the underlying gap chunk."]43
+ pub id: OwnedTransactionId,44
+ #[doc = "Pagination must reach this retained event before claiming a verified join."]45
+ pub older_boundary: Option<OwnedEventId>,46
+ #[doc = "The original event on the newer side, preserved as pages are inserted."]47
+ pub newer_boundary: Option<OwnedEventId>,48
+ #[doc = "Previously consumed cursors detect server pagination cycles across restarts."]49
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]50
+ pub visited_cursors: Vec<String>,51
+ #[doc = "Unavailable and joined intervals remain distinguishable after restart."]52
+ pub status: HistoryGapStatus,53
+}54
+55
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]56
+#[doc = "Persisted outcomes distinguish server exhaustion from verified overlap."]57
+pub enum HistoryGapStatus {58
+ #[doc = "Another bounded request can continue from the saved cursor."]59
+ Idle,60
+ #[doc = "The server returned the same cursor, requiring an explicit retry."]61
+ Paused,62
+ #[doc = "A failed request leaves the last successful cursor intact."]63
+ Failed {64
+ #[doc = "The request failure, retained for display after restart."]65
+ message: String,66
+ },67
+ #[doc = "Accessible history ended without reaching the retained boundary."]68
+ Unavailable,69
+ #[doc = "Pagination reached the retained boundary. The identity remains valid for stale callers."]70
+ Joined,3271
}@@ -42,7 +42,7 @@ use ruma::{
4242
};4343
4444
use super::DynEventCacheStore;45
-use crate::event_cache::{Gap, store::DEFAULT_CHUNK_CAPACITY};45
+use crate::event_cache::{Gap, HistoryGap, HistoryGapStatus, store::DEFAULT_CHUNK_CAPACITY};4646
4747
/// Create a test event with all data filled, for testing that linked chunk4848
/// correctly stores event data.@@ -137,6 +137,9 @@ pub trait EventCacheStoreIntegrationTests {
137137
/// the store.138138
async fn test_handle_updates_and_rebuild_linked_chunk(&self);139139
140
+ #[doc = "Gap identity and cursor advancement must share the transaction that inserts their page."]141
+ async fn test_history_gap_roundtrip_and_replacement(&self);142
+140143
/// Test that the next and previous fields only reference chunks that141144
/// already exist in the store.142145
async fn test_linked_chunk_exists_before_referenced(&self);@@ -268,7 +271,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
268271
previous: Some(CId::new(0)),269272
new: CId::new(1),270273
next: None,271
- gap: Gap { token: "parmesan".to_owned() },274
+ gap: Gap { token: "parmesan".to_owned(), history: None },272275
},273276
// another items chunk274277
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },@@ -326,6 +329,108 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
326329
assert!(chunks.next().is_none());327330
}328331
332
+ async fn test_history_gap_roundtrip_and_replacement(&self) {333
+ let room_id = room_id!("!r0:matrix.org");334
+ let linked_chunk_id = LinkedChunkId::Room(room_id);335
+ let history = HistoryGap {336
+ id: "history-gap-id".into(),337
+ older_boundary: Some(event_id!("$older-boundary").to_owned()),338
+ newer_boundary: Some(event_id!("$newer-boundary").to_owned()),339
+ visited_cursors: Vec::new(),340
+ status: HistoryGapStatus::Failed { message: "retry later".to_owned() },341
+ };342
+ let expected_history = history.clone();343
+344
+ self.handle_linked_chunk_updates(345
+ linked_chunk_id,346
+ vec![347
+ Update::NewItemsChunk { previous: None, new: CId::new(0), next: None },348
+ Update::PushItems {349
+ at: Position::new(CId::new(0), 0),350
+ items: vec![make_test_event(room_id, "before")],351
+ },352
+ Update::NewGapChunk {353
+ previous: Some(CId::new(0)),354
+ new: CId::new(1),355
+ next: None,356
+ gap: Gap { token: "old history token".to_owned(), history: Some(history) },357
+ },358
+ Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },359
+ Update::PushItems {360
+ at: Position::new(CId::new(2), 0),361
+ items: vec![make_test_event(room_id, "after")],362
+ },363
+ Update::NewGapChunk {364
+ previous: Some(CId::new(2)),365
+ new: CId::new(3),366
+ next: None,367
+ gap: Gap { token: "other gap token".to_owned(), history: None },368
+ },369
+ ],370
+ )371
+ .await372
+ .unwrap();373
+374
+ self.handle_linked_chunk_updates(375
+ linked_chunk_id,376
+ vec![377
+ Update::RemoveChunk(CId::new(1)),378
+ Update::NewItemsChunk {379
+ previous: Some(CId::new(0)),380
+ new: CId::new(4),381
+ next: Some(CId::new(2)),382
+ },383
+ Update::PushItems {384
+ at: Position::new(CId::new(4), 0),385
+ items: vec![386
+ make_test_event(room_id, "inserted first"),387
+ make_test_event(room_id, "inserted second"),388
+ ],389
+ },390
+ Update::NewGapChunk {391
+ previous: Some(CId::new(4)),392
+ new: CId::new(5),393
+ next: Some(CId::new(2)),394
+ gap: Gap {395
+ token: "new history token".to_owned(),396
+ history: Some(expected_history.clone()),397
+ },398
+ },399
+ ],400
+ )401
+ .await402
+ .unwrap();403
+404
+ let linked_chunk = lazy_loader::from_all_chunks::<DEFAULT_CHUNK_CAPACITY, _, _>(405
+ self.load_all_chunks(linked_chunk_id).await.unwrap(),406
+ )407
+ .unwrap()408
+ .unwrap();409
+ let chunks: Vec<_> = linked_chunk.chunks().collect();410
+ assert_eq!(chunks.len(), 5);411
+ assert_matches!(chunks[0].content(), ChunkContent::Items(events) => {412
+ assert_eq!(events.len(), 1);413
+ check_test_event(&events[0], "before");414
+ });415
+ assert_matches!(chunks[1].content(), ChunkContent::Items(events) => {416
+ assert_eq!(events.len(), 2);417
+ check_test_event(&events[0], "inserted first");418
+ check_test_event(&events[1], "inserted second");419
+ });420
+ assert_matches!(chunks[2].content(), ChunkContent::Gap(gap) => {421
+ assert_eq!(gap.token, "new history token");422
+ assert_eq!(gap.history.as_ref(), Some(&expected_history));423
+ });424
+ assert_matches!(chunks[3].content(), ChunkContent::Items(events) => {425
+ assert_eq!(events.len(), 1);426
+ check_test_event(&events[0], "after");427
+ });428
+ assert_matches!(chunks[4].content(), ChunkContent::Gap(gap) => {429
+ assert_eq!(gap.token, "other gap token");430
+ assert!(gap.history.is_none());431
+ });432
+ }433
+329434
async fn test_linked_chunk_exists_before_referenced(&self) {330435
let room_id = *DEFAULT_TEST_ROOM_ID;331436
let linked_chunk_id = LinkedChunkId::Room(room_id);@@ -361,7 +466,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
361466
previous: Some(CId::new(41)),362467
new: CId::new(42),363468
next: None,364
- gap: Gap { token: "gap".to_owned() },469
+ gap: Gap { token: "gap".to_owned(), history: None },365470
}],366471
)367472
.await@@ -374,7 +479,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
374479
previous: None,375480
new: CId::new(42),376481
next: Some(CId::new(43)),377
- gap: Gap { token: "gap".to_owned() },482
+ gap: Gap { token: "gap".to_owned(), history: None },378483
}],379484
)380485
.await@@ -459,7 +564,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
459564
previous: Some(CId::new(0)),460565
new: CId::new(1),461566
next: None,462
- gap: Gap { token: "parmesan".to_owned() },567
+ gap: Gap { token: "parmesan".to_owned(), history: None },463568
},464569
// another items chunk465570
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },@@ -708,7 +813,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
708813
previous: Some(CId::new(0)),709814
new: CId::new(1),710815
next: None,711
- gap: Gap { token: "morbier".to_owned() },816
+ gap: Gap { token: "morbier".to_owned(), history: None },712817
},713818
// new chunk for items714819
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },@@ -907,19 +1012,19 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
9071012
previous: None,9081013
new: CId::new(42),9091014
next: None,910
- gap: Gap { token: "raclette".to_owned() },1015
+ gap: Gap { token: "raclette".to_owned(), history: None },9111016
},9121017
Update::NewGapChunk {9131018
previous: Some(CId::new(42)),9141019
new: CId::new(43),9151020
next: None,916
- gap: Gap { token: "fondue".to_owned() },1021
+ gap: Gap { token: "fondue".to_owned(), history: None },9171022
},9181023
Update::NewGapChunk {9191024
previous: Some(CId::new(43)),9201025
new: CId::new(44),9211026
next: None,922
- gap: Gap { token: "tartiflette".to_owned() },1027
+ gap: Gap { token: "tartiflette".to_owned(), history: None },9231028
},9241029
Update::RemoveChunk(CId::new(43)),9251030
],@@ -1125,7 +1230,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
11251230
previous: Some(CId::new(42)),11261231
new: CId::new(54),11271232
next: None,1128
- gap: Gap { token: "fondue".to_owned() },1233
+ gap: Gap { token: "fondue".to_owned(), history: None },11291234
},11301235
Update::PushItems {11311236
at: Position::new(CId::new(42), 0),@@ -1156,7 +1261,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
11561261
previous: Some(CId::new(42)),11571262
new: CId::new(54),11581263
next: None,1159
- gap: Gap { token: "fondue".to_owned() },1264
+ gap: Gap { token: "fondue".to_owned(), history: None },11601265
},11611266
Update::PushItems {11621267
at: Position::new(CId::new(42), 0),@@ -1285,7 +1390,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
12851390
previous: Some(CId::new(0)),12861391
new: CId::new(1),12871392
next: None,1288
- gap: Gap { token: "bleu d'auvergne".to_owned() },1393
+ gap: Gap { token: "bleu d'auvergne".to_owned(), history: None },12891394
},12901395
// another items chunk12911396
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },@@ -1578,7 +1683,7 @@ impl EventCacheStoreIntegrationTests for DynEventCacheStore {
15781683
previous: Some(CId::new(0)),15791684
new: CId::new(1),15801685
next: None,1581
- gap: Gap { token: "brillat-savarin".to_owned() },1686
+ gap: Gap { token: "brillat-savarin".to_owned(), history: None },15821687
},15831688
Update::NewItemsChunk { previous: Some(CId::new(1)), new: CId::new(2), next: None },15841689
Update::PushItems {@@ -2483,6 +2588,13 @@ macro_rules! event_cache_store_integration_tests {
24832588
}24842589
24852590
#[async_test]2591
+ async fn test_history_gap_roundtrip_and_replacement() {2592
+ let event_cache_store =2593
+ get_event_cache_store().await.unwrap().into_event_cache_store();2594
+ event_cache_store.test_history_gap_roundtrip_and_replacement().await;2595
+ }2596
+2597
+ #[async_test]24862598
async fn test_linked_chunk_exists_before_referenced() {24872599
let event_cache_store =24882600
get_event_cache_store().await.unwrap().into_event_cache_store();@@ -196,6 +196,7 @@ impl EventCacheStore for IndexeddbEventCacheStore {
196196
linked_chunk_id: linked_chunk_id.to_owned(),197197
chunk_identifier: new.index(),198198
token: gap.token,199
+ history: gap.history,199200
})200201
.await?;201202
transaction@@ -236,7 +236,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> {
236236
.get_gap_by_id(linked_chunk_id, ChunkIdentifier::new(chunk.identifier))237237
.await?238238
.ok_or(TransactionError::ItemNotFound)?;239
- ChunkContent::Gap(RawGap { token: gap.token })239
+ ChunkContent::Gap(RawGap { token: gap.token, history: gap.history })240240
}241241
};242242
return Ok(Some(RawChunk {@@ -231,4 +231,6 @@ pub struct Gap {
231231
/// "end" field of a `/messages` response.232232
#[serde(alias = "prev_token")]233233
pub token: String,234
+ #[serde(default, skip_serializing_if = "Option::is_none")]235
+ pub history: Option<matrix_sdk_base::event_cache::HistoryGap>,234236
}@@ -385,8 +385,16 @@ impl TransactionExtForLinkedChunks for Transaction<'_> {
385385
|row| row.get(0),386386
)?;387387
let prev_token_bytes = store.decode_value(&encoded_prev_token)?;388
- let prev_token = serde_json::from_slice(&prev_token_bytes)?;389
- Ok(Gap { token: prev_token })388
+ #[derive(serde::Deserialize)]389
+ #[serde(untagged)]390
+ enum StoredGap {391
+ Legacy(String),392
+ History(Gap),393
+ }394
+ Ok(match serde_json::from_slice(&prev_token_bytes)? {395
+ StoredGap::Legacy(token) => Gap { token, history: None },396
+ StoredGap::History(gap) => gap,397
+ })390398
}391399
392400
fn load_events_content(@@ -689,7 +697,11 @@ impl EventCacheStore for SqliteEventCacheStore {
689697
}690698
691699
Update::NewGapChunk { previous, new, next, gap } => {692
- let serialized = serde_json::to_vec(&gap.token)?;700
+ let serialized = if gap.history.is_some() {701
+ serde_json::to_vec(&gap)?702
+ } else {703
+ serde_json::to_vec(&gap.token)?704
+ };693705
let prev_token = this.encode_value(serialized)?;694706
695707
let previous = previous.as_ref().map(ChunkIdentifier::index);@@ -1981,7 +1993,7 @@ mod tests {
19811993
previous: None,19821994
new: ChunkIdentifier::new(chunk_id),19831995
next: None,1984
- gap: Gap { token: "gap".to_owned() },1996
+ gap: Gap { token: "gap".to_owned(), history: None },19851997
}],19861998
)19871999
.await@@ -2081,7 +2093,7 @@ mod tests {
20812093
previous: None,20822094
new: ChunkIdentifier::new(2),20832095
next: None,2084
- gap: Gap { token: "new gap".to_owned() },2096
+ gap: Gap { token: "new gap".to_owned(), history: None },20852097
}],20862098
)20872099
.await@@ -17,6 +17,7 @@ use std::sync::Arc;
1717
use matrix_sdk::Room;1818
use matrix_sdk_base::{SendOutsideWasm, SyncOutsideWasm};1919
use ruma::{events::AnySyncTimelineEvent, room_version_rules::RoomVersionRules};20
+use tokio::sync::mpsc;2021
use tracing::{Instrument, Span, info_span};2122
2223
use super::{@@ -162,7 +163,11 @@ impl TimelineBuilder {
162163
room.client().event_cache().subscribe()?;163164
164165
let (room_event_cache, event_cache_drop) = room.event_cache().await?;165
- let (_, event_subscriber) = room_event_cache.subscribe().await?;166
+ if matches!(focus, TimelineFocus::Live { .. }) {167
+ room_event_cache.load_retained_history().await?;168
+ }169
+ let (initial_events, initial_gaps, event_subscriber) =170
+ room_event_cache.subscribe_with_history().await?;166171
167172
let is_room_encrypted = room168173
.latest_encryption_state()@@ -180,8 +185,17 @@ impl TimelineBuilder {
180185
settings,181186
);182187
183
- let InitFocusResult { focus_task, has_events } =184
- controller.init_focus(&focus, &room_event_cache).await?;188
+ let InitFocusResult { focus_task, has_events } = controller189
+ .init_focus(&focus, &room_event_cache, (initial_events, initial_gaps))190
+ .await?;191
+192
+ let (history_flush, history_flush_requests) = if matches!(focus, TimelineFocus::Live { .. })193
+ {194
+ let (sender, receiver) = mpsc::channel(1);195
+ (Some(sender), Some(receiver))196
+ } else {197
+ (None, None)198
+ };185199
186200
let room_update_join_handle = room187201
.client()@@ -201,6 +215,7 @@ impl TimelineBuilder {
201215
controller.clone(),202216
event_subscriber,203217
focus.clone(),218
+ history_flush_requests,204219
)205220
.instrument(span)206221
})@@ -238,6 +253,7 @@ impl TimelineBuilder {
238253
let timeline = Timeline {239254
controller,240255
event_cache: room_event_cache,256
+ history_flush,241257
drop_handle: Arc::new(TimelineDropHandle {242258
_crypto_drop_handles: crypto_drop_handles,243259
_room_update_join_handle: room_update_join_handle,@@ -59,6 +59,7 @@ pub(crate) struct RemoteEventContext<'a> {
5959
6060
#[derive(Clone, Debug)]6161
pub(in crate::timeline) struct TimelineMetadata {62
+ pub history_gaps: Vec<matrix_sdk::event_cache::HistoryGapPosition>,6263
// **** CONSTANT FIELDS ****6364
/// An optional prefix for internal IDs, defined during construction of the6465
/// timeline.@@ -136,6 +137,7 @@ impl TimelineMetadata {
136137
is_room_encrypted: bool,137138
) -> Self {138139
Self {140
+ history_gaps: Vec::new(),139141
subscriber_skip_count: SkipCount::new(),140142
own_user_id,141143
next_internal_id: Default::default(),@@ -154,6 +156,7 @@ impl TimelineMetadata {
154156
}155157
156158
pub(super) fn clear(&mut self) {159
+ self.history_gaps.clear();157160
// Note: we don't clear the next internal id to avoid bad cases of stale unique158161
// ids across timeline clears.159162
self.aggregations.clear();@@ -668,6 +668,27 @@ impl<P: RoomDataProvider> TimelineController<P> {
668668
.await669669
}670670
671
+ pub(super) async fn handle_history_update(672
+ &self,673
+ diffs: Vec<VectorDiff<TimelineEvent>>,674
+ gaps: Option<Vec<matrix_sdk::event_cache::HistoryGapPosition>>,675
+ origin: RemoteEventOrigin,676
+ ) {677
+ let mut state = self.state.write().await;678
+ let mut txn = state.transaction();679
+ txn.handle_remote_events_with_diffs(680
+ diffs,681
+ origin,682
+ &self.room_data_provider,683
+ &self.settings,684
+ )685
+ .await;686
+ if let Some(gaps) = gaps {687
+ txn.meta.history_gaps = gaps;688
+ }689
+ txn.commit();690
+ }691
+671692
/// Only handle aggregations received as [`VectorDiff`]s.672693
pub(super) async fn handle_remote_aggregations(673694
&self,@@ -703,6 +724,18 @@ impl<P: RoomDataProvider> TimelineController<P> {
703724
Events: IntoIterator,704725
<Events as IntoIterator>::Item: Into<TimelineEvent>,705726
{727
+ self.replace_with_initial_history(events, Vec::new(), origin).await;728
+ }729
+730
+ pub(super) async fn replace_with_initial_history<Events>(731
+ &self,732
+ events: Events,733
+ gaps: Vec<matrix_sdk::event_cache::HistoryGapPosition>,734
+ origin: RemoteEventOrigin,735
+ ) where736
+ Events: IntoIterator,737
+ <Events as IntoIterator>::Item: Into<TimelineEvent>,738
+ {706739
let mut state = self.state.write().await;707740
708741
let track_read_markers = &self.settings.track_read_receipts;@@ -723,6 +756,7 @@ impl<P: RoomDataProvider> TimelineController<P> {
723756
state724757
.replace_with_remote_events(725758
events,759
+ gaps,726760
origin,727761
&self.room_data_provider,728762
&self.settings,@@ -1374,15 +1408,16 @@ impl TimelineController {
13741408
&self,13751409
focus: &TimelineFocus,13761410
room_event_cache: &RoomEventCache,1411
+ initial_history: (Vec<TimelineEvent>, Vec<matrix_sdk::event_cache::HistoryGapPosition>),13771412
) -> Result<InitFocusResult, Error> {13781413
match focus {13791414
TimelineFocus::Live { .. } => {13801415
// Retrieve the cached events, and add them to the timeline.1381
- let events = room_event_cache.events().await?;1416
+ let (events, gaps) = initial_history;13821417
13831418
let has_events = !events.is_empty();13841419
1385
- self.replace_with_initial_remote_events(events, RemoteEventOrigin::Cache).await;1420
+ self.replace_with_initial_history(events, gaps, RemoteEventOrigin::Cache).await;13861421
13871422
match room_event_cache.pagination().status().get() {13881423
PaginationStatus::Idle { hit_timeline_start } => {@@ -230,6 +230,7 @@ impl<P: RoomDataProvider> TimelineState<P> {
230230
pub(super) async fn replace_with_remote_events<Events>(231231
&mut self,232232
events: Events,233
+ gaps: Vec<matrix_sdk::event_cache::HistoryGapPosition>,233234
origin: RemoteEventOrigin,234235
room_data_provider: &P,235236
settings: &TimelineSettings,@@ -246,6 +247,7 @@ impl<P: RoomDataProvider> TimelineState<P> {
246247
settings,247248
)248249
.await;250
+ txn.meta.history_gaps = gaps;249251
txn.commit();250252
}251253
@@ -1029,9 +1029,9 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
10291029
if entry.is_remote_event()10301030
|| entry.as_virtual().is_some_and(|vitem| match vitem {10311031
VirtualTimelineItem::DateDivider(_) => false,1032
- VirtualTimelineItem::ReadMarker | VirtualTimelineItem::TimelineStart => {1033
- true1034
- }1032
+ VirtualTimelineItem::HistoryGap(_)1033
+ | VirtualTimelineItem::ReadMarker1034
+ | VirtualTimelineItem::TimelineStart => true,10351035
})10361036
{10371037
ObservableItemsTransactionEntry::remove(entry);@@ -1070,16 +1070,85 @@ impl<'a, P: RoomDataProvider> TimelineStateTransaction<'a, P> {
10701070
self.meta.update_read_marker(&mut self.items);10711071
}10721072
1073
- pub(super) fn commit(self) {1073
+ fn reconcile_history_gaps(&mut self) {1074
+ for index in (0..self.items.len()).rev() {1075
+ if let Some(VirtualTimelineItem::HistoryGap(gap)) = self.items[index].as_virtual() {1076
+ if !self.meta.history_gaps.iter().any(|position| {1077
+ position.gap.id == gap.id1078
+ && !matches!(1079
+ position.gap.status,1080
+ matrix_sdk::event_cache::HistoryGapStatus::Joined1081
+ )1082
+ }) {1083
+ self.items.remove(index);1084
+ }1085
+ }1086
+ }1087
+ for position in self.meta.history_gaps.clone() {1088
+ if matches!(position.gap.status, matrix_sdk::event_cache::HistoryGapStatus::Joined) {1089
+ continue;1090
+ }1091
+ let mut index = if let Some(next) = &position.next_event_id {1092
+ let mut events = self.items.all_remote_events().iter();1093
+ if !events.any(|event| &event.event_id == next) {1094
+ continue;1095
+ }1096
+ self.items1097
+ .all_remote_events()1098
+ .get_by_event_id(next)1099
+ .and_then(|event| event.timeline_item_index)1100
+ .or_else(|| events.find_map(|event| event.timeline_item_index))1101
+ } else {1102
+ None1103
+ };1104
+ let target = index.get_or_insert_with(|| {1105
+ self.items1106
+ .iter_all_regions()1107
+ .find_map(|(index, item)| item.is_local_echo().then_some(index))1108
+ .unwrap_or(self.items.len())1109
+ });1110
+ let previous = self.items.iter_all_regions().find_map(|(index, item)| {1111
+ matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(gap)) if gap.id == position.gap.id)1112
+ .then_some((index, item.clone()))1113
+ });1114
+ if let Some((index, item)) = &previous {1115
+ if *index + 1 == *target1116
+ && matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(gap)) if gap == &position.gap)1117
+ {1118
+ continue;1119
+ }1120
+ }1121
+ let kind = VirtualTimelineItem::HistoryGap(position.gap.clone());1122
+ let item = if let Some((index, item)) = previous {1123
+ self.items.remove(index);1124
+ if index < *target {1125
+ *target -= 1;1126
+ }1127
+ item.with_kind(kind)1128
+ } else {1129
+ self.meta.new_timeline_item(kind)1130
+ };1131
+ self.items.insert(*target, item, None);1132
+ }1133
+ }1134
+1135
+ pub(super) fn commit(mut self) {1136
+ self.reconcile_history_gaps();10741137
// Update the `subscriber_skip_count` value.10751138
let previous_number_of_items = self.number_of_items_when_transaction_started;10761139
let next_number_of_items = self.items.len();10771140
1078
- if previous_number_of_items != next_number_of_items {1079
- let count = self1080
- .meta1081
- .subscriber_skip_count1082
- .compute_next(previous_number_of_items, next_number_of_items);1141
+ let has_history_gaps = self.meta.history_gaps.iter().any(|position| {1142
+ !matches!(position.gap.status, matrix_sdk::event_cache::HistoryGapStatus::Joined)1143
+ });1144
+ if previous_number_of_items != next_number_of_items || has_history_gaps {1145
+ let count = if has_history_gaps {1146
+ 01147
+ } else {1148
+ self.meta1149
+ .subscriber_skip_count1150
+ .compute_next(previous_number_of_items, next_number_of_items)1151
+ };10831152
self.meta10841153
.subscriber_skip_count10851154
.update(count, matches!(self.focus, TimelineFocusKind::Live { .. }));@@ -154,7 +154,8 @@ impl DateDividerAdjuster {
154154
}155155
156156
TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)157
- | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {157
+ | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart)158
+ | TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(_)) => {158159
// Nothing to do.159160
}160161
}@@ -251,7 +252,8 @@ impl DateDividerAdjuster {
251252
}252253
253254
TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)254
- | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {255
+ | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart)256
+ | TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(_)) => {255257
// Nothing to do.256258
}257259
}@@ -317,7 +319,8 @@ impl DateDividerAdjuster {
317319
}318320
319321
TimelineItemKind::Virtual(VirtualTimelineItem::ReadMarker)320
- | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {322
+ | TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart)323
+ | TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(_)) => {321324
// Nothing to do.322325
}323326
}@@ -56,6 +56,7 @@ use ruma::{
5656
};5757
use subscriber::TimelineWithDropHandle;5858
use thiserror::Error;59
+use tokio::sync::{mpsc, oneshot};5960
use tracing::{instrument, trace, warn};6061
6162
use self::{@@ -118,6 +119,8 @@ pub struct Timeline {
118119
/// The event cache specialized for this room's view.119120
event_cache: RoomEventCache,120121
122
+ history_flush: Option<mpsc::Sender<oneshot::Sender<Result<(), Error>>>>,123
+121124
/// References to long-running tasks held by the timeline.122125
drop_handle: Arc<TimelineDropHandle>,123126
}@@ -224,6 +227,19 @@ pub struct AttachmentConfig {
224227
}225228
226229
impl Timeline {230
+ #[doc = "Wait until this live timeline has applied all history currently retained by its room cache."]231
+ pub async fn flush_history(&self) -> Result<(), Error> {232
+ let Some(flush) = &self.history_flush else {233
+ return Ok(());234
+ };235
+ let (reply, result) = oneshot::channel();236
+ flush.send(reply).await.map_err(|_| {237
+ Error::EventCacheError(matrix_sdk::event_cache::EventCacheError::ClientDropped)238
+ })?;239
+ result.await.map_err(|_| {240
+ Error::EventCacheError(matrix_sdk::event_cache::EventCacheError::ClientDropped)241
+ })?242
+ }227243
/// Returns the room for this timeline.228244
pub fn room(&self) -> &Room {229245
self.controller.room()@@ -16,6 +16,7 @@
1616
1717
use std::collections::BTreeSet;1818
19
+use eyeball_im::VectorDiff;1920
use matrix_sdk::{2021
event_cache::{2122
EventFocusThreadMode, EventsOrigin, RoomEventCache, RoomEventCacheSubscriber,@@ -25,6 +26,7 @@ use matrix_sdk::{
2526
};2627
use ruma::OwnedEventId;2728
use tokio::sync::broadcast::{Receiver, error::RecvError};29
+use tokio::sync::{mpsc, oneshot};2830
use tracing::{error, instrument, trace, warn};2931
3032
use crate::timeline::{TimelineController, TimelineFocus, event_item::RemoteEventOrigin};@@ -203,13 +205,47 @@ pub(in crate::timeline) async fn room_event_cache_updates_task(
203205
timeline_controller: TimelineController,204206
mut room_event_cache_subscriber: RoomEventCacheSubscriber,205207
timeline_focus: TimelineFocus,208
+ mut history_flush_requests: Option<209
+ mpsc::Receiver<oneshot::Sender<Result<(), crate::timeline::Error>>>,210
+ >,206211
) {207212
trace!("Spawned the event subscriber task.");208213
209214
loop {210215
trace!("Waiting for an event.");211216
212
- let update = match room_event_cache_subscriber.recv().await {217
+ let update = tokio::select! {218
+ request = async {219
+ match history_flush_requests.as_mut() {220
+ Some(requests) => requests.recv().await,221
+ None => std::future::pending::<Option<oneshot::Sender<Result<(), crate::timeline::Error>>>>().await,222
+ }223
+ } => {224
+ if let Some(reply) = request {225
+ if let Err(error) = room_event_cache.wait_for_history().await {226
+ let _ = reply.send(Err(error.into()));227
+ continue;228
+ }229
+ match room_event_cache.subscribe_with_history().await {230
+ Ok((initial_events, gaps, subscriber)) => {231
+ room_event_cache_subscriber = subscriber;232
+ timeline_controller233
+ .replace_with_initial_history(initial_events, gaps, RemoteEventOrigin::Cache)234
+ .await;235
+ let _ = reply.send(Ok(()));236
+ }237
+ Err(error) => {238
+ let _ = reply.send(Err(error.into()));239
+ }240
+ }241
+ } else {242
+ history_flush_requests = None;243
+ }244
+ continue;245
+ }246
+ update = room_event_cache_subscriber.recv() => update,247
+ };248
+ let update = match update {213249
Ok(up) => up,214250
Err(RecvError::Closed) => break,215251
Err(RecvError::Lagged(num_skipped)) => {@@ -218,20 +254,36 @@ pub(in crate::timeline) async fn room_event_cache_updates_task(
218254
// The updates might have lagged, but the room event cache might have219255
// events, so retrieve them and add them back again to the timeline,220256
// after clearing it.221
- let initial_events = match room_event_cache.events().await {222
- Ok(initial_events) => initial_events,223
- Err(err) => {224
- error!(225
- ?err,226
- "Failed to replace the initial remote events in the event cache"227
- );228
- break;229
- }230
- };257
+ let (initial_events, gaps, subscriber) =258
+ match room_event_cache.subscribe_with_history().await {259
+ Ok(initial_events) => initial_events,260
+ Err(err) => {261
+ error!(262
+ ?err,263
+ "Failed to replace the initial remote events in the event cache"264
+ );265
+ break;266
+ }267
+ };268
+269
+ room_event_cache_subscriber = subscriber;231270
232
- timeline_controller233
- .replace_with_initial_remote_events(initial_events, RemoteEventOrigin::Cache)234
- .await;271
+ if matches!(timeline_focus, TimelineFocus::Live { .. }) {272
+ timeline_controller273
+ .replace_with_initial_history(274
+ initial_events,275
+ gaps,276
+ RemoteEventOrigin::Cache,277
+ )278
+ .await;279
+ } else if !matches!(timeline_focus, TimelineFocus::PinnedEvents) {280
+ timeline_controller281
+ .handle_remote_aggregations(282
+ vec![VectorDiff::Reset { values: initial_events.into() }],283
+ RemoteEventOrigin::Cache,284
+ )285
+ .await;286
+ }235287
236288
continue;237289
}@@ -243,7 +295,11 @@ pub(in crate::timeline) async fn room_event_cache_updates_task(
243295
timeline_controller.handle_fully_read_marker(event_id).await;244296
}245297
246
- RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, origin }) => {298
+ RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {299
+ diffs,300
+ origin,301
+ history_gaps,302
+ }) => {247303
trace!("Received new timeline events diffs");248304
let origin = match origin {249305
EventsOrigin::Sync => RemoteEventOrigin::Sync,@@ -254,7 +310,7 @@ pub(in crate::timeline) async fn room_event_cache_updates_task(
254310
let has_diffs = !diffs.is_empty();255311
256312
if matches!(timeline_focus, TimelineFocus::Live { .. }) {257
- timeline_controller.handle_remote_events_with_diffs(diffs, origin).await;313
+ timeline_controller.handle_history_update(diffs, history_gaps, origin).await;258314
} else if !matches!(timeline_focus, TimelineFocus::PinnedEvents) {259315
// Only handle the remote aggregation for a non-live timeline, that's not the260316
// pinned events one (since the latter handles remote aggregations on its own).@@ -66,6 +66,7 @@ mod polls;
6666
mod reactions;6767
mod read_receipts;6868
mod redaction;69
+mod segmented_history;6970
mod shields;7071
mod virt;7172
@@ -0,0 +1,228 @@
1
+use eyeball_im::VectorDiff;2
+use futures_util::{FutureExt, StreamExt};3
+use imbl::vector;4
+use matrix_sdk::event_cache::{HistoryGap, HistoryGapPosition, HistoryGapStatus};5
+use matrix_sdk_test::{ALICE, async_test};6
+use ruma::{7
+ TransactionId, event_id,8
+ events::{AnyMessageLikeEventContent, room::message::RoomMessageEventContent},9
+};10
+11
+use super::TestTimeline;12
+use crate::timeline::{VirtualTimelineItem, event_item::RemoteEventOrigin};13
+14
+#[async_test]15
+async fn test_segmented_history_initial_snapshot_keeps_retained_side() {16
+ let timeline = TestTimeline::new();17
+ let events: Vec<matrix_sdk::deserialized_responses::TimelineEvent> = (0..35)18
+ .map(|_| timeline.factory.text_msg("retained or recent").sender(*ALICE).into())19
+ .collect();20
+ let old = events[0].event_id().unwrap();21
+ let new = events[1].event_id().unwrap();22
+ let gap = HistoryGap {23
+ id: TransactionId::new(),24
+ older_boundary: Some(old.clone()),25
+ newer_boundary: Some(new.clone()),26
+ visited_cursors: Vec::new(),27
+ status: HistoryGapStatus::Idle,28
+ };29
+ timeline30
+ .controller31
+ .replace_with_initial_history(32
+ events,33
+ vec![HistoryGapPosition { gap, next_event_id: Some(new) }],34
+ RemoteEventOrigin::Cache,35
+ )36
+ .await;37
+ let (items, _) = timeline.controller.subscribe().await;38
+ assert_eq!(items.iter().filter(|item| item.is_remote_event()).count(), 35);39
+ assert!(40
+ items41
+ .iter()42
+ .any(|item| item.as_event().is_some_and(|event| event.event_id() == Some(&old)))43
+ );44
+ assert!(45
+ items46
+ .iter()47
+ .any(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(_))))48
+ );49
+}50
+51
+#[async_test]52
+async fn test_segmented_history_projects_hidden_boundaries_atomically() {53
+ let timeline = TestTimeline::new();54
+ let (mut items, mut stream) = timeline.controller.subscribe().await;55
+ let old = event_id!("$old");56
+ let hidden = event_id!("$reaction");57
+ let new = event_id!("$new");58
+ let gap = HistoryGap {59
+ id: TransactionId::new(),60
+ older_boundary: Some(old.to_owned()),61
+ newer_boundary: Some(hidden.to_owned()),62
+ visited_cursors: Vec::new(),63
+ status: HistoryGapStatus::Idle,64
+ };65
+ let f = &timeline.factory;66
+ timeline67
+ .controller68
+ .handle_history_update(69
+ vec![VectorDiff::Append {70
+ values: vector![71
+ f.text_msg("old").sender(*ALICE).event_id(old).into(),72
+ f.reaction(old, "👍").sender(*ALICE).event_id(hidden).into(),73
+ f.text_msg("new").sender(*ALICE).event_id(new).into(),74
+ ],75
+ }],76
+ Some(vec![HistoryGapPosition {77
+ gap: gap.clone(),78
+ next_event_id: Some(hidden.to_owned()),79
+ }]),80
+ RemoteEventOrigin::Cache,81
+ )82
+ .await;83
+ let diffs = stream.next().await.unwrap();84
+ for diff in diffs {85
+ diff.apply(&mut items);86
+ }87
+ let gap_index = items.iter().position(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(found)) if found == &gap)).unwrap();88
+ assert_eq!(items[gap_index - 1].as_event().unwrap().event_id(), Some(old));89
+ assert_eq!(items[gap_index + 1].as_event().unwrap().event_id(), Some(new));90
+ assert!(stream.next().now_or_never().is_none());91
+92
+ let gap_key = items[gap_index].unique_id().clone();93
+ let mut paused = gap.clone();94
+ paused.status = HistoryGapStatus::Paused;95
+ timeline96
+ .controller97
+ .handle_history_update(98
+ Vec::new(),99
+ Some(vec![HistoryGapPosition {100
+ gap: paused.clone(),101
+ next_event_id: Some(hidden.to_owned()),102
+ }]),103
+ RemoteEventOrigin::Cache,104
+ )105
+ .await;106
+ for diff in stream.next().await.unwrap() {107
+ diff.apply(&mut items);108
+ }109
+ assert_eq!(items[gap_index].unique_id(), &gap_key);110
+ assert!(111
+ matches!(items[gap_index].as_virtual(), Some(VirtualTimelineItem::HistoryGap(found)) if found == &paused)112
+ );113
+114
+ let old_key = items[gap_index - 1].unique_id().clone();115
+ let new_key = items[gap_index + 1].unique_id().clone();116
+ let middle = event_id!("$middle");117
+ timeline118
+ .controller119
+ .handle_history_update(120
+ vec![VectorDiff::Insert {121
+ index: 1,122
+ value: f.text_msg("middle").sender(*ALICE).event_id(middle).into(),123
+ }],124
+ Some(vec![HistoryGapPosition {125
+ gap: paused.clone(),126
+ next_event_id: Some(middle.to_owned()),127
+ }]),128
+ RemoteEventOrigin::Cache,129
+ )130
+ .await;131
+ for diff in stream.next().await.unwrap() {132
+ diff.apply(&mut items);133
+ }134
+ assert_eq!(135
+ items136
+ .iter()137
+ .find(|item| item.as_event().is_some_and(|event| event.event_id() == Some(old)))138
+ .unwrap()139
+ .unique_id(),140
+ &old_key141
+ );142
+ assert_eq!(143
+ items144
+ .iter()145
+ .find(|item| item.as_event().is_some_and(|event| event.event_id() == Some(new)))146
+ .unwrap()147
+ .unique_id(),148
+ &new_key149
+ );150
+ assert_eq!(151
+ items152
+ .iter()153
+ .find(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(_))))154
+ .unwrap()155
+ .unique_id(),156
+ &gap_key157
+ );158
+159
+ paused.status = HistoryGapStatus::Joined;160
+ timeline161
+ .controller162
+ .handle_history_update(163
+ Vec::new(),164
+ Some(vec![HistoryGapPosition { gap: paused, next_event_id: Some(hidden.to_owned()) }]),165
+ RemoteEventOrigin::Cache,166
+ )167
+ .await;168
+ for diff in stream.next().await.unwrap() {169
+ diff.apply(&mut items);170
+ }171
+ assert!(172
+ !items173
+ .iter()174
+ .any(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(_))))175
+ );176
+}177
+178
+#[async_test]179
+async fn test_segmented_history_reset_preserves_pending_sends() {180
+ let timeline = TestTimeline::new();181
+ timeline182
+ .handle_local_event(AnyMessageLikeEventContent::RoomMessage(183
+ RoomMessageEventContent::text_plain("pending"),184
+ ))185
+ .await;186
+ let pending_key = timeline187
+ .controller188
+ .items()189
+ .await190
+ .iter()191
+ .find(|item| item.is_local_echo())192
+ .unwrap()193
+ .unique_id()194
+ .clone();195
+ let old = event_id!("$old");196
+ let new = event_id!("$new");197
+ let gap = HistoryGap {198
+ id: TransactionId::new(),199
+ older_boundary: Some(old.to_owned()),200
+ newer_boundary: Some(new.to_owned()),201
+ visited_cursors: Vec::new(),202
+ status: HistoryGapStatus::Unavailable,203
+ };204
+ let f = &timeline.factory;205
+ timeline206
+ .controller207
+ .handle_history_update(208
+ vec![VectorDiff::Reset {209
+ values: vector![210
+ f.text_msg("old").sender(*ALICE).event_id(old).into(),211
+ f.text_msg("new").sender(*ALICE).event_id(new).into(),212
+ ],213
+ }],214
+ Some(vec![HistoryGapPosition { gap, next_event_id: Some(new.to_owned()) }]),215
+ RemoteEventOrigin::Cache,216
+ )217
+ .await;218
+ let items = timeline.controller.items().await;219
+ assert_eq!(items.iter().filter(|item| item.is_local_echo()).count(), 1);220
+ assert_eq!(items.iter().find(|item| item.is_local_echo()).unwrap().unique_id(), &pending_key);221
+ assert_eq!(222
+ items223
+ .iter()224
+ .filter(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(_))))225
+ .count(),226
+ 1227
+ );228
+}@@ -17,6 +17,8 @@ use ruma::MilliSecondsSinceUnixEpoch;
1717
/// A [`TimelineItem`](super::TimelineItem) that doesn't correspond to an event.1818
#[derive(Clone, Debug)]1919
pub enum VirtualTimelineItem {20
+ #[doc = "An unresolved interval between retained history sections."]21
+ HistoryGap(matrix_sdk::event_cache::HistoryGap),2022
/// A divider between messages of two days or months depending on the2123
/// timeline configuration.2224
///@@ -57,6 +57,7 @@ mod reactions;
5757
mod read_receipts;5858
mod redecryption;5959
mod reseed;60
+mod segmented_history;6061
mod replies;6162
mod subscribe;6263
mod thread;@@ -0,0 +1,156 @@
1
+use std::time::Duration;2
+3
+use futures_util::StreamExt;4
+use matrix_sdk::test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate};5
+use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};6
+use matrix_sdk_ui::timeline::{RoomExt, VirtualTimelineItem};7
+use ruma::{event_id, events::room::message::RoomMessageEventContent, room_id};8
+use tokio::time::timeout;9
+10
+#[async_test]11
+async fn test_timeline_fills_segmented_history_without_replacing_retained_items() {12
+ let server = MatrixMockServer::new().await;13
+ let client = server.client_builder().build().await;14
+ client.send_queue().set_enabled(false).await;15
+ client.event_cache().subscribe().unwrap();16
+ server.mock_room_state_encryption().plain().mount().await;17
+18
+ let room_id = room_id!("!segmented:example.org");19
+ let room = server.sync_joined_room(&client, room_id).await;20
+ let (cache, _drop_handles) = room.event_cache().await.unwrap();21
+ let event_factory = EventFactory::new().room(room_id).sender(&ALICE);22
+ let old_event_id = event_id!("$old");23
+ let hidden_event_id = event_id!("$hidden");24
+ let new_event_id = event_id!("$new");25
+ cache26
+ .reseed(27
+ vec![event_factory.text_msg("old").event_id(old_event_id).into_event()],28
+ Some("old-cursor".into()),29
+ )30
+ .await31
+ .unwrap();32
+ let gap = cache33
+ .append_history_section(34
+ vec![35
+ event_factory.reaction(old_event_id, "👍").event_id(hidden_event_id).into_event(),36
+ event_factory.text_msg("new").event_id(new_event_id).into_event(),37
+ ],38
+ Some("history-cursor".into()),39
+ )40
+ .await41
+ .unwrap()42
+ .unwrap();43
+44
+ let timeline = room.timeline().await.unwrap();45
+ let (mut timeline_stream_items, mut timeline_stream) = timeline.subscribe().await;46
+ let initial_event_ids: Vec<_> =47
+ timeline_stream_items.iter().filter_map(|item| item.as_event()?.event_id()).collect();48
+ assert_eq!(initial_event_ids, vec![old_event_id, new_event_id]);49
+ let gap_index = timeline_stream_items50
+ .iter()51
+ .position(|item| {52
+ matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(item_gap)) if item_gap.id == gap.id)53
+ })54
+ .unwrap();55
+ let old_unique_id = timeline_stream_items56
+ .iter()57
+ .find(|item| item.as_event().is_some_and(|event| event.event_id() == Some(old_event_id)))58
+ .unwrap()59
+ .unique_id()60
+ .clone();61
+ let new_unique_id = timeline_stream_items62
+ .iter()63
+ .find(|item| item.as_event().is_some_and(|event| event.event_id() == Some(new_event_id)))64
+ .unwrap()65
+ .unique_id()66
+ .clone();67
+ assert!(gap_index > 0);68
+ assert!(gap_index < timeline_stream_items.len() - 1);69
+70
+ timeline.send(RoomMessageEventContent::text_plain("pending local echo").into()).await.unwrap();71
+ timeout(Duration::from_secs(5), async {72
+ loop {73
+ if timeline_stream_items.iter().any(|item| {74
+ item.as_event().is_some_and(|event| {75
+ event76
+ .content()77
+ .as_message()78
+ .is_some_and(|message| message.body() == "pending local echo")79
+ })80
+ }) {81
+ break;82
+ }83
+ timeline_stream.next().await.unwrap();84
+ timeline_stream_items = timeline.items().await;85
+ }86
+ })87
+ .await88
+ .unwrap();89
+ let pending_unique_id = timeline_stream_items90
+ .iter()91
+ .find(|item| {92
+ item.as_event().is_some_and(|event| {93
+ event94
+ .content()95
+ .as_message()96
+ .is_some_and(|message| message.body() == "pending local echo")97
+ })98
+ })99
+ .unwrap()100
+ .unique_id()101
+ .clone();102
+103
+ server104
+ .mock_room_messages()105
+ .match_from("history-cursor")106
+ .ok(RoomMessagesResponseTemplate::default().end_token("next-cursor").events(vec![107
+ event_factory.text_msg("middle").event_id(event_id!("$middle")),108
+ event_factory.text_msg("old").event_id(old_event_id),109
+ ]))110
+ .mock_once()111
+ .mount()112
+ .await;113
+ cache.fill_history_gap_once(gap.id).await.unwrap();114
+115
+ timeout(Duration::from_secs(5), async {116
+ loop {117
+ timeline_stream_items = timeline.items().await;118
+ let has_middle = timeline_stream_items.iter().any(|item| {119
+ item.as_event().is_some_and(|event| event.event_id() == Some(event_id!("$middle")))120
+ });121
+ let has_gap = timeline_stream_items122
+ .iter()123
+ .any(|item| matches!(item.as_virtual(), Some(VirtualTimelineItem::HistoryGap(_))));124
+ if has_middle && !has_gap {125
+ break;126
+ }127
+ assert!(timeline_stream.next().await.is_some());128
+ }129
+ })130
+ .await131
+ .unwrap();132
+133
+ let final_event_ids: Vec<_> =134
+ timeline_stream_items.iter().filter_map(|item| item.as_event()?.event_id()).collect();135
+ assert_eq!(final_event_ids, vec![old_event_id, event_id!("$middle"), new_event_id]);136
+ let retained_items = [(old_event_id, &old_unique_id), (new_event_id, &new_unique_id)];137
+ for (event_id, unique_id) in retained_items {138
+ let item = timeline_stream_items139
+ .iter()140
+ .find(|item| item.as_event().is_some_and(|event| event.event_id() == Some(event_id)))141
+ .unwrap();142
+ assert_eq!(item.unique_id(), unique_id);143
+ }144
+ let pending_item = timeline_stream_items145
+ .iter()146
+ .find(|item| {147
+ item.as_event().is_some_and(|event| {148
+ event149
+ .content()150
+ .as_message()151
+ .is_some_and(|message| message.body() == "pending local echo")152
+ })153
+ })154
+ .unwrap();155
+ assert_eq!(pending_item.unique_id(), &pending_unique_id);156
+}@@ -213,5 +213,13 @@ required-features = ["testing", "e2e-encryption"]
213213
name = "history_reseed"214214
required-features = ["testing"]215215
216
+[[test]]217
+name = "segmented_history"218
+required-features = ["testing"]219
+220
+[[test]]221
+name = "recent_history"222
+required-features = ["testing"]223
+216224
[lints]217225
workspace = true@@ -274,13 +274,15 @@ impl EventFocusedCacheInner {
274274
) {275275
// Insert backward gap at the back if we have a token, and the events276276
// themselves.277
- self.chunk278
- .push_live_events(prev_gap_token.map(|prev_token| Gap { token: prev_token }), &events);277
+ self.chunk.push_live_events(278
+ prev_gap_token.map(|prev_token| Gap { token: prev_token, history: None }),279
+ &events,280
+ );279281
280282
// Insert forward gap at back if we have a token.281283
if let Some(next_token) = next_gap_token {282284
trace!("inserting forward pagination gap at back");283
- self.chunk.push_gap(Gap { token: next_token });285
+ self.chunk.push_gap(Gap { token: next_token, history: None });284286
}285287
}286288
@@ -302,7 +304,7 @@ impl EventFocusedCacheInner {
302304
fn notify_subscribers(&mut self, origin: EventsOrigin) {303305
let diffs = self.chunk.updates_as_vector_diffs();304306
if !diffs.is_empty() {305
- let _ = self.sender.send(TimelineVectorDiffs { diffs, origin });307
+ let _ = self.sender.send(TimelineVectorDiffs { diffs, origin, history_gaps: None });306308
}307309
}308310
@@ -365,7 +367,7 @@ impl EventFocusedCacheInner {
365367
events.reverse();366368
367369
let hit_end = new_token.is_none();368
- let new_gap = new_token.map(|t| Gap { token: t });370
+ let new_gap = new_token.map(|t| Gap { token: t, history: None });369371
370372
let hide_thread_events = match &self.pagination_mode {371373
EventFocusedPaginationMode::Room { hide_thread_events } => *hide_thread_events,@@ -488,7 +490,7 @@ impl EventFocusedCacheInner {
488490
let (events, new_token) = pagination?;489491
490492
let hit_end = new_token.is_none();491
- let new_gap = new_token.map(|t| Gap { token: t });493
+ let new_gap = new_token.map(|t| Gap { token: t, history: None });492494
493495
let hide_thread_events = match &self.pagination_mode {494496
EventFocusedPaginationMode::Room { hide_thread_events } => *hide_thread_events,@@ -615,6 +617,7 @@ impl EventFocusedCache {
615617
let _ = self.sender.send(TimelineVectorDiffs {616618
diffs: vec![VectorDiff::Clear],617619
origin: EventsOrigin::Cache,620
+ history_gaps: None,618621
});619622
let _ = self.linked_chunk_update_sender.send(RoomEventCacheLinkedChunkUpdate {620623
updates: vec![Update::Clear],@@ -318,6 +318,27 @@ impl EventLinkedChunk {
318318
self.chunks.push_items_back(events.iter().cloned());319319
}320320
321
+ pub(super) fn insert_history_before(322
+ &mut self,323
+ before: Option<&ruma::EventId>,324
+ events: Vec<Event>,325
+ gap: Option<Gap>,326
+ ) {327
+ let position = before.and_then(|id| {328
+ self.events().find_map(|(position, event)| {329
+ (event.event_id().as_deref() == Some(id)).then_some(position)330
+ })331
+ });332
+ if let Some(position) = position {333
+ self.chunks.insert_items_at(position, events).expect("existing event position");334
+ if let Some(gap) = gap {335
+ self.chunks.insert_gap_at(gap, position).expect("existing event position");336
+ }337
+ } else {338
+ self.push_live_events(gap, &events);339
+ }340
+ }341
+321342
/// Add events from a backwards pagination for this linked chunk by updating322343
/// the in-memory linked chunk with the results.323344
///@@ -598,7 +619,7 @@ fn chunk_debug_string(
598619
order_tracker: &OrderTracker<Event, Gap>,599620
) -> String {600621
match content {601
- ChunkContent::Gap(Gap { token: prev_token }) => {622
+ ChunkContent::Gap(Gap { token: prev_token, .. }) => {602623
format!("gap['{prev_token}']")603624
}604625
ChunkContent::Items(vec) => {@@ -695,7 +716,7 @@ mod tests {
695716
let mut linked_chunk = EventLinkedChunk::new();696717
697718
linked_chunk.chunks.push_items_back([event_0]);698
- linked_chunk.chunks.push_gap_back(Gap { token: "hello".to_owned() });719
+ linked_chunk.chunks.push_gap_back(Gap { token: "hello".to_owned(), history: None });699720
700721
let gap_chunk_id = linked_chunk701722
.chunks()@@ -735,9 +756,9 @@ mod tests {
735756
let mut linked_chunk = EventLinkedChunk::new();736757
737758
linked_chunk.chunks.push_items_back([event_0, event_1]);738
- linked_chunk.chunks.push_gap_back(Gap { token: "middle".to_owned() });759
+ linked_chunk.chunks.push_gap_back(Gap { token: "middle".to_owned(), history: None });739760
linked_chunk.chunks.push_items_back([event_2]);740
- linked_chunk.chunks.push_gap_back(Gap { token: "end".to_owned() });761
+ linked_chunk.chunks.push_gap_back(Gap { token: "end".to_owned(), history: None });741762
742763
// Remove the first gap.743764
let first_gap_id = linked_chunk@@ -770,7 +791,7 @@ mod tests {
770791
// Push some events.771792
let mut linked_chunk = EventLinkedChunk::new();772793
linked_chunk.chunks.push_items_back([event_0, event_1]);773
- linked_chunk.chunks.push_gap_back(Gap { token: "hello".to_owned() });794
+ linked_chunk.chunks.push_gap_back(Gap { token: "hello".to_owned(), history: None });774795
linked_chunk.chunks.push_items_back([event_2, event_3]);775796
776797
assert_events_eq!(@@ -843,7 +864,7 @@ mod tests {
843864
// Push some events.844865
let mut linked_chunk = EventLinkedChunk::new();845866
linked_chunk.chunks.push_items_back([event_0, event_1]);846
- linked_chunk.chunks.push_gap_back(Gap { token: "raclette".to_owned() });867
+ linked_chunk.chunks.push_gap_back(Gap { token: "raclette".to_owned(), history: None });847868
linked_chunk.chunks.push_items_back([event_2]);848869
849870
// Read the updates as `VectorDiff`.@@ -898,7 +919,7 @@ mod tests {
898919
.into_event(),899920
event_factory.text_msg("you").event_id(event_id!("$2")).into_event(),900921
]);901
- linked_chunk.chunks.push_gap_back(Gap { token: "raclette".to_owned() });922
+ linked_chunk.chunks.push_gap_back(Gap { token: "raclette".to_owned(), history: None });902923
903924
// Flush updates to the order tracker.904925
let _ = linked_chunk.updates_as_vector_diffs();@@ -188,6 +188,7 @@ impl<'c> ResetCaches<'c> {
188188
let updates_as_vector_diffs = room_state.reset().await?;189189
room.update_sender().send(190190
room::RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {191
+ history_gaps: Some(Vec::new()),191192
diffs: updates_as_vector_diffs,192193
origin: EventsOrigin::Cache,193194
}),@@ -202,6 +203,8 @@ impl<'c> ResetCaches<'c> {
202203
/// A diff update for an event cache timeline represented as a vector.203204
#[derive(Clone, Debug)]204205
pub struct TimelineVectorDiffs {206
+ #[doc = "The ordered room gaps after these diffs, or no change for focused caches."]207
+ pub history_gaps: Option<Vec<room::HistoryGapPosition>>,205208
/// New vector diff for the thread timeline.206209
pub diffs: Vec<VectorDiff<Event>>,207210
/// The origin that triggered this update.@@ -189,7 +189,8 @@ impl<'a> PinnedEventCacheStateLockWriteGuard<'a> {
189189
fn notify_subscribers(&mut self, origin: EventsOrigin) {190190
let diffs = self.state.chunk.updates_as_vector_diffs();191191
if !diffs.is_empty() {192
- let _ = self.state.sender.send(TimelineVectorDiffs { diffs, origin });192
+ let _ =193
+ self.state.sender.send(TimelineVectorDiffs { diffs, origin, history_gaps: None });193194
}194195
}195196
}@@ -0,0 +1,16 @@
1
+Install independently fetched events in topological order, oldest first, while2
+preserving retained history and its pagination cursors. Duplicate room event IDs3
+are retained once. A wholly duplicated or empty batch creates no new section.4
+5
+When retained events exist, the returned gap connects their newest event to the6
+oldest newly inserted event. Its cursor belongs to the supplied newer section.7
+An absent cursor leaves an unavailable interval rather than claiming continuity.8
+Without retained events, the cursor remains ordinary oldest-history pagination.9
+10
+Events and gap metadata commit in one encrypted event cache transaction before11
+publication. A failed write preserves the previous cache. Installation continues12
+if the caller drops the future. Active pinned, thread, and focused caches are not13
+cleared. Completion means cache publication, not application mirror consumption.14
+15
+This low-level operation does not reconcile the freshness of an independently16
+fetched batch with live arrivals. Callers must coordinate that acquisition.@@ -0,0 +1,20 @@
1
+Request one backward page of up to 50 server events using this gap's saved cursor.2
+Ordinary oldest-history pagination and other internal gaps keep their cursors.3
+4
+Only reaching the recorded older boundary verifies a join. Short or empty pages5
+with advancing cursors leave the gap idle. A repeated cursor pauses it. Exhausting6
+accessible history leaves it unavailable and preserves retained events. Joined7
+identities remain persisted so repeated calls can return their terminal outcome.8
+9
+Dropping the future before the response cancels the request without advancing10
+the cursor. Once installation starts, the page and cursor commit together even11
+if the caller stops waiting. Request failures retain the last successful cursor.12
+The original SDK error is returned after its failure state has been persisted.13
+14
+The room coordinator permits one active targeted fill or section installation.15
+Concurrent fills return `Busy`. Recent load callers share an in-flight operation.16
+Dropping one recent caller leaves the operation active for the remaining callers.17
+Dropping all callers cancels fetching, while a started commit runs to completion.18
+The coordinator keeps no strong cache reference while idle. Cache publication19
+precedes completion, while application mirror publication remains the caller's20
+responsibility. `wait_for_history` waits for earlier operations and their commits.@@ -0,0 +1,551 @@
1
+use std::{2
+ collections::{HashSet, VecDeque},3
+ sync::Weak,4
+};5
+6
+pub use matrix_sdk_base::event_cache::{HistoryGap, HistoryGapStatus};7
+use matrix_sdk_base::{8
+ event_cache::{Event, Gap},9
+ linked_chunk::ChunkContent,10
+};11
+use ruma::{OwnedEventId, OwnedTransactionId, TransactionId};12
+use tokio::sync::{mpsc, oneshot, watch};13
+14
+use super::{RoomEventCache, RoomEventCacheInner};15
+use crate::{16
+ event_cache::{EventCacheError, Result, caches::event_linked_chunk::EventLinkedChunk},17
+ room::MessagesOptions,18
+};19
+20
+#[derive(Clone, Debug, PartialEq, Eq)]21
+#[doc = "A durable interval positioned before the next identified raw event in cache order."]22
+pub struct HistoryGapPosition {23
+ #[doc = "The durable identity and continuity state of the interval."]24
+ pub gap: HistoryGap,25
+ #[doc = "Absent when no identified event follows the gap in the loaded window."]26
+ pub next_event_id: Option<OwnedEventId>,27
+}28
+29
+pub(super) fn gap_positions(candidate: &EventLinkedChunk) -> Vec<HistoryGapPosition> {30
+ let mut positions: Vec<HistoryGapPosition> = Vec::new();31
+ let mut pending = 0;32
+ for chunk in candidate.chunks() {33
+ match chunk.content() {34
+ ChunkContent::Gap(gap) => {35
+ if let Some(gap) = &gap.history {36
+ positions.push(HistoryGapPosition { gap: gap.clone(), next_event_id: None });37
+ }38
+ }39
+ ChunkContent::Items(events) => {40
+ if let Some(id) = events.iter().find_map(Event::event_id) {41
+ for position in &mut positions[pending..] {42
+ position.next_event_id = Some(id.clone());43
+ }44
+ pending = positions.len();45
+ }46
+ }47
+ }48
+ }49
+ positions50
+}51
+52
+#[derive(Clone, Debug, PartialEq, Eq)]53
+#[doc = "A bounded gap fill never falls back to another gap when its target disappears."]54
+pub enum HistoryGapFillOutcome {55
+ #[doc = "Also returned without a network request for a joined or unavailable interval."]56
+ Updated {57
+ #[doc = "The durable outcome, including its original boundary identities."]58
+ gap: HistoryGap,59
+ #[doc = "Newly inserted events after room-wide deduplication."]60
+ events: usize,61
+ },62
+ #[doc = "The identity is unknown or was removed by an explicit cache replacement."]63
+ NotFound,64
+ #[doc = "Another history operation owns the room coordinator. No request was started."]65
+ Busy,66
+}67
+68
+#[derive(Clone, Debug)]69
+#[doc = "A fresh server window bounded by four requests of fifty raw events."]70
+pub struct RecentHistoryOutcome {71
+ #[doc = "Number of server pages consumed, including empty pages."]72
+ pub pages: u32,73
+ #[doc = "Number of raw events returned by the server."]74
+ pub events: usize,75
+ #[doc = "The durable interval, absent when no retained boundary needs joining."]76
+ pub gap: Option<HistoryGap>,77
+}78
+79
+pub(super) struct HistoryRequest {80
+ operation: HistoryOperation,81
+ reply: oneshot::Sender<Result<HistoryResponse>>,82
+}83
+84
+enum HistoryOperation {85
+ Recent,86
+ Fill(OwnedTransactionId),87
+ Append(Vec<Event>, Option<String>),88
+ Restore,89
+ Barrier,90
+}91
+92
+#[derive(Clone)]93
+enum HistoryResponse {94
+ Recent(RecentHistoryOutcome),95
+ Fill(HistoryGapFillOutcome),96
+ Append(Option<HistoryGap>),97
+ Restored,98
+}99
+100
+async fn coordinate_history(101
+ weak_cache: Weak<RoomEventCacheInner>,102
+ mut requests: mpsc::Receiver<HistoryRequest>,103
+) {104
+ let mut pending = VecDeque::new();105
+ loop {106
+ let request = if let Some(request) = pending.pop_front() {107
+ request108
+ } else if let Some(request) = requests.recv().await {109
+ request110
+ } else {111
+ return;112
+ };113
+ let HistoryRequest { operation, reply } = request;114
+ if reply.is_closed() {115
+ continue;116
+ }117
+ let Some(inner) = weak_cache.upgrade() else {118
+ return;119
+ };120
+ let cache = RoomEventCache { inner };121
+ let recent = matches!(operation, HistoryOperation::Recent);122
+ let mut waiters = vec![reply];123
+ let (cancel, cancellation) = watch::channel(false);124
+ let operation = async {125
+ match operation {126
+ HistoryOperation::Recent => {127
+ cache.load_recent_history_inner(cancellation).await.map(HistoryResponse::Recent)128
+ }129
+ HistoryOperation::Fill(id) => {130
+ cache.fill_history_gap_inner(id, cancellation).await.map(HistoryResponse::Fill)131
+ }132
+ HistoryOperation::Append(events, cursor) => cache133
+ .append_history_section_inner(events, cursor)134
+ .await135
+ .map(HistoryResponse::Append),136
+ HistoryOperation::Restore => {137
+ cache.load_retained_history_inner().await.map(|()| HistoryResponse::Restored)138
+ }139
+ HistoryOperation::Barrier => Ok(HistoryResponse::Restored),140
+ }141
+ };142
+ tokio::pin!(operation);143
+ let mut cancelled = false;144
+ let mut accepting = true;145
+ loop {146
+ enum Action {147
+ Complete(Result<HistoryResponse>),148
+ Request(Option<HistoryRequest>),149
+ Cancel,150
+ }151
+ let action = tokio::select! {152
+ biased;153
+ result = &mut operation => Action::Complete(result),154
+ _ = futures_util::future::join_all(waiters.iter_mut().map(|reply| reply.closed())), if !cancelled => Action::Cancel,155
+ request = requests.recv(), if accepting => Action::Request(request),156
+ };157
+ match action {158
+ Action::Complete(result) => {159
+ for reply in waiters {160
+ let _ = reply.send(result.clone());161
+ }162
+ break;163
+ }164
+ Action::Request(Some(request)) => {165
+ if recent && !cancelled && matches!(request.operation, HistoryOperation::Recent)166
+ {167
+ waiters.push(request.reply);168
+ } else if !cancelled && matches!(request.operation, HistoryOperation::Fill(_)) {169
+ let _ = request170
+ .reply171
+ .send(Ok(HistoryResponse::Fill(HistoryGapFillOutcome::Busy)));172
+ } else {173
+ pending.push_back(request);174
+ }175
+ }176
+ Action::Request(None) => accepting = false,177
+ Action::Cancel => {178
+ cancelled = true;179
+ let _ = cancel.send(true);180
+ }181
+ }182
+ }183
+ }184
+}185
+186
+impl RoomEventCache {187
+ async fn request_history(&self, operation: HistoryOperation) -> Result<HistoryResponse> {188
+ let sender = self.inner.history_coordinator.get_or_init(|| {189
+ let (sender, requests) = mpsc::channel(16);190
+ matrix_sdk_common::executor::spawn(coordinate_history(191
+ std::sync::Arc::downgrade(&self.inner),192
+ requests,193
+ ));194
+ sender195
+ });196
+ let (reply, response) = oneshot::channel();197
+ sender198
+ .send(HistoryRequest { operation, reply })199
+ .await200
+ .map_err(|_| EventCacheError::ClientDropped)?;201
+ response.await.map_err(|_| EventCacheError::ClientDropped)?202
+ }203
+204
+ #[doc = "Waits for earlier history operations, including commits whose callers stopped waiting."]205
+ pub async fn wait_for_history(&self) -> Result<()> {206
+ self.request_history(HistoryOperation::Barrier).await.map(|_| ())207
+ }208
+209
+ #[doc = "Fetches from a fresh server position and retains sync arrivals during acquisition."]210
+ pub async fn load_recent_history(&self) -> Result<RecentHistoryOutcome> {211
+ match self.request_history(HistoryOperation::Recent).await? {212
+ HistoryResponse::Recent(outcome) => Ok(outcome),213
+ _ => unreachable!("coordinator response matches request"),214
+ }215
+ }216
+217
+ async fn load_recent_history_inner(218
+ &self,219
+ mut cancellation: watch::Receiver<bool>,220
+ ) -> Result<RecentHistoryOutcome> {221
+ let anchor = {222
+ let state = self.inner.state.write().await?;223
+ state.history_candidate().await?.revents().find_map(|(_, event)| event.event_id())224
+ };225
+ let room = self.inner.weak_room.get().ok_or(EventCacheError::ClientDropped)?;226
+ let mut cursor = None;227
+ let mut visited = Vec::new();228
+ let mut events = Vec::new();229
+ let mut pages = 0;230
+ let mut raw_count = 0;231
+ let mut joined = false;232
+ let mut stalled = false;233
+ for _ in 0..4 {234
+ let mut options = MessagesOptions::backward();235
+ options.from = cursor.clone();236
+ options.limit = 50u16.into();237
+ if *cancellation.borrow() {238
+ return Err(EventCacheError::ClientDropped);239
+ }240
+ let response = tokio::select! {241
+ biased;242
+ _ = cancellation.changed() => return Err(EventCacheError::ClientDropped),243
+ response = room.messages(options) => response,244
+ }245
+ .map_err(|error| EventCacheError::PaginationError(std::sync::Arc::new(error)))?;246
+ pages += 1;247
+ raw_count += response.chunk.len();248
+ let mut page = response.chunk;249
+ if let Some(index) = anchor.as_ref().and_then(|anchor| {250
+ page.iter().position(|event| event.event_id().as_ref() == Some(anchor))251
+ }) {252
+ page.truncate(index);253
+ joined = true;254
+ }255
+ events.extend(page);256
+ if let Some(consumed) = cursor.take() {257
+ visited.push(consumed);258
+ }259
+ cursor = response.end;260
+ stalled = cursor.as_ref().is_some_and(|next| visited.contains(next));261
+ if joined || cursor.is_none() || stalled {262
+ break;263
+ }264
+ }265
+ events.reverse();266
+ {267
+ let mut state = self.inner.state.write().await?;268
+ let mut candidate = state.history_candidate().await?;269
+ let baseline_count = candidate.events().count();270
+ let fallback = {271
+ let mut after_anchor = anchor.is_none();272
+ candidate.events().find_map(|(_, event)| {273
+ let id = event.event_id();274
+ if after_anchor {275
+ return id;276
+ }277
+ if id == anchor {278
+ after_anchor = true;279
+ }280
+ None281
+ })282
+ };283
+ let newer_boundary = events.iter().find_map(Event::event_id).or(fallback.clone());284
+ let history = anchor.map(|anchor| HistoryGap {285
+ id: TransactionId::new(),286
+ older_boundary: Some(anchor),287
+ newer_boundary: newer_boundary.clone(),288
+ status: if joined {289
+ HistoryGapStatus::Joined290
+ } else if cursor.is_none() {291
+ HistoryGapStatus::Unavailable292
+ } else if stalled {293
+ HistoryGapStatus::Paused294
+ } else {295
+ HistoryGapStatus::Idle296
+ },297
+ visited_cursors: visited,298
+ });299
+ let mut before = fallback;300
+ let mut inserted = Vec::new();301
+ for event in events.into_iter().rev() {302
+ let id = event.event_id();303
+ if id.as_ref().is_none_or(|id| {304
+ !candidate.events().any(|(_, event)| event.event_id().as_ref() == Some(id))305
+ }) {306
+ candidate.insert_history_before(before.as_deref(), vec![event.clone()], None);307
+ inserted.push(event);308
+ }309
+ if id.is_some() {310
+ before = id;311
+ }312
+ }313
+ if history.is_some() || cursor.is_some() {314
+ candidate.insert_history_before(315
+ before.as_deref(),316
+ Vec::new(),317
+ Some(Gap { token: cursor.unwrap_or_default(), history: history.clone() }),318
+ );319
+ }320
+ inserted.reverse();321
+ state.commit_history_candidate(candidate, inserted, baseline_count).await?;322
+ Ok(RecentHistoryOutcome { pages, events: raw_count, gap: history })323
+ }324
+ }325
+326
+ #[doc = "Loads retained sections before attaching a live timeline subscriber."]327
+ pub async fn load_retained_history(&self) -> Result<()> {328
+ match self.request_history(HistoryOperation::Restore).await? {329
+ HistoryResponse::Restored => Ok(()),330
+ _ => unreachable!("coordinator response matches request"),331
+ }332
+ }333
+334
+ async fn load_retained_history_inner(&self) -> Result<()> {335
+ let mut state = self.inner.state.write().await?;336
+ let candidate = state.history_candidate().await?;337
+ if !gaps(&candidate).is_empty()338
+ && candidate.chunks().count() != state.room_linked_chunk().chunks().count()339
+ {340
+ let baseline_count = candidate.events().count();341
+ state.commit_history_candidate(candidate, Vec::new(), baseline_count).await?;342
+ }343
+ Ok(())344
+ }345
+346
+ #[doc = "Returns loaded events and their gaps under the same cache lock."]347
+ pub async fn history_snapshot(&self) -> Result<(Vec<Event>, Vec<HistoryGapPosition>)> {348
+ let state = self.inner.state.read().await?;349
+ let chunk = state.room_linked_chunk();350
+ Ok((chunk.events().map(|(_, event)| event.clone()).collect(), gap_positions(chunk)))351
+ }352
+353
+ #[doc = "Includes terminal identities and gaps outside the currently loaded cache window."]354
+ pub async fn history_gaps(&self) -> Result<Vec<HistoryGap>> {355
+ let state = self.inner.state.write().await?;356
+ Ok(gaps(&state.history_candidate().await?))357
+ }358
+359
+ #[doc = include_str!("append_history_section.md")]360
+ pub async fn append_history_section(361
+ &self,362
+ events: Vec<Event>,363
+ prev_batch: Option<String>,364
+ ) -> Result<Option<HistoryGap>> {365
+ match self.request_history(HistoryOperation::Append(events, prev_batch)).await? {366
+ HistoryResponse::Append(gap) => Ok(gap),367
+ _ => unreachable!("coordinator response matches request"),368
+ }369
+ }370
+371
+ async fn append_history_section_inner(372
+ &self,373
+ events: Vec<Event>,374
+ prev_batch: Option<String>,375
+ ) -> Result<Option<HistoryGap>> {376
+ {377
+ let mut state = self.inner.state.write().await?;378
+ let mut candidate = state.history_candidate().await?;379
+ let baseline_count = candidate.events().count();380
+ let older_boundary = candidate.revents().find_map(|(_, event)| event.event_id());381
+ let mut known: HashSet<_> =382
+ candidate.events().filter_map(|(_, event)| event.event_id()).collect();383
+ let events: Vec<_> = events384
+ .into_iter()385
+ .filter(|event| event.event_id().is_none_or(|id| known.insert(id)))386
+ .collect();387
+ if events.is_empty() {388
+ return Ok(None);389
+ }390
+ let history = older_boundary.map(|older_boundary| HistoryGap {391
+ id: TransactionId::new(),392
+ older_boundary: Some(older_boundary),393
+ newer_boundary: events.iter().find_map(Event::event_id),394
+ visited_cursors: Vec::new(),395
+ status: if prev_batch.is_some() {396
+ HistoryGapStatus::Idle397
+ } else {398
+ HistoryGapStatus::Unavailable399
+ },400
+ });401
+ let gap = if history.is_some() || prev_batch.is_some() {402
+ Some(Gap { token: prev_batch.unwrap_or_default(), history: history.clone() })403
+ } else {404
+ None405
+ };406
+ candidate.push_live_events(gap, &events);407
+ state.commit_history_candidate(candidate, events, baseline_count).await?;408
+ Ok(history)409
+ }410
+ }411
+412
+ #[doc = include_str!("fill_history_gap_once.md")]413
+ pub async fn fill_history_gap_once(414
+ &self,415
+ gap_id: OwnedTransactionId,416
+ ) -> Result<HistoryGapFillOutcome> {417
+ match self.request_history(HistoryOperation::Fill(gap_id)).await? {418
+ HistoryResponse::Fill(outcome) => Ok(outcome),419
+ _ => unreachable!("coordinator response matches request"),420
+ }421
+ }422
+423
+ async fn fill_history_gap_inner(424
+ &self,425
+ gap_id: OwnedTransactionId,426
+ mut cancellation: watch::Receiver<bool>,427
+ ) -> Result<HistoryGapFillOutcome> {428
+ let initial = {429
+ let state = self.inner.state.write().await?;430
+ find_gap(&state.history_candidate().await?, &gap_id)431
+ };432
+ let Some(initial) = initial else {433
+ return Ok(HistoryGapFillOutcome::NotFound);434
+ };435
+ let history = initial.history.as_ref().expect("find_gap only returns history gaps");436
+ if matches!(history.status, HistoryGapStatus::Joined | HistoryGapStatus::Unavailable) {437
+ return Ok(HistoryGapFillOutcome::Updated { gap: history.clone(), events: 0 });438
+ }439
+ let room = self.inner.weak_room.get().ok_or(EventCacheError::ClientDropped)?;440
+ let mut options = MessagesOptions::backward().from(initial.token.as_str());441
+ options.limit = 50u16.into();442
+ if *cancellation.borrow() {443
+ return Err(EventCacheError::ClientDropped);444
+ }445
+ let response = tokio::select! {446
+ biased;447
+ _ = cancellation.changed() => return Err(EventCacheError::ClientDropped),448
+ response = room.messages(options) => response,449
+ };450
+ {451
+ let mut state = self.inner.state.write().await?;452
+ let mut candidate = state.history_candidate().await?;453
+ let baseline_count = candidate.events().count();454
+ let Some(current) = find_gap(&candidate, &gap_id) else {455
+ return Ok(HistoryGapFillOutcome::NotFound);456
+ };457
+ if current.token != initial.token {458
+ return Ok(HistoryGapFillOutcome::Busy);459
+ }460
+ if let Some(gap) = current.history.as_ref()461
+ && matches!(gap.status, HistoryGapStatus::Joined | HistoryGapStatus::Unavailable)462
+ {463
+ return Ok(HistoryGapFillOutcome::Updated { gap: gap.clone(), events: 0 });464
+ }465
+ let (events, next, failure) = match response {466
+ Ok(response) => (response.chunk, response.end, None),467
+ Err(error) => (Vec::new(), Some(current.token.clone()), Some(error)),468
+ };469
+ let (gap, events) = apply_page(470
+ &mut candidate,471
+ &gap_id,472
+ events,473
+ next,474
+ failure.as_ref().map(ToString::to_string),475
+ );476
+ let count = events.len();477
+ state.commit_history_candidate(candidate, events, baseline_count).await?;478
+ if let Some(error) = failure {479
+ return Err(EventCacheError::PaginationError(std::sync::Arc::new(error)));480
+ }481
+ Ok(HistoryGapFillOutcome::Updated { gap, events: count })482
+ }483
+ }484
+}485
+486
+fn gaps(candidate: &EventLinkedChunk) -> Vec<HistoryGap> {487
+ candidate488
+ .chunks()489
+ .filter_map(|chunk| match chunk.content() {490
+ ChunkContent::Gap(gap) => gap.history.clone(),491
+ _ => None,492
+ })493
+ .collect()494
+}495
+496
+fn find_gap(candidate: &EventLinkedChunk, gap_id: &TransactionId) -> Option<Gap> {497
+ candidate.chunks().find_map(|chunk| match chunk.content() {498
+ ChunkContent::Gap(gap)499
+ if gap.history.as_ref().is_some_and(|history| history.id == gap_id) =>500
+ {501
+ Some(gap.clone())502
+ }503
+ _ => None,504
+ })505
+}506
+507
+fn apply_page(508
+ candidate: &mut EventLinkedChunk,509
+ gap_id: &TransactionId,510
+ mut events: Vec<Event>,511
+ next: Option<String>,512
+ failure: Option<String>,513
+) -> (HistoryGap, Vec<Event>) {514
+ let gap_chunk = candidate.chunks().find(|chunk| matches!(515
+ chunk.content(), ChunkContent::Gap(gap) if gap.history.as_ref().is_some_and(|history| history.id == gap_id)516
+ )).expect("the gap was checked under the state lock");517
+ let chunk_id = gap_chunk.identifier();518
+ let ChunkContent::Gap(current) = gap_chunk.content() else { unreachable!() };519
+ let mut gap = current.clone();520
+ let history = gap.history.as_mut().expect("the gap was checked under the state lock");521
+ let overlap = history.older_boundary.as_ref().and_then(|boundary| {522
+ events.iter().position(|event| event.event_id().as_ref() == Some(boundary))523
+ });524
+ history.status = if let Some(message) = failure {525
+ HistoryGapStatus::Failed { message }526
+ } else if let Some(index) = overlap {527
+ events.truncate(index);528
+ HistoryGapStatus::Joined529
+ } else if next.is_none() {530
+ HistoryGapStatus::Unavailable531
+ } else if next.as_ref() == Some(&gap.token)532
+ || next.as_ref().is_some_and(|next| history.visited_cursors.contains(next))533
+ {534
+ HistoryGapStatus::Paused535
+ } else {536
+ HistoryGapStatus::Idle537
+ };538
+ if !history.visited_cursors.contains(&gap.token) {539
+ history.visited_cursors.push(gap.token.clone());540
+ }541
+ if let Some(next) = next {542
+ gap.token = next;543
+ }544
+ let mut known: HashSet<_> =545
+ candidate.events().filter_map(|(_, event)| event.event_id()).collect();546
+ events.retain(|event| event.event_id().is_none_or(|id| known.insert(id)));547
+ events.reverse();548
+ let history = gap.history.as_ref().expect("history metadata was preserved").clone();549
+ candidate.push_backwards_pagination_events(Some(chunk_id), Some(gap), &events);550
+ (history, events)551
+}@@ -12,7 +12,9 @@
1212
// See the License for the specific language governing permissions and1313
// limitations under the License.1414
15
+mod history;1516
pub mod pagination;17
+pub use history::{HistoryGapFillOutcome, HistoryGapPosition, RecentHistoryOutcome};1618
mod state;1719
mod subscriber;1820
mod updates;@@ -121,6 +123,14 @@ impl RoomEventCache {
121123
/// subscriber. Creating, and especially dropping, a122124
/// [`RoomEventCacheSubscriber`] isn't free, as it triggers side-effects.123125
pub async fn subscribe(&self) -> Result<(Vec<Event>, RoomEventCacheSubscriber)> {126
+ let (events, _, subscriber) = self.subscribe_with_history().await?;127
+ Ok((events, subscriber))128
+ }129
+130
+ #[doc = "Subscribes after taking an atomic snapshot of loaded events and ordered gaps."]131
+ pub async fn subscribe_with_history(132
+ &self,133
+ ) -> Result<(Vec<Event>, Vec<HistoryGapPosition>, RoomEventCacheSubscriber)> {124134
let state = self.inner.state.read().await?;125135
let events =126136
state.room_linked_chunk().events().map(|(_position, item)| item.clone()).collect();@@ -136,7 +146,7 @@ impl RoomEventCache {
136146
subscriber_count.clone(),137147
);138148
139
- Ok((events, subscriber))149
+ Ok((events, state.history_gap_positions(), subscriber))140150
}141151
142152
/// Subscribe to thread for a given root event, and get a (maybe empty)@@ -363,6 +373,7 @@ impl RoomEventCache {
363373
self.inner.update_sender.send(364374
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {365375
diffs: updates_as_vector_diffs,376
+ history_gaps: Some(history::gap_positions(state.room_linked_chunk())),366377
origin: EventsOrigin::Cache,367378
}),368379
Some(RoomEventCacheGenericUpdate { room_id: self.inner.room_id.clone() }),@@ -392,6 +403,7 @@ impl RoomEventCache {
392403
cache.inner.update_sender.send(393404
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {394405
diffs: vec![eyeball_im::VectorDiff::Reset { values: items }],406
+ history_gaps: Some(history::gap_positions(state.room_linked_chunk())),395407
origin: EventsOrigin::Cache,396408
}),397409
Some(RoomEventCacheGenericUpdate { room_id: cache.inner.room_id.clone() }),@@ -478,6 +490,7 @@ impl RoomEventCache {
478490
479491
/// The (non-cloneable) details of the `RoomEventCache`.480492
pub(super) struct RoomEventCacheInner {493
+ history_coordinator: std::sync::OnceLock<mpsc::Sender<history::HistoryRequest>>,481494
pub generation: Arc<AtomicU64>,482495
/// The room id for this room.483496
room_id: OwnedRoomId,@@ -516,6 +529,7 @@ impl RoomEventCacheInner {
516529
) -> Self {517530
Self {518531
generation,532
+ history_coordinator: Default::default(),519533
room_id,520534
weak_room,521535
state,@@ -632,8 +646,6 @@ impl RoomEventCacheInner {
632646
let (stored_prev_batch_token, timeline_event_diffs) =633647
state.handle_sync(timeline, &ephemeral_events).await?;634648
635
- drop(state);636
-637649
// Now that all events have been added, we can trigger the638650
// `pagination_token_notifier`.639651
if stored_prev_batch_token {@@ -646,12 +658,15 @@ impl RoomEventCacheInner {
646658
self.update_sender.send(647659
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {648660
diffs: timeline_event_diffs,661
+ history_gaps: Some(history::gap_positions(state.room_linked_chunk())),649662
origin: EventsOrigin::Sync,650663
}),651664
Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),652665
);653666
}654667
668
+ drop(state);669
+655670
if !ephemeral_events.is_empty() {656671
self.update_sender657672
.send(RoomEventCacheUpdate::AddEphemeralEvents { events: ephemeral_events }, None);@@ -1188,7 +1203,7 @@ mod timed_tests {
11881203
// Chunk IDs aren't supposed to be ordered, so use a random value here.11891204
new: ChunkIdentifier::new(42),11901205
next: None,1191
- gap: Gap { token: "comté".to_owned() },1206
+ gap: Gap { token: "comté".to_owned(), history: None },11921207
},11931208
// Another items chunk, non-empty this time.11941209
Update::NewItemsChunk {@@ -1345,7 +1360,7 @@ mod timed_tests {
13451360
// Chunk IDs aren't supposed to be ordered, so use a random value here.13461361
new: ChunkIdentifier::new(42),13471362
next: None,1348
- gap: Gap { token: "cheddar".to_owned() },1363
+ gap: Gap { token: "cheddar".to_owned(), history: None },13491364
},13501365
// Another items chunk, non-empty this time.13511366
Update::NewItemsChunk {@@ -1893,10 +1908,8 @@ mod timed_tests {
18931908
}18941909
}18951910
1896
- // Handle a gappy sync with two events (including one duplicate, so1897
- // deduplication kicks in), so that the linked chunk is shrunk to the1898
- // last chunk, and that the linked chunk only contains the last two1899
- // events.1911
+ // Handle a limited sync with an exact overlap at the retained newest1912
+ // event. The overlap proves continuity with the cached timeline.19001913
let evid4 = event_id!("$4");19011914
room_event_cache19021915
.handle_joined_room_update(JoinedRoomUpdate {@@ -1914,37 +1927,79 @@ mod timed_tests {
19141927
let state = room_event_cache.inner.state.read().await.unwrap();19151928
let room_linked_chunk = state.room_linked_chunk();19161929
1917
- // After the shrink, only evid3 and evid4 are loaded.1918
- let mut events = room_linked_chunk.events();1919
-1920
- let (pos, ev) = events.next().unwrap();1921
- assert_eq!(ev.event_id().as_deref(), Some(evid3));1922
- assert_eq!(room_linked_chunk.event_order(pos), Some(2));1930
+ let event_ids: Vec<_> =1931
+ room_linked_chunk.events().filter_map(|(_, event)| event.event_id()).collect();1932
+ assert_eq!(event_ids, vec![evid1, evid2, evid3, evid4]);1933
+ }19231934
1924
- let (pos, ev) = events.next().unwrap();1925
- assert_eq!(ev.event_id().as_deref(), Some(evid4));1926
- assert_eq!(room_linked_chunk.event_order(pos), Some(3));1935
+ assert!(room_event_cache.history_gaps().await.unwrap().is_empty());1936
+ }19271937
1928
- // No other loaded events.1929
- assert!(events.next().is_none());1938
+ #[async_test]1939
+ async fn test_limited_sync_discontinuities_survive_reload() {1940
+ let room_id = room_id!("!limited-history:saucisse.bzh");1941
+ let client = MockClientBuilder::new(None).build().await;1942
+ let event_cache = client.event_cache();1943
+ event_cache.subscribe().unwrap();1944
+ client.base_client().get_or_create_room(room_id, RoomState::Joined);1945
+ let room = client.get_room(room_id).unwrap();1946
+ let (room_event_cache, _drop_handles) = room.event_cache().await.unwrap();1947
+ let f = EventFactory::new().room(room_id).sender(*ALICE);1948
+ let old_id = event_id!("$limited-old");1949
+ let cursor_gap_id = event_id!("$limited-cursor-gap");1950
+ let cursorless_gap_id = event_id!("$limited-cursorless-gap");19301951
1931
- // But we can still get the order of previous events.1932
- assert_eq!(1933
- room_linked_chunk.event_order(Position::new(ChunkIdentifier::new(0), 0)),1934
- Some(0)1935
- );1936
- assert_eq!(1937
- room_linked_chunk.event_order(Position::new(ChunkIdentifier::new(0), 1)),1938
- Some(1)1939
- );1952
+ room_event_cache1953
+ .handle_joined_room_update(JoinedRoomUpdate {1954
+ timeline: Timeline {1955
+ events: vec![f.text_msg("old").event_id(old_id).into_event()],1956
+ ..Default::default()1957
+ },1958
+ ..Default::default()1959
+ })1960
+ .await1961
+ .unwrap();1962
+ room_event_cache1963
+ .handle_joined_room_update(JoinedRoomUpdate {1964
+ timeline: Timeline {1965
+ limited: true,1966
+ prev_batch: Some("cursor-gap".to_owned()),1967
+ events: vec![f.text_msg("cursor gap").event_id(cursor_gap_id).into_event()],1968
+ },1969
+ ..Default::default()1970
+ })1971
+ .await1972
+ .unwrap();1973
+ room_event_cache1974
+ .handle_joined_room_update(JoinedRoomUpdate {1975
+ timeline: Timeline {1976
+ limited: true,1977
+ events: vec![1978
+ f.text_msg("cursorless gap").event_id(cursorless_gap_id).into_event(),1979
+ ],1980
+ ..Default::default()1981
+ },1982
+ ..Default::default()1983
+ })1984
+ .await1985
+ .unwrap();19401986
1941
- // ev3 doesn't have an order with its previous position, since it's been1942
- // deduplicated.1943
- assert_eq!(1944
- room_linked_chunk.event_order(Position::new(ChunkIdentifier::new(1), 0)),1945
- None1946
- );1947
- }1987
+ let state = room_event_cache.inner.state.write().await.unwrap();1988
+ let candidate = state.history_candidate().await.unwrap();1989
+ let gaps: Vec<_> = candidate1990
+ .chunks()1991
+ .filter_map(|chunk| match chunk.content() {1992
+ ChunkContent::Gap(gap) => gap.history.clone(),1993
+ ChunkContent::Items(_) => None,1994
+ })1995
+ .collect();1996
+ assert_eq!(gaps.len(), 2);1997
+ assert_eq!(gaps[0].older_boundary.as_deref(), Some(old_id));1998
+ assert_eq!(gaps[0].newer_boundary.as_deref(), Some(cursor_gap_id));1999
+ assert_eq!(gaps[0].status, crate::event_cache::HistoryGapStatus::Idle);2000
+ assert_eq!(gaps[1].older_boundary.as_deref(), Some(cursor_gap_id));2001
+ assert_eq!(gaps[1].newer_boundary.as_deref(), Some(cursorless_gap_id));2002
+ assert_eq!(gaps[1].status, crate::event_cache::HistoryGapStatus::Unavailable);19482003
}19492004
19502005
#[async_test]@@ -167,7 +167,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
167167
168168
// If any in-memory chunk is a gap, don't load more events, and let the caller169169
// resolve the gap.170
- if let Some(prev_token) = state.room_linked_chunk().rgap().map(|gap| gap.token) {170
+ if let Some(prev_token) =171
+ state.room_linked_chunk().rchunks().find_map(|chunk| match chunk.content() {172
+ ChunkContent::Gap(gap) if gap.history.is_none() => Some(gap.token.clone()),173
+ _ => None,174
+ })175
+ {171176
return Ok(LoadMoreEventsBackwardsOutcome::Gap {172177
prev_token: Some(prev_token),173178
waited_for_initial_prev_token: state.waited_for_initial_prev_token(),@@ -255,7 +260,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
255260
let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();256261
257262
Ok(match chunk_content {258
- ChunkContent::Gap(gap) => {263
+ ChunkContent::Gap(gap) if gap.history.is_none() => {259264
trace!("reloaded chunk from disk (gap)");260265
261266
LoadMoreEventsBackwardsOutcome::Gap {@@ -264,6 +269,12 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
264269
}265270
}266271
272
+ ChunkContent::Gap(_) => LoadMoreEventsBackwardsOutcome::Events {273
+ events: Vec::new(),274
+ timeline_event_diffs,275
+ reached_start: false,276
+ },277
+267278
ChunkContent::Items(events) => {268279
trace!(?reached_start, "reloaded chunk from disk ({} items)", events.len());269280
@@ -314,7 +325,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
314325
reached_start: bool,315326
generation: u64,316327
) -> Result<Option<BackPaginationOutcome>> {317
- let _state = self.state.read().await?;328
+ let state = self.state.read().await?;318329
if self.generation.load(std::sync::atomic::Ordering::SeqCst) != generation {319330
return Ok(None);320331
}@@ -322,6 +333,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
322333
self.update_sender.send(323334
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {324335
diffs: timeline_event_diffs,336
+ history_gaps: Some(super::history::gap_positions(state.room_linked_chunk())),325337
origin: EventsOrigin::Cache,326338
}),327339
Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),@@ -353,7 +365,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
353365
let prev_gap_id = if let Some(token) = prev_token {354366
// Find the corresponding gap in the in-memory linked chunk.355367
let gap_chunk_id = state.room_linked_chunk().chunk_identifier(|chunk| {356
- matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)368
+ matches!(chunk.content(), ChunkContent::Gap(gap) if gap.history.is_none() && gap.token == token)357369
});358370
359371
if gap_chunk_id.is_none() {@@ -417,7 +429,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
417429
// the inverted order; reorder them.418430
let topo_ordered_events = events.iter().rev().cloned().collect::<Vec<_>>();419431
420
- let new_gap = new_token.map(|prev_token| Gap { token: prev_token });432
+ let new_gap = new_token.map(|prev_token| Gap { token: prev_token, history: None });421433
let reached_start = state.room_linked_chunk_mut().push_backwards_pagination_events(422434
prev_gap_id,423435
new_gap,@@ -448,6 +460,7 @@ impl PaginatedCache for Arc<RoomEventCacheInner> {
448460
self.update_sender.send(449461
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {450462
diffs: timeline_event_diffs,463
+ history_gaps: Some(super::history::gap_positions(state.room_linked_chunk())),451464
origin: EventsOrigin::Pagination,452465
}),453466
Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),@@ -26,7 +26,7 @@ use matrix_sdk_base::{
2626
RoomInfoNotableUpdateReasons, apply_redaction, check_validity_of_replacement_events,2727
deserialized_responses::{ThreadSummary, ThreadSummaryStatus},2828
event_cache::{29
- Event, Gap,29
+ Event, Gap, HistoryGap, HistoryGapStatus,3030
store::{EventCacheStoreLock, EventCacheStoreLockGuard, EventCacheStoreLockState},3131
},3232
linked_chunk::{@@ -38,7 +38,7 @@ use matrix_sdk_base::{
3838
};3939
use matrix_sdk_common::executor::spawn;4040
use ruma::{41
- EventId, OwnedEventId, OwnedRoomId, OwnedUserId,41
+ EventId, OwnedEventId, OwnedRoomId, OwnedUserId, TransactionId,4242
events::{4343
AnySyncEphemeralRoomEvent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,4444
MessageLikeEventType,@@ -422,6 +422,7 @@ impl<'a> lock::Reload for RoomEventCacheStateLockWriteGuard<'a> {
422422
self.state.update_sender.send(423423
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {424424
diffs,425
+ history_gaps: Some(super::history::gap_positions(&self.state.room_linked_chunk)),425426
origin: EventsOrigin::Cache,426427
}),427428
Some(RoomEventCacheGenericUpdate { room_id: self.state.room_id.clone() }),@@ -432,6 +433,10 @@ impl<'a> lock::Reload for RoomEventCacheStateLockWriteGuard<'a> {
432433
}433434
434435
impl<'a> RoomEventCacheStateLockReadGuard<'a> {436
+ pub(crate) fn history_gap_positions(&self) -> Vec<super::HistoryGapPosition> {437
+ super::history::gap_positions(&self.state.room_linked_chunk)438
+ }439
+435440
/// Return the subscriber count.436441
pub fn subscriber_count(&self) -> &Arc<AtomicUsize> {437442
&self.state.subscriber_count@@ -784,7 +789,10 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
784789
events.retain(|event| event.event_id().is_none_or(|id| event_ids.insert(id)));785790
let mut replacement = EventLinkedChunk::new();786791
replacement.reset();787
- replacement.push_live_events(prev_batch.clone().map(|token| Gap { token }), &events);792
+ replacement.push_live_events(793
+ prev_batch.clone().map(|token| Gap { token, history: None }),794
+ &events,795
+ );788796
let updates = replacement.store_updates().take();789797
self.send_updates_to_store(updates).await?;790798
let _ = replacement.updates_as_vector_diffs();@@ -794,6 +802,61 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
794802
Ok(())795803
}796804
805
+ pub(super) async fn history_candidate(&self) -> Result<EventLinkedChunk, EventCacheError> {806
+ let linked_chunk_id = LinkedChunkId::Room(&self.state.room_id);807
+ let (last, generator) = self.store.load_last_chunk(linked_chunk_id).await?;808
+ let metadata = self.store.load_all_chunks_metadata(linked_chunk_id).await?;809
+ let mut candidate = EventLinkedChunk::with_initial_linked_chunk(810
+ lazy_loader::from_last_chunk(last, generator)?,811
+ (!metadata.is_empty()).then_some(metadata),812
+ );813
+ loop {814
+ let first =815
+ candidate.chunks().next().expect("a linked chunk is never empty").identifier();816
+ let Some(previous) = self.store.load_previous_chunk(linked_chunk_id, first).await?817
+ else {818
+ break;819
+ };820
+ candidate.insert_new_chunk_as_first(previous)?;821
+ }822
+ let _ = candidate.store_updates().take();823
+ let _ = candidate.updates_as_vector_diffs();824
+ Ok(candidate)825
+ }826
+827
+ pub(crate) fn history_gap_positions(&self) -> Vec<super::HistoryGapPosition> {828
+ super::history::gap_positions(&self.state.room_linked_chunk)829
+ }830
+831
+ pub(super) async fn commit_history_candidate(832
+ &mut self,833
+ mut candidate: EventLinkedChunk,834
+ events: Vec<Event>,835
+ baseline_count: usize,836
+ ) -> Result<(), EventCacheError> {837
+ let use_incremental = self.state.room_linked_chunk.events().count() == baseline_count;838
+ self.send_updates_to_store(candidate.store_updates().take()).await?;839
+ self.state.room_linked_chunk = candidate;840
+ self.reset_pagination_status(false);841
+ let result =842
+ self.post_process_new_events(events, PostProcessingOrigin::Backpagination, None).await;843
+ let mut diffs = self.state.room_linked_chunk.updates_as_vector_diffs();844
+ if !use_incremental {845
+ let values =846
+ self.state.room_linked_chunk.events().map(|(_, event)| event.clone()).collect();847
+ diffs = vec![VectorDiff::Reset { values }];848
+ }849
+ self.state.update_sender.send(850
+ RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {851
+ diffs,852
+ history_gaps: Some(super::history::gap_positions(&self.state.room_linked_chunk)),853
+ origin: EventsOrigin::Cache,854
+ }),855
+ Some(RoomEventCacheGenericUpdate { room_id: self.state.room_id.clone() }),856
+ );857
+ result858
+ }859
+797860
fn reset_pagination_status(&self, hit_timeline_start: bool) {798861
let mut status = self.state.pagination_status.write();799862
self.state.generation.fetch_add(1, Ordering::SeqCst);@@ -849,6 +912,7 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
849912
ephemeral_events: &[Raw<AnySyncEphemeralRoomEvent>],850913
) -> Result<(bool, Vec<VectorDiff<Event>>), EventCacheError> {851914
let mut prev_batch = timeline.prev_batch.take();915
+ let had_prev_batch = prev_batch.is_some();852916
853917
let DeduplicationOutcome {854918
all_events: events,@@ -882,9 +946,23 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
882946
prev_batch = None;883947
}884948
949
+ let older_boundary = timeline950
+ .limited951
+ .then(|| self.state.room_linked_chunk.revents().find_map(|(_, event)| event.event_id()))952
+ .flatten();953
+ let newer_boundary =954
+ timeline.limited.then(|| events.iter().find_map(Event::event_id)).flatten();955
+ if older_boundary.is_some() && events.iter().any(|event| event.event_id() == older_boundary)956
+ {957
+ prev_batch = None;958
+ }959
+ let limited_discontinuity = older_boundary.is_some()960
+ && newer_boundary.is_some()961
+ && !events.iter().any(|event| event.event_id() == older_boundary);885962
let has_new_gap = prev_batch.is_some();963
+ let has_new_discontinuity = has_new_gap || limited_discontinuity;886964
887
- if has_new_gap {965
+ if has_new_discontinuity {888966
// Sad time: there's a gap, somewhere, in the timeline, and there's at least one889967
// non-duplicated event. We don't know which threads might have gappy, so we890968
// must invalidate them all :(@@ -943,15 +1021,40 @@ impl<'a> RoomEventCacheStateLockWriteGuard<'a> {
9431021
// events, because we are pushing all _new_ `events` at the back.9441022
self.remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids).await?;9451023
946
- self.state947
- .room_linked_chunk948
- .push_live_events(prev_batch.map(|prev_token| Gap { token: prev_token }), &events);1024
+ let gap = prev_batch1025
+ .map(|token| Gap {1026
+ token,1027
+ history: limited_discontinuity.then(|| HistoryGap {1028
+ id: TransactionId::new(),1029
+ older_boundary: older_boundary.clone(),1030
+ newer_boundary: newer_boundary.clone(),1031
+ visited_cursors: Vec::new(),1032
+ status: if had_prev_batch {1033
+ HistoryGapStatus::Idle1034
+ } else {1035
+ HistoryGapStatus::Unavailable1036
+ },1037
+ }),1038
+ })1039
+ .or_else(|| {1040
+ limited_discontinuity.then(|| Gap {1041
+ token: String::new(),1042
+ history: Some(HistoryGap {1043
+ id: TransactionId::new(),1044
+ older_boundary,1045
+ newer_boundary,1046
+ visited_cursors: Vec::new(),1047
+ status: HistoryGapStatus::Unavailable,1048
+ }),1049
+ })1050
+ });1051
+ self.state.room_linked_chunk.push_live_events(gap, &events);9491052
9501053
// Extract a new read receipt, if available.9511054
let new_receipt = extract_read_receipt(ephemeral_events);9521055
self.post_process_new_events(events, PostProcessingOrigin::Sync, new_receipt).await?;9531056
954
- if timeline.limited && has_new_gap {1057
+ if timeline.limited && has_new_discontinuity && !limited_discontinuity {9551058
// If there was a previous batch token for a limited timeline, unload the chunks9561059
// so it only contains the last one; otherwise, there might be a9571060
// valid gap in between, and observers may not render it (yet).@@ -130,6 +130,7 @@ impl ThreadEventCache {
130130
let _ = state.state.sender.send(TimelineVectorDiffs {131131
diffs: updates_as_vector_diffs,132132
origin: EventsOrigin::Cache,133
+ history_gaps: None,133134
});134135
}135136
@@ -150,6 +151,7 @@ impl ThreadEventCache {
150151
let _ = state.state.sender.send(TimelineVectorDiffs {151152
diffs: timeline_event_diffs,152153
origin: EventsOrigin::Sync,154
+ history_gaps: None,153155
});154156
}155157
@@ -181,6 +183,7 @@ impl ThreadEventCache {
181183
let _ = state.state.sender.send(TimelineVectorDiffs {182184
diffs: timeline_event_diffs,183185
origin: EventsOrigin::Sync,186
+ history_gaps: None,184187
});185188
}186189
@@ -218,7 +218,7 @@ impl PaginatedCache for ThreadEventCacheWrapper {
218218
// If the gap id is missing, it means that the gap disappeared during219219
// pagination; in this case, early return to the caller.220220
let Some(gap_id) = state.thread_linked_chunk().chunk_identifier(|chunk| {221
- matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)221
+ matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token, .. }) if *prev_token == token)222222
}) else {223223
// The previous token has gone missing, so the timeline has been reset in the224224
// meanwhile, but it's fine per this function's contract.@@ -233,7 +233,7 @@ impl PaginatedCache for ThreadEventCacheWrapper {
233233
// This is a backwards pagination, so the events were returned in the reverse234234
// topological order.235235
let topo_ordered_events = events.iter().cloned().rev().collect::<Vec<_>>();236
- let new_gap = new_token.map(|token| Gap { token });236
+ let new_gap = new_token.map(|token| Gap { token, history: None });237237
238238
let deduplication = state.filter_duplicate_events(topo_ordered_events);239239
@@ -266,10 +266,11 @@ impl PaginatedCache for ThreadEventCacheWrapper {
266266
267267
if !updates.is_empty() {268268
// Send the updates to the listeners.269
- let _ = state270
- .state271
- .sender272
- .send(TimelineVectorDiffs { diffs: updates, origin: EventsOrigin::Pagination });269
+ let _ = state.state.sender.send(TimelineVectorDiffs {270
+ diffs: updates,271
+ origin: EventsOrigin::Pagination,272
+ history_gaps: None,273
+ });273274
}274275
275276
Ok(Some(BackPaginationOutcome { reached_start, events }))@@ -124,8 +124,11 @@ impl<'a> lock::Reload for ThreadEventCacheStateLockWriteGuard<'a> {
124124
let diffs = self.state.thread_linked_chunk.updates_as_vector_diffs();125125
126126
if !diffs.is_empty() {127
- let _ =128
- self.state.sender.send(TimelineVectorDiffs { diffs, origin: EventsOrigin::Cache });127
+ let _ = self.state.sender.send(TimelineVectorDiffs {128
+ diffs,129
+ origin: EventsOrigin::Cache,130
+ history_gaps: None,131
+ });129132
}130133
131134
Ok(())@@ -70,8 +70,8 @@ pub use caches::{
7070
event_focused::EventFocusThreadMode,7171
pagination::{BackPaginationOutcome, PaginationStatus},7272
room::{73
- RoomEventCache, RoomEventCacheGenericUpdate, RoomEventCacheSubscriber,74
- RoomEventCacheUpdate, pagination::RoomPagination,73
+ HistoryGapFillOutcome, HistoryGapPosition, RecentHistoryOutcome, RoomEventCache, RoomEventCacheGenericUpdate,74
+ RoomEventCacheSubscriber, RoomEventCacheUpdate, pagination::RoomPagination,7575
},7676
thread::pagination::ThreadPagination,7777
};@@ -79,6 +79,7 @@ pub use caches::{
7979
pub use redecryptor::{DecryptionRetryRequest, RedecryptorReport};8080
8181
pub use crate::event_cache::automatic_pagination::AutomaticPagination;82
+pub use matrix_sdk_base::event_cache::{HistoryGap, HistoryGapStatus};8283
8384
/// An error observed in the [`EventCache`].8485
#[derive(thiserror::Error, Clone, Debug)]@@ -57,7 +57,7 @@
5757
//! subscribes to [`RedecryptorReport`] stream.5858
//!5959
//! ```markdown60
-//! 60
+//!6161
//! .----------------------.6262
//! | |6363
//! | Beeb, boop! |@@ -419,6 +419,7 @@ impl EventCache {
419419
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {420420
diffs: state.room_linked_chunk_mut().updates_as_vector_diffs(),421421
origin: EventsOrigin::Cache,422
+ history_gaps: Some(state.history_gap_positions()),422423
}),423424
Some(RoomEventCacheGenericUpdate { room_id: room_id.to_owned() }),424425
);@@ -177,6 +177,7 @@ pub(super) async fn auto_shrink_linked_chunk_task(
177177
RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {178178
diffs,179179
origin: EventsOrigin::Cache,180
+ history_gaps: Some(state.history_gap_positions()),180181
}),181182
None,182183
);@@ -1738,7 +1738,7 @@ async fn test_lazy_loading() {
17381738
previous: Some(ChunkIdentifier::new(0)),17391739
new: ChunkIdentifier::new(1),17401740
next: None,1741
- gap: Gap { token: "raclette".to_owned() },1741
+ gap: Gap { token: "raclette".to_owned(), history: None },17421742
},17431743
// chunk #317441744
Update::NewItemsChunk {@@ -0,0 +1,375 @@
1
+use std::time::Duration;2
+3
+use matrix_sdk::{4
+ event_cache::HistoryGapStatus,5
+ test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},6
+};7
+use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};8
+use ruma::{event_id, room_id, user_id};9
+10
+fn event_factory(room_id: &ruma::RoomId) -> EventFactory {11
+ EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"))12
+}13
+14
+#[async_test]15
+async fn test_recent_history_is_bounded_and_keeps_the_retained_window() {16
+ let server = MatrixMockServer::new().await;17
+ let client = server.client_builder().build().await;18
+ client.event_cache().subscribe().unwrap();19
+ let room_id = room_id!("!history:example.org");20
+ let room = server.sync_joined_room(&client, room_id).await;21
+ let (cache, _handles) = room.event_cache().await.unwrap();22
+ let f = event_factory(room_id);23
+ cache24
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)25
+ .await26
+ .unwrap();27
+28
+ server29
+ .mock_room_messages()30
+ .match_limit(50)31
+ .ok(RoomMessagesResponseTemplate::default()32
+ .end_token("cursor-1")33
+ .events(vec![f.text_msg("recent-4").event_id(event_id!("$recent4"))]))34
+ .mock_once()35
+ .mount()36
+ .await;37
+ server38
+ .mock_room_messages()39
+ .match_from("cursor-1")40
+ .match_limit(50)41
+ .ok(RoomMessagesResponseTemplate::default()42
+ .end_token("cursor-2")43
+ .events(vec![f.text_msg("recent-3").event_id(event_id!("$recent3"))]))44
+ .mock_once()45
+ .mount()46
+ .await;47
+ server48
+ .mock_room_messages()49
+ .match_from("cursor-2")50
+ .match_limit(50)51
+ .ok(RoomMessagesResponseTemplate::default()52
+ .end_token("cursor-3")53
+ .events(vec![f.text_msg("recent-2").event_id(event_id!("$recent2"))]))54
+ .mock_once()55
+ .mount()56
+ .await;57
+ server58
+ .mock_room_messages()59
+ .match_from("cursor-3")60
+ .match_limit(50)61
+ .ok(RoomMessagesResponseTemplate::default()62
+ .end_token("cursor-4")63
+ .events(vec![f.text_msg("recent-1").event_id(event_id!("$recent1"))]))64
+ .mock_once()65
+ .mount()66
+ .await;67
+68
+ let outcome = cache.load_recent_history().await.unwrap();69
+ assert_eq!(outcome.pages, 4);70
+ assert_eq!(outcome.events, 4);71
+ assert_eq!(outcome.gap.as_ref().unwrap().status, HistoryGapStatus::Idle);72
+ let ids: Vec<_> =73
+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();74
+ assert_eq!(75
+ ids,76
+ vec![77
+ event_id!("$old"),78
+ event_id!("$recent1"),79
+ event_id!("$recent2"),80
+ event_id!("$recent3"),81
+ event_id!("$recent4"),82
+ ]83
+ );84
+ assert_eq!(cache.history_gaps().await.unwrap().len(), 1);85
+}86
+87
+#[async_test]88
+async fn test_recent_history_joins_at_the_retained_boundary() {89
+ let server = MatrixMockServer::new().await;90
+ let client = server.client_builder().build().await;91
+ client.event_cache().subscribe().unwrap();92
+ let room_id = room_id!("!history:example.org");93
+ let room = server.sync_joined_room(&client, room_id).await;94
+ let (cache, _handles) = room.event_cache().await.unwrap();95
+ let f = event_factory(room_id);96
+ cache97
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)98
+ .await99
+ .unwrap();100
+ server101
+ .mock_room_messages()102
+ .match_limit(50)103
+ .ok(RoomMessagesResponseTemplate::default().end_token("unused").events(vec![104
+ f.text_msg("recent").event_id(event_id!("$recent")),105
+ f.text_msg("old").event_id(event_id!("$old")),106
+ ]))107
+ .mock_once()108
+ .mount()109
+ .await;110
+111
+ let outcome = cache.load_recent_history().await.unwrap();112
+ assert_eq!(outcome.pages, 1);113
+ assert_eq!(outcome.gap.as_ref().unwrap().status, HistoryGapStatus::Joined);114
+ assert_eq!(115
+ cache116
+ .events()117
+ .await118
+ .unwrap()119
+ .iter()120
+ .filter_map(|event| event.event_id())121
+ .collect::<Vec<_>>(),122
+ vec![event_id!("$old"), event_id!("$recent")]123
+ );124
+}125
+126
+#[async_test]127
+async fn test_recent_history_keeps_sync_arrivals_during_a_fetch() {128
+ let server = MatrixMockServer::new().await;129
+ let client = server.client_builder().build().await;130
+ client.event_cache().subscribe().unwrap();131
+ let room_id = room_id!("!history:example.org");132
+ let room = server.sync_joined_room(&client, room_id).await;133
+ let (cache, _handles) = room.event_cache().await.unwrap();134
+ let f = event_factory(room_id);135
+ cache136
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)137
+ .await138
+ .unwrap();139
+ server140
+ .mock_room_messages()141
+ .match_limit(50)142
+ .ok(RoomMessagesResponseTemplate::default()143
+ .events(vec![144
+ f.text_msg("live").event_id(event_id!("$live")),145
+ f.text_msg("recent").event_id(event_id!("$recent")),146
+ ])147
+ .with_delay(Duration::from_millis(100)))148
+ .mock_once()149
+ .mount()150
+ .await;151
+152
+ let mut load = Box::pin(cache.load_recent_history());153
+ tokio::select! {154
+ biased;155
+ result = &mut load => panic!("request finished before sync {result:?}"),156
+ _ = tokio::time::sleep(Duration::from_millis(20)) => {}157
+ }158
+ server159
+ .sync_room(160
+ &client,161
+ JoinedRoomBuilder::new(room_id)162
+ .add_timeline_event(f.text_msg("live").event_id(event_id!("$live"))),163
+ )164
+ .await;165
+ load.await.unwrap();166
+167
+ let ids: Vec<_> =168
+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();169
+ assert_eq!(ids, vec![event_id!("$old"), event_id!("$recent"), event_id!("$live")]);170
+}171
+172
+#[async_test]173
+async fn test_recent_history_continues_empty_pages_and_pauses_on_a_cursor_cycle() {174
+ let server = MatrixMockServer::new().await;175
+ let client = server.client_builder().build().await;176
+ client.event_cache().subscribe().unwrap();177
+ let room_id = room_id!("!history:example.org");178
+ let room = server.sync_joined_room(&client, room_id).await;179
+ let (cache, _handles) = room.event_cache().await.unwrap();180
+ let f = event_factory(room_id);181
+ cache182
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)183
+ .await184
+ .unwrap();185
+ server186
+ .mock_room_messages()187
+ .match_limit(50)188
+ .ok(RoomMessagesResponseTemplate::default().end_token("cursor-a"))189
+ .mock_once()190
+ .mount()191
+ .await;192
+ server193
+ .mock_room_messages()194
+ .match_from("cursor-a")195
+ .match_limit(50)196
+ .ok(RoomMessagesResponseTemplate::default()197
+ .end_token("cursor-b")198
+ .events(vec![f.text_msg("recent").event_id(event_id!("$recent"))]))199
+ .mock_once()200
+ .mount()201
+ .await;202
+ server203
+ .mock_room_messages()204
+ .match_from("cursor-b")205
+ .match_limit(50)206
+ .ok(RoomMessagesResponseTemplate::default().end_token("cursor-a"))207
+ .mock_once()208
+ .mount()209
+ .await;210
+211
+ let outcome = cache.load_recent_history().await.unwrap();212
+ assert_eq!(outcome.pages, 3);213
+ assert_eq!(outcome.events, 1);214
+ assert_eq!(outcome.gap.as_ref().unwrap().status, HistoryGapStatus::Paused);215
+ assert_eq!(216
+ cache217
+ .events()218
+ .await219
+ .unwrap()220
+ .iter()221
+ .filter_map(|event| event.event_id())222
+ .collect::<Vec<_>>(),223
+ vec![event_id!("$old"), event_id!("$recent")]224
+ );225
+}226
+227
+#[async_test]228
+async fn test_concurrent_recent_history_calls_share_the_request_and_gap() {229
+ let server = MatrixMockServer::new().await;230
+ let client = server.client_builder().build().await;231
+ client.event_cache().subscribe().unwrap();232
+ let room_id = room_id!("!history:example.org");233
+ let room = server.sync_joined_room(&client, room_id).await;234
+ let (cache, _handles) = room.event_cache().await.unwrap();235
+ let f = event_factory(room_id);236
+ cache237
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)238
+ .await239
+ .unwrap();240
+ server241
+ .mock_room_messages()242
+ .match_limit(50)243
+ .ok(RoomMessagesResponseTemplate::default()244
+ .events(vec![f.text_msg("recent").event_id(event_id!("$recent"))])245
+ .with_delay(Duration::from_millis(100)))246
+ .expect(1)247
+ .mock_once()248
+ .mount()249
+ .await;250
+251
+ let (first, second) = tokio::join!(cache.load_recent_history(), cache.load_recent_history());252
+ let first = first.unwrap();253
+ let second = second.unwrap();254
+ assert_eq!(first.gap.as_ref().unwrap().id, second.gap.as_ref().unwrap().id);255
+ assert_eq!(first.pages, 1);256
+ assert_eq!(second.pages, 1);257
+ assert_eq!(first.events, 1);258
+ assert_eq!(second.events, 1);259
+}260
+261
+#[async_test]262
+async fn test_cancelling_one_recent_history_waiter_keeps_the_shared_request_alive() {263
+ let server = MatrixMockServer::new().await;264
+ let client = server.client_builder().build().await;265
+ client.event_cache().subscribe().unwrap();266
+ let room_id = room_id!("!history:example.org");267
+ let room = server.sync_joined_room(&client, room_id).await;268
+ let (cache, _handles) = room.event_cache().await.unwrap();269
+ let f = event_factory(room_id);270
+ cache271
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)272
+ .await273
+ .unwrap();274
+ server275
+ .mock_room_messages()276
+ .match_limit(50)277
+ .ok(RoomMessagesResponseTemplate::default()278
+ .events(vec![f.text_msg("recent").event_id(event_id!("$recent"))])279
+ .with_delay(Duration::from_millis(100)))280
+ .expect(1)281
+ .mock_once()282
+ .mount()283
+ .await;284
+285
+ let mut first = Box::pin(cache.load_recent_history());286
+ tokio::select! {287
+ biased;288
+ result = &mut first => panic!("request finished before second waiter {result:?}"),289
+ _ = tokio::time::sleep(Duration::from_millis(20)) => {}290
+ }291
+ let second_cache = cache.clone();292
+ let second = tokio::spawn(async move { second_cache.load_recent_history().await });293
+ tokio::time::sleep(Duration::from_millis(20)).await;294
+ drop(first);295
+ let outcome = second.await.unwrap().unwrap();296
+ assert_eq!(outcome.pages, 1);297
+ assert_eq!(outcome.gap.unwrap().status, HistoryGapStatus::Unavailable);298
+ assert_eq!(299
+ cache300
+ .events()301
+ .await302
+ .unwrap()303
+ .iter()304
+ .filter_map(|event| event.event_id())305
+ .collect::<Vec<_>>(),306
+ vec![event_id!("$old"), event_id!("$recent")]307
+ );308
+}309
+310
+#[async_test]311
+async fn test_cancelling_all_recent_history_waiters_allows_a_fresh_retry() {312
+ let server = MatrixMockServer::new().await;313
+ let client = server.client_builder().build().await;314
+ client.event_cache().subscribe().unwrap();315
+ let room_id = room_id!("!history:example.org");316
+ let room = server.sync_joined_room(&client, room_id).await;317
+ let (cache, _handles) = room.event_cache().await.unwrap();318
+ let f = event_factory(room_id);319
+ cache320
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)321
+ .await322
+ .unwrap();323
+ server324
+ .mock_room_messages()325
+ .match_limit(50)326
+ .ok(RoomMessagesResponseTemplate::default()327
+ .events(vec![f.text_msg("discarded").event_id(event_id!("$discarded"))])328
+ .with_delay(Duration::from_millis(300)))329
+ .expect(1)330
+ .mock_once()331
+ .mount()332
+ .await;333
+ server334
+ .mock_room_messages()335
+ .match_limit(50)336
+ .ok(RoomMessagesResponseTemplate::default()337
+ .events(vec![f.text_msg("recent").event_id(event_id!("$recent"))]))338
+ .expect(1)339
+ .mock_once()340
+ .mount()341
+ .await;342
+343
+ let mut abandoned = Box::pin(cache.load_recent_history());344
+ tokio::select! {345
+ biased;346
+ result = &mut abandoned => panic!("request finished before cancellation {result:?}"),347
+ _ = tokio::time::sleep(Duration::from_millis(20)) => {}348
+ }349
+ drop(abandoned);350
+ tokio::time::sleep(Duration::from_millis(30)).await;351
+ assert_eq!(352
+ cache353
+ .events()354
+ .await355
+ .unwrap()356
+ .iter()357
+ .filter_map(|event| event.event_id())358
+ .collect::<Vec<_>>(),359
+ vec![event_id!("$old")]360
+ );361
+362
+ let outcome = cache.load_recent_history().await.unwrap();363
+ assert_eq!(outcome.pages, 1);364
+ assert_eq!(outcome.gap.unwrap().status, HistoryGapStatus::Unavailable);365
+ assert_eq!(366
+ cache367
+ .events()368
+ .await369
+ .unwrap()370
+ .iter()371
+ .filter_map(|event| event.event_id())372
+ .collect::<Vec<_>>(),373
+ vec![event_id!("$old"), event_id!("$recent")]374
+ );375
+}@@ -0,0 +1,472 @@
1
+use std::{2
+ sync::{Arc, atomic::Ordering},3
+ time::Duration,4
+};5
+6
+use matrix_sdk::{7
+ event_cache::{HistoryGapFillOutcome, HistoryGapStatus},8
+ store::StoreConfig,9
+ test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},10
+};11
+use matrix_sdk_base::event_cache::store::MemoryStore;12
+use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;13
+use matrix_sdk_test::{JoinedRoomBuilder, async_test, event_factory::EventFactory};14
+use ruma::{event_id, room_id, user_id};15
+16
+#[path = "history_reseed/failing_store.rs"]17
+mod failing_store;18
+19
+#[async_test]20
+async fn test_segmented_history_resumes_after_restart_and_joins_only_at_boundary() {21
+ let server = MatrixMockServer::new().await;22
+ let config = StoreConfig::new(CrossProcessLockConfig::multi_process("segmented-history"))23
+ .state_store(matrix_sdk_base::store::MemoryStore::new())24
+ .event_cache_store(Arc::new(MemoryStore::new()));25
+ let room_id = room_id!("!history:example.org");26
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));27
+ let gap_id;28
+ {29
+ let client = server30
+ .client_builder()31
+ .on_builder(|builder| builder.store_config(config.clone()))32
+ .build()33
+ .await;34
+ client.event_cache().subscribe().unwrap();35
+ let room = server.sync_joined_room(&client, room_id).await;36
+ let (cache, _handles) = room.event_cache().await.unwrap();37
+ cache38
+ .reseed(39
+ vec![f.text_msg("retained").event_id(event_id!("$old")).into_event()],40
+ Some("old-edge".into()),41
+ )42
+ .await43
+ .unwrap();44
+ let gap = cache45
+ .append_history_section(46
+ vec![f.text_msg("recent").event_id(event_id!("$new")).into_event()],47
+ Some("recent-edge".into()),48
+ )49
+ .await50
+ .unwrap()51
+ .unwrap();52
+ gap_id = gap.id.clone();53
+ let mut expected_gap = gap.clone();54
+ expected_gap.visited_cursors.push("recent-edge".into());55
+ server56
+ .mock_room_messages()57
+ .match_from("recent-edge")58
+ .match_limit(50)59
+ .ok(RoomMessagesResponseTemplate::default().end_token("after-empty"))60
+ .mock_once()61
+ .mount()62
+ .await;63
+ assert_eq!(64
+ cache.fill_history_gap_once(gap.id.clone()).await.unwrap(),65
+ HistoryGapFillOutcome::Updated { gap: expected_gap.clone(), events: 0 }66
+ );67
+ assert_eq!(cache.history_gaps().await.unwrap(), vec![expected_gap]);68
+ assert_eq!(cache.events().await.unwrap().len(), 2);69
+ }70
+ let client =71
+ server.client_builder().on_builder(|builder| builder.store_config(config)).build().await;72
+ client.event_cache().subscribe().unwrap();73
+ let room = client.get_room(room_id).unwrap();74
+ let (cache, _handles) = room.event_cache().await.unwrap();75
+ assert_eq!(cache.history_gaps().await.unwrap()[0].id, gap_id);76
+ cache.load_retained_history().await.unwrap();77
+ let (restored, gaps) = cache.history_snapshot().await.unwrap();78
+ assert_eq!(79
+ restored.iter().filter_map(|event| event.event_id()).collect::<Vec<_>>(),80
+ vec![event_id!("$old"), event_id!("$new")]81
+ );82
+ assert_eq!(gaps[0].gap.id, gap_id);83
+ server84
+ .mock_room_messages()85
+ .match_from("after-empty")86
+ .match_limit(50)87
+ .ok(RoomMessagesResponseTemplate::default()88
+ .end_token("bridge-edge")89
+ .events(vec![f.text_msg("middle").event_id(event_id!("$middle"))]))90
+ .mock_once()91
+ .mount()92
+ .await;93
+ let HistoryGapFillOutcome::Updated { gap, events } =94
+ cache.fill_history_gap_once(gap_id.clone()).await.unwrap()95
+ else {96
+ panic!()97
+ };98
+ assert_eq!(events, 1);99
+ assert_eq!(gap.status, HistoryGapStatus::Idle);100
+ server101
+ .mock_room_messages()102
+ .match_from("bridge-edge")103
+ .ok(RoomMessagesResponseTemplate::default()104
+ .end_token("irrelevant")105
+ .events(vec![f.text_msg("retained").event_id(event_id!("$old"))]))106
+ .mock_once()107
+ .mount()108
+ .await;109
+ let HistoryGapFillOutcome::Updated { gap, events } =110
+ cache.fill_history_gap_once(gap_id.clone()).await.unwrap()111
+ else {112
+ panic!()113
+ };114
+ assert_eq!(gap.status, HistoryGapStatus::Joined);115
+ assert_eq!(events, 0);116
+ assert_eq!(117
+ cache.fill_history_gap_once(gap_id).await.unwrap(),118
+ HistoryGapFillOutcome::Updated { gap, events: 0 }119
+ );120
+ let ids: Vec<_> =121
+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();122
+ assert_eq!(ids, vec![event_id!("$old"), event_id!("$middle"), event_id!("$new")]);123
+}124
+125
+#[async_test]126
+async fn test_subscribe_with_history_reports_atomic_gap_snapshot_and_updates() {127
+ let server = MatrixMockServer::new().await;128
+ let client = server.client_builder().build().await;129
+ client.event_cache().subscribe().unwrap();130
+ let room_id = room_id!("!history:example.org");131
+ let room = server.sync_joined_room(&client, room_id).await;132
+ let (cache, _handles) = room.event_cache().await.unwrap();133
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));134
+ cache135
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)136
+ .await137
+ .unwrap();138
+ cache139
+ .append_history_section(140
+ vec![f.reaction(event_id!("$old"), "👍").event_id(event_id!("$hidden")).into_event()],141
+ Some("hidden-cursor".into()),142
+ )143
+ .await144
+ .unwrap()145
+ .unwrap();146
+ cache147
+ .append_history_section(148
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],149
+ Some("new-cursor".into()),150
+ )151
+ .await152
+ .unwrap()153
+ .unwrap();154
+155
+ let (snapshot, gaps, mut subscriber) = cache.subscribe_with_history().await.unwrap();156
+ let snapshot_ids: Vec<_> = snapshot.iter().filter_map(|event| event.event_id()).collect();157
+ assert_eq!(snapshot_ids, vec![event_id!("$old"), event_id!("$hidden"), event_id!("$new")]);158
+ assert_eq!(gaps[0].next_event_id.as_deref(), Some(event_id!("$hidden")));159
+ let gap_id = gaps[0].gap.id.clone();160
+161
+ server162
+ .mock_room_messages()163
+ .match_from("hidden-cursor")164
+ .ok(RoomMessagesResponseTemplate::default()165
+ .end_token("next-cursor")166
+ .events(vec![f.text_msg("middle").event_id(event_id!("$middle"))]))167
+ .mock_once()168
+ .mount()169
+ .await;170
+ cache.fill_history_gap_once(gap_id.clone()).await.unwrap();171
+172
+ let update =173
+ tokio::time::timeout(Duration::from_secs(2), subscriber.recv()).await.unwrap().unwrap();174
+ let matrix_sdk::event_cache::RoomEventCacheUpdate::UpdateTimelineEvents(update) = update else {175
+ panic!("expected timeline update")176
+ };177
+ let gaps = update.history_gaps.unwrap();178
+ assert_eq!(gaps[0].gap.id, gap_id);179
+ assert_eq!(gaps[0].next_event_id.as_deref(), Some(event_id!("$middle")));180
+ let mut visible: imbl::Vector<_> = snapshot.into();181
+ for diff in update.diffs {182
+ diff.apply(&mut visible);183
+ }184
+ let ids: Vec<_> = visible.iter().filter_map(|event| event.event_id()).collect();185
+ assert_eq!(186
+ ids,187
+ vec![event_id!("$old"), event_id!("$middle"), event_id!("$hidden"), event_id!("$new")]188
+ );189
+}190
+191
+#[async_test]192
+async fn test_segmented_history_keeps_unavailable_intervals_and_independent_cursors() {193
+ let server = MatrixMockServer::new().await;194
+ let client = server.client_builder().build().await;195
+ client.event_cache().subscribe().unwrap();196
+ let room_id = room_id!("!history:example.org");197
+ let room = server.sync_joined_room(&client, room_id).await;198
+ let (cache, _handles) = room.event_cache().await.unwrap();199
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));200
+ cache201
+ .reseed(202
+ vec![f.text_msg("old").event_id(event_id!("$old")).into_event()],203
+ Some("old-edge".into()),204
+ )205
+ .await206
+ .unwrap();207
+ let first = cache208
+ .append_history_section(209
+ vec![f.text_msg("middle").event_id(event_id!("$middle")).into_event()],210
+ Some("first".into()),211
+ )212
+ .await213
+ .unwrap()214
+ .unwrap();215
+ let second = cache216
+ .append_history_section(217
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],218
+ Some("second".into()),219
+ )220
+ .await221
+ .unwrap()222
+ .unwrap();223
+ server224
+ .mock_room_messages()225
+ .match_from("first")226
+ .ok(RoomMessagesResponseTemplate::default().end_token("first"))227
+ .mock_once()228
+ .mount()229
+ .await;230
+ let HistoryGapFillOutcome::Updated { gap, .. } =231
+ cache.fill_history_gap_once(first.id.clone()).await.unwrap()232
+ else {233
+ panic!()234
+ };235
+ assert_eq!(gap.status, HistoryGapStatus::Paused);236
+ server237
+ .mock_room_messages()238
+ .match_from("second")239
+ .ok(RoomMessagesResponseTemplate::default())240
+ .mock_once()241
+ .mount()242
+ .await;243
+ let HistoryGapFillOutcome::Updated { gap, .. } =244
+ cache.fill_history_gap_once(second.id).await.unwrap()245
+ else {246
+ panic!()247
+ };248
+ assert_eq!(gap.status, HistoryGapStatus::Unavailable);249
+ assert_eq!(cache.history_gaps().await.unwrap()[0].status, HistoryGapStatus::Paused);250
+ assert_eq!(cache.events().await.unwrap().len(), 3);251
+}252
+253
+#[async_test]254
+async fn test_segmented_history_failed_commit_keeps_events_and_cursor_retryable() {255
+ let server = MatrixMockServer::new().await;256
+ let store = Arc::new(failing_store::FailingStore::default());257
+ let config = StoreConfig::new(CrossProcessLockConfig::multi_process("history-write-failure"))258
+ .event_cache_store(store.clone());259
+ let client =260
+ server.client_builder().on_builder(|builder| builder.store_config(config)).build().await;261
+ client.event_cache().subscribe().unwrap();262
+ let room_id = room_id!("!history:example.org");263
+ let room = server.sync_joined_room(&client, room_id).await;264
+ let (cache, _handles) = room.event_cache().await.unwrap();265
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));266
+ cache267
+ .reseed(268
+ vec![f.text_msg("old").event_id(event_id!("$old")).into_event()],269
+ Some("old-edge".into()),270
+ )271
+ .await272
+ .unwrap();273
+ store.fail_writes.store(true, Ordering::SeqCst);274
+ assert!(275
+ cache276
+ .append_history_section(277
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],278
+ Some("first".into()),279
+ )280
+ .await281
+ .is_err()282
+ );283
+ assert_eq!(cache.events().await.unwrap().len(), 1);284
+ assert!(cache.history_gaps().await.unwrap().is_empty());285
+ store.fail_writes.store(false, Ordering::SeqCst);286
+ let gap = cache287
+ .append_history_section(288
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],289
+ Some("first".into()),290
+ )291
+ .await292
+ .unwrap()293
+ .unwrap();294
+ server295
+ .mock_room_messages()296
+ .match_from("first")297
+ .ok(RoomMessagesResponseTemplate::default()298
+ .end_token("next")299
+ .events(vec![f.text_msg("inserted").event_id(event_id!("$inserted"))]))300
+ .expect(2)301
+ .mount()302
+ .await;303
+ store.fail_writes.store(true, Ordering::SeqCst);304
+ assert!(cache.fill_history_gap_once(gap.id.clone()).await.is_err());305
+ assert_eq!(cache.events().await.unwrap().len(), 2);306
+ assert_eq!(cache.history_gaps().await.unwrap(), vec![gap.clone()]);307
+ store.fail_writes.store(false, Ordering::SeqCst);308
+ let HistoryGapFillOutcome::Updated { events, .. } =309
+ cache.fill_history_gap_once(gap.id).await.unwrap()310
+ else {311
+ panic!()312
+ };313
+ assert_eq!(events, 1);314
+ assert_eq!(cache.events().await.unwrap().len(), 3);315
+}316
+317
+#[async_test]318
+async fn test_segmented_history_request_failure_preserves_existing_progress() {319
+ let server = MatrixMockServer::new().await;320
+ let client = server.client_builder().build().await;321
+ client.event_cache().subscribe().unwrap();322
+ let room_id = room_id!("!history:example.org");323
+ let room = server.sync_joined_room(&client, room_id).await;324
+ let (cache, _handles) = room.event_cache().await.unwrap();325
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));326
+ cache327
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)328
+ .await329
+ .unwrap();330
+ let gap = cache331
+ .append_history_section(332
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],333
+ Some("first".into()),334
+ )335
+ .await336
+ .unwrap()337
+ .unwrap();338
+ server.mock_room_messages().match_from("first").error_unrecognized().mock_once().mount().await;339
+ assert!(cache.fill_history_gap_once(gap.id.clone()).await.is_err());340
+ assert!(matches!(341
+ cache.history_gaps().await.unwrap()[0].status,342
+ HistoryGapStatus::Failed { .. }343
+ ));344
+ assert_eq!(cache.events().await.unwrap().len(), 2);345
+ server346
+ .mock_room_messages()347
+ .match_from("first")348
+ .ok(RoomMessagesResponseTemplate::default()349
+ .events(vec![f.text_msg("old").event_id(event_id!("$old"))]))350
+ .mock_once()351
+ .mount()352
+ .await;353
+ let HistoryGapFillOutcome::Updated { gap, .. } =354
+ cache.fill_history_gap_once(gap.id).await.unwrap()355
+ else {356
+ panic!()357
+ };358
+ assert_eq!(gap.status, HistoryGapStatus::Joined);359
+}360
+361
+#[async_test]362
+async fn test_segmented_history_cancellation_keeps_the_saved_cursor() {363
+ let server = MatrixMockServer::new().await;364
+ let client = server.client_builder().build().await;365
+ client.event_cache().subscribe().unwrap();366
+ let room_id = room_id!("!history:example.org");367
+ let room = server.sync_joined_room(&client, room_id).await;368
+ let (cache, _handles) = room.event_cache().await.unwrap();369
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));370
+ cache371
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)372
+ .await373
+ .unwrap();374
+ let gap = cache375
+ .append_history_section(376
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],377
+ Some("saved".into()),378
+ )379
+ .await380
+ .unwrap()381
+ .unwrap();382
+ server383
+ .mock_room_messages()384
+ .match_from("saved")385
+ .ok(RoomMessagesResponseTemplate::default()386
+ .end_token("uncommitted")387
+ .with_delay(Duration::from_millis(300)))388
+ .mock_once()389
+ .mount()390
+ .await;391
+ let mut fill = Box::pin(cache.fill_history_gap_once(gap.id.clone()));392
+ tokio::select! {393
+ biased;394
+ result = &mut fill => panic!("request finished before cancellation {result:?}"),395
+ _ = tokio::time::sleep(Duration::from_millis(20)) => {}396
+ }397
+ assert_eq!(398
+ cache.fill_history_gap_once(gap.id.clone()).await.unwrap(),399
+ HistoryGapFillOutcome::Busy400
+ );401
+ drop(fill);402
+ assert_eq!(cache.history_gaps().await.unwrap(), vec![gap.clone()]);403
+ server404
+ .mock_room_messages()405
+ .match_from("saved")406
+ .ok(RoomMessagesResponseTemplate::default()407
+ .events(vec![f.text_msg("old").event_id(event_id!("$old"))]))408
+ .mock_once()409
+ .mount()410
+ .await;411
+ let HistoryGapFillOutcome::Updated { gap, .. } =412
+ cache.fill_history_gap_once(gap.id).await.unwrap()413
+ else {414
+ panic!()415
+ };416
+ assert_eq!(gap.status, HistoryGapStatus::Joined);417
+}418
+419
+#[async_test]420
+async fn test_segmented_history_preserves_sync_arrivals_during_a_fill() {421
+ let server = MatrixMockServer::new().await;422
+ let client = server.client_builder().build().await;423
+ client.event_cache().subscribe().unwrap();424
+ let room_id = room_id!("!history:example.org");425
+ let room = server.sync_joined_room(&client, room_id).await;426
+ let (cache, _handles) = room.event_cache().await.unwrap();427
+ let f = EventFactory::new().room(room_id).sender(user_id!("@alice:example.org"));428
+ cache429
+ .reseed(vec![f.text_msg("old").event_id(event_id!("$old")).into_event()], None)430
+ .await431
+ .unwrap();432
+ let gap = cache433
+ .append_history_section(434
+ vec![f.text_msg("new").event_id(event_id!("$new")).into_event()],435
+ Some("saved".into()),436
+ )437
+ .await438
+ .unwrap()439
+ .unwrap();440
+ let (_, mut updates) = cache.subscribe().await.unwrap();441
+ server442
+ .mock_room_messages()443
+ .match_from("saved")444
+ .ok(RoomMessagesResponseTemplate::default()445
+ .end_token("next")446
+ .events(vec![f.text_msg("middle").event_id(event_id!("$middle"))])447
+ .with_delay(Duration::from_millis(100)))448
+ .mock_once()449
+ .mount()450
+ .await;451
+ let mut fill = Box::pin(cache.fill_history_gap_once(gap.id));452
+ tokio::select! {453
+ biased;454
+ result = &mut fill => panic!("request finished before sync {result:?}"),455
+ _ = tokio::time::sleep(Duration::from_millis(20)) => {}456
+ }457
+ server458
+ .sync_room(459
+ &client,460
+ JoinedRoomBuilder::new(room_id)461
+ .add_timeline_event(f.text_msg("live").event_id(event_id!("$live"))),462
+ )463
+ .await;464
+ tokio::time::timeout(Duration::from_secs(2), updates.recv()).await.unwrap().unwrap();465
+ fill.await.unwrap();466
+ let ids: Vec<_> =467
+ cache.events().await.unwrap().iter().filter_map(|event| event.event_id()).collect();468
+ assert_eq!(469
+ ids,470
+ vec![event_id!("$old"), event_id!("$middle"), event_id!("$new"), event_id!("$live")]471
+ );472
+}