Commit c9f0fd0ee1cc
tarball16 files changed+1096-1069
@@ -2002,7 +2002,7 @@ dependencies = [
20022002
[[package]]20032003
name = "matrix-sdk"20042004
version = "0.18.0"2005
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2005
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"20062006
dependencies = [20072007
"anymap2",20082008
"aquamarine",@@ -2065,7 +2065,7 @@ dependencies = [
20652065
[[package]]20662066
name = "matrix-sdk-base"20672067
version = "0.18.0"2068
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2068
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"20692069
dependencies = [20702070
"as_variant",20712071
"assert_matches",@@ -2095,7 +2095,7 @@ dependencies = [
20952095
[[package]]20962096
name = "matrix-sdk-common"20972097
version = "0.18.0"2098
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2098
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"20992099
dependencies = [21002100
"eyeball-im",21012101
"futures-core",@@ -2118,7 +2118,7 @@ dependencies = [
21182118
[[package]]21192119
name = "matrix-sdk-crypto"21202120
version = "0.18.0"2121
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2121
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"21222122
dependencies = [21232123
"aes",21242124
"aquamarine",@@ -2159,7 +2159,7 @@ dependencies = [
21592159
[[package]]21602160
name = "matrix-sdk-indexeddb"21612161
version = "0.18.0"2162
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2162
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"21632163
dependencies = [21642164
"async-trait",21652165
"base64",@@ -2191,7 +2191,7 @@ dependencies = [
21912191
[[package]]21922192
name = "matrix-sdk-sqlite"21932193
version = "0.18.0"2194
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2194
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"21952195
dependencies = [21962196
"as_variant",21972197
"async-trait",@@ -2218,7 +2218,7 @@ dependencies = [
22182218
[[package]]22192219
name = "matrix-sdk-store-encryption"22202220
version = "0.18.0"2221
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2221
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"22222222
dependencies = [22232223
"base64",22242224
"blake3",@@ -2239,7 +2239,7 @@ dependencies = [
22392239
[[package]]22402240
name = "matrix-sdk-test"22412241
version = "0.18.0"2242
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2242
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"22432243
dependencies = [22442244
"as_variant",22452245
"ctor",@@ -2262,7 +2262,7 @@ dependencies = [
22622262
[[package]]22632263
name = "matrix-sdk-test-macros"22642264
version = "0.18.0"2265
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2265
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"22662266
dependencies = [22672267
"quote",22682268
"syn 2.0.119",@@ -2271,7 +2271,7 @@ dependencies = [
22712271
[[package]]22722272
name = "matrix-sdk-ui"22732273
version = "0.18.0"2274
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=9b9b16982dcab5dd499547bb87e121b9d0cba6b5#9b9b16982dcab5dd499547bb87e121b9d0cba6b5"2274
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"22752275
dependencies = [22762276
"as_variant",22772277
"async-rx",@@ -26,11 +26,11 @@ indexeddb = [
2626
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }2727
http = "1"2828
imbl = { version = "6.1.0", default-features = false }29
-matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false, features = ["e2e-encryption"] }30
-matrix-sdk-base = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false }31
-matrix-sdk-crypto = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false }32
-matrix-sdk-store-encryption = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", optional = true }33
-matrix-sdk-ui = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false }29
+matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false, features = ["e2e-encryption"] }30
+matrix-sdk-base = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false }31
+matrix-sdk-crypto = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false }32
+matrix-sdk-store-encryption = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", optional = true }33
+matrix-sdk-ui = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false }3434
ruma = { version = "0.16.0", default-features = false, features = ["unstable-msc2545", "unstable-msc4095"] }3535
serde = { version = "1.0.219", features = ["derive", "rc"], optional = true }3636
serde_json = "1.0.143"@@ -48,8 +48,8 @@ getrandom3 = { package = "getrandom", version = "0.3", features = ["wasm_js"] }
4848
getrandom4 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }4949
indexed_db_futures = { package = "matrix_indexed_db_futures", version = "0.7.0", optional = true, features = ["typed-arrays"] }5050
js-sys = { version = "0.3.105", optional = true }51
-matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false, features = ["js"] }52
-matrix-sdk-ui = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false, features = ["js"] }51
+matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false, features = ["js"] }52
+matrix-sdk-ui = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false, features = ["js"] }5353
# the sdk's own wasm reqwest leaves out stream, which the bounded media download needs5454
reqwest = { version = "0.13.1", default-features = false, features = ["stream"] }5555
uuid = { version = "1", features = ["js"] }@@ -64,8 +64,8 @@ web-sys = { version = "0.3.105", optional = true, features = [
6464
] }6565
6666
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies]67
-matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5", default-features = false, features = ["testing"] }68
-matrix-sdk-test = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "9b9b16982dcab5dd499547bb87e121b9d0cba6b5" }67
+matrix-sdk = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e", default-features = false, features = ["testing"] }68
+matrix-sdk-test = { git = "ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk", rev = "49a9cd1aff19df1de7e46710cb6c984a1bddc30e" }6969
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread", "signal", "time"] }7070
wiremock = "0.6.5"7171
@@ -0,0 +1,164 @@
1
+# Segmented history implementation2
+3
+The durable implementation is in the macltok `segmented-history` feature4
+worktree. Its SDK dependency has been published at5
+`49a9cd1aff19df1de7e46710cb6c984a1bddc30e` on the SDK `segmented-history`6
+bookmark. The macltok manifest and lockfile are being updated to that revision.7
+The macltok feature bookmark will also be named `segmented-history`. Malice has8
+not yet been pinned to the macltok revision.9
+10
+## Current behavior11
+12
+Opening a room loads retained history and its gap rows, then fetches a bounded13
+recent window from a fresh server position. The recent fetch is capped at four14
+pages of 50 raw events. It preserves retained sections and their older cursors.15
+If it cannot establish continuity, it installs a durable gap between the16
+sections. It stops when continuity is established or accessible history ends.17
+18
+`HistoryRepairCommand::FillGap` takes a room ID and stable gap transaction ID.19
+It fetches up to four backward pages of 50 events and advances only that gap's20
+saved cursor. Fill progress and events commit atomically. Concurrent history21
+operations for the room return `RoomBusy`. An empty page with an advancing22
+cursor keeps the gap. A repeated cursor pauses it. Accessible history ending23
+before the retained boundary marks the interval unavailable. Only reaching the24
+retained boundary joins sections. Already joined gaps are harmless to stale25
+callers.26
+27
+The cache stores gap identity, boundary evidence, cursor, and status alongside28
+the encrypted linked event chunks. SQLite supports legacy token data and the29
+new metadata. IndexedDB stores the optional encrypted metadata field. Room30
+close and restart retain sections and fill cursors. Timeline snapshots and31
+incremental updates project gaps as `TimelineItem::HistoryGap`. Gap rows retain32
+their identity while status changes. Joined markers disappear and unavailable33
+intervals remain visible.34
+35
+The recent fetch can complete while live sync continues. Cache installation36
+uses a candidate state and commits events, gap metadata, and cursor together.37
+Sync publication is serialized with gap installation. Existing loaded timelines38
+receive incremental insertions that preserve retained item identities and39
+pending sends. Loading history outside the current timeline window still needs40
+a reset.41
+42
+Each SDK room cache owns a coordinator task. Recent callers share an in-flight43
+request. Cancelling one caller preserves the operation for other subscribers,44
+and cancelling all callers stops fetching before installation. Started commits45
+finish before the coordinator accepts conflicting work. No mutex guard crosses46
+the network request. The idle task holds only a weak cache reference, so it does47
+not keep a room cache alive. The SDK UI calls `wait_for_history` as a fence,48
+then takes an atomic event and gap snapshot while replacing its cache49
+subscription. The macltok watcher is stopped before its timeline reset is sent,50
+so old diffs cannot arrive after the reset. A mirror publication acknowledgement51
+completes before the command response is released. The room actor starts recent52
+loading in the background when the room opens, so opening does not wait for the53
+network request.54
+55
+Limited sync results persist explicit gap markers in the event cache. Later sync56
+updates preserve those markers until the pagination chain reaches the retained57
+boundary. The integration uses the room's live timeline flush and watcher reset58
+path. It does not reseed the cache. Thread and pinned timelines remain open59
+through this update path.60
+61
+## Public and wire shapes62
+63
+`HistoryRepairCommand::FillGap { room_id, gap_id }` is the supported targeted64
+fill operation, sent through `ClientHandle::execute` as65
+`Command::HistoryRepair`. It returns `CommandOutput::Done` on success. The older66
+policy and staged repair command variants remain available for serde67
+compatibility, but room opening no longer consults the saved policy. `Continue`68
+is invalid because continuation must identify its gap. `GetPolicy` and69
+`SetPolicy` remain compatibility operations and do not control opening.70
+71
+Targeted fill progress is published as a `HistoryRepairReport` with `gap_id`72
+set and `phase` set to `Bridging`. This report supplies the loading state while73
+the fill runs. Recent loading has its own room keyed `RecentHistoryState` with74
+`Idle`, `Loading`, `Loaded`, or `Failed` values. The mirror exposes that state in75
+`recent_history`, separately from gap continuity and repair progress.76
+77
+`TimelineItem::HistoryGap` carries a `HistoryGap` with `id`,78
+`older_boundary`, `newer_boundary`, `lifecycle`, `continuity`, and `failure`.79
+Lifecycle values are `Idle`, `Fetching`, `Paused`, `Failed`, and `Unavailable`.80
+Continuity values are `Unresolved`, `Verified`, and `AccessibleStart`. The gap81
+ID is an owned transaction ID. Boundary values are optional owned event IDs.82
+Failure is an optional displayable string. `TimelineState.items` contains these83
+items in order with event rows. Mirror snapshots serialize the same item shape,84
+and incremental timeline changes carry it through the normal reset or changed85
+diff.86
+87
+Serde enums use externally tagged variants. A command payload is shaped as88
+follows, with the outer `Command` adding its `HistoryRepair` tag.89
+90
+```json91
+{92
+ "FillGap": {93
+ "room_id": "!room:example.org",94
+ "gap_id": "opaque-gap-transaction-id"95
+ }96
+}97
+```98
+99
+The final history repair publication acknowledgement is internal, skipped by100
+serde, and not part of application wire data. The mirror applies the report101
+before acknowledging it, so command completion follows publication.102
+103
+## Remaining publication and Malice work104
+105
+Publish the SDK revision on the user's fork, then publish the macltok revision106
+that pins it. Update the Malice macltok dependency in both native and worker107
+manifests and regenerate the applicable lockfiles against that same revision.108
+The current worktree has not recorded a final publishable commit or revision109
+for these changes.110
+111
+Malice should remove the saved policy and threshold controls from room opening.112
+Render each inline gap between its neighboring event sections. A fill should113
+preserve the visible event anchor, composer draft, reply or edit state, pending114
+sends, and encryption identity. Keep event-context windows independent from115
+live room history.116
+117
+## Lost interval investigation118
+119
+The reported failure showed retained events followed by newly sent messages,120
+with an omitted interval of roughly eight to ten hours presented as continuous.121
+Older pagination continued before the retained buffer, and a page refresh did122
+not recover the missing range. A refresh can reconnect to the existing123
+SharedWorker, so it does not prove that the engine restarted.124
+125
+The earlier SDK cache path could discard a limited-sync boundary token when a126
+timeline was reported unlimited despite cached events. It could also discard a127
+token when all incoming events were duplicates. Both cases can hide a real128
+discontinuity from older pagination. Initial pagination could also finish129
+against retained history before fresh sync arrived. These are concrete failure130
+paths, but none has been confirmed as the production cause. The durable gaps131
+preserve discontinuities when observed. Startup, subscription, and sync132
+diagnostics are still needed to identify where this particular interval was133
+lost.134
+135
+## Validation commands136
+137
+Run native checks from `../macltok-history-repair` with the repository's Nix138
+toolchain.139
+140
+```sh141
+nix develop --command cargo test --all-targets142
+nix develop --command cargo test --all-targets --features sqlite143
+nix develop --command cargo clippy --all-targets -- -D warnings144
+nix develop --command cargo clippy --all-targets --features sqlite -- -D warnings145
+```146
+147
+For the browser worker runtime, use the web devshell. The harness starts a local148
+homeserver and runs both worker targets in Firefox.149
+150
+```sh151
+nix develop .#web -c nu harness/web/run.nu152
+```153
+154
+The flake checks include native and WebAssembly compilation with IndexedDB and155
+the web worker runtime. `nix flake check` is the aggregate gate and remains156
+pending.157
+158
+Seven focused SDK recent-history tests and seven segmented-history tests pass.159
+The SDK WebAssembly compile passes with IndexedDB enabled. The SDK UI timeline160
+checks pass with 123 unit tests, seven room unit tests, and 14 timed integration161
+tests. Two headless Firefox tests pass for the IndexedDB shared gap storage162
+contract with plaintext and encrypted stores. The full flake gate remains163
+pending. Complete worker restart and encrypted room recovery have not been164
+tested.@@ -1907,7 +1907,7 @@ dependencies = [
19071907
[[package]]19081908
name = "matrix-sdk"19091909
version = "0.18.0"1910
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"1910
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"19111911
dependencies = [19121912
"anymap2",19131913
"aquamarine",@@ -1967,7 +1967,7 @@ dependencies = [
19671967
[[package]]19681968
name = "matrix-sdk-base"19691969
version = "0.18.0"1970
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"1970
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"19711971
dependencies = [19721972
"as_variant",19731973
"async-trait",@@ -1993,7 +1993,7 @@ dependencies = [
19931993
[[package]]19941994
name = "matrix-sdk-common"19951995
version = "0.18.0"1996
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"1996
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"19971997
dependencies = [19981998
"eyeball-im",19991999
"futures-core",@@ -2016,7 +2016,7 @@ dependencies = [
20162016
[[package]]20172017
name = "matrix-sdk-crypto"20182018
version = "0.18.0"2019
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"2019
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"20202020
dependencies = [20212021
"aes",20222022
"aquamarine",@@ -2056,7 +2056,7 @@ dependencies = [
20562056
[[package]]20572057
name = "matrix-sdk-indexeddb"20582058
version = "0.18.0"2059
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"2059
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"20602060
dependencies = [20612061
"async-trait",20622062
"base64",@@ -2088,7 +2088,7 @@ dependencies = [
20882088
[[package]]20892089
name = "matrix-sdk-sqlite"20902090
version = "0.18.0"2091
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"2091
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"20922092
dependencies = [20932093
"as_variant",20942094
"async-trait",@@ -2115,7 +2115,7 @@ dependencies = [
21152115
[[package]]21162116
name = "matrix-sdk-store-encryption"21172117
version = "0.18.0"2118
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"2118
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"21192119
dependencies = [21202120
"base64",21212121
"blake3",@@ -2136,7 +2136,7 @@ dependencies = [
21362136
[[package]]21372137
name = "matrix-sdk-ui"21382138
version = "0.18.0"2139
-source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=7aace883fbe03d4c3c118b55188fe9519ba8ce48#7aace883fbe03d4c3c118b55188fe9519ba8ce48"2139
+source = "git+ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk?rev=49a9cd1aff19df1de7e46710cb6c984a1bddc30e#49a9cd1aff19df1de7e46710cb6c984a1bddc30e"21402140
dependencies = [21412141
"as_variant",21422142
"async-rx",@@ -326,8 +326,6 @@ pub struct Engine {
326326
mentions: crate::mentions::service::Handle,327327
mentions_service: Option<crate::mentions::service::Service>,328328
mentions_updates: Option<watch::Receiver<crate::MentionsPage>>,329
- repair_gate: Arc<tokio::sync::Mutex<()>>,330
- repair_shutdown: Arc<std::sync::atomic::AtomicBool>,331329
}332330
333331
impl Engine {@@ -437,8 +435,6 @@ impl Engine {
437435
mentions,438436
mentions_service: Some(mentions_service),439437
mentions_updates: Some(mentions_updates),440
- repair_gate: Arc::default(),441
- repair_shutdown: Arc::default(),442438
};443439
Ok((engine, handle, events))444440
}@@ -988,7 +984,8 @@ impl Engine {
988984
HistoryRepairCommand::Start { room_id, .. }989985
| HistoryRepairCommand::Continue { room_id, .. }990986
| HistoryRepairCommand::Cancel { room_id }991
- | HistoryRepairCommand::StartRecent { room_id } => room_id,987
+ | HistoryRepairCommand::StartRecent { room_id }988
+ | HistoryRepairCommand::FillGap { room_id, .. } => room_id,992989
HistoryRepairCommand::GetPolicy | HistoryRepairCommand::SetPolicy(_) => {993990
let mentions = self.mentions.clone();994991
self.tasks@@ -1084,9 +1081,6 @@ impl Engine {
10841081
initial_history_limit: self.config.initial_history_limit,10851082
supervisor,10861083
mentions: self.mentions.clone(),1087
- sync: self.sync.clone(),1088
- repair_gate: self.repair_gate.clone(),1089
- repair_shutdown: self.repair_shutdown.clone(),10901084
},10911085
panics,10921086
opening: vec![reply],@@ -1165,15 +1159,11 @@ impl Engine {
11651159
}11661160
11671161
async fn shutdown(&mut self) {1168
- self.repair_shutdown1169
- .store(true, std::sync::atomic::Ordering::Release);11701162
if let Some(handler) = self.verification_handler.take() {11711163
self.client.remove_event_handler(handler);11721164
}11731165
self.port.close();1174
- let repair_gate = self.repair_gate.lock().await;11751166
self.sync.stop().await;1176
- drop(repair_gate);11771167
if self.mentions_service.is_none() {11781168
self.mentions.stop().await;11791169
}@@ -1,68 +1,67 @@
1
-use std::{2
- collections::HashSet,3
- sync::{4
- Arc,5
- atomic::{AtomicBool, Ordering},6
- },7
- time::Duration,8
-};9
-10
-use matrix_sdk::{Room, deserialized_responses::TimelineEvent, room::MessagesOptions};11
-use matrix_sdk_ui::sync_service::SyncService;12
-use ruma::{OwnedEventId, time::Instant};13
-use tokio::sync::{OwnedMutexGuard, mpsc, oneshot, watch};1
+use matrix_sdk::{Room, event_cache::HistoryGapFillOutcome};2
+use tokio::sync::watch;143
154
use super::{16
- executor::{JoinHandle, spawn},5
+ executor::{JoinError, JoinHandle, spawn},176
timeline::SessionContext,187
};198
use crate::{20
- CommandOutput, Error, ErrorKind, Event, HistoryContinuity, HistoryRepairBudget,21
- HistoryRepairCommand, HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairReport,22
- HistoryRepairStop, HistoryRepairStrategy, transport::Responder,9
+ CommandOutput, Error, ErrorKind, HistoryContinuity, HistoryRepairBudget, HistoryRepairCommand,10
+ HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairReport, HistoryRepairStop,11
+ HistoryRepairStrategy, RecentHistoryState, transport::Responder,2312
};2413
25
-const PAGE_SIZE: u16 = 50;26
-const STAGING_LIMIT: usize = 10_000;14
+const MAX_GAP_PAGES: u32 = 4;2715
28
-pub(super) struct InstallRequest {29
- pub(super) events: Vec<TimelineEvent>,30
- pub(super) prev_batch: Option<String>,31
- pub(super) reply: oneshot::Sender<Result<(), Error>>,16
+struct Running {17
+ task: JoinHandle<CompletionResult>,18
+ cancel: watch::Sender<bool>,19
+ responders: Vec<PendingResponder>,20
+ recent: bool,3221
}3322
34
-struct Stage {35
- events: Vec<TimelineEvent>,36
- cursor: Option<String>,37
- anchor: Option<OwnedEventId>,38
- report: HistoryRepairReport,23
+struct PendingResponder {24
+ responder: Responder,25
+ cancel_ack: bool,3926
}4027
41
-struct Running {42
- task: JoinHandle<Stage>,43
- cancel: watch::Sender<bool>,44
- reply: Responder,28
+pub(super) struct Completion {29
+ pub(super) report: HistoryRepairReport,30
+ pub(super) recent_state: Option<RecentHistoryState>,31
+ pub(super) result: Result<CommandOutput, Error>,32
+ responders: Vec<PendingResponder>,33
+}34
+35
+impl Completion {36
+ pub(super) fn resolve(self, result: Result<CommandOutput, Error>, cancellation: Result<(), Error>) {37
+ for responder in self.responders {38
+ if responder.cancel_ack {39
+ responder.responder.done(cancellation.clone());40
+ } else {41
+ responder.responder.send(result.clone());42
+ }43
+ }44
+ }45
+}46
+47
+struct CompletionResult {48
+ report: HistoryRepairReport,49
+ recent_state: Option<RecentHistoryState>,50
+ result: Result<CommandOutput, Error>,4551
}4652
4753
pub(super) struct RepairController {4854
room: Room,4955
context: SessionContext,50
- installs: mpsc::Sender<InstallRequest>,51
- requests: mpsc::Receiver<InstallRequest>,5256
running: Option<Running>,53
- stage: Option<Stage>,5457
}5558
5659
impl RepairController {5760
pub(super) fn new(room: Room, context: SessionContext) -> Self {58
- let (installs, requests) = mpsc::channel(1);5961
Self {6062
room,6163
context,62
- installs,63
- requests,6464
running: None,65
- stage: None,6665
}6766
}6867
@@ -70,202 +69,179 @@ impl RepairController {
7069
self.running.is_some()7170
}7271
72
+ pub(super) async fn start_recent(&mut self) {73
+ self.start_recent_with(HistoryRepairPolicy::default(), None, true, Vec::new())74
+ .await;75
+ }76
+7377
pub(super) async fn execute(&mut self, command: HistoryRepairCommand, reply: Responder) {74
- if matches!(command, HistoryRepairCommand::Cancel { .. }) {75
- if let Some(running) = &self.running {76
- running.cancel.send_replace(true);78
+ match command {79
+ HistoryRepairCommand::Cancel { .. } => {80
+ if let Some(running) = self.running.as_mut() {81
+ running.responders.push(PendingResponder {82
+ responder: reply,83
+ cancel_ack: true,84
+ });85
+ running.cancel.send_replace(true);86
+ } else {87
+ reply.done(Ok(()));88
+ }7789
}78
- reply.done(Ok(()));79
- return;80
- }81
- if self.is_running() {82
- reply.done(Err(Error::new(83
- ErrorKind::RoomBusy,84
- "room history repair is running",85
- )));86
- return;87
- }88
- let (stage, budget) = match command {8990
HistoryRepairCommand::Start { policy, budget, .. } => {9091
if let Err(error) = validate_budget(&budget) {9192
reply.done(Err(error));9293
return;9394
}94
- let policy = match policy {95
- Some(policy) => policy,96
- None => match self.context.mentions.repair_policy().await {97
- Ok(policy) => policy,98
- Err(error) => {99
- reply.done(Err(error));100
- return;101
- }102
- },103
- };104
- let coverage = match self105
- .context106
- .mentions107
- .repair_coverage(self.room.room_id().to_owned())108
- .await109
- {110
- Ok(coverage) => coverage,111
- Err(error) => {112
- reply.done(Err(error));113
- return;114
- }115
- };116
- let duration = coverage.duration_ms();117
- let report = HistoryRepairReport {118
- room_id: self.room.room_id().to_owned(),119
- requested_policy: policy.clone(),120
- strategy: HistoryRepairStrategy::Recent,121
- coverage_duration_ms: duration,122
- threshold_evaluated: duration.is_some()123
- && coverage.unresolved124
- && matches!(policy, HistoryRepairPolicy::BridgeShortGaps { .. }),125
- pages: 0,126
- events: 0,127
- phase: HistoryRepairPhase::FetchingRecent,128
- continuity: HistoryContinuity::Unresolved,129
- recent_available: false,130
- stop: None,131
- failure: None,132
- };133
- (134
- Stage {135
- events: Vec::new(),136
- cursor: None,137
- anchor: None,138
- report,139
- },140
- budget,95
+ let policy = policy.unwrap_or_default();96
+ self.start_recent_with(policy, Some(reply), true, Vec::new())97
+ .await;98
+ }99
+ HistoryRepairCommand::StartRecent { .. } => {100
+ self.start_recent_with(101
+ HistoryRepairPolicy::default(),102
+ Some(reply),103
+ true,104
+ Vec::new(),141105
)106
+ .await;142107
}143
- HistoryRepairCommand::Continue { budget, .. } => {144
- if let Err(error) = validate_budget(&budget) {145
- reply.done(Err(error));146
- return;147
- }148
- let Some(mut stage) = self.stage.take() else {149
- reply.done(Err(Error::new(150
- ErrorKind::Invalid,151
- "no history bridge to continue",152
- )));153
- return;154
- };155
- if !stage.report.recent_available156
- || stage.report.continuity != HistoryContinuity::Unresolved157
- {158
- reply.done(Err(Error::new(159
- ErrorKind::Invalid,160
- "history bridge is not resumable",161
- )));162
- self.stage = Some(stage);163
- return;164
- }165
- stage.report.strategy = HistoryRepairStrategy::Bridge;166
- stage.report.phase = HistoryRepairPhase::Bridging;167
- stage.report.stop = None;168
- stage.report.failure = None;169
- (stage, budget)108
+ HistoryRepairCommand::FillGap { gap_id, .. } => {109
+ self.start_gap_fill(gap_id, reply).await;170110
}171
- HistoryRepairCommand::StartRecent { .. } => {172
- let Some(mut stage) = self.stage.take() else {173
- reply.done(Err(Error::new(174
- ErrorKind::Invalid,175
- "no recent history repair",176
- )));177
- return;178
- };179
- if !stage.report.recent_available {180
- self.stage = Some(stage);181
- reply.done(Err(Error::new(182
- ErrorKind::Invalid,183
- "recent history is not available, retry the repair",184
- )));185
- return;111
+ HistoryRepairCommand::Continue { .. } => reply.done(Err(Error::new(112
+ ErrorKind::Invalid,113
+ "continue requires a history gap ID, use FillGap",114
+ ))),115
+ HistoryRepairCommand::GetPolicy => {116
+ reply.send(117
+ self.context118
+ .mentions119
+ .repair_policy()120
+ .await121
+ .map(CommandOutput::HistoryRepairPolicy),122
+ );123
+ }124
+ HistoryRepairCommand::SetPolicy(policy) => {125
+ reply.done(self.context.mentions.set_repair_policy(policy).await);126
+ }127
+ }128
+ }129
+130
+ async fn start_recent_with(131
+ &mut self,132
+ policy: HistoryRepairPolicy,133
+ responder: Option<Responder>,134
+ publish_recent: bool,135
+ mut responders: Vec<PendingResponder>,136
+ ) {137
+ if let Some(running) = &mut self.running {138
+ if let Some(responder) = responder {139
+ if running.recent {140
+ running.responders.push(PendingResponder { responder, cancel_ack: false });141
+ } else {142
+ responder.done(Err(room_busy()));186143
}187
- stage.report.strategy = HistoryRepairStrategy::Recent;188
- stage.report.phase = HistoryRepairPhase::Ready;189
- stage.report.stop = None;190
- stage.report.failure = None;191
- let report = stage.report.clone();192
- let published = self193
- .context194
- .events195
- .publish_repair(report.clone())196
- .await197
- .map_err(|_| Error::interrupted());198
- reply.send(published.map(|_| CommandOutput::HistoryRepair(report)));199
- return;200144
}201
- _ => {202
- reply.done(Err(Error::new(203
- ErrorKind::Invalid,204
- "repair command was misrouted",205
- )));206
- return;145
+ return;146
+ }147
+ if let Some(responder) = responder {148
+ responders.push(PendingResponder {149
+ responder,150
+ cancel_ack: false,151
+ });152
+ }153
+ if publish_recent154
+ && self155
+ .context156
+ .events157
+ .send(crate::Event::RecentHistoryChanged {158
+ room_id: self.room.room_id().to_owned(),159
+ state: RecentHistoryState::Loading,160
+ })161
+ .await162
+ .is_err()163
+ {164
+ for responder in responders {165
+ responder.responder.done(Err(Error::interrupted()));207166
}208
- };167
+ return;168
+ }209169
let (cancel, receiver) = watch::channel(false);210170
let room = self.room.clone();211
- let context = self.context.clone();212
- let installs = self.installs.clone();213
- let task =214
- spawn(async move { run(room, context, installs, receiver, stage, budget).await });171
+ let room_id = self.room.room_id().to_owned();172
+ let task = spawn(async move { load_recent(room, room_id, policy, receiver).await });215173
self.running = Some(Running {216174
task,217175
cancel,218
- reply,176
+ responders,177
+ recent: true,219178
});220179
}221180
222
- pub(super) async fn next_install(&mut self) -> Option<InstallRequest> {223
- let running = self.running.as_mut()?;224
- tokio::select! {225
- request = self.requests.recv() => request,226
- result = &mut running.task => {227
- let running = self.running.take().expect("repair task exists");228
- match result {229
- Ok(stage) => {230
- let report = stage.report.clone();231
- let published = self.context.events.publish_repair(report.clone()).await232
- .map_err(|_| Error::interrupted());233
- running.reply.send(published.map(|_| CommandOutput::HistoryRepair(report)));234
- self.stage = Some(stage);235
- }236
- Err(_) => running.reply.done(Err(Error::interrupted())),237
- }238
- None239
- }181
+ async fn start_gap_fill(&mut self, gap_id: ruma::OwnedTransactionId, responder: Responder) {182
+ if self.is_running() {183
+ responder.done(Err(room_busy()));184
+ return;185
+ }186
+ let room = self.room.clone();187
+ let room_id = self.room.room_id().to_owned();188
+ let mut progress = report(room_id.clone(), HistoryRepairPolicy::default());189
+ progress.gap_id = Some(gap_id.clone());190
+ progress.strategy = HistoryRepairStrategy::Bridge;191
+ progress.phase = HistoryRepairPhase::Bridging;192
+ if self.context.events.send(crate::Event::HistoryRepairChanged(progress)).await.is_err() {193
+ responder.done(Err(Error::interrupted()));194
+ return;240195
}196
+ let (cancel, receiver) = watch::channel(false);197
+ let task = spawn(async move { fill_gap(room, room_id, gap_id, receiver).await });198
+ self.running = Some(Running {199
+ task,200
+ cancel,201
+ responders: vec![PendingResponder {202
+ responder,203
+ cancel_ack: false,204
+ }],205
+ recent: false,206
+ });241207
}242208
243
- pub(super) async fn shutdown(&mut self) {244
- if let Some(mut running) = self.running.take() {245
- running.task.abort();246
- match (&mut running.task).await {247
- Ok(stage) => {248
- let report = stage.report;249
- let published = self250
- .context251
- .events252
- .publish_repair(report.clone())253
- .await254
- .map_err(|_| Error::interrupted());255
- running256
- .reply257
- .send(published.map(|_| CommandOutput::HistoryRepair(report)));258
- }259
- Err(_) => running.reply.done(Err(Error::interrupted())),260
- }261
- }209
+ pub(super) async fn next_completion(&mut self) -> Option<Completion> {210
+ let running = self.running.as_mut()?;211
+ let result = (&mut running.task).await;212
+ Some(self.finish_task(result))262213
}263
-}264214
265
-impl Drop for RepairController {266
- fn drop(&mut self) {267
- if let Some(running) = &self.running {268
- running.task.abort();215
+ pub(super) async fn shutdown(&mut self) -> Option<Completion> {216
+ let running = self.running.as_mut()?;217
+ running.cancel.send_replace(true);218
+ let result = (&mut running.task).await;219
+ Some(self.finish_task(result))220
+ }221
+222
+ fn finish_task(&mut self, result: Result<CompletionResult, JoinError>) -> Completion {223
+ let running = self.running.take().expect("repair task exists");224
+ let result = match result {225
+ Ok(result) => result,226
+ Err(_) => CompletionResult {227
+ report: failed_report(228
+ self.room.room_id().to_owned(),229
+ HistoryRepairPolicy::default(),230
+ Error::interrupted(),231
+ ),232
+ recent_state: running.recent.then(|| {233
+ RecentHistoryState::Failed(234
+ "recent history operation was interrupted".to_owned(),235
+ )236
+ }),237
+ result: Err(Error::interrupted()),238
+ },239
+ };240
+ Completion {241
+ report: result.report,242
+ recent_state: result.recent_state,243
+ result: result.result,244
+ responders: running.responders,269245
}270246
}271247
}@@ -283,326 +259,182 @@ fn validate_budget(budget: &HistoryRepairBudget) -> Result<(), Error> {
283259
Ok(())284260
}285261
286
-fn should_bridge(policy: &HistoryRepairPolicy, duration: Option<u64>, boundary: bool) -> bool {287
- match policy {288
- HistoryRepairPolicy::Bridge => true,289
- HistoryRepairPolicy::StartRecent => false,290
- HistoryRepairPolicy::BridgeShortGaps { threshold_ms } => {291
- boundary292
- && *threshold_ms > 0293
- && duration.is_some_and(|duration| duration <= *threshold_ms)294
- }295
- }296
-}297
-298
-async fn run(262
+async fn load_recent(299263
room: Room,300
- context: SessionContext,301
- installs: mpsc::Sender<InstallRequest>,264
+ room_id: ruma::OwnedRoomId,265
+ policy: HistoryRepairPolicy,302266
mut cancel: watch::Receiver<bool>,303
- mut stage: Stage,304
- budget: HistoryRepairBudget,305
-) -> Stage {306
- let started = Instant::now();307
- let initial_pages = stage.report.pages;308
- let initial_events = stage.report.events;309
- let result = async {310
- if !stage.report.recent_available {267
+) -> CompletionResult {268
+ let base = report(room_id.clone(), policy);269
+ let outcome = tokio::select! {270
+ biased;271
+ _ = cancel.changed() => Err(Error::interrupted()),272
+ result = async {311273
let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;312
- stage.anchor = cache313
- .events()314
- .await315
- .map_err(cache_error)?316
- .iter()317
- .find_map(TimelineEvent::event_id);318
- let _ = context319
- .events320
- .send(Event::HistoryRepairChanged(stage.report.clone()))321
- .await;322
- let guard = pause_sync(&context, &mut cancel, &started, &budget).await?;323
- let page = fetch(&room, None, &mut cancel, &started, &budget).await?;324
- stage.report.pages += 1;325
- stage.report.events += page.chunk.len() as u32;326
- stage.cursor = page.end;327
- stage.events = page.chunk.into_iter().rev().collect();328
- deduplicate(&mut stage.events);329
- install(&installs, &stage).await?;330
- stage.report.recent_available = true;331
- if contains_anchor(&stage) {332
- stage.report.strategy = HistoryRepairStrategy::Overlap;333
- stage.report.continuity = HistoryContinuity::Verified;334
- } else if stage.cursor.is_none() {335
- stage.report.continuity = HistoryContinuity::AccessibleStart;336
- }337
- if stage.report.continuity != HistoryContinuity::Unresolved {338
- context339
- .mentions340
- .repair_resolved(room.room_id().to_owned())341
- .await?;342
- }343
- guard.finish().await;344
- let _ = context345
- .events346
- .send(Event::HistoryRepairChanged(stage.report.clone()))347
- .await;348
- if stage.report.continuity != HistoryContinuity::Unresolved {349
- return Ok(());274
+ cache.load_recent_history().await.map_err(cache_error)275
+ } => result,276
+ };277
+ match outcome {278
+ Ok(outcome) => {279
+ let mut report = base;280
+ report.pages = outcome.pages;281
+ report.events = outcome.events.min(u32::MAX as usize) as u32;282
+ report.recent_available = true;283
+ report.phase = HistoryRepairPhase::Ready;284
+ match outcome.gap.map(|gap| gap.status) {285
+ None => report.continuity = HistoryContinuity::AccessibleStart,286
+ Some(matrix_sdk::event_cache::HistoryGapStatus::Joined) => {287
+ report.continuity = HistoryContinuity::Verified288
+ }289
+ Some(matrix_sdk::event_cache::HistoryGapStatus::Unavailable) => {290
+ report.continuity = HistoryContinuity::AccessibleStart;291
+ report.stop = Some(HistoryRepairStop::HistoryUnavailable);292
+ report.phase = HistoryRepairPhase::Paused;293
+ }294
+ Some(matrix_sdk::event_cache::HistoryGapStatus::Paused) => {295
+ report.stop = Some(HistoryRepairStop::CursorStalled);296
+ report.phase = HistoryRepairPhase::Paused;297
+ }298
+ Some(matrix_sdk::event_cache::HistoryGapStatus::Failed { message }) => {299
+ let error = Error::new(ErrorKind::Network, message.clone());300
+ report.failure = Some(error.clone());301
+ report.phase = HistoryRepairPhase::Paused;302
+ }303
+ Some(matrix_sdk::event_cache::HistoryGapStatus::Idle) => {}350304
}351
- if !should_bridge(352
- &stage.report.requested_policy,353
- stage.report.coverage_duration_ms,354
- stage.report.threshold_evaluated,355
- ) {356
- return Ok(());305
+ CompletionResult {306
+ report: report.clone(),307
+ recent_state: Some(RecentHistoryState::Loaded),308
+ result: Ok(CommandOutput::HistoryRepair(report)),357309
}358
- stage.report.strategy = HistoryRepairStrategy::Bridge;359
- }360
- stage.report.phase = HistoryRepairPhase::Bridging;361
- let _ = context362
- .events363
- .send(Event::HistoryRepairChanged(stage.report.clone()))364
- .await;365
- if contains_anchor(&stage) {366
- stage.report.continuity = HistoryContinuity::Verified;367
- } else if stage.cursor.is_none() {368
- stage.report.continuity = HistoryContinuity::AccessibleStart;369310
}370
- while stage.report.continuity == HistoryContinuity::Unresolved {371
- if *cancel.borrow() {372
- stage.report.stop = Some(HistoryRepairStop::Cancelled);373
- break;374
- }375
- if stage.report.pages - initial_pages + 1 >= u32::from(budget.max_pages)376
- || stage.report.events - initial_events + 2 * u32::from(PAGE_SIZE)377
- > budget.max_events378
- || stage.events.len() + 2 * usize::from(PAGE_SIZE) > STAGING_LIMIT379
- || started.elapsed() >= Duration::from_millis(budget.max_duration_ms)380
- {381
- stage.report.stop = Some(HistoryRepairStop::Budget);382
- break;383
- }384
- if stage.anchor.is_none() {385
- stage.report.stop = Some(HistoryRepairStop::NoAnchor);386
- break;387
- }388
- let Some(cursor) = stage.cursor.clone() else {389
- break;311
+ Err(error) => CompletionResult {312
+ report: failed_report(room_id, base.requested_policy, error.clone()),313
+ recent_state: Some(RecentHistoryState::Failed(error.message.clone())),314
+ result: Err(error),315
+ },316
+ }317
+}318
+319
+async fn fill_gap(320
+ room: Room,321
+ room_id: ruma::OwnedRoomId,322
+ gap_id: ruma::OwnedTransactionId,323
+ mut cancel: watch::Receiver<bool>,324
+) -> CompletionResult {325
+ let mut report = report(room_id, HistoryRepairPolicy::default());326
+ report.gap_id = Some(gap_id.clone());327
+ report.strategy = HistoryRepairStrategy::Bridge;328
+ let outcome = async {329
+ let (cache, _handles) = room.event_cache().await.map_err(cache_error)?;330
+ let mut last_gap = None;331
+ for _ in 0..MAX_GAP_PAGES {332
+ let next = tokio::select! {333
+ biased;334
+ _ = cancel.changed() => return Err(Error::interrupted()),335
+ result = cache.fill_history_gap_once(gap_id.clone()) => result.map_err(cache_error)?,390336
};391
- let page = fetch(&room, Some(&cursor), &mut cancel, &started, &budget).await?;392
- stage.report.pages += 1;393
- stage.report.events += page.chunk.len() as u32;394
- if page.end.as_deref() == Some(&cursor) {395
- stage.report.stop = Some(HistoryRepairStop::CursorStalled);396
- break;397
- }398
- let mut older: Vec<_> = page.chunk.into_iter().rev().collect();399
- older.append(&mut stage.events);400
- deduplicate(&mut older);401
- stage.events = older;402
- stage.cursor = page.end;403
- if contains_anchor(&stage) {404
- stage.report.continuity = HistoryContinuity::Verified;405
- } else if stage.cursor.is_none() {406
- stage.report.continuity = HistoryContinuity::AccessibleStart;407
- }408
- if stage.report.continuity == HistoryContinuity::Unresolved {409
- let _ = context410
- .events411
- .send(Event::HistoryRepairChanged(stage.report.clone()))412
- .await;337
+ match next {338
+ HistoryGapFillOutcome::NotFound => {339
+ return Err(Error::new(ErrorKind::EventNotFound, "history gap was not found"));340
+ }341
+ HistoryGapFillOutcome::Busy => return Err(room_busy()),342
+ HistoryGapFillOutcome::Updated { gap, events } => {343
+ report.pages += 1;344
+ report.events = report.events.saturating_add(events.min(u32::MAX as usize) as u32);345
+ match gap.status.clone() {346
+ matrix_sdk::event_cache::HistoryGapStatus::Joined => {347
+ report.continuity = HistoryContinuity::Verified;348
+ last_gap = Some(gap);349
+ break;350
+ }351
+ matrix_sdk::event_cache::HistoryGapStatus::Unavailable => {352
+ report.continuity = HistoryContinuity::AccessibleStart;353
+ report.stop = Some(HistoryRepairStop::HistoryUnavailable);354
+ last_gap = Some(gap);355
+ break;356
+ }357
+ matrix_sdk::event_cache::HistoryGapStatus::Paused => {358
+ report.stop = Some(HistoryRepairStop::CursorStalled);359
+ last_gap = Some(gap);360
+ break;361
+ }362
+ matrix_sdk::event_cache::HistoryGapStatus::Failed { message } => {363
+ return Err(Error::new(ErrorKind::Network, message));364
+ }365
+ matrix_sdk::event_cache::HistoryGapStatus::Idle => last_gap = Some(gap),366
+ }367
+ }413368
}414369
}415
- if stage.report.continuity != HistoryContinuity::Unresolved416
- && stage.report.strategy == HistoryRepairStrategy::Bridge370
+ if report.pages == MAX_GAP_PAGES371
+ && report.continuity == HistoryContinuity::Unresolved372
+ && report.stop.is_none()417373
{418
- let guard = pause_sync(&context, &mut cancel, &started, &budget).await?;419
- let head = fetch(&room, None, &mut cancel, &started, &budget).await?;420
- stage.report.pages += 1;421
- stage.report.events += head.chunk.len() as u32;422
- let head: Vec<_> = head.chunk.into_iter().rev().collect();423
- if !extend_head(&mut stage.events, head) {424
- stage.report.continuity = HistoryContinuity::Unresolved;425
- stage.report.stop = Some(HistoryRepairStop::HistoryUnavailable);426
- return Ok(());427
- }428
- install(&installs, &stage).await?;429
- context430
- .mentions431
- .repair_resolved(room.room_id().to_owned())432
- .await?;433
- guard.finish().await;374
+ report.stop = Some(HistoryRepairStop::Budget);434375
}435
- Ok::<_, Error>(())376
+ Ok::<_, Error>(last_gap)436377
}437378
.await;438
- if let Err(error) = result {439
- if error.kind == ErrorKind::Interrupted {440
- stage.report.stop = Some(HistoryRepairStop::Cancelled);441
- } else if error.kind == ErrorKind::RoomBusy {442
- stage.report.stop = Some(HistoryRepairStop::Budget);443
- } else {444
- stage.report.failure = Some(error);445
- }446
- stage.report.continuity = HistoryContinuity::Unresolved;447
- }448
- stage.report.phase = if stage.report.stop.is_some() || stage.report.failure.is_some() {449
- HistoryRepairPhase::Paused450
- } else {451
- HistoryRepairPhase::Ready452
- };453
- stage454
-}455
-456
-fn contains_anchor(stage: &Stage) -> bool {457
- stage.anchor.as_ref().is_some_and(|anchor| {458
- stage459
- .events460
- .iter()461
- .any(|event| event.event_id().as_ref() == Some(anchor))462
- })463
-}464
-465
-fn deduplicate(events: &mut Vec<TimelineEvent>) {466
- let mut seen = HashSet::new();467
- events.retain(|event| event.event_id().is_some_and(|id| seen.insert(id)));468
-}469
-470
-fn extend_head(events: &mut Vec<TimelineEvent>, head: Vec<TimelineEvent>) -> bool {471
- let ids: HashSet<_> = events.iter().filter_map(TimelineEvent::event_id).collect();472
- let Some(overlap) = head473
- .iter()474
- .position(|event| event.event_id().is_some_and(|id| ids.contains(&id)))475
- else {476
- return events.is_empty() && head.is_empty();477
- };478
- events.extend(head.into_iter().skip(overlap + 1));479
- deduplicate(events);480
- true481
-}482
-483
-async fn install(installs: &mpsc::Sender<InstallRequest>, stage: &Stage) -> Result<(), Error> {484
- let (reply, response) = oneshot::channel();485
- installs486
- .send(InstallRequest {487
- events: stage.events.clone(),488
- prev_batch: stage.cursor.clone(),489
- reply,490
- })491
- .await492
- .map_err(|_| Error::interrupted())?;493
- response.await.map_err(|_| Error::interrupted())?494
-}495
-496
-async fn fetch(497
- room: &Room,498
- from: Option<&str>,499
- cancel: &mut watch::Receiver<bool>,500
- started: &Instant,501
- budget: &HistoryRepairBudget,502
-) -> Result<matrix_sdk::room::Messages, Error> {503
- let remaining = Duration::from_millis(budget.max_duration_ms).saturating_sub(started.elapsed());504
- if *cancel.borrow() {505
- return Err(Error::interrupted());506
- }507
- let mut options = MessagesOptions::backward().from(from);508
- options.limit = PAGE_SIZE.into();509
- tokio::select! {510
- _ = cancel.changed() => Err(Error::interrupted()),511
- result = matrix_sdk::timeout::timeout(room.messages(options), remaining) => {512
- let page = result.map_err(|_| Error::new(ErrorKind::RoomBusy, "repair work duration exhausted"))?513
- .map_err(|source| Error::from_sdk("fetching repair history failed", source))?;514
- if page.chunk.len() > usize::from(PAGE_SIZE) {515
- return Err(Error::new(ErrorKind::Invalid, "server exceeded the repair page limit"));516
- }517
- Ok(page)379
+ let command_result = match outcome {380
+ Ok(_) => {381
+ report.recent_available = true;382
+ report.phase = if report.stop.is_some() {383
+ HistoryRepairPhase::Paused384
+ } else {385
+ HistoryRepairPhase::Ready386
+ };387
+ Ok(CommandOutput::Done)518388
}519
- }520
-}521
-522
-struct SyncPause {523
- sync: Arc<SyncService>,524
- shutdown: Arc<AtomicBool>,525
- gate: Option<OwnedMutexGuard<()>>,526
-}527
-528
-impl SyncPause {529
- async fn finish(mut self) {530
- if !self.shutdown.load(Ordering::Acquire) {531
- self.sync.start().await;389
+ Err(error) => {390
+ report.failure = Some(error.clone());391
+ report.phase = HistoryRepairPhase::Paused;392
+ Err(error)532393
}533
- self.gate.take();394
+ };395
+ CompletionResult {396
+ report,397
+ recent_state: None,398
+ result: command_result,534399
}535400
}536401
537
-impl Drop for SyncPause {538
- fn drop(&mut self) {539
- if let Some(gate) = self.gate.take() {540
- let sync = self.sync.clone();541
- let shutdown = self.shutdown.clone();542
- spawn(async move {543
- if !shutdown.load(Ordering::Acquire) {544
- sync.start().await;545
- }546
- drop(gate);547
- });548
- }402
+fn report(room_id: ruma::OwnedRoomId, policy: HistoryRepairPolicy) -> HistoryRepairReport {403
+ HistoryRepairReport {404
+ room_id,405
+ gap_id: None,406
+ requested_policy: policy,407
+ strategy: HistoryRepairStrategy::Recent,408
+ coverage_duration_ms: None,409
+ threshold_evaluated: false,410
+ pages: 0,411
+ events: 0,412
+ phase: HistoryRepairPhase::FetchingRecent,413
+ continuity: HistoryContinuity::Unresolved,414
+ recent_available: false,415
+ stop: None,416
+ failure: None,549417
}550418
}551419
552
-async fn pause_sync(553
- context: &SessionContext,554
- cancel: &mut watch::Receiver<bool>,555
- started: &Instant,556
- budget: &HistoryRepairBudget,557
-) -> Result<SyncPause, Error> {558
- let remaining = Duration::from_millis(budget.max_duration_ms).saturating_sub(started.elapsed());559
- let gate = tokio::select! {560
- _ = cancel.changed() => return Err(Error::interrupted()),561
- result = matrix_sdk::timeout::timeout(context.repair_gate.clone().lock_owned(), remaining) =>562
- result.map_err(|_| Error::new(ErrorKind::RoomBusy, "repair work duration exhausted"))?,563
- };564
- if context.repair_shutdown.load(Ordering::Acquire) {565
- return Err(Error::interrupted());566
- }567
- let guard = SyncPause {568
- sync: context.sync.clone(),569
- shutdown: context.repair_shutdown.clone(),570
- gate: Some(gate),571
- };572
- context.sync.stop().await;573
- Ok(guard)420
+fn failed_report(421
+ room_id: ruma::OwnedRoomId,422
+ policy: HistoryRepairPolicy,423
+ error: Error,424
+) -> HistoryRepairReport {425
+ let mut report = report(room_id, policy);426
+ report.phase = HistoryRepairPhase::Paused;427
+ report.failure = Some(error);428
+ report574429
}575430
576431
fn cache_error(source: matrix_sdk::event_cache::EventCacheError) -> Error {577
- Error::with_source(578
- ErrorKind::Store,579
- "room history cache operation failed",580
- source,581
- )432
+ Error::with_source(ErrorKind::Store, "room history operation failed", source)582433
}583434
584
-#[cfg(test)]585
-mod tests {586
- use super::*;587
-588
- #[test]589
- fn threshold_requires_known_coverage_and_an_independent_boundary() {590
- let policy = HistoryRepairPolicy::BridgeShortGaps { threshold_ms: 100 };591
- assert!(should_bridge(&policy, Some(100), true));592
- assert!(should_bridge(&policy, Some(99), true));593
- assert!(!should_bridge(&policy, Some(101), true));594
- assert!(!should_bridge(&policy, None, true));595
- assert!(!should_bridge(&policy, Some(1), false));596
- assert!(!should_bridge(597
- &HistoryRepairPolicy::BridgeShortGaps { threshold_ms: 0 },598
- Some(0),599
- true600
- ));601
- assert!(should_bridge(&HistoryRepairPolicy::Bridge, None, false));602
- assert!(!should_bridge(603
- &HistoryRepairPolicy::StartRecent,604
- Some(1),605
- true606
- ));607
- }435
+fn room_busy() -> Error {436
+ Error::new(437
+ ErrorKind::RoomBusy,438
+ "room history operation is already running",439
+ )608440
}@@ -6,11 +6,11 @@ use ruma::OwnedRoomId;
66
use tokio::sync::mpsc;77
88
use super::executor::{AbortHandle, JoinError, TaskSet, is_panic, resume_unwind};9
-use super::repair::{InstallRequest, RepairController};9
+use super::repair::{Completion, RepairController};1010
use super::timeline::{SessionContext, TaskGuard, TimelineSession};1111
use crate::{12
- Error, ErrorKind, Event, HistoryRepairCommand, TimelineCommand, TimelineId, TypingState,13
- transport::Responder,12
+ Error, ErrorKind, Event, HistoryRepairCommand, RecentHistoryState, TimelineCommand, TimelineId,13
+ TypingState, transport::Responder,1414
};1515
1616
pub(super) enum ActorRequest {@@ -93,9 +93,6 @@ impl RoomActor {
9393
else {9494
return;9595
};96
- for reply in self.opening.drain(..) {97
- reply.done(Ok(()));98
- }9996
let (typing_guard, typing) = self.room.subscribe_to_typing_notifications();10097
let mut typing_watcher = TaskGuard(self.context.supervisor.spawn(10198
"typing watcher",@@ -107,8 +104,14 @@ impl RoomActor {
107104
work: TaskSet::new(),108105
};109106
let mut repair = RepairController::new(self.room.clone(), self.context.clone());107
+ repair.start_recent().await;108
+ for reply in self.opening.drain(..) {109
+ reply.done(Ok(()));110
+ }110111
let close = self.serve_open(&mut timelines, &mut repair).await;111
- repair.shutdown().await;112
+ if let Some(completion) = repair.shutdown().await {113
+ self.finish_repair(completion, &mut timelines).await;114
+ }112115
if close.is_none() {113116
self.requests.close();114117
let _ = self.notes.send(ActorNote::Leaving {@@ -137,6 +140,46 @@ impl RoomActor {
137140
}138141
}139142
143
+ 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
+ }153
+ }154
+ 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
+ {177
+ resolved = Err(Error::interrupted());178
+ cancellation = Err(Error::interrupted());179
+ }180
+ completion.resolve(resolved, cancellation);181
+ }182
+140183
/// returns the close request's responder, if a close is why we stopped141184
async fn serve_open(142185
&mut self,@@ -167,9 +210,9 @@ impl RoomActor {
167210
repair.execute(command, reply).await;168211
}169212
},170
- install = repair.next_install(), if repair.is_running() => {171
- if let Some(install) = install {172
- timelines.install_history(install, &self.room, &self.context).await;213
+ completion = repair.next_completion(), if repair.is_running() => {214
+ if let Some(completion) = completion {215
+ self.finish_repair(completion, timelines).await;173216
}174217
}175218
Some(built) = timelines.threads.builds.join_next(), if !timelines.threads.builds.is_empty() => {@@ -240,88 +283,6 @@ struct Timelines {
240283
}241284
242285
impl Timelines {243
- async fn install_history(244
- &mut self,245
- install: InstallRequest,246
- room: &Room,247
- context: &SessionContext,248
- ) {249
- let InstallRequest {250
- events,251
- prev_batch,252
- reply,253
- } = install;254
- let result = self255
- .install_history_inner(events, prev_batch, room, context)256
- .await;257
- let _ = reply.send(result);258
- }259
-260
- async fn install_history_inner(261
- &mut self,262
- events: Vec<matrix_sdk::deserialized_responses::TimelineEvent>,263
- prev_batch: Option<String>,264
- room: &Room,265
- context: &SessionContext,266
- ) -> Result<(), Error> {267
- let threads_capacity = self.threads.capacity;268
- self.threads.close().await;269
- self.threads = Threads::new(threads_capacity);270
- self.room.pause_for_reseed().await;271
-272
- let reseed = async {273
- let (cache, _drop_handles) = room.event_cache().await.map_err(|source| {274
- Error::with_source(ErrorKind::Timeline, "history event cache failed", source)275
- })?;276
- cache.reseed(events, prev_batch).await.map_err(|source| {277
- Error::with_source(ErrorKind::Timeline, "history cache reseed failed", source)278
- })279
- }280
- .await;281
- if let Err(error) = reseed {282
- return match self.room.resume_after_failed_reseed().await {283
- Ok(()) => Err(error),284
- Err(restart_error) => Err(restart_error),285
- };286
- }287
-288
- let timeline = match build_timeline(289
- room,290
- TimelineFocus::Live {291
- hide_threaded_events: true,292
- },293
- )294
- .await295
- {296
- Ok(timeline) => timeline,297
- Err(source) => {298
- let error =299
- Error::with_source(ErrorKind::Timeline, "room timeline rebuild failed", source);300
- return match self.room.resume_after_failed_reseed().await {301
- Ok(()) => Err(error),302
- Err(restart_error) => Err(restart_error),303
- };304
- }305
- };306
- let timeline_id = self.room.id.clone();307
- let mut context = context.clone();308
- context.initial_history_limit = 0;309
- let timeline = match TimelineSession::start(timeline_id, timeline, context).await {310
- Ok(timeline) => timeline,311
- Err(source) => {312
- let error =313
- Error::with_source(ErrorKind::Timeline, "room timeline reset failed", source);314
- return match self.room.resume_after_failed_reseed().await {315
- Ok(()) => Err(error),316
- Err(restart_error) => Err(restart_error),317
- };318
- }319
- };320
- let previous = std::mem::replace(&mut self.room, timeline);321
- drop(previous);322
- Ok(())323
- }324
-325286
async fn execute(&mut self, command: TimelineCommand, reply: Responder, room: &Room) {326287
match command {327288
TimelineCommand::OpenThread {@@ -12,10 +12,9 @@ use matrix_sdk::{
1212
},1313
},1414
};15
-use matrix_sdk_ui::sync_service::SyncService;1615
use matrix_sdk_ui::timeline::{Timeline, TimelineEventItemId};1716
use ruma::{OwnedEventId, OwnedUserId};18
-use tokio::sync::{Mutex, mpsc};17
+use tokio::sync::mpsc;1918
2019
use super::executor::{EngineBoxFuture, JoinHandle, TaskSet, spawn};2120
use crate::{@@ -68,9 +67,6 @@ pub(super) struct SessionContext {
6867
pub(super) events: EventSender,6968
pub(super) media: Arc<MediaService>,7069
pub(super) mentions: crate::mentions::service::Handle,71
- pub(super) sync: Arc<SyncService>,72
- pub(super) repair_gate: Arc<Mutex<()>>,73
- pub(super) repair_shutdown: Arc<std::sync::atomic::AtomicBool>,7470
pub(super) current_user_id: Option<OwnedUserId>,7571
pub(super) initial_history_limit: u16,7672
pub(super) supervisor: Supervisor,@@ -118,7 +114,7 @@ impl TimelineSession {
118114
member_fetch,119115
context,120116
};121
- if session.context.initial_history_limit > 0 {117
+ if session.context.initial_history_limit > 0 && !matches!(session.id, TimelineId::Room(_)) {122118
session.paginate(session.context.initial_history_limit, None);123119
}124120
Ok(session)@@ -135,6 +131,19 @@ impl TimelineSession {
135131
Ok(())136132
}137133
134
+ pub(super) async fn publish_history(&mut self) -> Result<(), Error> {135
+ self.timeline136
+ .flush_history()137
+ .await138
+ .map_err(timeline_error)?;139
+ self.resync().await?;140
+ self.context141
+ .events142
+ .publish_barrier()143
+ .await144
+ .map_err(|_| Error::interrupted())145
+ }146
+138147
pub(super) fn paginate(&self, limit: u16, reply: Option<Responder>) {139148
if let Err(error) = self.pagination.try_send(PaginationRequest { limit, reply }) {140149
let (request, error) = match error {@@ -153,10 +162,6 @@ impl TimelineSession {
153162
}154163
}155164
156
- pub(super) async fn pause_for_reseed(&mut self) {157
- self.stop_without_close().await;158
- }159
-160165
async fn stop_without_close(&mut self) {161166
self.member_fetch.abort();162167
self.pagination_worker.abort();@@ -168,19 +173,6 @@ impl TimelineSession {
168173
self.context.media.release(&self.scope).await;169174
}170175
171
- pub(super) async fn resume_after_failed_reseed(&mut self) -> Result<(), Error> {172
- self.watcher = start_watcher(&self.id, &self.timeline, &self.context, &self.scope)173
- .await174
- .map_err(|_| Error::new(ErrorKind::Timeline, "timeline reset could not be sent"))?;175
- (self.pagination, self.pagination_worker) =176
- start_pagination_worker(&self.id, &self.timeline, &self.context);177
- self.member_fetch = self.context.supervisor.spawn("member fetch", {178
- let timeline = self.timeline.clone();179
- async move { timeline.fetch_members().await }180
- });181
- Ok(())182
- }183
-184176
/// pending paginations answer `Interrupted` as their responders drop185177
pub(super) async fn close(mut self) {186178
self.stop_without_close().await;@@ -64,17 +64,18 @@ pub use model::{
6464
MessageEditContent, MessageKind, MessageMentions, NotificationCommand, OwnDevice, PackCommand,6565
PackImage, PackImageInfo, PackUsage, Patch, PowerLevelChanges, Profile, PushNotification,6666
PushNotificationEvent, PushNotificationRoom, PushNotificationSender, PushNotificationStatus,67
- Reaction, RecoveryState, ReplyDetails, ReplyReference, Retain, RichText, RoomCategory,68
- RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership, RoomNotificationMode,69
- RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary, RoomVisibility, SendState,70
- Sender, SpaceChild, Termination, ThreadSummary, TimelineCommand, TimelineContent, TimelineDiff,71
- TimelineDiffError, TimelineEvent, TimelineId, TimelineItem, TimelineItemId, TypingState,72
- UploadDetails, UploadOutcome, UploadThumbnail, UtdCause, VerificationAction,73
- VerificationFailure, VerificationState, VerificationTarget,67
+ Reaction, RecentHistoryState, RecoveryState, ReplyDetails, ReplyReference, Retain, RichText,68
+ RoomCategory, RoomCommand, RoomHero, RoomListFilter, RoomMatcher, RoomMembership,69
+ RoomNotificationMode, RoomNotificationSettings, RoomPowerLevels, RoomPreset, RoomSummary,70
+ RoomVisibility, SendState, Sender, SpaceChild, Termination, ThreadSummary, TimelineCommand,71
+ TimelineContent, TimelineDiff, TimelineDiffError, TimelineEvent, TimelineId, TimelineItem,72
+ TimelineItemId, TypingState, UploadDetails, UploadOutcome, UploadThumbnail, UtdCause,73
+ VerificationAction, VerificationFailure, VerificationState, VerificationTarget,7474
};7575
pub use repair_model::{76
- HistoryContinuity, HistoryRepairBudget, HistoryRepairCommand, HistoryRepairPhase,77
- HistoryRepairPolicy, HistoryRepairReport, HistoryRepairStop, HistoryRepairStrategy,76
+ HistoryContinuity, HistoryGap, HistoryGapLifecycle, HistoryRepairBudget, HistoryRepairCommand,77
+ HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairReport, HistoryRepairStop,78
+ HistoryRepairStrategy,7879
};7980
pub use ruma;8081
#[doc(hidden)]@@ -20,9 +20,10 @@ use tokio::sync::broadcast;
2020
2121
use crate::{2222
ClientHandle, Command, ConnectionState, EncryptionState, EngineFailure, ErrorKind, Event,23
- HistoryRepairReport, HistoryState, ImagePacks, RoomSummary, TimelineCommand, TimelineDiff,24
- TimelineId, TimelineItem, TypingState, UploadOutcome, VerificationState, VerificationTarget,25
- engine::executor::EngineBoxFuture, transport::PublicationRegistration,23
+ HistoryRepairReport, HistoryState, ImagePacks, RecentHistoryState, RoomSummary,24
+ TimelineCommand, TimelineDiff, TimelineId, TimelineItem, TypingState, UploadOutcome,25
+ VerificationState, VerificationTarget, engine::executor::EngineBoxFuture,26
+ transport::PublicationRegistration,2627
};2728
2829
#[cfg(not(target_family = "wasm"))]@@ -49,6 +50,8 @@ pub struct State {
4950
pub rooms: Vector<RoomSummary>,5051
pub image_packs: ImagePacks,5152
pub timelines: imbl::HashMap<TimelineId, TimelineState>,53
+ #[cfg_attr(feature = "serde", serde(default))]54
+ pub recent_history: imbl::HashMap<OwnedRoomId, RecentHistoryState>,5255
pub repairs: imbl::HashMap<OwnedRoomId, HistoryRepairReport>,5356
pub typing: imbl::HashMap<OwnedRoomId, Vec<OwnedUserId>>,5457
pub uploads: imbl::HashMap<OwnedTransactionId, UploadState>,@@ -136,6 +139,10 @@ pub enum StateChange {
136139
timeline_id: TimelineId,137140
state: HistoryState,138141
},142
+ RecentHistory {143
+ room_id: OwnedRoomId,144
+ state: RecentHistoryState,145
+ },139146
HistoryRepair(HistoryRepairReport),140147
Typing(TypingState),141148
Upload {@@ -209,6 +216,7 @@ impl Mirror {
209216
rooms: Vector::new(),210217
image_packs: ImagePacks::default(),211218
timelines: imbl::HashMap::new(),219
+ recent_history: imbl::HashMap::new(),212220
repairs: imbl::HashMap::new(),213221
typing: imbl::HashMap::new(),214222
uploads: imbl::HashMap::new(),@@ -363,6 +371,16 @@ impl Mirror {
363371
state: history,364372
})365373
}),374
+ Event::RecentHistoryChanged {375
+ room_id,376
+ state: recent,377
+ } => self.publish(|state| {378
+ state.recent_history.insert(room_id.clone(), recent.clone());379
+ Some(StateChange::RecentHistory {380
+ room_id,381
+ state: recent,382
+ })383
+ }),366384
Event::HistoryRepairChanged(report) => self.publish(|state| {367385
state.repairs.insert(report.room_id.clone(), report.clone());368386
Some(StateChange::HistoryRepair(report))@@ -379,6 +397,11 @@ impl Mirror {
379397
acknowledgement.complete(true);380398
}381399
}400
+ Event::PublicationBarrier { acknowledgement } => {401
+ if let Some(acknowledgement) = acknowledgement {402
+ acknowledgement.complete(true);403
+ }404
+ }382405
Event::TimelineClosed { timeline_id } => {383406
self.cancel_resync(&timeline_id);384407
self.publish(|state| {@@ -907,6 +907,10 @@ pub enum Event {
907907
timeline_id: TimelineId,908908
state: HistoryState,909909
},910
+ RecentHistoryChanged {911
+ room_id: OwnedRoomId,912
+ state: RecentHistoryState,913
+ },910914
HistoryRepairChanged(crate::HistoryRepairReport),911915
#[doc(hidden)]912916
HistoryRepairPublished {@@ -914,6 +918,11 @@ pub enum Event {
914918
#[cfg_attr(feature = "serde", serde(skip))]915919
acknowledgement: Option<crate::PublicationAck>,916920
},921
+ #[doc(hidden)]922
+ PublicationBarrier {923
+ #[cfg_attr(feature = "serde", serde(skip))]924
+ acknowledgement: Option<crate::PublicationAck>,925
+ },917926
/// the timeline was closed, or its room was left, and nothing more comes for it918927
/// until another `TimelineReset`919928
TimelineClosed {@@ -1376,6 +1385,7 @@ pub struct OwnDevice {
13761385
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]13771386
pub enum TimelineItem {13781387
Event(Box<TimelineEvent>),1388
+ HistoryGap(crate::HistoryGap),13791389
DayDivider { timestamp_ms: u64 },13801390
ReadMarker,13811391
TimelineStart,@@ -2111,6 +2121,15 @@ pub enum HistoryState {
21112121
21122122
#[derive(Clone, Debug, Eq, PartialEq)]21132123
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]2124
+pub enum RecentHistoryState {2125
+ Idle,2126
+ Loading,2127
+ Loaded,2128
+ Failed(String),2129
+}2130
+2131
+#[derive(Clone, Debug, Eq, PartialEq)]2132
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]21142133
#[non_exhaustive]21152134
pub struct EncryptionState {21162135
pub device_id: OwnedDeviceId,@@ -19,9 +19,10 @@ use ruma::{
1919
};2020
2121
use crate::{22
- Change, MediaId, MembershipChange, Message, MessageKind, MessageMentions, Reaction,23
- ReplyDetails, ReplyReference, SendState, Sender, ThreadSummary, TimelineContent, TimelineDiff,24
- TimelineEvent, TimelineItem, TimelineItemId, UtdCause,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,2526
};2627
2728
use super::link_preview::sender_previews;@@ -68,6 +69,41 @@ impl Normaliser<'_> {
6869
TimelineItemKind::Virtual(VirtualTimelineItem::TimelineStart) => {6970
TimelineItem::TimelineStart7071
}72
+ 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)96
+ }97
+ };98
+ TimelineItem::HistoryGap(HistoryGap {99
+ id: gap.id.clone(),100
+ older_boundary: gap.older_boundary.clone(),101
+ newer_boundary: gap.newer_boundary.clone(),102
+ lifecycle,103
+ continuity,104
+ failure,105
+ })106
+ }71107
}72108
}73109
@@ -1,11 +1,11 @@
1
-use ruma::OwnedRoomId;1
+use ruma::{OwnedEventId, OwnedRoomId, OwnedTransactionId};22
33
#[cfg(feature = "serde")]44
use serde::{Deserialize, Serialize};55
66
use crate::Error;77
8
-#[derive(Clone, Debug, PartialEq)]8
+#[derive(Clone, Debug, PartialEq, Eq)]99
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]1010
pub enum HistoryRepairPolicy {1111
StartRecent,@@ -30,8 +30,8 @@ pub struct HistoryRepairBudget {
3030
impl Default for HistoryRepairBudget {3131
fn default() -> Self {3232
Self {33
- max_pages: 10,34
- max_events: 1000,33
+ max_pages: 4,34
+ max_events: 200,3535
max_duration_ms: 15_000,3636
}3737
}@@ -55,11 +55,36 @@ pub enum HistoryRepairCommand {
5555
StartRecent {5656
room_id: OwnedRoomId,5757
},58
+ FillGap {59
+ room_id: OwnedRoomId,60
+ gap_id: OwnedTransactionId,61
+ },5862
GetPolicy,5963
SetPolicy(HistoryRepairPolicy),6064
}6165
62
-#[derive(Clone, Debug, PartialEq)]66
+#[derive(Clone, Debug, PartialEq, Eq)]67
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]68
+pub struct HistoryGap {69
+ pub id: OwnedTransactionId,70
+ pub older_boundary: Option<OwnedEventId>,71
+ pub newer_boundary: Option<OwnedEventId>,72
+ pub lifecycle: HistoryGapLifecycle,73
+ pub continuity: HistoryContinuity,74
+ pub failure: Option<String>,75
+}76
+77
+#[derive(Clone, Debug, PartialEq, Eq)]78
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]79
+pub enum HistoryGapLifecycle {80
+ Idle,81
+ Fetching,82
+ Paused,83
+ Failed,84
+ Unavailable,85
+}86
+87
+#[derive(Clone, Debug, PartialEq, Eq)]6388
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]6489
pub enum HistoryRepairStrategy {6590
Recent,@@ -67,7 +92,7 @@ pub enum HistoryRepairStrategy {
6792
Bridge,6893
}6994
70
-#[derive(Clone, Debug, PartialEq)]95
+#[derive(Clone, Debug, PartialEq, Eq)]7196
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]7297
pub enum HistoryContinuity {7398
Unresolved,@@ -98,6 +123,8 @@ pub enum HistoryRepairStop {
98123
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]99124
pub struct HistoryRepairReport {100125
pub room_id: OwnedRoomId,126
+ #[cfg_attr(feature = "serde", serde(default))]127
+ pub gap_id: Option<OwnedTransactionId>,101128
pub requested_policy: HistoryRepairPolicy,102129
pub strategy: HistoryRepairStrategy,103130
pub coverage_duration_ms: Option<u64>,@@ -132,6 +159,7 @@ mod tests {
132159
assert_eq!(decoded_command, command);133160
134161
let report = HistoryRepairReport {162
+ gap_id: None,135163
room_id: room_id!("!room:example.org").to_owned(),136164
requested_policy: HistoryRepairPolicy::Bridge,137165
strategy: HistoryRepairStrategy::Overlap,@@ -375,6 +375,18 @@ impl ClientHandle {
375375
.await376376
}377377
378
+ pub async fn fill_history_gap(379
+ &self,380
+ room_id: OwnedRoomId,381
+ gap_id: ruma::OwnedTransactionId,382
+ ) -> Result<(), Error> {383
+ self.run(Command::HistoryRepair(HistoryRepairCommand::FillGap {384
+ room_id,385
+ gap_id,386
+ }))387
+ .await388
+ }389
+378390
pub async fn start_recent_history(379391
&self,380392
room_id: OwnedRoomId,@@ -1620,6 +1632,26 @@ impl EventSender {
16201632
self.events.send(event).await.map_err(|_| SendEventError)16211633
}16221634
1635
+ pub(crate) async fn publish_barrier(&self) -> Result<(), SendEventError> {1636
+ let Some((acknowledgement, published)) = self.publication.acknowledgement() else {1637
+ return Ok(());1638
+ };1639
+ if let Err(error) = self1640
+ .send(Event::PublicationBarrier {1641
+ acknowledgement: Some(acknowledgement.clone()),1642
+ })1643
+ .await1644
+ {1645
+ acknowledgement.complete(false);1646
+ return Err(error);1647
+ }1648
+ drop(acknowledgement);1649
+ match published.await {1650
+ Ok(true) => Ok(()),1651
+ Ok(false) | Err(_) => Err(SendEventError),1652
+ }1653
+ }1654
+16231655
pub(crate) async fn publish_repair(16241656
&self,16251657
report: crate::HistoryRepairReport,@@ -1812,6 +1844,7 @@ mod tests {
18121844
18131845
fn report() -> HistoryRepairReport {18141846
HistoryRepairReport {1847
+ gap_id: None,18151848
room_id: room_id!("!room:example.org").to_owned(),18161849
requested_policy: HistoryRepairPolicy::Bridge,18171850
strategy: HistoryRepairStrategy::Bridge,@@ -4,7 +4,8 @@ use std::time::Duration;
44
55
use macltok::{66
ClientHandle, Command, Engine, EngineConfig, ErrorKind, Event, EventStream, HistoryState,7
- MediaSize, MessageContent, RoomCommand, TimelineCommand, TimelineId, TimelineItem,7
+ MediaSize, MessageContent, RecentHistoryState, RoomCommand, TimelineCommand, TimelineId,8
+ TimelineItem,89
};910
use matrix_sdk::{1011
ruma::{api::client::sync::sync_events::v5, event_id, mxc_uri, room_id},@@ -315,9 +316,9 @@ async fn closes_and_reopens_a_room_in_command_order() {
315316
.try_send(Command::Room(RoomCommand::Open(room_id.to_owned())))316317
.unwrap();317318
318
- // biased select drains every buffered reset before it ever reports a ready reply,319
- // so the engine can race ahead and land both resets before either open is observed319
+ // biased select drains buffered events before it reports ready replies320320
let mut resets = 0;321
+ let mut recent_active = false;321322
let mut order = Vec::new();322323
let (mut close_done, mut replacement_open_done, mut second_close_done, mut final_open_done) =323324
(false, false, false, false);@@ -326,11 +327,20 @@ async fn closes_and_reopens_a_room_in_command_order() {
326327
tokio::select! {327328
biased;328329
event = events.recv() => {329
- if let Event::TimelineReset { timeline_id, .. } = event.expect("event stream closed")330
- && timeline_id.room_id() == room_id331
- {332
- resets += 1;333
- assert!(resets <= 2, "a room generation reset more than once");330
+ 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
+ {340
+ resets += 1;341
+ assert!(resets <= 2, "a room generation reset more than once");342
+ }343
+ _ => {}334344
}335345
}336346
result = &mut close, if !close_done => {@@ -3,9 +3,8 @@ mod support;
33
use std::time::Duration;44
55
use macltok::{6
- ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryContinuity,7
- HistoryRepairBudget, HistoryRepairPhase, HistoryRepairPolicy, HistoryRepairStop,8
- HistoryRepairStrategy, Termination, TimelineItem,6
+ ClientHandle, Engine, EngineConfig, EngineError, Event, EventStream, HistoryGap,7
+ HistoryRepairPhase, HistoryRepairPolicy, RecentHistoryState, Termination, TimelineItem,98
};109
#[cfg(feature = "mirror")]1110
use macltok::{Mirror, TimelineId};@@ -26,7 +25,7 @@ struct TestRoom {
2625
running: tokio::task::JoinHandle<Result<Termination, EngineError>>,2726
}2827
29
-async fn open_room(room_id: &RoomId) -> TestRoom {28
+async fn start_room(room_id: &RoomId) -> TestRoom {3029
let server = MatrixMockServer::new().await;3130
server3231
.mock_sliding_sync()@@ -38,15 +37,8 @@ async fn open_room(room_id: &RoomId) -> TestRoom {
3837
server.sync_joined_room(&client, room_id).await;3938
let sync_client = client.clone();4039
let config = EngineConfig::default().with_initial_history_limit(0);41
- let (engine, handle, mut events) = Engine::from_client(client, config).await.unwrap();40
+ let (engine, handle, events) = Engine::from_client(client, config).await.unwrap();4241
let running = tokio::spawn(engine.run());43
-44
- handle.open_room(room_id.to_owned()).await.unwrap();45
- receive_until(&mut events, |event| {46
- matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)47
- })48
- .await;49
-5042
TestRoom {5143
server,5244
sync_client,@@ -56,7 +48,15 @@ async fn open_room(room_id: &RoomId) -> TestRoom {
5648
}5749
}5850
59
-async fn sync_cached_event(room: &mut TestRoom, room_id: &RoomId, id: &EventId, body: &str) {51
+async fn open_room(room: &mut TestRoom, room_id: &RoomId) {52
+ room.handle.open_room(room_id.to_owned()).await.unwrap();53
+ receive_until(&mut room.events, |event| {54
+ matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)55
+ })56
+ .await;57
+}58
+59
+async fn cache_event_before_open(room: &TestRoom, room_id: &RoomId, id: &EventId, body: &str) {6060
room.server6161
.sync_room(6262
&room.sync_client,@@ -68,12 +68,6 @@ async fn sync_cached_event(room: &mut TestRoom, room_id: &RoomId, id: &EventId,
6868
),6969
)7070
.await;71
- receive_until(&mut room.events, |event| {72
- event_messages(event)73
- .into_iter()74
- .any(|event| event.event_id.as_deref() == Some(id))75
- })76
- .await;7771
}7872
7973
fn history_event(@@ -89,94 +83,178 @@ fn history_event(
8983
.into_raw_timeline()9084
}9185
92
-fn report_budget(max_pages: u16) -> HistoryRepairBudget {93
- HistoryRepairBudget {94
- max_pages,95
- ..HistoryRepairBudget::default()86
+async fn wait_for_recent(room: &mut TestRoom, room_id: &RoomId) -> RecentHistoryState {87
+ let event = receive_until(&mut room.events, |event| {88
+ matches!(event, Event::RecentHistoryChanged { room_id: id, state, .. }89
+ if id == room_id && !matches!(state, RecentHistoryState::Loading))90
+ })91
+ .await;92
+ match event {93
+ Event::RecentHistoryChanged { state, .. } => state,94
+ _ => unreachable!(),95
+ }96
+}97
+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
+ )103
+ .await104
+ {105
+ Event::HistoryRepairChanged(report) => report,106
+ _ => unreachable!(),96107
}97108
}98109
110
+fn gap_in(items: &imbl::Vector<TimelineItem>) -> Option<HistoryGap> {111
+ items.iter().find_map(|item| match item {112
+ TimelineItem::HistoryGap(gap) => Some(gap.clone()),113
+ _ => None,114
+ })115
+}116
+99117
#[tokio::test]100
-async fn recent_repair_starts_without_a_cursor_and_keeps_its_end_for_pagination() {118
+async fn automatic_recent_load_is_bounded_and_keeps_cached_history_behind_a_gap() {101119
let room_id = room_id!("!recent-repair:localhost");102
- let stale_id = event_id!("$stale:localhost");103
- let fresh_id = event_id!("$fresh:localhost");104
- let older_id = event_id!("$older:localhost");105
- let mut room = open_room(room_id).await;106
- sync_cached_event(&mut room, room_id, stale_id, "cached before repair").await;107
-108
- room.server109
- .mock_room_messages()110
- .ok(RoomMessagesResponseTemplate::default()111
- .end_token("recent-prev")112
- .events(vec![history_event(room_id, fresh_id, "fresh page")]))113
- .mock_once()114
- .mount()115
- .await;116
- let report = room117
- .handle118
- .history_repair(119
- room_id.to_owned(),120
- Some(HistoryRepairPolicy::StartRecent),121
- HistoryRepairBudget::default(),122
- )120
+ let old_id = event_id!("$old:localhost");121
+ let new_ids = [122
+ event_id!("$fresh-a:localhost"),123
+ event_id!("$fresh-b:localhost"),124
+ event_id!("$fresh-c:localhost"),125
+ event_id!("$fresh-d:localhost"),126
+ ];127
+ let mut room = start_room(room_id).await;128
+ room.handle129
+ .set_history_repair_policy(HistoryRepairPolicy::Bridge)123130
.await124131
.unwrap();125
- assert_eq!(report.strategy, HistoryRepairStrategy::Recent);126
- assert_eq!(report.phase, HistoryRepairPhase::Ready);127
- assert!(report.recent_available);128
-132
+ for (index, id) in new_ids.iter().enumerate() {133
+ let token = format!("recent-{index}");134
+ let response = RoomMessagesResponseTemplate::default()135
+ .end_token(token.clone())136
+ .events(vec![history_event(room_id, id, "recent event")]);137
+ if index > 0 {138
+ let previous = format!("recent-{}", index - 1);139
+ room.server140
+ .mock_room_messages()141
+ .match_from(&previous)142
+ .ok(response)143
+ .mock_once()144
+ .mount()145
+ .await;146
+ } else {147
+ room.server148
+ .mock_room_messages()149
+ .ok(response)150
+ .mock_once()151
+ .mount()152
+ .await;153
+ }154
+ }155
+ cache_event_before_open(&room, room_id, old_id, "retained old event").await;156
+ open_room(&mut room, room_id).await;129157
let reset = receive_until(&mut room.events, |event| {130
- matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)131
- && event_messages(event)132
- .into_iter()133
- .any(|event| event.event_id.as_deref() == Some(fresh_id))158
+ let items = match event {159
+ Event::TimelineReset { timeline_id, items } if timeline_id.room_id() == room_id => {160
+ items161
+ }162
+ _ => return false,163
+ };164
+ 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
+ })134175
})135176
.await;136
- let reset_ids = event_messages(&reset)137
- .into_iter()138
- .filter_map(|event| event.event_id.as_ref().map(ToString::to_string))139
- .collect::<Vec<_>>();140
- assert_eq!(reset_ids, vec![fresh_id.to_string()]);141
-177
+ let Event::TimelineReset { items, .. } = reset else {178
+ unreachable!();179
+ };180
+ assert!(gap_in(&items).is_some());181
+ assert_eq!(182
+ wait_for_recent(&mut room, room_id).await,183
+ RecentHistoryState::Loaded184
+ );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);142191
let requests = room.server.received_requests().await.unwrap();143192
let messages = requests144193
.iter()145194
.filter(|request| request.url.path().ends_with("/messages"))146195
.collect::<Vec<_>>();147
- assert_eq!(messages.len(), 1);196
+ assert_eq!(messages.len(), 4);148197
assert!(!messages[0].url.query_pairs().any(|(key, _)| key == "from"));198
+ shut_down(&room.handle, &mut room.events, room.running).await;199
+}149200
201
+#[tokio::test]202
+async fn explicit_recent_start_coalesces_with_the_automatic_load() {203
+ let room_id = room_id!("!recent-coalesce:localhost");204
+ let fresh_id = event_id!("$coalesced:localhost");205
+ let mut room = start_room(room_id).await;150206
room.server151207
.mock_room_messages()152
- .match_from("recent-prev")153
- .ok(154
- RoomMessagesResponseTemplate::default().events(vec![history_event(155
- room_id,156
- older_id,157
- "older page",158
- )]),159
- )208
+ .ok(RoomMessagesResponseTemplate::default()209
+ .with_delay(Duration::from_millis(300))210
+ .events(vec![history_event(room_id, fresh_id, "shared result")]))160211
.mock_once()161212
.mount()162213
.await;163
- room.handle164
- .paginate(room_id.to_owned().into(), 10)165
- .await166
- .unwrap();167
- receive_until(&mut room.events, |event| {168
- event_messages(event).into_iter().any(|event| {169
- support::message(event).is_some_and(|message| message.body == "older page")170
- })214
+ open_room(&mut room, room_id).await;215
+ tokio::time::timeout(Duration::from_secs(5), async {216
+ loop {217
+ let requested = room218
+ .server219
+ .received_requests()220
+ .await221
+ .unwrap()222
+ .iter()223
+ .any(|request| request.url.path().ends_with("/messages"));224
+ if requested {225
+ break;226
+ }227
+ tokio::task::yield_now().await;228
+ }171229
})172
- .await;230
+ .await231
+ .expect("automatic recent request did not start");173232
233
+ let report = room234
+ .handle235
+ .start_recent_history(room_id.to_owned())236
+ .await237
+ .unwrap();238
+ assert_eq!(report.pages, 1);239
+ assert_eq!(report.events, 1);240
+ assert_eq!(241
+ wait_for_recent(&mut room, room_id).await,242
+ RecentHistoryState::Loaded243
+ );244
+ let requests = room.server.received_requests().await.unwrap();245
+ assert_eq!(246
+ requests247
+ .iter()248
+ .filter(|request| request.url.path().ends_with("/messages"))249
+ .count(),250
+ 1251
+ );174252
shut_down(&room.handle, &mut room.events, room.running).await;175253
}176254
177255
#[cfg(feature = "mirror")]178256
#[tokio::test]179
-async fn repair_ack_waits_until_the_mirror_has_applied_the_reset_and_report() {257
+async fn fill_gap_returns_after_the_mirror_contains_the_committed_rows() {180258
let server = MatrixMockServer::new().await;181259
server182260
.mock_sliding_sync()@@ -184,10 +262,13 @@ async fn repair_ack_waits_until_the_mirror_has_applied_the_reset_and_report() {
184262
.mount()185263
.await;186264
let client = server.client_builder().build().await;187
- let room_id = room_id!("!repair-mirror:localhost");265
+ let room_id = room_id!("!fill-gap-mirror:localhost");188266
server.mock_room_state_encryption().plain().mount().await;189267
server.sync_joined_room(&client, room_id).await;190268
let sync_client = client.clone();269
+ let old_id = event_id!("$gap-old:localhost");270
+ let fresh_id = event_id!("$gap-fresh:localhost");271
+ let middle_id = event_id!("$gap-middle:localhost");191272
let (engine, handle, events) = Engine::from_client(192273
client,193274
EngineConfig::default().with_initial_history_limit(0),@@ -197,79 +278,114 @@ async fn repair_ack_waits_until_the_mirror_has_applied_the_reset_and_report() {
197278
let running = tokio::spawn(engine.run());198279
let (mirror, mirror_handle) = Mirror::new(handle.clone(), events);199280
let mirror_running = tokio::spawn(mirror.run());200
-201
- handle.open_room(room_id.to_owned()).await.unwrap();202
- let stale_id = event_id!("$mirror-stale:localhost");281
+ server282
+ .mock_room_messages()283
+ .ok(RoomMessagesResponseTemplate::default()284
+ .end_token("recent-gap")285
+ .events(vec![history_event(room_id, fresh_id, "new window")]))286
+ .mock_once()287
+ .mount()288
+ .await;289
+ server290
+ .mock_room_messages()291
+ .match_from("recent-gap")292
+ .ok(RoomMessagesResponseTemplate::default().end_token("recent-gap"))293
+ .mock_once()294
+ .mount()295
+ .await;203296
server204297
.sync_room(205298
&sync_client,206299
JoinedRoomBuilder::new(room_id).add_timeline_event(207300
EventFactory::new()208
- .text_msg("cached event")209
- .event_id(stale_id)301
+ .text_msg("old retained")302
+ .event_id(old_id)210303
.sender(&ALICE),211304
),212305
)213306
.await;307
+ handle.open_room(room_id.to_owned()).await.unwrap();214308
let timeline_id = TimelineId::from(room_id.to_owned());215
- tokio::time::timeout(Duration::from_secs(2), async {309
+ let gap = tokio::time::timeout(Duration::from_secs(5), async {216310
loop {217
- let has_cached_event = mirror_handle311
+ if let Some(gap) = mirror_handle218312
.snapshot()219313
.timelines220314
.get(&timeline_id)221
- .is_some_and(|timeline| {222
- timeline.items.iter().any(|item| {223
- matches!(item, TimelineItem::Event(event)224
- if event.event_id.as_deref() == Some(stale_id))225
- })226
- });227
- if has_cached_event {315
+ .and_then(|timeline| gap_in(&timeline.items))316
+ {317
+ break gap;318
+ }319
+ tokio::task::yield_now().await;320
+ }321
+ })322
+ .await323
+ .expect("the recent history gap did not reach the mirror");324
+ tokio::time::timeout(Duration::from_secs(5), async {325
+ loop {326
+ if mirror_handle.snapshot().recent_history.get(room_id)327
+ == Some(&RecentHistoryState::Loaded)328
+ {228329
break;229330
}230331
tokio::task::yield_now().await;231332
}232333
})233334
.await234
- .expect("the cached timeline event did not reach the mirror");235
-236
- let fresh_id = event_id!("$mirror-fresh:localhost");335
+ .expect("automatic recent loading did not finish");237336
server238337
.mock_room_messages()239
- .ok(240
- RoomMessagesResponseTemplate::default().events(vec![history_event(241
- room_id,242
- fresh_id,243
- "fresh event",244
- )]),245
- )338
+ .match_from("recent-gap")339
+ .ok(RoomMessagesResponseTemplate::default()340
+ .with_delay(Duration::from_millis(300))341
+ .events(vec![342
+ history_event(room_id, middle_id, "middle page"),343
+ history_event(room_id, old_id, "old retained overlap"),344
+ ]))246345
.mock_once()247346
.mount()248347
.await;249
- let report = handle250
- .history_repair(251
- room_id.to_owned(),252
- Some(HistoryRepairPolicy::StartRecent),253
- HistoryRepairBudget::default(),254
- )255
- .await256
- .unwrap();257348
258
- let state = mirror_handle.snapshot();259
- assert_eq!(state.repairs.get(room_id), Some(&report));260
- let timeline = state349
+ let fill_handle = handle.clone();350
+ let target_gap_id = gap.id.clone();351
+ let fill_gap_id = target_gap_id.clone();352
+ let fill = tokio::spawn(async move {353
+ fill_handle354
+ .fill_history_gap(room_id.to_owned(), fill_gap_id)355
+ .await356
+ });357
+ tokio::time::timeout(Duration::from_secs(5), async {358
+ loop {359
+ let snapshot = mirror_handle.snapshot();360
+ 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::Bridging363
+ });364
+ if active_gap {365
+ break;366
+ }367
+ tokio::task::yield_now().await;368
+ }369
+ })370
+ .await371
+ .expect("the active repair report did not identify its target gap");372
+ fill.await.unwrap().unwrap();373
+ let snapshot = mirror_handle.snapshot();374
+ let timeline = snapshot261375
.timelines262376
.get(&timeline_id)263
- .expect("the repaired timeline was not applied before acknowledgement");264
- let ids = timeline377
+ .expect("the room timeline disappeared");378
+ let event_ids = timeline265379
.items266380
.iter()267381
.filter_map(|item| match item {268
- TimelineItem::Event(event) => event.event_id.as_ref().map(ToString::to_string),382
+ TimelineItem::Event(event) => event.event_id.as_deref(),269383
_ => None,270384
})271385
.collect::<Vec<_>>();272
- assert_eq!(ids, vec![fresh_id.to_string()]);386
+ assert!(event_ids.contains(&fresh_id));387
+ assert!(event_ids.contains(&middle_id));388
+ assert!(event_ids.contains(&old_id));273389
274390
handle.shutdown().await.unwrap();275391
assert_eq!(running.await.unwrap().unwrap(), Termination::ShutDown);@@ -277,90 +393,69 @@ async fn repair_ack_waits_until_the_mirror_has_applied_the_reset_and_report() {
277393
}278394
279395
#[tokio::test]280
-async fn failed_recent_fetch_leaves_the_existing_timeline_in_place() {281
- let room_id = room_id!("!failed-repair:localhost");282
- let stale_id = event_id!("$kept:localhost");283
- let mut room = open_room(room_id).await;284
- sync_cached_event(&mut room, room_id, stale_id, "keep this event").await;285
-396
+async fn cancelling_a_gap_fill_keeps_committed_progress_for_retry() {397
+ let room_id = room_id!("!cancel-gap-fill:localhost");398
+ let old_id = event_id!("$cancel-gap-old:localhost");399
+ let fresh_id = event_id!("$cancel-gap-fresh:localhost");400
+ let middle_id = event_id!("$cancel-gap-middle:localhost");401
+ let mut room = start_room(room_id).await;286402
room.server287403
.mock_room_messages()288
- .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({289
- "errcode": "M_FORBIDDEN",290
- "error": "history is unavailable",291
- })))404
+ .ok(RoomMessagesResponseTemplate::default()405
+ .end_token("gap-cursor-1")406
+ .events(vec![history_event(room_id, fresh_id, "recent")]))292407
.mock_once()293408
.mount()294409
.await;295
- let report = room296
- .handle297
- .history_repair(298
- room_id.to_owned(),299
- Some(HistoryRepairPolicy::StartRecent),300
- HistoryRepairBudget::default(),301
- )302
- .await303
- .unwrap();304
- assert_eq!(report.phase, HistoryRepairPhase::Paused);305
- assert!(report.failure.is_some());306
-307
- let mut reset_after_failure = false;308
- while let Ok(event) = room.events.try_recv() {309
- reset_after_failure |= matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id);310
- }311
- assert!(!reset_after_failure);312
-313
- shut_down(&room.handle, &mut room.events, room.running).await;314
-}315
-316
-#[tokio::test]317
-async fn cancelling_a_delayed_bridge_page_preserves_the_recent_stage() {318
- let room_id = room_id!("!cancel-repair:localhost");319
- let anchor_id = event_id!("$cancel-anchor:localhost");320
- let recent_id = event_id!("$cancel-recent:localhost");321
- let mut room = open_room(room_id).await;322
- sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;323
-324410
room.server325411
.mock_room_messages()326
- .ok(RoomMessagesResponseTemplate::default()327
- .end_token("cancel-prev")328
- .events(vec![history_event(room_id, recent_id, "recent")]))412
+ .match_from("gap-cursor-1")413
+ .ok(RoomMessagesResponseTemplate::default().end_token("gap-cursor-1"))329414
.mock_once()330415
.mount()331416
.await;332
- let initial = room333
- .handle334
- .history_repair(335
- room_id.to_owned(),336
- Some(HistoryRepairPolicy::Bridge),337
- report_budget(1),338
- )339
- .await340
- .unwrap();341
- assert_eq!(initial.stop, Some(HistoryRepairStop::Budget));342
- assert!(initial.recent_available);343
- receive_until(&mut room.events, |event| {344
- matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)345
- && event_messages(event)346
- .into_iter()347
- .any(|event| event.event_id.as_deref() == Some(recent_id))417
+ cache_event_before_open(&room, room_id, old_id, "old cached").await;418
+ 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())348422
})349423
.await;350
-424
+ let gap = match reset {425
+ Event::TimelineReset { items, .. } => gap_in(&items).unwrap(),426
+ _ => unreachable!(),427
+ };428
+ assert_eq!(429
+ wait_for_recent(&mut room, room_id).await,430
+ RecentHistoryState::Loaded431
+ );351432
room.server352433
.mock_room_messages()353
- .match_from("cancel-prev")434
+ .match_from("gap-cursor-1")435
+ .ok(RoomMessagesResponseTemplate::default()436
+ .end_token("gap-cursor-2")437
+ .events(vec![history_event(438
+ room_id,439
+ middle_id,440
+ "committed progress",441
+ )]))442
+ .mock_once()443
+ .mount()444
+ .await;445
+ room.server446
+ .mock_room_messages()447
+ .match_from("gap-cursor-2")354448
.ok(RoomMessagesResponseTemplate::default()355449
.with_delay(Duration::from_secs(5))356
- .events(vec![history_event(room_id, anchor_id, "cached anchor")]))450
+ .events(vec![history_event(room_id, old_id, "old overlap")]))357451
.mock_once()358452
.mount()359453
.await;360
- let repair_handle = room.handle.clone();361
- let repair = tokio::spawn(async move {362
- repair_handle363
- .continue_history_repair(room_id.to_owned(), HistoryRepairBudget::default())454
+ let fill_handle = room.handle.clone();455
+ let gap_id = gap.id.clone();456
+ let fill = tokio::spawn(async move {457
+ fill_handle458
+ .fill_history_gap(room_id.to_owned(), gap_id)364459
.await365460
});366461
tokio::time::timeout(Duration::from_secs(5), async {@@ -376,7 +471,7 @@ async fn cancelling_a_delayed_bridge_page_preserves_the_recent_stage() {
376471
&& request377472
.url378473
.query_pairs()379
- .any(|(key, value)| key == "from" && value == "cancel-prev")474
+ .any(|(key, value)| key == "from" && value == "gap-cursor-2")380475
});381476
if requested {382477
break;@@ -385,227 +480,70 @@ async fn cancelling_a_delayed_bridge_page_preserves_the_recent_stage() {
385480
}386481
})387482
.await388
- .expect("bridge request did not start");483
+ .expect("the second gap page did not start");389484
room.handle390485
.cancel_history_repair(room_id.to_owned())391486
.await392487
.unwrap();393
- let report = repair.await.unwrap().unwrap();394
- assert_eq!(report.phase, HistoryRepairPhase::Paused);395
- assert_eq!(report.stop, Some(HistoryRepairStop::Cancelled));396
- assert!(report.recent_available);397
- assert_eq!(report.pages, 1);398
-399
- let mut reset_after_cancel = false;400
- while let Ok(event) = room.events.try_recv() {401
- reset_after_cancel |= matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id);402
- }403
- assert!(!reset_after_cancel);404
-405
- shut_down(&room.handle, &mut room.events, room.running).await;406
-}407
-408
-#[tokio::test]409
-async fn bridge_reaches_the_cached_anchor_deduplicates_overlap_and_can_continue_after_budget() {410
- let room_id = room_id!("!bridge-repair:localhost");411
- let anchor_id = event_id!("$anchor:localhost");412
- let middle_id = event_id!("$middle:localhost");413
- let recent_id = event_id!("$recent:localhost");414
- let latest_id = event_id!("$latest:localhost");415
- let mut room = open_room(room_id).await;416
- sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;488
+ let error = fill.await.unwrap().unwrap_err();489
+ assert_eq!(error.kind, macltok::ErrorKind::Interrupted);417490
418491
room.server419492
.mock_room_messages()420
- .ok(RoomMessagesResponseTemplate::default()421
- .end_token("recent-prev")422
- .events(vec![423
- history_event(room_id, recent_id, "recent"),424
- history_event(room_id, middle_id, "middle"),425
- ]))426
- .mock_once()427
- .mount()428
- .await;429
- let initial = room430
- .handle431
- .history_repair(432
- room_id.to_owned(),433
- Some(HistoryRepairPolicy::Bridge),434
- report_budget(1),493
+ .match_from("gap-cursor-2")494
+ .ok(495
+ RoomMessagesResponseTemplate::default().events(vec![history_event(496
+ room_id,497
+ old_id,498
+ "old overlap",499
+ )]),435500
)436
- .await437
- .unwrap();438
- assert_eq!(initial.phase, HistoryRepairPhase::Paused);439
- assert_eq!(initial.stop, Some(HistoryRepairStop::Budget));440
- assert!(initial.recent_available);441
-442
- wiremock::Mock::given(|request: &wiremock::Request| {443
- request.url.path().ends_with("/messages")444
- && !request.url.query_pairs().any(|(key, _)| key == "from")445
- })446
- .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({447
- "start": "head-start",448
- "chunk": [449
- history_event(room_id, latest_id, "latest"),450
- history_event(room_id, recent_id, "recent overlap"),451
- history_event(room_id, middle_id, "middle overlap"),452
- ],453
- "state": [],454
- })))455
- .up_to_n_times(1)456
- .expect(1)457
- .mount(room.server.server())458
- .await;459
- room.server460
- .mock_room_messages()461
- .match_from("recent-prev")462
- .ok(RoomMessagesResponseTemplate::default()463
- .end_token("older-prev")464
- .events(vec![465
- history_event(room_id, middle_id, "middle overlap"),466
- history_event(room_id, anchor_id, "cached anchor overlap"),467
- ]))468501
.mock_once()469502
.mount()470503
.await;471
-472
- let report = room473
- .handle474
- .continue_history_repair(room_id.to_owned(), HistoryRepairBudget::default())504
+ room.handle505
+ .fill_history_gap(room_id.to_owned(), gap.id)475506
.await476507
.unwrap();477
- assert_eq!(report.strategy, HistoryRepairStrategy::Bridge);478
- assert_eq!(report.phase, HistoryRepairPhase::Ready, "{report:?}");479
- assert_eq!(report.continuity, HistoryContinuity::Verified);480
- assert_eq!(report.pages, 3);481
-482
- let reset = receive_until(&mut room.events, |event| {483
- matches!(event, Event::TimelineReset { timeline_id, .. } if timeline_id.room_id() == room_id)484
- && event_messages(event)485
- .into_iter()486
- .any(|event| event.event_id.as_deref() == Some(latest_id))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))))487515
})488516
.await;489
- let reset_ids = event_messages(&reset)490
- .into_iter()491
- .filter_map(|event| event.event_id.as_ref().map(ToString::to_string))517
+ let ids = event_messages(&final_reset)518
+ .iter()519
+ .filter_map(|event| event.event_id.as_deref())492520
.collect::<Vec<_>>();493
- assert_eq!(494
- reset_ids,495
- vec![496
- anchor_id.to_string(),497
- middle_id.to_string(),498
- recent_id.to_string(),499
- latest_id.to_string(),500
- ]501
- );502
-521
+ assert!(ids.contains(&fresh_id));522
+ assert!(ids.contains(&middle_id));523
+ assert!(ids.contains(&old_id));503524
shut_down(&room.handle, &mut room.events, room.running).await;504525
}505526
506527
#[tokio::test]507
-async fn a_recent_window_containing_the_cached_anchor_overlaps_under_every_policy() {508
- let cases = [509
- (510
- room_id!("!policy-recent:localhost"),511
- event_id!("$policy-recent-anchor:localhost"),512
- HistoryRepairPolicy::StartRecent,513
- ),514
- (515
- room_id!("!policy-bridge:localhost"),516
- event_id!("$policy-bridge-anchor:localhost"),517
- HistoryRepairPolicy::Bridge,518
- ),519
- (520
- room_id!("!policy-short-gap:localhost"),521
- event_id!("$policy-short-anchor:localhost"),522
- HistoryRepairPolicy::BridgeShortGaps { threshold_ms: 0 },523
- ),524
- ];525
-526
- for (room_id, anchor_id, policy) in cases {527
- let mut room = open_room(room_id).await;528
- sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;529
- room.server530
- .mock_room_messages()531
- .ok(532
- RoomMessagesResponseTemplate::default().events(vec![history_event(533
- room_id,534
- anchor_id,535
- "same anchor in recent history",536
- )]),537
- )538
- .mock_once()539
- .mount()540
- .await;541
-542
- let report = room543
- .handle544
- .history_repair(545
- room_id.to_owned(),546
- Some(policy.clone()),547
- HistoryRepairBudget::default(),548
- )549
- .await550
- .unwrap();551
- assert_eq!(report.requested_policy, policy);552
- assert_eq!(report.strategy, HistoryRepairStrategy::Overlap);553
- assert_eq!(report.continuity, HistoryContinuity::Verified);554
- assert_eq!(report.pages, 1);555
- assert!(report.recent_available);556
-557
- let requests = room.server.received_requests().await.unwrap();558
- assert_eq!(559
- requests560
- .iter()561
- .filter(|request| request.url.path().ends_with("/messages"))562
- .count(),563
- 1564
- );565
- shut_down(&room.handle, &mut room.events, room.running).await;566
- }567
-}568
-569
-#[tokio::test]570
-async fn short_gap_policy_stays_recent_when_coverage_is_unknown() {571
- let room_id = room_id!("!unknown-coverage:localhost");572
- let anchor_id = event_id!("$unknown-anchor:localhost");573
- let fresh_id = event_id!("$unknown-fresh:localhost");574
- let mut room = open_room(room_id).await;575
- sync_cached_event(&mut room, room_id, anchor_id, "cached anchor").await;528
+async fn failed_automatic_recent_load_is_public_and_keeps_the_cached_timeline() {529
+ let room_id = room_id!("!failed-recent:localhost");530
+ let old_id = event_id!("$failed-old:localhost");531
+ let mut room = start_room(room_id).await;576532
room.server577533
.mock_room_messages()578
- .ok(RoomMessagesResponseTemplate::default()579
- .end_token("recent-prev")580
- .events(vec![history_event(room_id, fresh_id, "recent window")]))534
+ .respond_with(ResponseTemplate::new(403).set_body_json(serde_json::json!({535
+ "errcode": "M_FORBIDDEN",536
+ "error": "history is unavailable",537
+ })))581538
.mock_once()582539
.mount()583540
.await;584
-585
- let report = room586
- .handle587
- .history_repair(588
- room_id.to_owned(),589
- Some(HistoryRepairPolicy::BridgeShortGaps {590
- threshold_ms: 60_000,591
- }),592
- HistoryRepairBudget::default(),593
- )594
- .await595
- .unwrap();596
- assert_eq!(report.coverage_duration_ms, None);597
- assert!(!report.threshold_evaluated);598
- assert_eq!(report.strategy, HistoryRepairStrategy::Recent);599
- assert_eq!(report.continuity, HistoryContinuity::Unresolved);600
- assert_eq!(report.pages, 1);601
-602
- let requests = room.server.received_requests().await.unwrap();603
- assert_eq!(604
- requests605
- .iter()606
- .filter(|request| request.url.path().ends_with("/messages"))607
- .count(),608
- 1609
- );541
+ cache_event_before_open(&room, room_id, old_id, "kept event").await;542
+ open_room(&mut room, room_id).await;543
+ let state = wait_for_recent(&mut room, room_id).await;544
+ 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());610548
shut_down(&room.handle, &mut room.events, room.running).await;611549
}