Commit d9b96952ec60
tarball13 files changed+756-927
@@ -1,198 +1,113 @@
1
-# Segmented history implementation2
-3
-The durable implementation is in the macltok `segmented-history` feature4
-worktree. The combined SDK revision5
-`9b2c17fb10703d90b177b02588912a34cecfa11f` is published on its6
-`segmented-history` bookmark. It includes the reaction compatibility fix from7
-`c0a` and segmented history from `49a9`. The macltok history branch includes8
-pack fixes from `e4bb841a` and the `macltok-pack-fixes` worktree through9
-`d1313f47`. All nine SDK pins and both lockfiles use the combined SDK revision.10
-The unified macltok bookmark is named `segmented-history`.11
-12
-## Current behavior13
-14
-Opening a room loads retained history and its gap rows, then fetches a bounded15
-recent window from a fresh server position. The recent fetch is capped at four16
-pages of 50 raw events. It preserves retained sections and their older cursors.17
-If it cannot establish continuity, it installs a durable gap between the18
-sections. It stops when continuity is established or accessible history ends.19
-20
-`HistoryRepairCommand::FillGap` takes a room ID and stable gap transaction ID.21
-It fetches up to four backward pages of 50 events and advances only that gap's22
-saved cursor. Fill progress and events commit atomically. Concurrent history23
-operations for the room return `RoomBusy`. An empty page with an advancing24
-cursor keeps the gap. A repeated cursor pauses it. Accessible history ending25
-before the retained boundary marks the interval unavailable. Only reaching the26
-retained boundary joins sections. Already joined gaps are harmless to stale27
-callers.28
-29
-The cache stores gap identity, boundary evidence, cursor, and status alongside30
-the encrypted linked event chunks. SQLite supports legacy token data and the31
-new metadata. IndexedDB stores the optional encrypted metadata field. Room32
-close and restart retain sections and fill cursors. Timeline snapshots and33
-incremental updates project gaps as `TimelineItem::HistoryGap`. Gap rows retain34
-their identity while status changes. Joined markers disappear and unavailable35
-intervals remain visible.36
-37
-The recent fetch can complete while live sync continues. Cache installation38
-uses a candidate state and commits events, gap metadata, and cursor together.39
-Sync publication is serialized with gap installation. Existing loaded timelines40
-receive incremental insertions that preserve retained item identities and41
-pending sends. Loading history outside the current timeline window still needs42
-a reset.43
-44
-Each SDK room cache owns a coordinator task. Recent callers share an in-flight45
-request. Cancelling one caller preserves the operation for other subscribers,46
-and cancelling all callers stops fetching before installation. Started commits47
-finish before the coordinator accepts conflicting work. No mutex guard crosses48
-the network request. The idle task holds only a weak cache reference, so it does49
-not keep a room cache alive. The SDK UI calls `wait_for_history` as a fence,50
-then takes an atomic event and gap snapshot while replacing its cache51
-subscription. The macltok watcher is stopped before its timeline reset is sent,52
-so old diffs cannot arrive after the reset. A mirror publication acknowledgement53
-completes before the command response is released. The room actor starts recent54
-loading in the background when the room opens, so opening does not wait for the55
-network request.56
-57
-Limited sync results persist explicit gap markers in the event cache. Later sync58
-updates preserve those markers until the pagination chain reaches the retained59
-boundary. The integration uses the room's live timeline flush and watcher reset60
-path. It does not reseed the cache. Thread and pinned timelines remain open61
-through this update path.62
-63
-## Public and wire shapes64
-65
-`ClientHandle::fill_history_gap(room_id, gap_id)` is the convenience method for66
-the targeted fill. It returns `Result<(), Error>`. The underlying command is67
-`HistoryRepairCommand::FillGap { room_id, gap_id }`, sent as68
-`Command::HistoryRepair`. The older policy and staged repair command variants69
-remain available for serde compatibility, but room opening no longer consults70
-the saved policy. `Continue` is invalid because continuation must identify its71
-gap. `GetPolicy` and `SetPolicy` remain compatibility operations and do not72
-control opening.73
-74
-The inline gap lifecycle value `Fetching` is reserved and is not currently75
-emitted by timeline projection. During a targeted fill, loading is signaled by76
-a `HistoryRepairReport` with `gap_id` set and `phase` set to `Bridging`. Recent77
-loading has its own room keyed `RecentHistoryState` with78
-`Idle`, `Loading`, `Loaded`, or `Failed` values. The mirror exposes that state in79
-`recent_history`, separately from gap continuity and repair progress.80
-81
-`TimelineItem::HistoryGap` carries a `HistoryGap` with `id`,82
-`older_boundary`, `newer_boundary`, `lifecycle`, `continuity`, and `failure`.83
-Lifecycle values are `Idle`, `Fetching`, `Paused`, `Failed`, and `Unavailable`.84
-Continuity values are `Unresolved`, `Verified`, and `AccessibleStart`. The gap85
-ID is an owned transaction ID. Boundary values are optional owned event IDs.86
-Failure is an optional displayable string. `TimelineState.items` contains these87
-items in order with event rows. Mirror snapshots serialize the same item shape,88
-and incremental timeline changes carry it through the normal reset or changed89
-diff.90
-91
-Serde enums use externally tagged variants. A command payload is shaped as92
-follows, with the outer `Command` adding its `HistoryRepair` tag.1
+# segmented history2
+3
+a room's history is sections of events with durable gaps between them. the sdk4
+event cache keeps the sections, gap identity, boundaries and each gap's cursor,5
+encrypted at rest, across room close and restart. macltok shows gaps inline and6
+fills them on request.7
+8
+## opening a room9
+10
+opening loads retained history and its gaps, then starts a recent load in the11
+background, so opening never waits on the network. the recent load fetches up12
+to four pages of 50 events from a fresh server position. if it reaches the13
+newest retained event the sections join. if it doesn't, a gap is left between14
+the fresh window and what was retained.15
+16
+`Event::RecentHistoryChanged` reports it, one event per edge.17
+18
+- `Loading` as it starts19
+- `Loaded`, `Cancelled` or `Failed(message)` as it settles20
+- `Idle` when the room closes, whatever was happening21
+22
+`Failed` stays until `HistoryRepairCommand::RetryRecent` or a reopen. a failed23
+load falls back to paginating `EngineConfig::initial_history_limit` events, so24
+the room isn't left empty.25
+26
+## filling a gap27
+28
+`HistoryRepairCommand::FillGap { room_id, gap_id }` walks one gap backwards, up29
+to four pages of 50, each page committing its events and the gap's new cursor30
+together. `Event::HistoryRepairChanged` carries a `HistoryRepairReport`, once31
+with phase `Bridging` as the fill starts and once more as it settles.32
+33
+- `Ready`, continuity `Verified`. the older section was reached and the gap is gone34
+- `Paused` with a stop. `Budget` after four pages, `CursorStalled` when the35
+ server repeats a cursor, `HistoryUnavailable` (continuity `AccessibleStart`)36
+ when accessible history ends first, `Cancelled`37
+- `Paused` with a `failure`, and the gap row goes `Failed`38
+39
+a gap that is already joined or unavailable answers at once and counts no page.40
+41
+## one operation per room42
+43
+a room runs one history operation at a time. `RetryRecent` joins a recent load44
+that is already running. everything else that overlaps, a fill during a recent45
+load, a recent load during a fill, a second fill, answers `RoomBusy`.46
+47
+`HistoryRepairCommand::Cancel` stops whatever is running. its own answer is48
+`Ok`, the cancelled command answers `Interrupted`, and the published outcome is49
+`Cancelled`, never a failure. pages already committed stay.50
+51
+## the timeline52
+53
+gaps appear in `TimelineState.items` as `TimelineItem::HistoryGap`, between the54
+sections they separate. a row keeps its id while its lifecycle changes, and is55
+removed once the gap joins. joined gaps never appear.56
+57
+history lands as `TimelineChanged` diffs, not a `TimelineReset`. the sdk58
+currently replays the whole live timeline when it applies committed history,59
+so those diffs can span the timeline until it gets a non-replacing fence.60
+61
+a command's answer comes after its outcome is published, and after the mirror62
+has applied it when one is attached.63
+64
+## known holes65
+66
+a limited sync for a room with no events in memory records a plain gap instead67
+of a history gap (review 2026-10-25 S1), so an interval can still render as68
+continuous. both `prev_batch` drops in the sdk's `handle_sync` remain.69
+70
+## wire shapes71
+72
+serde enums are externally tagged.9373
9474
```json95
-{96
- "HistoryRepair": {97
- "FillGap": {98
- "room_id": "!room:example.org",99
- "gap_id": "opaque-gap-transaction-id"100
- }101
- }102
-}75
+{ "HistoryRepair": { "FillGap": { "room_id": "!room:example.org", "gap_id": "opaque" } } }76
+{ "HistoryRepair": { "RetryRecent": { "room_id": "!room:example.org" } } }77
+{ "HistoryRepair": { "Cancel": { "room_id": "!room:example.org" } } }10378
```10479
105
-A gap row in `TimelineState.items` has the following shape.80
+a gap row.10681
10782
```json10883
{10984
"HistoryGap": {110
- "id": "opaque-gap-transaction-id",85
+ "id": "opaque",11186
"older_boundary": "$older:example.org",11287
"newer_boundary": "$newer:example.org",11388
"lifecycle": "Idle",114
- "continuity": "Unresolved",11589
"failure": null11690
}11791
}11892
```11993
120
-The mirror snapshot stores recent loading separately by room ID. This fragment121
-shows the serialized `State.recent_history` field.94
+lifecycle is `Idle`, `Paused`, `Failed` or `Unavailable`. `failure` is set with95
+`Failed`.96
+97
+a settled fill.12298
12399
```json124100
{125
- "recent_history": {126
- "!room:example.org": "Loading"127
- }101
+ "room_id": "!room:example.org",102
+ "gap_id": "opaque",103
+ "pages": 2,104
+ "events": 63,105
+ "phase": "Ready",106
+ "continuity": "Verified",107
+ "stop": null,108
+ "failure": null128109
}129110
```130111
131
-The final history repair publication acknowledgement is internal, skipped by132
-serde, and not part of application wire data. The mirror applies the report133
-before acknowledging it, so command completion follows publication.134
-135
-## Malice integration136
-137
-Pin the published macltok `segmented-history` revision in Malice's native and138
-worker manifests and regenerate their lockfiles. The SDK dependency is already139
-pinned by macltok.140
-141
-Malice should remove the saved policy and threshold controls from room opening.142
-Render each inline gap between its neighboring event sections. A fill should143
-preserve the visible event anchor, composer draft, reply or edit state, pending144
-sends, and encryption identity. Keep event-context windows independent from145
-live room history.146
-147
-## Lost interval investigation148
-149
-The reported failure showed retained events followed by newly sent messages,150
-with an omitted interval of roughly eight to ten hours presented as continuous.151
-Older pagination continued before the retained buffer, and a page refresh did152
-not recover the missing range. A refresh can reconnect to the existing153
-SharedWorker, so it does not prove that the engine restarted.154
-155
-The earlier SDK cache path could discard a limited-sync boundary token when a156
-timeline was reported unlimited despite cached events. It could also discard a157
-token when all incoming events were duplicates. Both cases can hide a real158
-discontinuity from older pagination. Initial pagination could also finish159
-against retained history before fresh sync arrived. These are concrete failure160
-paths, but none has been confirmed as the production cause. The durable gaps161
-preserve discontinuities when observed. Startup, subscription, and sync162
-diagnostics are still needed to identify where this particular interval was163
-lost.164
-165
-## Validation commands166
-167
-Run native checks from `../macltok-history-repair` with the repository's Nix168
-toolchain.169
-170
-```sh171
-nix develop --command cargo test --all-targets172
-nix develop --command cargo test --all-targets --features sqlite173
-nix develop --command cargo clippy --all-targets -- -D warnings174
-nix develop --command cargo clippy --all-targets --features sqlite -- -D warnings175
-```176
-177
-For the browser worker runtime, use the web devshell. The harness starts a local178
-homeserver and runs both worker targets in Firefox.179
-180
-```sh181
-nix develop .#web -c nu harness/web/run.nu182
-```183
-184
-The flake checks include native and WebAssembly compilation with IndexedDB and185
-the web worker runtime. The default native gate passed all 86 tests. The186
-aggregate `nix flake check` run stopped during the SQLite and mirror build.187
-The remaining aggregate gates were waived for this handoff and have not passed.188
-The focused native history repair and engine suites passed all 11 tests with189
-SQLite, mirror, and serde enabled against the published combined SDK revision.190
-191
-Seven focused SDK recent-history tests and seven segmented-history tests pass.192
-The SDK WebAssembly compile passes with IndexedDB enabled. SDK UI timeline193
-checks pass with 123 unit tests. Separate SDK room checks pass with seven unit194
-tests and 14 timed integration tests. Two headless Firefox tests pass for the195
-IndexedDB shared gap storage contract with plaintext and encrypted stores. The196
-full SQLite and mirror gate, aggregate WebAssembly gate, and browser worker197
-runtime gate remain unverified. Complete worker restart and encrypted room198
-recovery remain integration acceptance checks.112
+the mirror keeps `State.recent_history`, a room without an entry being idle,113
+and `State.repairs`, the last fill report per room. closing a room drops both.@@ -61,6 +61,7 @@ use crate::{
6161
pub struct EngineConfig {6262
pub command_capacity: usize,6363
pub event_capacity: usize,64
+ /// what a thread paginates as it opens, and a room if its recent load fails. 0 for neither6465
pub initial_history_limit: u16,6566
pub max_media_bytes: usize,6667
pub max_media_sources: usize,@@ -981,30 +982,9 @@ impl Engine {
981982
fn route_repair(&mut self, command: crate::HistoryRepairCommand, reply: Responder) {982983
use crate::HistoryRepairCommand;983984
let room_id = match &command {984
- HistoryRepairCommand::Start { room_id, .. }985
- | HistoryRepairCommand::Continue { room_id, .. }986
- | HistoryRepairCommand::Cancel { room_id }987
- | HistoryRepairCommand::StartRecent { room_id }985
+ HistoryRepairCommand::Cancel { room_id }986
+ | HistoryRepairCommand::RetryRecent { room_id }988987
| HistoryRepairCommand::FillGap { room_id, .. } => room_id,989
- HistoryRepairCommand::GetPolicy | HistoryRepairCommand::SetPolicy(_) => {990
- let mentions = self.mentions.clone();991
- self.tasks992
- .spawn(TaskContext::Command("history repair policy"), async move {993
- let result = match command {994
- HistoryRepairCommand::GetPolicy => mentions995
- .repair_policy()996
- .await997
- .map(crate::CommandOutput::HistoryRepairPolicy),998
- HistoryRepairCommand::SetPolicy(policy) => mentions999
- .set_repair_policy(policy)1000
- .await1001
- .map(|_| crate::CommandOutput::Done),1002
- _ => unreachable!(),1003
- };1004
- reply.send(result);1005
- });1006
- return;1007
- }1008988
};1009989
match self.rooms.get(room_id) {1010990
Some(RoomSlot::Active { sender, .. }) => {@@ -1080,7 +1060,6 @@ impl Engine {
10801060
current_user_id: self.client.user_id().map(ToOwned::to_owned),10811061
initial_history_limit: self.config.initial_history_limit,10821062
supervisor,1083
- mentions: self.mentions.clone(),10841063
},10851064
panics,10861065
opening: vec![reply],@@ -1,4 +1,8 @@
1
-use matrix_sdk::{Room, event_cache::HistoryGapFillOutcome};1
+use matrix_sdk::{2
+ Room,3
+ event_cache::{EventCacheError, HistoryGapFillOutcome, HistoryGapStatus},4
+};5
+use ruma::{OwnedRoomId, OwnedTransactionId};26
use tokio::sync::watch;37
48
use super::{@@ -6,18 +10,19 @@ use super::{
610
timeline::SessionContext,711
};812
use crate::{9
- CommandOutput, Error, ErrorKind, HistoryContinuity, HistoryRepairBudget, HistoryRepairCommand,10
- HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairReport, HistoryRepairStop,11
- HistoryRepairStrategy, RecentHistoryState, transport::Responder,13
+ CommandOutput, Error, ErrorKind, Event, HistoryContinuity, HistoryRepairCommand,14
+ HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState,15
+ transport::Responder,1216
};1317
1418
const MAX_GAP_PAGES: u32 = 4;1519
1620
struct Running {17
- task: JoinHandle<CompletionResult>,21
+ task: JoinHandle<Finished>,1822
cancel: watch::Sender<bool>,1923
responders: Vec<PendingResponder>,20
- recent: bool,24
+ gap_id: Option<OwnedTransactionId>,25
+ closing: bool,2126
}2227
2328
struct PendingResponder {@@ -25,14 +30,33 @@ struct PendingResponder {
2530
cancel_ack: bool,2631
}2732
33
+pub(super) enum Outcome {34
+ Recent(RecentHistoryState),35
+ Fill(HistoryRepairReport),36
+}37
+2838
pub(super) struct Completion {29
- pub(super) report: HistoryRepairReport,30
- pub(super) recent_state: Option<RecentHistoryState>,39
+ pub(super) outcome: Outcome,40
+ /// history went into the cache, so the room timeline has to show it before anyone hears41
+ pub(super) committed: bool,42
+ pub(super) closing: bool,3143
pub(super) result: Result<CommandOutput, Error>,3244
responders: Vec<PendingResponder>,3345
}3446
3547
impl Completion {48
+ /// the room timeline couldn't show what was committed49
+ pub(super) fn unpublished(&mut self, error: Error) {50
+ match &mut self.outcome {51
+ Outcome::Recent(state) => *state = RecentHistoryState::Failed(error.message.clone()),52
+ Outcome::Fill(report) => {53
+ report.phase = HistoryRepairPhase::Paused;54
+ report.failure = Some(error.clone());55
+ }56
+ }57
+ self.result = Err(error);58
+ }59
+3660
pub(super) fn resolve(3761
self,3862
result: Result<CommandOutput, Error>,@@ -48,10 +72,16 @@ impl Completion {
4872
}4973
}5074
51
-struct CompletionResult {52
- report: HistoryRepairReport,53
- recent_state: Option<RecentHistoryState>,54
- result: Result<CommandOutput, Error>,75
+enum Stopped {76
+ Done,77
+ Cancelled,78
+ Failed(Error),79
+}80
+81
+struct Finished {82
+ stopped: Stopped,83
+ committed: bool,84
+ report: Option<HistoryRepairReport>,5585
}5686
5787
pub(super) struct RepairController {@@ -74,8 +104,7 @@ impl RepairController {
74104
}75105
76106
pub(super) async fn start_recent(&mut self) {77
- self.start_recent_with(HistoryRepairPolicy::default(), None, true, Vec::new())78
- .await;107
+ self.start_recent_with(None).await;79108
}80109
81110
pub(super) async fn execute(&mut self, command: HistoryRepairCommand, reply: Responder) {@@ -91,56 +120,17 @@ impl RepairController {
91120
reply.done(Ok(()));92121
}93122
}94
- HistoryRepairCommand::Start { policy, budget, .. } => {95
- if let Err(error) = validate_budget(&budget) {96
- reply.done(Err(error));97
- return;98
- }99
- let policy = policy.unwrap_or_default();100
- self.start_recent_with(policy, Some(reply), true, Vec::new())101
- .await;102
- }103
- HistoryRepairCommand::StartRecent { .. } => {104
- self.start_recent_with(105
- HistoryRepairPolicy::default(),106
- Some(reply),107
- true,108
- Vec::new(),109
- )110
- .await;111
- }123
+ HistoryRepairCommand::RetryRecent { .. } => self.start_recent_with(Some(reply)).await,112124
HistoryRepairCommand::FillGap { gap_id, .. } => {113125
self.start_gap_fill(gap_id, reply).await;114126
}115
- HistoryRepairCommand::Continue { .. } => reply.done(Err(Error::new(116
- ErrorKind::Invalid,117
- "continue requires a history gap ID, use FillGap",118
- ))),119
- HistoryRepairCommand::GetPolicy => {120
- reply.send(121
- self.context122
- .mentions123
- .repair_policy()124
- .await125
- .map(CommandOutput::HistoryRepairPolicy),126
- );127
- }128
- HistoryRepairCommand::SetPolicy(policy) => {129
- reply.done(self.context.mentions.set_repair_policy(policy).await);130
- }131127
}132128
}133129
134
- async fn start_recent_with(135
- &mut self,136
- policy: HistoryRepairPolicy,137
- responder: Option<Responder>,138
- publish_recent: bool,139
- mut responders: Vec<PendingResponder>,140
- ) {130
+ async fn start_recent_with(&mut self, responder: Option<Responder>) {141131
if let Some(running) = &mut self.running {142132
if let Some(responder) = responder {143
- if running.recent {133
+ if running.gap_id.is_none() {144134
running.responders.push(PendingResponder {145135
responder,146136
cancel_ack: false,@@ -151,55 +141,45 @@ impl RepairController {
151141
}152142
return;153143
}154
- if let Some(responder) = responder {155
- responders.push(PendingResponder {156
- responder,157
- cancel_ack: false,158
- });159
- }160
- if publish_recent161
- && self162
- .context163
- .events164
- .send(crate::Event::RecentHistoryChanged {165
- room_id: self.room.room_id().to_owned(),166
- state: RecentHistoryState::Loading,167
- })168
- .await169
- .is_err()170
- {171
- for responder in responders {172
- responder.responder.done(Err(Error::interrupted()));144
+ let event = Event::RecentHistoryChanged {145
+ room_id: self.room.room_id().to_owned(),146
+ state: RecentHistoryState::Loading,147
+ };148
+ if self.context.events.send(event).await.is_err() {149
+ if let Some(responder) = responder {150
+ responder.done(Err(Error::interrupted()));173151
}174152
return;175153
}176154
let (cancel, receiver) = watch::channel(false);177155
let room = self.room.clone();178
- let room_id = self.room.room_id().to_owned();179
- let task = spawn(async move { load_recent(room, room_id, policy, receiver).await });156
+ let task = spawn(async move { load_recent(room, receiver).await });180157
self.running = Some(Running {181158
task,182159
cancel,183
- responders,184
- recent: true,160
+ responders: responder161
+ .map(|responder| PendingResponder {162
+ responder,163
+ cancel_ack: false,164
+ })165
+ .into_iter()166
+ .collect(),167
+ gap_id: None,168
+ closing: false,185169
});186170
}187171
188
- async fn start_gap_fill(&mut self, gap_id: ruma::OwnedTransactionId, responder: Responder) {172
+ async fn start_gap_fill(&mut self, gap_id: OwnedTransactionId, responder: Responder) {189173
if self.is_running() {190174
responder.done(Err(room_busy()));191175
return;192176
}193177
let room = self.room.clone();194
- let room_id = self.room.room_id().to_owned();195
- let mut progress = report(room_id.clone(), HistoryRepairPolicy::default());196
- progress.gap_id = Some(gap_id.clone());197
- progress.strategy = HistoryRepairStrategy::Bridge;198
- progress.phase = HistoryRepairPhase::Bridging;178
+ let progress = report(self.room.room_id().to_owned(), gap_id.clone());199179
if self200180
.context201181
.events202
- .send(crate::Event::HistoryRepairChanged(progress))182
+ .send(Event::HistoryRepairChanged(progress.clone()))203183
.await204184
.is_err()205185
{@@ -207,7 +187,7 @@ impl RepairController {
207187
return;208188
}209189
let (cancel, receiver) = watch::channel(false);210
- let task = spawn(async move { fill_gap(room, room_id, gap_id, receiver).await });190
+ let task = spawn(async move { fill_gap(room, progress, receiver).await });211191
self.running = Some(Running {212192
task,213193
cancel,@@ -215,7 +195,8 @@ impl RepairController {
215195
responder,216196
cancel_ack: false,217197
}],218
- recent: false,198
+ gap_id: Some(gap_id),199
+ closing: false,219200
});220201
}221202
@@ -227,222 +208,174 @@ impl RepairController {
227208
228209
pub(super) async fn shutdown(&mut self) -> Option<Completion> {229210
let running = self.running.as_mut()?;211
+ running.closing = true;230212
running.cancel.send_replace(true);231213
let result = (&mut running.task).await;232214
Some(self.finish_task(result))233215
}234216
235
- fn finish_task(&mut self, result: Result<CompletionResult, JoinError>) -> Completion {217
+ fn finish_task(&mut self, result: Result<Finished, JoinError>) -> Completion {236218
let running = self.running.take().expect("repair task exists");237
- let result = match result {238
- Ok(result) => result,239
- Err(_) => CompletionResult {240
- report: failed_report(241
- self.room.room_id().to_owned(),242
- HistoryRepairPolicy::default(),243
- Error::interrupted(),244
- ),245
- recent_state: running.recent.then(|| {246
- RecentHistoryState::Failed(247
- "recent history operation was interrupted".to_owned(),248
- )249
- }),250
- result: Err(Error::interrupted()),251
- },219
+ let finished = result.unwrap_or(Finished {220
+ stopped: Stopped::Failed(Error::interrupted()),221
+ committed: false,222
+ report: None,223
+ });224
+ let result = match &finished.stopped {225
+ Stopped::Done => Ok(CommandOutput::Done),226
+ Stopped::Cancelled => Err(Error::interrupted()),227
+ Stopped::Failed(error) => Err(error.clone()),228
+ };229
+ let outcome = match running.gap_id {230
+ None => Outcome::Recent(match finished.stopped {231
+ _ if running.closing => RecentHistoryState::Idle,232
+ Stopped::Done => RecentHistoryState::Loaded,233
+ Stopped::Cancelled => RecentHistoryState::Cancelled,234
+ Stopped::Failed(error) => RecentHistoryState::Failed(error.message),235
+ }),236
+ Some(gap_id) => {237
+ let mut report = finished238
+ .report239
+ .unwrap_or_else(|| report(self.room.room_id().to_owned(), gap_id));240
+ match finished.stopped {241
+ Stopped::Done => {}242
+ Stopped::Cancelled => report.stop = Some(HistoryRepairStop::Cancelled),243
+ Stopped::Failed(error) => report.failure = Some(error),244
+ }245
+ report.phase = if report.stop.is_none() && report.failure.is_none() {246
+ HistoryRepairPhase::Ready247
+ } else {248
+ HistoryRepairPhase::Paused249
+ };250
+ Outcome::Fill(report)251
+ }252252
};253253
Completion {254
- report: result.report,255
- recent_state: result.recent_state,256
- result: result.result,254
+ outcome,255
+ committed: finished.committed,256
+ closing: running.closing,257
+ result,257258
responders: running.responders,258259
}259260
}260261
}261262
262
-fn validate_budget(budget: &HistoryRepairBudget) -> Result<(), Error> {263
- if !(1..=100).contains(&budget.max_pages)264
- || !(50..=10_000).contains(&budget.max_events)265
- || !(1..=60_000).contains(&budget.max_duration_ms)266
- {267
- return Err(Error::new(268
- ErrorKind::Invalid,269
- "repair budget requires 1 to 100 pages, 50 to 10000 events, and 1 to 60000 milliseconds",270
- ));271
- }272
- Ok(())273
-}274
-275
-async fn load_recent(276
- room: Room,277
- room_id: ruma::OwnedRoomId,278
- policy: HistoryRepairPolicy,279
- mut cancel: watch::Receiver<bool>,280
-) -> CompletionResult {281
- let base = report(room_id.clone(), policy);263
+async fn load_recent(room: Room, mut cancel: watch::Receiver<bool>) -> Finished {282264
let outcome = tokio::select! {283265
biased;284
- _ = cancel.changed() => Err(Error::interrupted()),266
+ _ = cancel.changed() => None,285267
result = async {286
- let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;287
- cache.load_recent_history().await.map_err(cache_error)288
- } => result,268
+ let (cache, _handles) = room.event_cache().await?;269
+ cache.load_recent_history().await270
+ } => Some(result),289271
};290
- match outcome {291
- Ok(outcome) => {292
- let mut report = base;293
- report.pages = outcome.pages;294
- report.events = outcome.events.min(u32::MAX as usize) as u32;295
- report.recent_available = true;296
- report.phase = HistoryRepairPhase::Ready;297
- match outcome.gap.map(|gap| gap.status) {298
- None => report.continuity = HistoryContinuity::AccessibleStart,299
- Some(matrix_sdk::event_cache::HistoryGapStatus::Joined) => {300
- report.continuity = HistoryContinuity::Verified301
- }302
- Some(matrix_sdk::event_cache::HistoryGapStatus::Unavailable) => {303
- report.continuity = HistoryContinuity::AccessibleStart;304
- report.stop = Some(HistoryRepairStop::HistoryUnavailable);305
- report.phase = HistoryRepairPhase::Paused;306
- }307
- Some(matrix_sdk::event_cache::HistoryGapStatus::Paused) => {308
- report.stop = Some(HistoryRepairStop::CursorStalled);309
- report.phase = HistoryRepairPhase::Paused;310
- }311
- Some(matrix_sdk::event_cache::HistoryGapStatus::Failed { message }) => {312
- let error = Error::new(ErrorKind::Network, message.clone());313
- report.failure = Some(error.clone());314
- report.phase = HistoryRepairPhase::Paused;315
- }316
- Some(matrix_sdk::event_cache::HistoryGapStatus::Idle) => {}317
- }318
- CompletionResult {319
- report: report.clone(),320
- recent_state: Some(RecentHistoryState::Loaded),321
- result: Ok(CommandOutput::HistoryRepair(report)),322
- }323
- }324
- Err(error) => CompletionResult {325
- report: failed_report(room_id, base.requested_policy, error.clone()),326
- recent_state: Some(RecentHistoryState::Failed(error.message.clone())),327
- result: Err(error),328
- },272
+ let stopped = match outcome {273
+ None => Stopped::Cancelled,274
+ Some(Ok(_)) => Stopped::Done,275
+ Some(Err(error)) => Stopped::Failed(cache_error(error)),276
+ };277
+ Finished {278
+ committed: matches!(stopped, Stopped::Done),279
+ stopped,280
+ report: None,329281
}330282
}331283
332284
async fn fill_gap(333285
room: Room,334
- room_id: ruma::OwnedRoomId,335
- gap_id: ruma::OwnedTransactionId,286
+ mut report: HistoryRepairReport,336287
mut cancel: watch::Receiver<bool>,337
-) -> CompletionResult {338
- let mut report = report(room_id, HistoryRepairPolicy::default());339
- report.gap_id = Some(gap_id.clone());340
- report.strategy = HistoryRepairStrategy::Bridge;341
- let outcome = async {288
+) -> Finished {289
+ let mut committed = false;290
+ let stopped = async {342291
let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;343
- let mut last_gap = None;344292
for _ in 0..MAX_GAP_PAGES {345293
let next = tokio::select! {346294
biased;347
- _ = cancel.changed() => return Err(Error::interrupted()),348
- result = cache.fill_history_gap_once(gap_id.clone()) => result.map_err(cache_error)?,295
+ _ = cancel.changed() => return Ok(Stopped::Cancelled),296
+ result = cache.fill_history_gap_once(report.gap_id.clone()) => {297
+ result.map_err(cache_error)?298
+ }349299
};350
- match next {300
+ let (gap, events) = match next {351301
HistoryGapFillOutcome::NotFound => {352
- return Err(Error::new(ErrorKind::EventNotFound, "history gap was not found"));302
+ return Err(Error::new(303
+ ErrorKind::EventNotFound,304
+ "history gap was not found",305
+ ));353306
}354307
HistoryGapFillOutcome::Busy => return Err(room_busy()),355
- HistoryGapFillOutcome::Updated { gap, events } => {356
- report.pages += 1;357
- report.events = report.events.saturating_add(events.min(u32::MAX as usize) as u32);358
- match gap.status.clone() {359
- matrix_sdk::event_cache::HistoryGapStatus::Joined => {360
- report.continuity = HistoryContinuity::Verified;361
- last_gap = Some(gap);362
- break;363
- }364
- matrix_sdk::event_cache::HistoryGapStatus::Unavailable => {365
- report.continuity = HistoryContinuity::AccessibleStart;366
- report.stop = Some(HistoryRepairStop::HistoryUnavailable);367
- last_gap = Some(gap);368
- break;369
- }370
- matrix_sdk::event_cache::HistoryGapStatus::Paused => {371
- report.stop = Some(HistoryRepairStop::CursorStalled);372
- last_gap = Some(gap);373
- break;374
- }375
- matrix_sdk::event_cache::HistoryGapStatus::Failed { message } => {376
- return Err(Error::new(ErrorKind::Network, message));377
- }378
- matrix_sdk::event_cache::HistoryGapStatus::Idle => last_gap = Some(gap),379
- }308
+ HistoryGapFillOutcome::Updated { gap, events } => (gap, events),309
+ };310
+ // the sdk answers an already joined or unavailable gap without asking the server.311
+ // an empty first page that settles the gap looks the same and goes uncounted too312
+ let settled_before = report.pages == 0313
+ && events == 0314
+ && matches!(315
+ gap.status,316
+ HistoryGapStatus::Joined | HistoryGapStatus::Unavailable317
+ );318
+ if !settled_before {319
+ report.pages += 1;320
+ committed = true;321
+ }322
+ report.events = report323
+ .events324
+ .saturating_add(u32::try_from(events).unwrap_or(u32::MAX));325
+ match gap.status {326
+ HistoryGapStatus::Joined => {327
+ report.continuity = HistoryContinuity::Verified;328
+ return Ok(Stopped::Done);380329
}330
+ HistoryGapStatus::Unavailable => {331
+ report.continuity = HistoryContinuity::AccessibleStart;332
+ report.stop = Some(HistoryRepairStop::HistoryUnavailable);333
+ return Ok(Stopped::Done);334
+ }335
+ HistoryGapStatus::Paused => {336
+ report.stop = Some(HistoryRepairStop::CursorStalled);337
+ return Ok(Stopped::Done);338
+ }339
+ HistoryGapStatus::Failed { message } => {340
+ return Err(Error::new(ErrorKind::Network, message));341
+ }342
+ HistoryGapStatus::Idle => {}381343
}382344
}383
- if report.pages == MAX_GAP_PAGES384
- && report.continuity == HistoryContinuity::Unresolved385
- && report.stop.is_none()386
- {387
- report.stop = Some(HistoryRepairStop::Budget);388
- }389
- Ok::<_, Error>(last_gap)345
+ report.stop = Some(HistoryRepairStop::Budget);346
+ Ok(Stopped::Done)390347
}391
- .await;392
- let command_result = match outcome {393
- Ok(_) => {394
- report.recent_available = true;395
- report.phase = if report.stop.is_some() {396
- HistoryRepairPhase::Paused397
- } else {398
- HistoryRepairPhase::Ready399
- };400
- Ok(CommandOutput::Done)401
- }402
- Err(error) => {403
- report.failure = Some(error.clone());404
- report.phase = HistoryRepairPhase::Paused;405
- Err(error)406
- }407
- };408
- CompletionResult {409
- report,410
- recent_state: None,411
- result: command_result,348
+ .await349
+ .unwrap_or_else(Stopped::Failed);350
+ Finished {351
+ stopped,352
+ committed,353
+ report: Some(report),412354
}413355
}414356
415
-fn report(room_id: ruma::OwnedRoomId, policy: HistoryRepairPolicy) -> HistoryRepairReport {357
+fn report(room_id: OwnedRoomId, gap_id: OwnedTransactionId) -> HistoryRepairReport {416358
HistoryRepairReport {417359
room_id,418
- gap_id: None,419
- requested_policy: policy,420
- strategy: HistoryRepairStrategy::Recent,421
- coverage_duration_ms: None,422
- threshold_evaluated: false,360
+ gap_id,423361
pages: 0,424362
events: 0,425
- phase: HistoryRepairPhase::FetchingRecent,363
+ phase: HistoryRepairPhase::Bridging,426364
continuity: HistoryContinuity::Unresolved,427
- recent_available: false,428365
stop: None,429366
failure: None,430367
}431368
}432369
433
-fn failed_report(434
- room_id: ruma::OwnedRoomId,435
- policy: HistoryRepairPolicy,436
- error: Error,437
-) -> HistoryRepairReport {438
- let mut report = report(room_id, policy);439
- report.phase = HistoryRepairPhase::Paused;440
- report.failure = Some(error);441
- report442
-}443
-444
-fn cache_error(source: matrix_sdk::event_cache::EventCacheError) -> Error {445
- Error::with_source(ErrorKind::Store, "room history operation failed", source)370
+fn cache_error(source: EventCacheError) -> Error {371
+ let kind = match &source {372
+ EventCacheError::PaginationError(error) => crate::error::classify_sdk_error(error),373
+ EventCacheError::InitialPaginationError(_) => ErrorKind::Network,374
+ EventCacheError::RoomNotFound { .. } => ErrorKind::RoomUnknown,375
+ EventCacheError::ClientDropped => ErrorKind::Interrupted,376
+ _ => ErrorKind::Store,377
+ };378
+ Error::with_source(kind, "room history operation failed", source)446379
}447380
448381
fn room_busy() -> Error {@@ -6,7 +6,7 @@ use ruma::OwnedRoomId;
66
use tokio::sync::mpsc;77
88
use super::executor::{AbortHandle, JoinError, TaskSet, is_panic, resume_unwind};9
-use super::repair::{Completion, RepairController};9
+use super::repair::{Completion, Outcome, RepairController};1010
use super::timeline::{SessionContext, TaskGuard, TimelineSession};1111
use crate::{1212
Error, ErrorKind, Event, HistoryRepairCommand, RecentHistoryState, TimelineCommand, TimelineId,@@ -109,8 +109,23 @@ impl RoomActor {
109109
reply.done(Ok(()));110110
}111111
let close = self.serve_open(&mut timelines, &mut repair).await;112
- if let Some(completion) = repair.shutdown().await {113
- self.finish_repair(completion, &mut timelines).await;112
+ let recent_settled = match repair.shutdown().await {113
+ Some(completion) => {114
+ let recent = matches!(completion.outcome, Outcome::Recent(_));115
+ self.finish_repair(completion, &mut timelines).await;116
+ recent117
+ }118
+ None => false,119
+ };120
+ if !recent_settled {121
+ let _ = self122
+ .context123
+ .events124
+ .send(Event::RecentHistoryChanged {125
+ room_id: self.room_id.clone(),126
+ state: RecentHistoryState::Idle,127
+ })128
+ .await;114129
}115130
if close.is_none() {116131
self.requests.close();@@ -141,42 +156,36 @@ impl RoomActor {
141156
}142157
143158
async fn finish_repair(&self, mut completion: Completion, timelines: &mut Timelines) {144
- let publication = timelines.room.publish_history().await;145
- let mut cancellation = publication.clone();146
- if let Err(error) = publication {147
- completion.report.failure = Some(error.clone());148
- completion.report.phase = crate::HistoryRepairPhase::Paused;149
- completion.result = Err(error.clone());150
- if completion.recent_state.is_some() {151
- completion.recent_state = Some(RecentHistoryState::Failed(error.message));152
- }159
+ let mut cancellation = Ok(());160
+ if completion.committed161
+ && !completion.closing162
+ && completion.result.is_ok()163
+ && let Err(error) = timelines.room.publish_history().await164
+ {165
+ completion.unpublished(error.clone());166
+ cancellation = Err(error);153167
}168
+ let event = match &completion.outcome {169
+ Outcome::Recent(state) => Event::RecentHistoryChanged {170
+ room_id: self.room_id.clone(),171
+ state: state.clone(),172
+ },173
+ Outcome::Fill(report) => Event::HistoryRepairChanged(report.clone()),174
+ };154175
let mut resolved = completion.result.clone();155
- if let Some(state) = completion.recent_state.clone() {156
- if self157
- .context158
- .events159
- .send(Event::RecentHistoryChanged {160
- room_id: self.room_id.clone(),161
- state,162
- })163
- .await164
- .is_err()165
- {166
- resolved = Err(Error::interrupted());167
- cancellation = Err(Error::interrupted());168
- }169
- }170
- if self171
- .context172
- .events173
- .publish_repair(completion.report.clone())174
- .await175
- .is_err()176
- {176
+ let events = &self.context.events;177
+ if events.send(event).await.is_err() || events.publish_barrier().await.is_err() {177178
resolved = Err(Error::interrupted());178179
cancellation = Err(Error::interrupted());179180
}181
+ let limit = self.context.initial_history_limit;182
+ if matches!(183
+ completion.outcome,184
+ Outcome::Recent(RecentHistoryState::Failed(_))185
+ ) && limit > 0186
+ {187
+ timelines.room.paginate(limit, None);188
+ }180189
completion.resolve(resolved, cancellation);181190
}182191
@@ -14,7 +14,7 @@ use matrix_sdk::{
1414
};1515
use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};1616
use ruma::{OwnedEventId, OwnedUserId};17
-use tokio::sync::mpsc;17
+use tokio::sync::{mpsc, oneshot};1818
1919
use super::executor::{EngineBoxFuture, JoinHandle, TaskSet, spawn};2020
use crate::{@@ -66,7 +66,6 @@ impl Drop for TaskGuard {
6666
pub(super) struct SessionContext {6767
pub(super) events: EventSender,6868
pub(super) media: Arc<MediaService>,69
- pub(super) mentions: crate::mentions::service::Handle,7069
pub(super) current_user_id: Option<OwnedUserId>,7170
pub(super) initial_history_limit: u16,7271
pub(super) supervisor: Supervisor,@@ -78,6 +77,7 @@ pub(super) struct TimelineSession {
7877
context: SessionContext,7978
scope: MediaScope,8079
watcher: JoinHandle<()>,80
+ fences: mpsc::UnboundedSender<Fence>,8181
pagination: mpsc::Sender<PaginationRequest>,8282
pagination_worker: JoinHandle<()>,8383
member_fetch: JoinHandle<()>,@@ -96,7 +96,7 @@ impl TimelineSession {
9696
) -> Result<Self, SendEventError> {9797
let timeline = Arc::new(timeline);9898
let scope = MediaScope::new();99
- let watcher = start_watcher(&id, &timeline, &context, &scope).await?;99
+ let (watcher, fences) = start_watcher(&id, &timeline, &context, &scope).await?;100100
let (pagination, pagination_worker) = start_pagination_worker(&id, &timeline, &context);101101
// under lazy-loaded members the sdk leaves sender profiles unavailable until someone102102
// asks, and a /members fetched any other way never reaches the timeline's items@@ -109,6 +109,7 @@ impl TimelineSession {
109109
timeline,110110
scope,111111
watcher,112
+ fences,112113
pagination,113114
pagination_worker,114115
member_fetch,@@ -125,23 +126,22 @@ impl TimelineSession {
125126
pub(super) async fn resync(&mut self) -> Result<(), Error> {126127
self.watcher.abort();127128
let _ = (&mut self.watcher).await;128
- self.watcher = start_watcher(&self.id, &self.timeline, &self.context, &self.scope)129
- .await130
- .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;129
+ (self.watcher, self.fences) =130
+ start_watcher(&self.id, &self.timeline, &self.context, &self.scope)131
+ .await132
+ .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;131133
Ok(())132134
}133135
134
- pub(super) async fn publish_history(&mut self) -> Result<(), Error> {136
+ /// returns once the watcher has passed on the history the room cache committed137
+ pub(super) async fn publish_history(&self) -> Result<(), Error> {135138
self.timeline136139
.flush_history()137140
.await138141
.map_err(timeline_error)?;139
- self.resync().await?;140
- self.context141
- .events142
- .publish_barrier()143
- .await144
- .map_err(|_| Error::interrupted())142
+ let (fence, drained) = oneshot::channel();143
+ self.fences.send(fence).map_err(|_| Error::interrupted())?;144
+ drained.await.map_err(|_| Error::interrupted())145145
}146146
147147
pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {@@ -404,15 +404,19 @@ impl ReplyFetcher {
404404
}405405
}406406
407
+/// asks the watcher to pass on every diff the timeline already holds, then answers408
+type Fence = oneshot::Sender<()>;409
+407410
async fn start_watcher(408411
id: &TimelineId,409412
timeline: &Arc<Timeline>,410413
context: &SessionContext,411414
scope: &MediaScope,412
-) -> Result<JoinHandle<()>, SendEventError> {415
+) -> Result<(JoinHandle<()>, mpsc::UnboundedSender<Fence>), SendEventError> {413416
let (items, mut stream) = timeline.subscribe().await;414417
let current_user_id = context.current_user_id.clone();415
- let normalised = normalise_timeline(items.iter(), current_user_id.as_deref(), scope);418
+ let (normalised, mut hidden) =419
+ normalise_timeline(items.iter(), current_user_id.as_deref(), scope);416420
context.media.register(normalised.media).await;417421
context418422
.events@@ -427,31 +431,56 @@ async fn start_watcher(
427431
fetches: TaskSet::new(),428432
};429433
replies.fetch(normalised.missing_replies);434
+ let (fences, mut fence_requests) = mpsc::unbounded_channel::<Fence>();430435
let id = id.clone();431436
let events = context.events.clone();432437
let media = context.media.clone();433438
let scope = scope.clone();434
- Ok(context.supervisor.spawn("timeline watcher", async move {439
+ let task = context.supervisor.spawn("timeline watcher", async move {435440
loop {436
- let diffs = tokio::select! {441
+ let (diffs, fence) = tokio::select! {437442
diffs = stream.next() => match diffs {438
- Some(diffs) => diffs,443
+ Some(diffs) => (vec![diffs], None),439444
None => return,440445
},446
+ Some(fence) = fence_requests.recv() => {447
+ let mut ready = Vec::new();448
+ while let Some(diffs) = stream.next().now_or_never() {449
+ match diffs {450
+ Some(diffs) => ready.push(diffs),451
+ None => return,452
+ }453
+ }454
+ (ready, Some(fence))455
+ }441456
Some(_) = replies.fetches.join_next(), if !replies.fetches.is_empty() => continue,442457
};443
- let normalised = normalise_timeline_diffs(diffs, current_user_id.as_deref(), &scope);444
- media.register(normalised.media).await;445
- replies.fetch(normalised.missing_replies);446
- let event = Event::TimelineChanged {447
- timeline_id: id.clone(),448
- diffs: normalised.diffs,449
- };450
- if events.send(event).await.is_err() {451
- return;458
+ for diffs in diffs {459
+ let normalised = normalise_timeline_diffs(460
+ diffs,461
+ current_user_id.as_deref(),462
+ &scope,463
+ &mut hidden,464
+ );465
+ media.register(normalised.media).await;466
+ replies.fetch(normalised.missing_replies);467
+ if normalised.diffs.is_empty() {468
+ continue;469
+ }470
+ let event = Event::TimelineChanged {471
+ timeline_id: id.clone(),472
+ diffs: normalised.diffs,473
+ };474
+ if events.send(event).await.is_err() {475
+ return;476
+ }477
+ }478
+ if let Some(fence) = fence {479
+ let _ = fence.send(());452480
}453481
}454
- }))482
+ });483
+ Ok((task, fences))455484
}456485
457486
async fn run_pagination(@@ -74,9 +74,8 @@ pub use model::{
7474
VerificationAction, VerificationFailure, VerificationState, VerificationTarget,7575
};7676
pub use repair_model::{77
- HistoryContinuity, HistoryGap, HistoryGapLifecycle, HistoryRepairBudget, HistoryRepairCommand,78
- HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairReport, HistoryRepairStop,79
- HistoryRepairStrategy,77
+ HistoryContinuity, HistoryGap, HistoryGapLifecycle, HistoryRepairCommand, HistoryRepairPhase,78
+ HistoryRepairReport, HistoryRepairStop,8079
};8180
pub use ruma;8281
#[doc(hidden)]@@ -50,7 +50,7 @@ pub struct State {
5050
pub rooms: Vector<RoomSummary>,5151
pub image_packs: ImagePacks,5252
pub timelines: imbl::HashMap<TimelineId, TimelineState>,53
- #[cfg_attr(feature = "serde", serde(default))]53
+ /// a room without an entry is idle. closing a room drops its entries here and in `repairs`5454
pub recent_history: imbl::HashMap<OwnedRoomId, RecentHistoryState>,5555
pub repairs: imbl::HashMap<OwnedRoomId, HistoryRepairReport>,5656
pub typing: imbl::HashMap<OwnedRoomId, Vec<OwnedUserId>>,@@ -375,7 +375,11 @@ impl Mirror {
375375
room_id,376376
state: recent,377377
} => self.publish(|state| {378
- state.recent_history.insert(room_id.clone(), recent.clone());378
+ if recent == RecentHistoryState::Idle {379
+ state.recent_history.remove(&room_id);380
+ } else {381
+ state.recent_history.insert(room_id.clone(), recent.clone());382
+ }379383
Some(StateChange::RecentHistory {380384
room_id,381385
state: recent,@@ -385,18 +389,6 @@ impl Mirror {
385389
state.repairs.insert(report.room_id.clone(), report.clone());386390
Some(StateChange::HistoryRepair(report))387391
}),388
- Event::HistoryRepairPublished {389
- report,390
- acknowledgement,391
- } => {392
- self.publish(|state| {393
- state.repairs.insert(report.room_id.clone(), report.clone());394
- Some(StateChange::HistoryRepair(report))395
- });396
- if let Some(acknowledgement) = acknowledgement {397
- acknowledgement.complete(true);398
- }399
- }400392
Event::PublicationBarrier { acknowledgement } => {401393
if let Some(acknowledgement) = acknowledgement {402394
acknowledgement.complete(true);@@ -406,6 +398,9 @@ impl Mirror {
406398
self.cancel_resync(&timeline_id);407399
self.publish(|state| {408400
state.timelines.remove(&timeline_id)?;401
+ if let TimelineId::Room(room_id) = &timeline_id {402
+ state.repairs.remove(room_id);403
+ }409404
Some(StateChange::Timeline {410405
timeline_id,411406
change: TimelineChange::Closed,@@ -672,8 +672,6 @@ pub enum CommandOutput {
672672
EventContextPage(EventContextPage),673673
MentionsPage(crate::MentionsPage),674674
MentionContext(crate::SavedMentionContext),675
- HistoryRepair(crate::HistoryRepairReport),676
- HistoryRepairPolicy(crate::HistoryRepairPolicy),677675
}678676
679677
#[derive(Clone, Debug, Eq, PartialEq)]@@ -916,12 +914,6 @@ pub enum Event {
916914
},917915
HistoryRepairChanged(crate::HistoryRepairReport),918916
#[doc(hidden)]919
- HistoryRepairPublished {920
- report: crate::HistoryRepairReport,921
- #[cfg_attr(feature = "serde", serde(skip))]922
- acknowledgement: Option<crate::PublicationAck>,923
- },924
- #[doc(hidden)]925917
PublicationBarrier {926918
#[cfg_attr(feature = "serde", serde(skip))]927919
acknowledgement: Option<crate::PublicationAck>,@@ -2125,9 +2117,12 @@ pub enum HistoryState {
21252117
#[derive(Clone, Debug, Eq, PartialEq)]21262118
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]21272119
pub enum RecentHistoryState {2120
+ /// nothing running and nothing to show, which is also what closing the room leaves21282121
Idle,21292122
Loading,21302123
Loaded,2124
+ Cancelled,2125
+ /// stays until a retry or the room is reopened21312126
Failed(String),21322127
}21332128
@@ -19,10 +19,9 @@ use ruma::{
1919
};2020
2121
use crate::{22
- Change, HistoryContinuity, HistoryGap, HistoryGapLifecycle, MediaId, MembershipChange, Message,23
- MessageKind, MessageMentions, Reaction, ReplyDetails, ReplyReference, SendState, Sender,24
- ThreadSummary, TimelineContent, TimelineDiff, TimelineEvent, TimelineItem, TimelineItemId,25
- UtdCause,22
+ Change, HistoryGap, HistoryGapLifecycle, MediaId, MembershipChange, Message, MessageKind,23
+ MessageMentions, Reaction, ReplyDetails, ReplyReference, SendState, Sender, ThreadSummary,24
+ TimelineContent, TimelineDiff, TimelineEvent, TimelineItem, TimelineItemId, UtdCause,2625
};2726
2827
use super::link_preview::sender_previews;@@ -53,10 +52,16 @@ struct Normaliser<'a> {
5352
}5453
5554
impl Normaliser<'_> {56
- fn item(&mut self, item: &SdkTimelineItem) -> TimelineItem {55
+ /// `None` for rows macltok leaves out56
+ fn item(&mut self, item: &SdkTimelineItem) -> Option<TimelineItem> {57
+ if let TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(gap)) = item.kind()58
+ && gap.status == matrix_sdk::event_cache::HistoryGapStatus::Joined59
+ {60
+ return None;61
+ }5762
self.media5863
.extend(media_registrations_for_item(item, self.scope));59
- match item.kind() {64
+ Some(match item.kind() {6065
TimelineItemKind::Event(event) => {6166
TimelineItem::Event(Box::new(self.event(item, event)))6267
}@@ -70,41 +75,26 @@ impl Normaliser<'_> {
7075
TimelineItem::TimelineStart7176
}7277
TimelineItemKind::Virtual(VirtualTimelineItem::HistoryGap(gap)) => {73
- let (lifecycle, continuity, failure) = match &gap.status {74
- matrix_sdk::event_cache::HistoryGapStatus::Idle => (75
- HistoryGapLifecycle::Idle,76
- HistoryContinuity::Unresolved,77
- None,78
- ),79
- matrix_sdk::event_cache::HistoryGapStatus::Paused => (80
- HistoryGapLifecycle::Paused,81
- HistoryContinuity::Unresolved,82
- None,83
- ),84
- matrix_sdk::event_cache::HistoryGapStatus::Failed { message } => (85
- HistoryGapLifecycle::Failed,86
- HistoryContinuity::Unresolved,87
- Some(message.clone()),88
- ),89
- matrix_sdk::event_cache::HistoryGapStatus::Unavailable => (90
- HistoryGapLifecycle::Unavailable,91
- HistoryContinuity::AccessibleStart,92
- None,93
- ),94
- matrix_sdk::event_cache::HistoryGapStatus::Joined => {95
- (HistoryGapLifecycle::Idle, HistoryContinuity::Verified, None)78
+ use matrix_sdk::event_cache::HistoryGapStatus;79
+ let (lifecycle, failure) = match &gap.status {80
+ HistoryGapStatus::Idle | HistoryGapStatus::Joined => {81
+ (HistoryGapLifecycle::Idle, None)82
+ }83
+ HistoryGapStatus::Paused => (HistoryGapLifecycle::Paused, None),84
+ HistoryGapStatus::Failed { message } => {85
+ (HistoryGapLifecycle::Failed, Some(message.clone()))9686
}87
+ HistoryGapStatus::Unavailable => (HistoryGapLifecycle::Unavailable, None),9788
};9889
TimelineItem::HistoryGap(HistoryGap {9990
id: gap.id.clone(),10091
older_boundary: gap.older_boundary.clone(),10192
newer_boundary: gap.newer_boundary.clone(),10293
lifecycle,103
- continuity,10494
failure,10595
})10696
}107
- }97
+ })10898
}10999
110100
fn event(&mut self, item: &SdkTimelineItem, event: &EventTimelineItem) -> TimelineEvent {@@ -163,25 +153,28 @@ pub(crate) fn normalise_timeline<'a>(
163153
items: impl Iterator<Item = &'a Arc<SdkTimelineItem>>,164154
current_user_id: Option<&UserId>,165155
scope: &MediaScope,166
-) -> NormalisedTimeline {156
+) -> (NormalisedTimeline, HiddenRows) {167157
let mut normaliser = Normaliser {168158
current_user_id,169159
scope,170160
media: Vec::new(),171161
missing_replies: Vec::new(),172162
};173
- let items = items.map(|item| normaliser.item(item)).collect();174
- NormalisedTimeline {175
- items,163
+ let mut hidden = HiddenRows::default();164
+ let items = hidden.reset(items.map(|item| normaliser.item(item)));165
+ let normalised = NormalisedTimeline {166
+ items: items.into_iter().collect(),176167
media: normaliser.media,177168
missing_replies: normaliser.missing_replies,178
- }169
+ };170
+ (normalised, hidden)179171
}180172
181173
pub(crate) fn normalise_timeline_diffs(182174
diffs: Vec<VectorDiff<Arc<SdkTimelineItem>>>,183175
current_user_id: Option<&UserId>,184176
scope: &MediaScope,177
+ hidden: &mut HiddenRows,185178
) -> NormalisedTimelineDiffs {186179
let mut normaliser = Normaliser {187180
current_user_id,@@ -191,23 +184,7 @@ pub(crate) fn normalise_timeline_diffs(
191184
};192185
let diffs = diffs193186
.into_iter()194
- .map(|diff| match diff.map(|item| normaliser.item(&item)) {195
- VectorDiff::Append { values } => TimelineDiff::Append {196
- values: values.into_iter().collect(),197
- },198
- VectorDiff::Clear => TimelineDiff::Clear,199
- VectorDiff::PushFront { value } => TimelineDiff::PushFront { value },200
- VectorDiff::PushBack { value } => TimelineDiff::PushBack { value },201
- VectorDiff::PopFront => TimelineDiff::PopFront,202
- VectorDiff::PopBack => TimelineDiff::PopBack,203
- VectorDiff::Insert { index, value } => TimelineDiff::Insert { index, value },204
- VectorDiff::Set { index, value } => TimelineDiff::Set { index, value },205
- VectorDiff::Remove { index } => TimelineDiff::Remove { index },206
- VectorDiff::Truncate { length } => TimelineDiff::Truncate { length },207
- VectorDiff::Reset { values } => TimelineDiff::Reset {208
- values: values.into_iter().collect(),209
- },210
- })187
+ .filter_map(|diff| hidden.translate(diff.map(|item| normaliser.item(&item))))211188
.collect();212189
NormalisedTimelineDiffs {213190
diffs,@@ -216,6 +193,135 @@ pub(crate) fn normalise_timeline_diffs(
216193
}217194
}218195
196
+/// which sdk rows were left out, so diff indices can step over them197
+#[derive(Default)]198
+pub(crate) struct HiddenRows {199
+ rows: imbl::Vector<bool>,200
+ hidden: usize,201
+}202
+203
+impl HiddenRows {204
+ fn visible_index(&self, index: usize) -> usize {205
+ if self.hidden == 0 {206
+ return index;207
+ }208
+ self.rows209
+ .iter()210
+ .take(index)211
+ .filter(|hidden| !**hidden)212
+ .count()213
+ }214
+215
+ fn push(&mut self, front: bool, item: &Option<TimelineItem>) {216
+ self.hidden += usize::from(item.is_none());217
+ if front {218
+ self.rows.push_front(item.is_none());219
+ } else {220
+ self.rows.push_back(item.is_none());221
+ }222
+ }223
+224
+ fn forget(&mut self, hidden: bool) -> bool {225
+ self.hidden -= usize::from(hidden);226
+ !hidden227
+ }228
+229
+ fn reset(&mut self, items: impl Iterator<Item = Option<TimelineItem>>) -> Vec<TimelineItem> {230
+ *self = Self::default();231
+ items232
+ .filter_map(|item| {233
+ self.push(false, &item);234
+ item235
+ })236
+ .collect()237
+ }238
+239
+ fn translate(&mut self, diff: VectorDiff<Option<TimelineItem>>) -> Option<TimelineDiff> {240
+ Some(match diff {241
+ VectorDiff::Append { values } => {242
+ let values: Vec<_> = values243
+ .into_iter()244
+ .filter_map(|item| {245
+ self.push(false, &item);246
+ item247
+ })248
+ .collect();249
+ if values.is_empty() {250
+ return None;251
+ }252
+ TimelineDiff::Append { values }253
+ }254
+ VectorDiff::Clear => {255
+ *self = Self::default();256
+ TimelineDiff::Clear257
+ }258
+ VectorDiff::PushFront { value } => {259
+ self.push(true, &value);260
+ TimelineDiff::PushFront { value: value? }261
+ }262
+ VectorDiff::PushBack { value } => {263
+ self.push(false, &value);264
+ TimelineDiff::PushBack { value: value? }265
+ }266
+ VectorDiff::PopFront => {267
+ let hidden = self.rows.pop_front()?;268
+ self.forget(hidden).then_some(TimelineDiff::PopFront)?269
+ }270
+ VectorDiff::PopBack => {271
+ let hidden = self.rows.pop_back()?;272
+ self.forget(hidden).then_some(TimelineDiff::PopBack)?273
+ }274
+ VectorDiff::Insert { index, value } => {275
+ let visible = self.visible_index(index);276
+ self.hidden += usize::from(value.is_none());277
+ self.rows.insert(index, value.is_none());278
+ TimelineDiff::Insert {279
+ index: visible,280
+ value: value?,281
+ }282
+ }283
+ VectorDiff::Set { index, value } => {284
+ let visible = self.visible_index(index);285
+ let was_hidden = self.rows.set(index, value.is_none());286
+ self.hidden = self.hidden + usize::from(value.is_none()) - usize::from(was_hidden);287
+ match (was_hidden, value) {288
+ (false, Some(value)) => TimelineDiff::Set {289
+ index: visible,290
+ value,291
+ },292
+ (false, None) => TimelineDiff::Remove { index: visible },293
+ (true, Some(value)) => TimelineDiff::Insert {294
+ index: visible,295
+ value,296
+ },297
+ (true, None) => return None,298
+ }299
+ }300
+ VectorDiff::Remove { index } => {301
+ let visible = self.visible_index(index);302
+ let hidden = self.rows.remove(index);303
+ self.forget(hidden)304
+ .then_some(TimelineDiff::Remove { index: visible })?305
+ }306
+ VectorDiff::Truncate { length } => {307
+ let visible = self.visible_index(length);308
+ let dropped = self309
+ .rows310
+ .iter()311
+ .skip(length)312
+ .filter(|hidden| **hidden)313
+ .count();314
+ self.hidden -= dropped;315
+ self.rows.truncate(length);316
+ TimelineDiff::Truncate { length: visible }317
+ }318
+ VectorDiff::Reset { values } => TimelineDiff::Reset {319
+ values: self.reset(values.into_iter()),320
+ },321
+ })322
+ }323
+}324
+219325
/// `media_identity` is `None` for embedded reply content, which never gets fetchable media of its own220326
fn content(content: &TimelineItemContent, media_identity: Option<&str>) -> TimelineContent {221327
match content {@@ -485,3 +591,52 @@ pub(super) fn formatted_body(message: &MessageType) -> Option<String> {
485591
}?;486592
(formatted.format == MessageFormat::Html).then(|| formatted.body.clone())487593
}594
+595
+#[cfg(test)]596
+mod tests {597
+ use super::*;598
+599
+ fn row(n: u64) -> Option<TimelineItem> {600
+ Some(TimelineItem::DayDivider { timestamp_ms: n })601
+ }602
+603
+ #[test]604
+ fn hidden_rows_keep_diff_indices_on_the_visible_timeline() {605
+ let mut sdk: imbl::Vector<Option<TimelineItem>> = imbl::Vector::new();606
+ let mut hidden = HiddenRows::default();607
+ let mut visible: imbl::Vector<TimelineItem> = imbl::Vector::new();608
+ let diffs = [609
+ VectorDiff::Reset {610
+ values: [row(0), None, row(1)].into_iter().collect(),611
+ },612
+ VectorDiff::Insert {613
+ index: 1,614
+ value: row(2),615
+ },616
+ VectorDiff::Set {617
+ index: 2,618
+ value: row(3),619
+ },620
+ VectorDiff::Set {621
+ index: 0,622
+ value: None,623
+ },624
+ VectorDiff::PushFront { value: None },625
+ VectorDiff::Remove { index: 3 },626
+ VectorDiff::Append {627
+ values: [None, row(4)].into_iter().collect(),628
+ },629
+ VectorDiff::PopFront,630
+ VectorDiff::Truncate { length: 2 },631
+ VectorDiff::PopBack,632
+ ];633
+ for diff in diffs {634
+ diff.clone().apply(&mut sdk);635
+ if let Some(diff) = hidden.translate(diff) {636
+ diff.try_apply(&mut visible).unwrap();637
+ }638
+ let expected: imbl::Vector<_> = sdk.iter().flatten().cloned().collect();639
+ assert_eq!(visible, expected);640
+ }641
+ }642
+}@@ -5,62 +5,19 @@ use serde::{Deserialize, Serialize};
55
66
use crate::Error;77
8
-#[derive(Clone, Debug, PartialEq, Eq)]9
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]10
-pub enum HistoryRepairPolicy {11
- StartRecent,12
- Bridge,13
- BridgeShortGaps { threshold_ms: u64 },14
-}15
-16
-impl Default for HistoryRepairPolicy {17
- fn default() -> Self {18
- Self::StartRecent19
- }20
-}21
-22
-#[derive(Clone, Debug, PartialEq)]23
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]24
-pub struct HistoryRepairBudget {25
- pub max_pages: u16,26
- pub max_events: u32,27
- pub max_duration_ms: u64,28
-}29
-30
-impl Default for HistoryRepairBudget {31
- fn default() -> Self {32
- Self {33
- max_pages: 4,34
- max_events: 200,35
- max_duration_ms: 15_000,36
- }37
- }38
-}39
-408
#[derive(Clone, Debug, PartialEq)]419
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]10
+#[non_exhaustive]4211
pub enum HistoryRepairCommand {43
- Start {44
- room_id: OwnedRoomId,45
- policy: Option<HistoryRepairPolicy>,46
- budget: HistoryRepairBudget,47
- },48
- Continue {49
- room_id: OwnedRoomId,50
- budget: HistoryRepairBudget,51
- },52
- Cancel {53
- room_id: OwnedRoomId,54
- },55
- StartRecent {56
- room_id: OwnedRoomId,57
- },12
+ /// runs the open-time recent load again, joining one already running. the outcome13
+ /// arrives as `RecentHistoryChanged`14
+ RetryRecent { room_id: OwnedRoomId },5815
FillGap {5916
room_id: OwnedRoomId,6017
gap_id: OwnedTransactionId,6118
},62
- GetPolicy,63
- SetPolicy(HistoryRepairPolicy),19
+ /// stops whichever history operation the room is running20
+ Cancel { room_id: OwnedRoomId },6421
}6522
6623
#[derive(Clone, Debug, PartialEq, Eq)]@@ -70,7 +27,6 @@ pub struct HistoryGap {
7027
pub older_boundary: Option<OwnedEventId>,7128
pub newer_boundary: Option<OwnedEventId>,7229
pub lifecycle: HistoryGapLifecycle,73
- pub continuity: HistoryContinuity,7430
pub failure: Option<String>,7531
}7632
@@ -78,22 +34,14 @@ pub struct HistoryGap {
7834
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]7935
pub enum HistoryGapLifecycle {8036
Idle,81
- Fetching,8237
Paused,8338
Failed,39
+ /// accessible history ends before the older section, so this interval stays8440
Unavailable,8541
}8642
8743
#[derive(Clone, Debug, PartialEq, Eq)]8844
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]89
-pub enum HistoryRepairStrategy {90
- Recent,91
- Overlap,92
- Bridge,93
-}94
-95
-#[derive(Clone, Debug, PartialEq, Eq)]96
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]9745
pub enum HistoryContinuity {9846
Unresolved,9947
Verified,@@ -103,7 +51,6 @@ pub enum HistoryContinuity {
10351
#[derive(Clone, Debug, PartialEq)]10452
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]10553
pub enum HistoryRepairPhase {106
- FetchingRecent,10754
Bridging,10855
Ready,10956
Paused,@@ -115,66 +62,19 @@ pub enum HistoryRepairStop {
11562
Cancelled,11663
Budget,11764
HistoryUnavailable,118
- NoAnchor,11965
CursorStalled,12066
}12167
68
+/// one gap fill. `Bridging` when it starts, then once more when it settles12269
#[derive(Clone, Debug, PartialEq)]12370
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]12471
pub struct HistoryRepairReport {12572
pub room_id: OwnedRoomId,126
- #[cfg_attr(feature = "serde", serde(default))]127
- pub gap_id: Option<OwnedTransactionId>,128
- pub requested_policy: HistoryRepairPolicy,129
- pub strategy: HistoryRepairStrategy,130
- pub coverage_duration_ms: Option<u64>,131
- pub threshold_evaluated: bool,73
+ pub gap_id: OwnedTransactionId,13274
pub pages: u32,13375
pub events: u32,13476
pub phase: HistoryRepairPhase,13577
pub continuity: HistoryContinuity,136
- pub recent_available: bool,13778
pub stop: Option<HistoryRepairStop>,13879
pub failure: Option<Error>,13980
}140
-141
-#[cfg(all(test, feature = "serde"))]142
-mod tests {143
- use super::*;144
- use crate::ErrorKind;145
- use ruma::room_id;146
-147
- #[test]148
- fn history_repair_types_round_trip_through_serde() {149
- let command = HistoryRepairCommand::Start {150
- room_id: room_id!("!room:example.org").to_owned(),151
- policy: Some(HistoryRepairPolicy::BridgeShortGaps {152
- threshold_ms: 86_400_000,153
- }),154
- budget: HistoryRepairBudget::default(),155
- };156
- let encoded_command = serde_json::to_value(&command).unwrap();157
- let decoded_command: HistoryRepairCommand =158
- serde_json::from_value(encoded_command).unwrap();159
- assert_eq!(decoded_command, command);160
-161
- let report = HistoryRepairReport {162
- gap_id: None,163
- room_id: room_id!("!room:example.org").to_owned(),164
- requested_policy: HistoryRepairPolicy::Bridge,165
- strategy: HistoryRepairStrategy::Overlap,166
- coverage_duration_ms: Some(86_400_000),167
- threshold_evaluated: false,168
- pages: 3,169
- events: 42,170
- phase: HistoryRepairPhase::Paused,171
- continuity: HistoryContinuity::Unresolved,172
- recent_available: true,173
- stop: Some(HistoryRepairStop::Budget),174
- failure: Some(Error::new(ErrorKind::Timeline, "repair failed")),175
- };176
- let encoded_report = serde_json::to_value(&report).unwrap();177
- let decoded_report: HistoryRepairReport = serde_json::from_value(encoded_report).unwrap();178
- assert_eq!(decoded_report, report);179
- }180
-}@@ -29,15 +29,15 @@ use tokio::sync::{OnceCell, mpsc, oneshot, watch};
2929
use crate::{3030
AccountCommand, AttachmentUpload, Command, CommandOutput, ConnectionState, CreateRoomOptions,3131
EncryptionCommand, EncryptionState, Error, ErrorKind, Event, EventContext, EventContextCommand,32
- EventContextPage, EventRef, HistoryRepairBudget, HistoryRepairCommand, HistoryRepairPolicy,33
- HistoryRepairReport, ImagePackSource, ImagePacks, InteractiveAuth, InteractiveAuthOutcome,34
- LifecycleCommand, LinkPreview, Media, MediaCommand, MediaId, MediaLocation, MediaSize,35
- MemberFilter, MemberSummary, MentionKey, MentionsBoundary, MentionsCommand, MentionsPage,36
- MessageContent, MessageEditContent, NotificationCommand, OwnDevice, PackCommand, PackImageInfo,37
- PackUsage, Patch, PowerLevelChanges, Profile, PushNotification, PushNotificationEvent,38
- PushNotificationStatus, RoomCommand, RoomListFilter, RoomNotificationMode,39
- RoomNotificationSettings, RoomPowerLevels, RoomSummary, SavedMentionContext, SpaceChild,40
- TimelineCommand, TimelineId, VerificationAction, VerificationTarget,32
+ EventContextPage, EventRef, HistoryRepairCommand, ImagePackSource, ImagePacks, InteractiveAuth,33
+ InteractiveAuthOutcome, LifecycleCommand, LinkPreview, Media, MediaCommand, MediaId,34
+ MediaLocation, MediaSize, MemberFilter, MemberSummary, MentionKey, MentionsBoundary,35
+ MentionsCommand, MentionsPage, MessageContent, MessageEditContent, NotificationCommand,36
+ OwnDevice, PackCommand, PackImageInfo, PackUsage, Patch, PowerLevelChanges, Profile,37
+ PushNotification, PushNotificationEvent, PushNotificationStatus, RoomCommand, RoomListFilter,38
+ RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomSummary,39
+ SavedMentionContext, SpaceChild, TimelineCommand, TimelineId, VerificationAction,40
+ VerificationTarget,4141
encryption::encryption_state,4242
engine::{MediaService, StoreLock},4343
model::{PushNotificationRoom, PushNotificationSender},@@ -332,42 +332,6 @@ impl ClientHandle {
332332
.await333333
}334334
335
- pub async fn history_repair(336
- &self,337
- room_id: OwnedRoomId,338
- policy: Option<HistoryRepairPolicy>,339
- budget: HistoryRepairBudget,340
- ) -> Result<HistoryRepairReport, Error> {341
- match self342
- .execute(Command::HistoryRepair(HistoryRepairCommand::Start {343
- room_id,344
- policy,345
- budget,346
- }))347
- .await?348
- {349
- CommandOutput::HistoryRepair(report) => Ok(report),350
- output => Err(unexpected("history repair", &output)),351
- }352
- }353
-354
- pub async fn continue_history_repair(355
- &self,356
- room_id: OwnedRoomId,357
- budget: HistoryRepairBudget,358
- ) -> Result<HistoryRepairReport, Error> {359
- match self360
- .execute(Command::HistoryRepair(HistoryRepairCommand::Continue {361
- room_id,362
- budget,363
- }))364
- .await?365
- {366
- CommandOutput::HistoryRepair(report) => Ok(report),367
- output => Err(unexpected("history repair", &output)),368
- }369
- }370
-371335
pub async fn cancel_history_repair(&self, room_id: OwnedRoomId) -> Result<(), Error> {372336
self.run(Command::HistoryRepair(HistoryRepairCommand::Cancel {373337
room_id,@@ -387,38 +351,10 @@ impl ClientHandle {
387351
.await388352
}389353
390
- pub async fn start_recent_history(391
- &self,392
- room_id: OwnedRoomId,393
- ) -> Result<HistoryRepairReport, Error> {394
- match self395
- .execute(Command::HistoryRepair(HistoryRepairCommand::StartRecent {396
- room_id,397
- }))398
- .await?399
- {400
- CommandOutput::HistoryRepair(report) => Ok(report),401
- output => Err(unexpected("history repair", &output)),402
- }403
- }404
-405
- pub async fn history_repair_policy(&self) -> Result<HistoryRepairPolicy, Error> {406
- match self407
- .execute(Command::HistoryRepair(HistoryRepairCommand::GetPolicy))408
- .await?409
- {410
- CommandOutput::HistoryRepairPolicy(policy) => Ok(policy),411
- output => Err(unexpected("history repair policy", &output)),412
- }413
- }414
-415
- pub async fn set_history_repair_policy(416
- &self,417
- policy: HistoryRepairPolicy,418
- ) -> Result<(), Error> {419
- self.run(Command::HistoryRepair(HistoryRepairCommand::SetPolicy(420
- policy,421
- )))354
+ pub async fn retry_recent_history(&self, room_id: OwnedRoomId) -> Result<(), Error> {355
+ self.run(Command::HistoryRepair(HistoryRepairCommand::RetryRecent {356
+ room_id,357
+ }))422358
.await423359
}424360
@@ -1689,30 +1625,6 @@ impl EventSender {
16891625
}16901626
}16911627
1692
- pub(crate) async fn publish_repair(1693
- &self,1694
- report: crate::HistoryRepairReport,1695
- ) -> Result<(), SendEventError> {1696
- let Some((acknowledgement, published)) = self.publication.acknowledgement() else {1697
- return self.send(Event::HistoryRepairChanged(report)).await;1698
- };1699
- if let Err(error) = self1700
- .send(Event::HistoryRepairPublished {1701
- report,1702
- acknowledgement: Some(acknowledgement.clone()),1703
- })1704
- .await1705
- {1706
- acknowledgement.complete(false);1707
- return Err(error);1708
- }1709
- drop(acknowledgement);1710
- match published.await {1711
- Ok(true) => Ok(()),1712
- Ok(false) | Err(_) => Err(SendEventError),1713
- }1714
- }1715
-17161628
pub(crate) async fn reserve(&self) -> Result<mpsc::Permit<'_, Event>, SendEventError> {17171629
self.events.reserve().await.map_err(|_| SendEventError)17181630
}@@ -1874,31 +1786,9 @@ fn unexpected(wanted: &str, output: &CommandOutput) -> Error {
18741786
#[cfg(test)]18751787
mod tests {18761788
use super::*;1877
- use crate::{1878
- HistoryContinuity, HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairStrategy,1879
- };1880
- use ruma::room_id;1881
-1882
- fn report() -> HistoryRepairReport {1883
- HistoryRepairReport {1884
- gap_id: None,1885
- room_id: room_id!("!room:example.org").to_owned(),1886
- requested_policy: HistoryRepairPolicy::Bridge,1887
- strategy: HistoryRepairStrategy::Bridge,1888
- coverage_duration_ms: None,1889
- threshold_evaluated: false,1890
- pages: 0,1891
- events: 0,1892
- phase: HistoryRepairPhase::Ready,1893
- continuity: HistoryContinuity::Verified,1894
- recent_available: true,1895
- stop: None,1896
- failure: None,1897
- }1898
- }18991789
19001790
#[tokio::test]1901
- async fn publish_repair_waits_for_mirror_publication() {1791
+ async fn publish_barrier_waits_for_mirror_publication() {19021792
let publication = Arc::new(PublicationRegistry::default());19031793
let _registration = publication.register_mirror();19041794
let (events, mut received) = mpsc::channel(1);@@ -1906,18 +1796,14 @@ mod tests {
19061796
events,19071797
publication,19081798
};1909
- let expected = report();1910
- let expected_for_task = expected.clone();1911
- let publisher = tokio::spawn(async move { sender.publish_repair(expected_for_task).await });1799
+ let publisher = tokio::spawn(async move { sender.publish_barrier().await });19121800
1913
- let Some(Event::HistoryRepairPublished {1914
- report,1801
+ let Some(Event::PublicationBarrier {19151802
acknowledgement: Some(acknowledgement),19161803
}) = received.recv().await19171804
else {1918
- panic!("repair event was not published");1805
+ panic!("barrier was not published");19191806
};1920
- assert_eq!(report, expected);19211807
tokio::task::yield_now().await;19221808
assert!(!publisher.is_finished());19231809
@@ -1926,7 +1812,7 @@ mod tests {
19261812
}19271813
19281814
#[tokio::test]1929
- async fn publish_repair_fails_when_last_mirror_drops() {1815
+ async fn publish_barrier_fails_when_last_mirror_drops() {19301816
let publication = Arc::new(PublicationRegistry::default());19311817
let registration = publication.register_mirror();19321818
let (events, mut received) = mpsc::channel(1);@@ -1934,13 +1820,12 @@ mod tests {
19341820
events,19351821
publication,19361822
};1937
- let publisher = tokio::spawn(async move { sender.publish_repair(report()).await });1823
+ let publisher = tokio::spawn(async move { sender.publish_barrier().await });19381824
19391825
assert!(matches!(19401826
received.recv().await,1941
- Some(Event::HistoryRepairPublished {1827
+ Some(Event::PublicationBarrier {19421828
acknowledgement: Some(_),1943
- ..19441829
})19451830
));19461831
drop(registration);@@ -4,8 +4,7 @@ use std::time::Duration;
44
55
use macltok::{66
ClientHandle, Command, Engine, EngineConfig, ErrorKind, Event, EventStream, HistoryState,7
- MediaSize, MessageContent, RecentHistoryState, RoomCommand, TimelineCommand, TimelineId,8
- TimelineItem,7
+ MediaSize, MessageContent, RoomCommand, TimelineCommand, TimelineId, TimelineItem,98
};109
use matrix_sdk::{1110
ruma::{api::client::sync::sync_events::v5, event_id, mxc_uri, room_id},@@ -318,7 +317,6 @@ async fn closes_and_reopens_a_room_in_command_order() {
318317
319318
// biased select drains buffered events before it reports ready replies320319
let mut resets = 0;321
- let mut recent_active = false;322320
let mut order = Vec::new();323321
let (mut close_done, mut replacement_open_done, mut second_close_done, mut final_open_done) =324322
(false, false, false, false);@@ -328,15 +326,7 @@ async fn closes_and_reopens_a_room_in_command_order() {
328326
biased;329327
event = events.recv() => {330328
match event.expect("event stream closed") {331
- Event::RecentHistoryChanged { room_id: changed, state } if changed == room_id => {332
- recent_active = matches!(state, RecentHistoryState::Loading);333
- }334
- Event::TimelineClosed { timeline_id } if timeline_id.room_id() == room_id => {335
- recent_active = false;336
- }337
- Event::TimelineReset { timeline_id, .. }338
- if timeline_id.room_id() == room_id && !recent_active =>339
- {329
+ Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id => {340330
resets += 1;341331
assert!(resets <= 2, "a room generation reset more than once");342332
}@@ -2,19 +2,20 @@ mod support;
22
33
use std::time::Duration;44
5
+#[cfg(feature = "mirror")]6
+use macltok::Mirror;57
use macltok::{68
ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryGap,7
- HistoryRepairPhase, HistoryRepairPolicy, RecentHistoryState, Termination, TimelineItem,9
+ HistoryRepairPhase, HistoryRepairReport, HistoryRepairStop, RecentHistoryState, Termination,10
+ TimelineId, TimelineItem,811
};9
-#[cfg(feature = "mirror")]10
-use macltok::{Mirror, TimelineId};1112
use matrix_sdk::{1213
Client,1314
ruma::{EventId, RoomId, api::client::sync::sync_events::v5, event_id, room_id},1415
test_utils::mocks::{MatrixMockServer, RoomMessagesResponseTemplate},1516
};1617
use matrix_sdk_test::{ALICE, JoinedRoomBuilder, event_factory::EventFactory};17
-use support::{event_messages, receive_until, shut_down};18
+use support::shut_down;1819
use wiremock::ResponseTemplate;1920
2021
struct TestRoom {@@ -23,6 +24,9 @@ struct TestRoom {
2324
handle: ClientHandle,2425
events: EventStream,2526
running: tokio::task::JoinHandle<Result<Termination, EngineError>>,27
+ /// the open room's timeline, kept current by every helper that reads events28
+ items: imbl::Vector<TimelineItem>,29
+ opened: bool,2630
}2731
2832
async fn start_room(room_id: &RoomId) -> TestRoom {@@ -45,12 +49,62 @@ async fn start_room(room_id: &RoomId) -> TestRoom {
4549
handle,4650
events,4751
running,52
+ items: imbl::Vector::new(),53
+ opened: false,4854
}4955
}5056
57
+/// the next event that satisfies `predicate`. only the open itself may reset the room58
+/// timeline, history after it has to arrive as diffs59
+async fn next_until(room: &mut TestRoom, mut predicate: impl FnMut(&Event) -> bool) -> Event {60
+ tokio::time::timeout(Duration::from_secs(5), async {61
+ loop {62
+ let event = room.events.recv().await.expect("event stream closed");63
+ match &event {64
+ Event::TimelineReset {65
+ timeline_id: TimelineId::Room(_),66
+ items,67
+ } => {68
+ assert!(!room.opened, "history reset the room timeline");69
+ room.opened = true;70
+ room.items = items.clone();71
+ }72
+ Event::TimelineChanged {73
+ timeline_id: TimelineId::Room(_),74
+ diffs,75
+ } => {76
+ for diff in diffs.clone() {77
+ diff.try_apply(&mut room.items)78
+ .expect("diff did not fit the timeline");79
+ }80
+ }81
+ _ => {}82
+ }83
+ if predicate(&event) {84
+ return event;85
+ }86
+ }87
+ })88
+ .await89
+ .expect("expected event did not arrive")90
+}91
+92
+async fn timeline_until(93
+ room: &mut TestRoom,94
+ predicate: impl Fn(&imbl::Vector<TimelineItem>) -> bool,95
+) {96
+ tokio::time::timeout(Duration::from_secs(5), async {97
+ while !predicate(&room.items) {98
+ next_until(room, |_| true).await;99
+ }100
+ })101
+ .await102
+ .expect("the timeline never got there");103
+}104
+51105
async fn open_room(room: &mut TestRoom, room_id: &RoomId) {52106
room.handle.open_room(room_id.to_owned()).await.unwrap();53
- receive_until(&mut room.events, |event| {107
+ next_until(room, |event| {54108
matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)55109
})56110
.await;@@ -84,7 +138,7 @@ fn history_event(
84138
}85139
86140
async fn wait_for_recent(room: &mut TestRoom, room_id: &RoomId) -> RecentHistoryState {87
- let event = receive_until(&mut room.events, |event| {141
+ let event = next_until(room, |event| {88142
matches!(event, Event::RecentHistoryChanged { room_id: id, state, .. }89143
if id == room_id && !matches!(state, RecentHistoryState::Loading))90144
})@@ -95,11 +149,11 @@ async fn wait_for_recent(room: &mut TestRoom, room_id: &RoomId) -> RecentHistory
95149
}96150
}97151
98
-async fn wait_for_report(room: &mut TestRoom, room_id: &RoomId) -> macltok::HistoryRepairReport {99
- match receive_until(100
- &mut room.events,101
- |event| matches!(event, Event::HistoryRepairChanged(report) if &report.room_id == room_id),102
- )152
+async fn wait_for_settled_fill(room: &mut TestRoom) -> HistoryRepairReport {153
+ match next_until(room, |event| {154
+ matches!(event, Event::HistoryRepairChanged(report)155
+ if report.phase != HistoryRepairPhase::Bridging)156
+ })103157
.await104158
{105159
Event::HistoryRepairChanged(report) => report,@@ -107,6 +161,12 @@ async fn wait_for_report(room: &mut TestRoom, room_id: &RoomId) -> macltok::Hist
107161
}108162
}109163
164
+fn has_event(items: &imbl::Vector<TimelineItem>, id: &EventId) -> bool {165
+ items.iter().any(166
+ |item| matches!(item, TimelineItem::Event(event) if event.event_id.as_deref() == Some(id)),167
+ )168
+}169
+110170
fn gap_in(items: &imbl::Vector<TimelineItem>) -> Option<HistoryGap> {111171
items.iter().find_map(|item| match item {112172
TimelineItem::HistoryGap(gap) => Some(gap.clone()),@@ -125,10 +185,6 @@ async fn automatic_recent_load_is_bounded_and_keeps_cached_history_behind_a_gap(
125185
event_id!("$fresh-d:localhost"),126186
];127187
let mut room = start_room(room_id).await;128
- room.handle129
- .set_history_repair_policy(HistoryRepairPolicy::Bridge)130
- .await131
- .unwrap();132188
for (index, id) in new_ids.iter().enumerate() {133189
let token = format!("recent-{index}");134190
let response = RoomMessagesResponseTemplate::default()@@ -154,40 +210,16 @@ async fn automatic_recent_load_is_bounded_and_keeps_cached_history_behind_a_gap(
154210
}155211
cache_event_before_open(&room, room_id, old_id, "retained old event").await;156212
open_room(&mut room, room_id).await;157
- let reset = receive_until(&mut room.events, |event| {158
- let items = match event {159
- Event::TimelineReset { timeline_id, items } if timeline_id.room_id() == room_id => {160
- items161
- }162
- _ => return false,163
- };213
+ timeline_until(&mut room, |items| {164214
gap_in(items).is_some()165
- && items.iter().any(|item| {166
- matches!(item, TimelineItem::Event(event)167
- if event.event_id.as_deref() == Some(old_id))168
- })169
- && new_ids.iter().all(|id| {170
- items.iter().any(|item| {171
- matches!(item,172
- TimelineItem::Event(event) if event.event_id.as_deref() == Some(id))173
- })174
- })215
+ && has_event(items, old_id)216
+ && new_ids.iter().all(|id| has_event(items, id))175217
})176218
.await;177
- let Event::TimelineReset { items, .. } = reset else {178
- unreachable!();179
- };180
- assert!(gap_in(&items).is_some());181219
assert_eq!(182220
wait_for_recent(&mut room, room_id).await,183221
RecentHistoryState::Loaded184222
);185
- let report = wait_for_report(&mut room, room_id).await;186
- assert_eq!(report.requested_policy, HistoryRepairPolicy::StartRecent);187
- assert_eq!(report.phase, HistoryRepairPhase::Ready);188
- assert_eq!(report.pages, 4);189
- assert_eq!(report.events, 4);190
- assert!(report.recent_available);191223
let requests = room.server.received_requests().await.unwrap();192224
let messages = requests193225
.iter()@@ -230,13 +262,10 @@ async fn explicit_recent_start_coalesces_with_the_automatic_load() {
230262
.await231263
.expect("automatic recent request did not start");232264
233
- let report = room234
- .handle235
- .start_recent_history(room_id.to_owned())265
+ room.handle266
+ .retry_recent_history(room_id.to_owned())236267
.await237268
.unwrap();238
- assert_eq!(report.pages, 1);239
- assert_eq!(report.events, 1);240269
assert_eq!(241270
wait_for_recent(&mut room, room_id).await,242271
RecentHistoryState::Loaded@@ -358,8 +387,7 @@ async fn fill_gap_returns_after_the_mirror_contains_the_committed_rows() {
358387
loop {359388
let snapshot = mirror_handle.snapshot();360389
let active_gap = snapshot.repairs.get(room_id).is_some_and(|report| {361
- report.gap_id.as_ref() == Some(&target_gap_id)362
- && report.phase == HistoryRepairPhase::Bridging390
+ report.gap_id == target_gap_id && report.phase == HistoryRepairPhase::Bridging363391
});364392
if active_gap {365393
break;@@ -416,15 +444,8 @@ async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {
416444
.await;417445
cache_event_before_open(&room, room_id, old_id, "old cached").await;418446
open_room(&mut room, room_id).await;419
- let reset = receive_until(&mut room.events, |event| {420
- matches!(event, Event::TimelineReset { timeline_id, items }421
- if timeline_id.room_id() == room_id && gap_in(items).is_some())422
- })423
- .await;424
- let gap = match reset {425
- Event::TimelineReset { items, .. } => gap_in(&items).unwrap(),426
- _ => unreachable!(),427
- };447
+ timeline_until(&mut room, |items| gap_in(items).is_some()).await;448
+ let gap = gap_in(&room.items).unwrap();428449
assert_eq!(429450
wait_for_recent(&mut room, room_id).await,430451
RecentHistoryState::Loaded@@ -487,6 +508,9 @@ async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {
487508
.unwrap();488509
let error = fill.await.unwrap().unwrap_err();489510
assert_eq!(error.kind, macltok::ErrorKind::Interrupted);511
+ let report = wait_for_settled_fill(&mut room).await;512
+ assert_eq!(report.stop, Some(HistoryRepairStop::Cancelled));513
+ assert!(report.failure.is_none());490514
491515
room.server492516
.mock_room_messages()@@ -505,22 +529,13 @@ async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {
505529
.fill_history_gap(room_id.to_owned(), gap.id)506530
.await507531
.unwrap();508
- let final_reset = receive_until(&mut room.events, |event| {509
- matches!(event, Event::TimelineReset { timeline_id, items }510
- if timeline_id.room_id() == room_id511
- && items.iter().any(|item| matches!(item, TimelineItem::Event(event)512
- if event.event_id.as_deref() == Some(middle_id)))513
- && items.iter().any(|item| matches!(item, TimelineItem::Event(event)514
- if event.event_id.as_deref() == Some(old_id))))532
+ timeline_until(&mut room, |items| {533
+ [fresh_id, middle_id, old_id]534
+ .iter()535
+ .all(|id| has_event(items, id))536
+ && gap_in(items).is_none()515537
})516538
.await;517
- let ids = event_messages(&final_reset)518
- .iter()519
- .filter_map(|event| event.event_id.as_deref())520
- .collect::<Vec<_>>();521
- assert!(ids.contains(&fresh_id));522
- assert!(ids.contains(&middle_id));523
- assert!(ids.contains(&old_id));524539
shut_down(&room.handle, &mut room.events, room.running).await;525540
}526541
@@ -542,8 +557,38 @@ async fn failed_automatic_recent_load_is_public_and_keeps_the_cached_timeline()
542557
open_room(&mut room, room_id).await;543558
let state = wait_for_recent(&mut room, room_id).await;544559
assert!(matches!(state, RecentHistoryState::Failed(_)));545
- let report = wait_for_report(&mut room, room_id).await;546
- assert_eq!(report.phase, HistoryRepairPhase::Paused);547
- assert!(report.failure.is_some());560
+ shut_down(&room.handle, &mut room.events, room.running).await;561
+}562
+563
+#[tokio::test]564
+async fn closing_during_a_recent_load_settles_idle_not_failed() {565
+ let room_id = room_id!("!close-recent:localhost");566
+ let mut room = start_room(room_id).await;567
+ room.server568
+ .mock_room_messages()569
+ .ok(RoomMessagesResponseTemplate::default()570
+ .with_delay(Duration::from_secs(5))571
+ .events(vec![history_event(572
+ room_id,573
+ event_id!("$never:localhost"),574
+ "too late",575
+ )]))576
+ .mock_once()577
+ .mount()578
+ .await;579
+ open_room(&mut room, room_id).await;580
+ room.handle.close_room(room_id.to_owned()).await.unwrap();581
+ let mut states = Vec::new();582
+ next_until(&mut room, |event| {583
+ if let Event::RecentHistoryChanged { state, .. } = event {584
+ states.push(state.clone());585
+ }586
+ matches!(event, Event::TimelineClosed { timeline_id } if timeline_id.room_id() == room_id)587
+ })588
+ .await;589
+ assert_eq!(590
+ states,591
+ [RecentHistoryState::Loading, RecentHistoryState::Idle]592
+ );548593
shut_down(&room.handle, &mut room.events, room.running).await;549594
}