atagen

macltok

public
Star0
Watch0
3dab4a4e73f48986ea8e34215bdeb7d5d3870773
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/search/mod.rs

c9effeac326aatagen2026-10-25 10:32 UTC1
mod feed;
c9effeac326aatagen2026-10-25 10:32 UTC2
#[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
c9effeac326aatagen2026-10-25 10:32 UTC3
mod index;
c9effeac326aatagen2026-10-25 10:32 UTC4
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC5
pub(crate) mod store;
74befbc72bafatagen2026-10-25 11:18 UTC6
mod text;
c9effeac326aatagen2026-10-25 10:32 UTC7
c9effeac326aatagen2026-10-25 10:32 UTC8
use std::{
74befbc72bafatagen2026-10-25 11:18 UTC9
    collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
c9effeac326aatagen2026-10-25 10:32 UTC10
    sync::Arc,
c9effeac326aatagen2026-10-25 10:32 UTC11
    time::Duration,
c9effeac326aatagen2026-10-25 10:32 UTC12
};
c9effeac326aatagen2026-10-25 10:32 UTC13
c9effeac326aatagen2026-10-25 10:32 UTC14
use futures_util::{
c9effeac326aatagen2026-10-25 10:32 UTC15
    StreamExt,
74befbc72bafatagen2026-10-25 11:18 UTC16
    future::Abortable,
c9effeac326aatagen2026-10-25 10:32 UTC17
    stream::{AbortHandle, FuturesUnordered, SelectAll, abortable},
c9effeac326aatagen2026-10-25 10:32 UTC18
};
c9effeac326aatagen2026-10-25 10:32 UTC19
use matrix_sdk::{
c9effeac326aatagen2026-10-25 10:32 UTC20
    Client, Room, RoomState, SendOutsideWasm,
c9effeac326aatagen2026-10-25 10:32 UTC21
    deserialized_responses::TimelineEvent as SdkTimelineEvent,
c9effeac326aatagen2026-10-25 10:32 UTC22
    event_cache::{RoomEventCacheSubscriber, RoomEventCacheUpdate},
74befbc72bafatagen2026-10-25 11:18 UTC23
    room::{Messages, MessagesOptions},
c9effeac326aatagen2026-10-25 10:32 UTC24
};
c9effeac326aatagen2026-10-25 10:32 UTC25
use matrix_sdk_base::RoomInfoNotableUpdateReasons;
c9effeac326aatagen2026-10-25 10:32 UTC26
use matrix_sdk_ui::eyeball_im::VectorDiff;
c9effeac326aatagen2026-10-25 10:32 UTC27
use ruma::{
c9effeac326aatagen2026-10-25 10:32 UTC28
    OwnedRoomId, OwnedUserId, UInt,
74befbc72bafatagen2026-10-25 11:18 UTC29
    api::client::filter::RoomEventFilter,
c9effeac326aatagen2026-10-25 10:32 UTC30
    events::{
74befbc72bafatagen2026-10-25 11:18 UTC31
        ignored_user_list::IgnoredUserListEventContent, relation::RelationType,
c9effeac326aatagen2026-10-25 10:32 UTC32
        room::encrypted::OriginalSyncRoomEncryptedEvent,
c9effeac326aatagen2026-10-25 10:32 UTC33
    },
c9effeac326aatagen2026-10-25 10:32 UTC34
    serde::Raw,
c9effeac326aatagen2026-10-25 10:32 UTC35
};
c9effeac326aatagen2026-10-25 10:32 UTC36
use serde_json::Value;
74befbc72bafatagen2026-10-25 11:18 UTC37
use sha2::{Digest, Sha256};
c9effeac326aatagen2026-10-25 10:32 UTC38
use tokio::sync::{Mutex, broadcast, mpsc, oneshot};
c9effeac326aatagen2026-10-25 10:32 UTC39
c9effeac326aatagen2026-10-25 10:32 UTC40
use self::{
c9effeac326aatagen2026-10-25 10:32 UTC41
    feed::{Effect, Observed, Redactions},
c9effeac326aatagen2026-10-25 10:32 UTC42
    index::{Changes, Doc, Hit, KeywordIndex},
c9effeac326aatagen2026-10-25 10:32 UTC43
};
c9effeac326aatagen2026-10-25 10:32 UTC44
use crate::{
74befbc72bafatagen2026-10-25 11:18 UTC45
    CommandOutput, EngineConfig, Error, ErrorKind, Event, RoomSearchCoverage, SearchBackfill,
74befbc72bafatagen2026-10-25 11:18 UTC46
    SearchCloseReason, SearchCommand, SearchCoverage, SearchHit, SearchInMemory, SearchMatch,
74befbc72bafatagen2026-10-25 11:18 UTC47
    SearchPage, SearchScope, SearchSession,
c9effeac326aatagen2026-10-25 10:32 UTC48
    engine::{MediaService, SearchStore, context, executor::EngineBoxFuture},
c9effeac326aatagen2026-10-25 10:32 UTC49
    normalise::{MediaRegistration, MediaScope},
74befbc72bafatagen2026-10-25 11:18 UTC50
    transport::EventSender,
c9effeac326aatagen2026-10-25 10:32 UTC51
};
c9effeac326aatagen2026-10-25 10:32 UTC52
c9effeac326aatagen2026-10-25 10:32 UTC53
const MAX_LIMIT: u16 = 50;
c9effeac326aatagen2026-10-25 10:32 UTC54
/// a consumer that never closes its searches still holds at most this many
c9effeac326aatagen2026-10-25 10:32 UTC55
const MAX_SEARCHES: usize = 8;
74befbc72bafatagen2026-10-25 11:18 UTC56
/// hits read back at once while a page is put together
74befbc72bafatagen2026-10-25 11:18 UTC57
const PREVIEWS_AT_ONCE: usize = 8;
74befbc72bafatagen2026-10-25 11:18 UTC58
const DECRYPTS_AT_ONCE: usize = 8;
c9effeac326aatagen2026-10-25 10:32 UTC59
const COMMIT_AFTER_MS: u64 = 5_000;
c9effeac326aatagen2026-10-25 10:32 UTC60
const COMMIT_BUFFERED: usize = 2_000;
c9effeac326aatagen2026-10-25 10:32 UTC61
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC62
const COMMIT_RETRY_MS: u64 = 60_000;
74befbc72bafatagen2026-10-25 11:18 UTC63
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
74befbc72bafatagen2026-10-25 11:18 UTC64
const COMMIT_ATTEMPTS: u32 = 5;
c9effeac326aatagen2026-10-25 10:32 UTC65
const IDLE_SUBSCRIPTION_MS: u64 = 5 * 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC66
const BACKFILL_RETRY_MS: u64 = 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC67
const BACKFILL_RETRY_MAX_MS: u64 = 15 * 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC68
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
74befbc72bafatagen2026-10-25 11:18 UTC69
const SCHEMA: u64 = 2;
74befbc72bafatagen2026-10-25 11:18 UTC70
/// waiting undecryptable events are saved in this many blobs, so one arriving rewrites a sixteenth
74befbc72bafatagen2026-10-25 11:18 UTC71
const UTD_SHARDS: u8 = 16;
74befbc72bafatagen2026-10-25 11:18 UTC72
const BACKFILL_TYPES: [&str; 6] = [
74befbc72bafatagen2026-10-25 11:18 UTC73
    "m.room.message",
74befbc72bafatagen2026-10-25 11:18 UTC74
    "m.room.encrypted",
74befbc72bafatagen2026-10-25 11:18 UTC75
    "m.room.redaction",
74befbc72bafatagen2026-10-25 11:18 UTC76
    "m.sticker",
74befbc72bafatagen2026-10-25 11:18 UTC77
    "m.poll.start",
74befbc72bafatagen2026-10-25 11:18 UTC78
    "org.matrix.msc3381.poll.start",
74befbc72bafatagen2026-10-25 11:18 UTC79
];
c9effeac326aatagen2026-10-25 10:32 UTC80
c9effeac326aatagen2026-10-25 10:32 UTC81
#[cfg(not(target_family = "wasm"))]
c9effeac326aatagen2026-10-25 10:32 UTC82
type BoxStream<T> = futures_util::stream::BoxStream<'static, T>;
c9effeac326aatagen2026-10-25 10:32 UTC83
#[cfg(target_family = "wasm")]
c9effeac326aatagen2026-10-25 10:32 UTC84
type BoxStream<T> = futures_util::stream::LocalBoxStream<'static, T>;
c9effeac326aatagen2026-10-25 10:32 UTC85
c9effeac326aatagen2026-10-25 10:32 UTC86
/// a local index of message text. keyword search over tantivy is the only one today, an
c9effeac326aatagen2026-10-25 10:32 UTC87
/// embedding index can stand beside it later without the public api noticing
c9effeac326aatagen2026-10-25 10:32 UTC88
#[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
c9effeac326aatagen2026-10-25 10:32 UTC89
pub(crate) trait MessageIndex: SendOutsideWasm {
c9effeac326aatagen2026-10-25 10:32 UTC90
    fn get(&self, key: &str) -> Option<Doc>;
c9effeac326aatagen2026-10-25 10:32 UTC91
    fn by_content(&self, content: &str) -> Option<Doc>;
c9effeac326aatagen2026-10-25 10:32 UTC92
    /// replaces whatever `doc.key` held
c9effeac326aatagen2026-10-25 10:32 UTC93
    fn put(&mut self, doc: Doc);
c9effeac326aatagen2026-10-25 10:32 UTC94
    fn remove(&mut self, key: &str) -> bool;
74befbc72bafatagen2026-10-25 11:18 UTC95
    /// every provisional doc waiting on `target`, whoever sent its edit
74befbc72bafatagen2026-10-25 11:18 UTC96
    fn remove_provisional(&mut self, target: &str) -> bool;
c9effeac326aatagen2026-10-25 10:32 UTC97
    fn buffered(&self) -> usize;
c9effeac326aatagen2026-10-25 10:32 UTC98
    /// newest first, strictly older than `before`. also hands back the analysed query terms
c9effeac326aatagen2026-10-25 10:32 UTC99
    fn search(
c9effeac326aatagen2026-10-25 10:32 UTC100
        &mut self,
c9effeac326aatagen2026-10-25 10:32 UTC101
        query: &str,
c9effeac326aatagen2026-10-25 10:32 UTC102
        rooms: Option<&[String]>,
c9effeac326aatagen2026-10-25 10:32 UTC103
        before: Option<(u64, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC104
        limit: usize,
c9effeac326aatagen2026-10-25 10:32 UTC105
    ) -> Result<(Vec<Hit>, Vec<String>), Error>;
c9effeac326aatagen2026-10-25 10:32 UTC106
    fn maintain(&mut self) -> Result<(), Error>;
c9effeac326aatagen2026-10-25 10:32 UTC107
    fn changes(&mut self) -> Result<Changes, Error>;
c9effeac326aatagen2026-10-25 10:32 UTC108
    fn committed(&mut self, changes: &Changes);
74befbc72bafatagen2026-10-25 11:18 UTC109
    /// stops tracking what a commit would need, once nothing will be committed again
74befbc72bafatagen2026-10-25 11:18 UTC110
    fn detach(&mut self);
c9effeac326aatagen2026-10-25 10:32 UTC111
}
c9effeac326aatagen2026-10-25 10:32 UTC112
c9effeac326aatagen2026-10-25 10:32 UTC113
#[derive(Clone, Debug, Default)]
c9effeac326aatagen2026-10-25 10:32 UTC114
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
c9effeac326aatagen2026-10-25 10:32 UTC115
struct RoomProgress {
c9effeac326aatagen2026-10-25 10:32 UTC116
    oldest: Option<u64>,
c9effeac326aatagen2026-10-25 10:32 UTC117
    newest: Option<u64>,
c9effeac326aatagen2026-10-25 10:32 UTC118
    /// where the next backfill page starts, `None` before the first one
c9effeac326aatagen2026-10-25 10:32 UTC119
    tail: Option<String>,
c9effeac326aatagen2026-10-25 10:32 UTC120
    started: bool,
c9effeac326aatagen2026-10-25 10:32 UTC121
    start_reached: bool,
c9effeac326aatagen2026-10-25 10:32 UTC122
}
c9effeac326aatagen2026-10-25 10:32 UTC123
c9effeac326aatagen2026-10-25 10:32 UTC124
#[derive(Clone, Debug)]
c9effeac326aatagen2026-10-25 10:32 UTC125
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
c9effeac326aatagen2026-10-25 10:32 UTC126
struct Pending {
c9effeac326aatagen2026-10-25 10:32 UTC127
    room: String,
c9effeac326aatagen2026-10-25 10:32 UTC128
    session_id: String,
c9effeac326aatagen2026-10-25 10:32 UTC129
    ts: u64,
c9effeac326aatagen2026-10-25 10:32 UTC130
    raw: String,
c9effeac326aatagen2026-10-25 10:32 UTC131
}
c9effeac326aatagen2026-10-25 10:32 UTC132
c9effeac326aatagen2026-10-25 10:32 UTC133
#[derive(Clone, Copy)]
c9effeac326aatagen2026-10-25 10:32 UTC134
struct Budget {
c9effeac326aatagen2026-10-25 10:32 UTC135
    pages_per_room: u16,
c9effeac326aatagen2026-10-25 10:32 UTC136
    page_size: u16,
c9effeac326aatagen2026-10-25 10:32 UTC137
    interval_ms: u64,
c9effeac326aatagen2026-10-25 10:32 UTC138
    max_pending: usize,
c9effeac326aatagen2026-10-25 10:32 UTC139
}
c9effeac326aatagen2026-10-25 10:32 UTC140
c9effeac326aatagen2026-10-25 10:32 UTC141
impl Budget {
c9effeac326aatagen2026-10-25 10:32 UTC142
    fn from_config(config: &EngineConfig) -> Self {
c9effeac326aatagen2026-10-25 10:32 UTC143
        Self {
c9effeac326aatagen2026-10-25 10:32 UTC144
            pages_per_room: config.search_backfill_pages_per_room,
c9effeac326aatagen2026-10-25 10:32 UTC145
            page_size: config.search_backfill_page_size.max(1),
c9effeac326aatagen2026-10-25 10:32 UTC146
            interval_ms: config.search_backfill_interval_ms,
c9effeac326aatagen2026-10-25 10:32 UTC147
            max_pending: config.max_search_undecrypted,
c9effeac326aatagen2026-10-25 10:32 UTC148
        }
c9effeac326aatagen2026-10-25 10:32 UTC149
    }
c9effeac326aatagen2026-10-25 10:32 UTC150
}
c9effeac326aatagen2026-10-25 10:32 UTC151
c9effeac326aatagen2026-10-25 10:32 UTC152
struct RawPage {
c9effeac326aatagen2026-10-25 10:32 UTC153
    hits: Vec<Hit>,
c9effeac326aatagen2026-10-25 10:32 UTC154
    terms: Vec<String>,
c9effeac326aatagen2026-10-25 10:32 UTC155
    more: bool,
c9effeac326aatagen2026-10-25 10:32 UTC156
    coverage: SearchCoverage,
c9effeac326aatagen2026-10-25 10:32 UTC157
}
c9effeac326aatagen2026-10-25 10:32 UTC158
c9effeac326aatagen2026-10-25 10:32 UTC159
enum Request {
c9effeac326aatagen2026-10-25 10:32 UTC160
    Search {
c9effeac326aatagen2026-10-25 10:32 UTC161
        query: String,
c9effeac326aatagen2026-10-25 10:32 UTC162
        rooms: Option<Vec<String>>,
c9effeac326aatagen2026-10-25 10:32 UTC163
        before: Option<(u64, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC164
        limit: usize,
c9effeac326aatagen2026-10-25 10:32 UTC165
        coverage: Vec<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC166
        reply: oneshot::Sender<Result<RawPage, Error>>,
c9effeac326aatagen2026-10-25 10:32 UTC167
    },
c9effeac326aatagen2026-10-25 10:32 UTC168
    Coverage(Vec<OwnedRoomId>, oneshot::Sender<SearchCoverage>),
c9effeac326aatagen2026-10-25 10:32 UTC169
    /// a hit that turned out redacted when its preview was read
c9effeac326aatagen2026-10-25 10:32 UTC170
    Forget(String),
c9effeac326aatagen2026-10-25 10:32 UTC171
    Stop(oneshot::Sender<()>),
c9effeac326aatagen2026-10-25 10:32 UTC172
}
c9effeac326aatagen2026-10-25 10:32 UTC173
c9effeac326aatagen2026-10-25 10:32 UTC174
#[derive(Clone)]
c9effeac326aatagen2026-10-25 10:32 UTC175
pub(crate) struct Handle {
c9effeac326aatagen2026-10-25 10:32 UTC176
    requests: mpsc::Sender<Request>,
c9effeac326aatagen2026-10-25 10:32 UTC177
    client: Client,
c9effeac326aatagen2026-10-25 10:32 UTC178
    media: Arc<MediaService>,
c9effeac326aatagen2026-10-25 10:32 UTC179
    sessions: Arc<Sessions>,
c9effeac326aatagen2026-10-25 10:32 UTC180
}
c9effeac326aatagen2026-10-25 10:32 UTC181
c9effeac326aatagen2026-10-25 10:32 UTC182
enum Work {
c9effeac326aatagen2026-10-25 10:32 UTC183
    Backfill {
c9effeac326aatagen2026-10-25 10:32 UTC184
        room: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC185
        head: bool,
74befbc72bafatagen2026-10-25 11:18 UTC186
        from: Option<String>,
74befbc72bafatagen2026-10-25 11:18 UTC187
        result: Result<Messages, Error>,
c9effeac326aatagen2026-10-25 10:32 UTC188
    },
74befbc72bafatagen2026-10-25 11:18 UTC189
    /// the room was left while its page was in flight
74befbc72bafatagen2026-10-25 11:18 UTC190
    BackfillCancelled,
c9effeac326aatagen2026-10-25 10:32 UTC191
    Decrypted {
c9effeac326aatagen2026-10-25 10:32 UTC192
        key: String,
c9effeac326aatagen2026-10-25 10:32 UTC193
        room: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC194
        value: Option<Value>,
c9effeac326aatagen2026-10-25 10:32 UTC195
    },
74befbc72bafatagen2026-10-25 11:18 UTC196
    /// the original first, then whatever edits of it the cache or server still has
c9effeac326aatagen2026-10-25 10:32 UTC197
    Refetched {
c9effeac326aatagen2026-10-25 10:32 UTC198
        room: OwnedRoomId,
74befbc72bafatagen2026-10-25 11:18 UTC199
        values: Vec<Value>,
c9effeac326aatagen2026-10-25 10:32 UTC200
    },
c9effeac326aatagen2026-10-25 10:32 UTC201
}
c9effeac326aatagen2026-10-25 10:32 UTC202
74befbc72bafatagen2026-10-25 11:18 UTC203
enum RoomUpdate {
74befbc72bafatagen2026-10-25 11:18 UTC204
    Events(Vec<SdkTimelineEvent>),
74befbc72bafatagen2026-10-25 11:18 UTC205
    Lagged,
74befbc72bafatagen2026-10-25 11:18 UTC206
    Closed,
74befbc72bafatagen2026-10-25 11:18 UTC207
}
74befbc72bafatagen2026-10-25 11:18 UTC208
c9effeac326aatagen2026-10-25 10:32 UTC209
struct Failure {
c9effeac326aatagen2026-10-25 10:32 UTC210
    message: String,
c9effeac326aatagen2026-10-25 10:32 UTC211
    retry_at_ms: u64,
74befbc72bafatagen2026-10-25 11:18 UTC212
    delay_ms: u64,
74befbc72bafatagen2026-10-25 11:18 UTC213
}
74befbc72bafatagen2026-10-25 11:18 UTC214
74befbc72bafatagen2026-10-25 11:18 UTC215
enum Persistence {
74befbc72bafatagen2026-10-25 11:18 UTC216
    Store(SearchStore),
74befbc72bafatagen2026-10-25 11:18 UTC217
    Memory(SearchInMemory),
c9effeac326aatagen2026-10-25 10:32 UTC218
}
c9effeac326aatagen2026-10-25 10:32 UTC219
c9effeac326aatagen2026-10-25 10:32 UTC220
pub(crate) struct Service {
c9effeac326aatagen2026-10-25 10:32 UTC221
    client: Client,
c9effeac326aatagen2026-10-25 10:32 UTC222
    index: Box<dyn MessageIndex>,
74befbc72bafatagen2026-10-25 11:18 UTC223
    persistence: Persistence,
74befbc72bafatagen2026-10-25 11:18 UTC224
    #[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
74befbc72bafatagen2026-10-25 11:18 UTC225
    commit_failures: u32,
74befbc72bafatagen2026-10-25 11:18 UTC226
    /// storage keys a load found nothing pointing at, dropped with the next commit
74befbc72bafatagen2026-10-25 11:18 UTC227
    #[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
74befbc72bafatagen2026-10-25 11:18 UTC228
    orphans: Vec<String>,
c9effeac326aatagen2026-10-25 10:32 UTC229
    rooms: BTreeMap<String, RoomProgress>,
74befbc72bafatagen2026-10-25 11:18 UTC230
    /// each room's newest indexed message when this engine started, what a head pass looks for
74befbc72bafatagen2026-10-25 11:18 UTC231
    baselines: HashMap<String, u64>,
c9effeac326aatagen2026-10-25 10:32 UTC232
    pending: BTreeMap<String, Pending>,
74befbc72bafatagen2026-10-25 11:18 UTC233
    dirty_shards: BTreeSet<u8>,
74befbc72bafatagen2026-10-25 11:18 UTC234
    decrypt_queue: VecDeque<String>,
74befbc72bafatagen2026-10-25 11:18 UTC235
    decrypting: usize,
c9effeac326aatagen2026-10-25 10:32 UTC236
    redactions: Redactions,
c9effeac326aatagen2026-10-25 10:32 UTC237
    dirty_since: Option<u64>,
c9effeac326aatagen2026-10-25 10:32 UTC238
    commit_after: u64,
c9effeac326aatagen2026-10-25 10:32 UTC239
    budget: Budget,
c9effeac326aatagen2026-10-25 10:32 UTC240
    requests: mpsc::Receiver<Request>,
c9effeac326aatagen2026-10-25 10:32 UTC241
    subscriptions: HashMap<OwnedRoomId, (AbortHandle, u64)>,
74befbc72bafatagen2026-10-25 11:18 UTC242
    updates: SelectAll<BoxStream<(OwnedRoomId, RoomUpdate)>>,
c9effeac326aatagen2026-10-25 10:32 UTC243
    work: FuturesUnordered<EngineBoxFuture<'static, Work>>,
c9effeac326aatagen2026-10-25 10:32 UTC244
    pages: HashMap<OwnedRoomId, u16>,
c9effeac326aatagen2026-10-25 10:32 UTC245
    heads: VecDeque<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC246
    headed: HashSet<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC247
    head_tokens: HashMap<OwnedRoomId, String>,
74befbc72bafatagen2026-10-25 11:18 UTC248
    backfilling: Option<(OwnedRoomId, AbortHandle)>,
c9effeac326aatagen2026-10-25 10:32 UTC249
    next_backfill: u64,
c9effeac326aatagen2026-10-25 10:32 UTC250
    failures: HashMap<OwnedRoomId, Failure>,
c9effeac326aatagen2026-10-25 10:32 UTC251
    turn: usize,
c9effeac326aatagen2026-10-25 10:32 UTC252
}
c9effeac326aatagen2026-10-25 10:32 UTC253
c9effeac326aatagen2026-10-25 10:32 UTC254
fn now() -> u64 {
c9effeac326aatagen2026-10-25 10:32 UTC255
    ruma::MilliSecondsSinceUnixEpoch::now().get().into()
c9effeac326aatagen2026-10-25 10:32 UTC256
}
c9effeac326aatagen2026-10-25 10:32 UTC257
c9effeac326aatagen2026-10-25 10:32 UTC258
fn value_of(event: &SdkTimelineEvent) -> Option<Value> {
c9effeac326aatagen2026-10-25 10:32 UTC259
    serde_json::from_str(event.raw().json().get()).ok()
c9effeac326aatagen2026-10-25 10:32 UTC260
}
c9effeac326aatagen2026-10-25 10:32 UTC261
c9effeac326aatagen2026-10-25 10:32 UTC262
fn encode_cursor((ts, tie): (u64, u64)) -> String {
c9effeac326aatagen2026-10-25 10:32 UTC263
    format!("{ts:x}.{tie:x}")
c9effeac326aatagen2026-10-25 10:32 UTC264
}
c9effeac326aatagen2026-10-25 10:32 UTC265
c9effeac326aatagen2026-10-25 10:32 UTC266
fn decode_cursor(cursor: &str) -> Option<(u64, u64)> {
c9effeac326aatagen2026-10-25 10:32 UTC267
    let (ts, tie) = cursor.split_once('.')?;
c9effeac326aatagen2026-10-25 10:32 UTC268
    Some((
c9effeac326aatagen2026-10-25 10:32 UTC269
        u64::from_str_radix(ts, 16).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC270
        u64::from_str_radix(tie, 16).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC271
    ))
c9effeac326aatagen2026-10-25 10:32 UTC272
}
c9effeac326aatagen2026-10-25 10:32 UTC273
74befbc72bafatagen2026-10-25 11:18 UTC274
fn shard(key: &str) -> u8 {
74befbc72bafatagen2026-10-25 11:18 UTC275
    Sha256::digest(key.as_bytes())[0] % UTD_SHARDS
74befbc72bafatagen2026-10-25 11:18 UTC276
}
74befbc72bafatagen2026-10-25 11:18 UTC277
74befbc72bafatagen2026-10-25 11:18 UTC278
/// a 400 to a page with a saved token means the server no longer takes the token
74befbc72bafatagen2026-10-25 11:18 UTC279
fn token_rejected(error: &Error) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC280
    error
74befbc72bafatagen2026-10-25 11:18 UTC281
        .server
74befbc72bafatagen2026-10-25 11:18 UTC282
        .as_ref()
74befbc72bafatagen2026-10-25 11:18 UTC283
        .is_some_and(|server| server.status == 400 || server.errcode == "M_INVALID_PARAM")
74befbc72bafatagen2026-10-25 11:18 UTC284
}
74befbc72bafatagen2026-10-25 11:18 UTC285
c9effeac326aatagen2026-10-25 10:32 UTC286
impl Handle {
c9effeac326aatagen2026-10-25 10:32 UTC287
    pub(crate) async fn execute(&self, command: SearchCommand) -> Result<CommandOutput, Error> {
c9effeac326aatagen2026-10-25 10:32 UTC288
        match command {
c9effeac326aatagen2026-10-25 10:32 UTC289
            SearchCommand::Search {
c9effeac326aatagen2026-10-25 10:32 UTC290
                query,
c9effeac326aatagen2026-10-25 10:32 UTC291
                scope,
c9effeac326aatagen2026-10-25 10:32 UTC292
                limit,
c9effeac326aatagen2026-10-25 10:32 UTC293
            } => {
74befbc72bafatagen2026-10-25 11:18 UTC294
                let media_scope = self.media.context_scope();
74befbc72bafatagen2026-10-25 11:18 UTC295
                let prefix = media_prefix(&media_scope);
74befbc72bafatagen2026-10-25 11:18 UTC296
                let (page, media) = self.search(&query, &scope, None, limit, &prefix).await?;
74befbc72bafatagen2026-10-25 11:18 UTC297
                let search_id = self
74befbc72bafatagen2026-10-25 11:18 UTC298
                    .sessions
74befbc72bafatagen2026-10-25 11:18 UTC299
                    .open(&self.media, query, scope, media_scope, media)
74befbc72bafatagen2026-10-25 11:18 UTC300
                    .await;
74befbc72bafatagen2026-10-25 11:18 UTC301
                Ok(CommandOutput::SearchSession(SearchSession {
74befbc72bafatagen2026-10-25 11:18 UTC302
                    search_id,
74befbc72bafatagen2026-10-25 11:18 UTC303
                    page,
74befbc72bafatagen2026-10-25 11:18 UTC304
                }))
c9effeac326aatagen2026-10-25 10:32 UTC305
            }
c9effeac326aatagen2026-10-25 10:32 UTC306
            SearchCommand::More {
c9effeac326aatagen2026-10-25 10:32 UTC307
                search_id,
c9effeac326aatagen2026-10-25 10:32 UTC308
                cursor,
c9effeac326aatagen2026-10-25 10:32 UTC309
                limit,
c9effeac326aatagen2026-10-25 10:32 UTC310
            } => {
74befbc72bafatagen2026-10-25 11:18 UTC311
                let (query, scope, prefix) = self.sessions.get(&search_id).await?;
74befbc72bafatagen2026-10-25 11:18 UTC312
                let (page, media) = self
74befbc72bafatagen2026-10-25 11:18 UTC313
                    .search(&query, &scope, Some(cursor.as_str()), limit, &prefix)
74befbc72bafatagen2026-10-25 11:18 UTC314
                    .await?;
74befbc72bafatagen2026-10-25 11:18 UTC315
                self.sessions
74befbc72bafatagen2026-10-25 11:18 UTC316
                    .register(&self.media, &search_id, media)
74befbc72bafatagen2026-10-25 11:18 UTC317
                    .await?;
c9effeac326aatagen2026-10-25 10:32 UTC318
                Ok(CommandOutput::SearchPage(page))
c9effeac326aatagen2026-10-25 10:32 UTC319
            }
c9effeac326aatagen2026-10-25 10:32 UTC320
            SearchCommand::Close { search_id } => {
c9effeac326aatagen2026-10-25 10:32 UTC321
                self.sessions.close(&self.media, &search_id).await?;
c9effeac326aatagen2026-10-25 10:32 UTC322
                Ok(CommandOutput::Done)
c9effeac326aatagen2026-10-25 10:32 UTC323
            }
c9effeac326aatagen2026-10-25 10:32 UTC324
            SearchCommand::Coverage(scope) => {
c9effeac326aatagen2026-10-25 10:32 UTC325
                let (reply, response) = oneshot::channel();
c9effeac326aatagen2026-10-25 10:32 UTC326
                self.requests
c9effeac326aatagen2026-10-25 10:32 UTC327
                    .send(Request::Coverage(self.scope_rooms(&scope), reply))
c9effeac326aatagen2026-10-25 10:32 UTC328
                    .await
c9effeac326aatagen2026-10-25 10:32 UTC329
                    .map_err(|_| Error::interrupted())?;
c9effeac326aatagen2026-10-25 10:32 UTC330
                Ok(CommandOutput::SearchCoverage(
c9effeac326aatagen2026-10-25 10:32 UTC331
                    response.await.map_err(|_| Error::interrupted())?,
c9effeac326aatagen2026-10-25 10:32 UTC332
                ))
c9effeac326aatagen2026-10-25 10:32 UTC333
            }
c9effeac326aatagen2026-10-25 10:32 UTC334
        }
c9effeac326aatagen2026-10-25 10:32 UTC335
    }
c9effeac326aatagen2026-10-25 10:32 UTC336
c9effeac326aatagen2026-10-25 10:32 UTC337
    /// the media service is cleared beside this, so nothing is released one by one
c9effeac326aatagen2026-10-25 10:32 UTC338
    pub(crate) async fn stop(&self) {
c9effeac326aatagen2026-10-25 10:32 UTC339
        let (reply, response) = oneshot::channel();
c9effeac326aatagen2026-10-25 10:32 UTC340
        if self.requests.send(Request::Stop(reply)).await.is_ok() {
c9effeac326aatagen2026-10-25 10:32 UTC341
            let _ = response.await;
c9effeac326aatagen2026-10-25 10:32 UTC342
        }
c9effeac326aatagen2026-10-25 10:32 UTC343
        self.sessions.open.lock().await.clear();
c9effeac326aatagen2026-10-25 10:32 UTC344
    }
c9effeac326aatagen2026-10-25 10:32 UTC345
c9effeac326aatagen2026-10-25 10:32 UTC346
    fn scope_rooms(&self, scope: &SearchScope) -> Vec<OwnedRoomId> {
c9effeac326aatagen2026-10-25 10:32 UTC347
        match scope {
c9effeac326aatagen2026-10-25 10:32 UTC348
            SearchScope::Room(room_id) => vec![room_id.clone()],
c9effeac326aatagen2026-10-25 10:32 UTC349
            SearchScope::Rooms(room_ids) => room_ids.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC350
            SearchScope::All => self
c9effeac326aatagen2026-10-25 10:32 UTC351
                .client
c9effeac326aatagen2026-10-25 10:32 UTC352
                .joined_rooms()
c9effeac326aatagen2026-10-25 10:32 UTC353
                .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC354
                .map(|room| room.room_id().to_owned())
c9effeac326aatagen2026-10-25 10:32 UTC355
                .collect(),
c9effeac326aatagen2026-10-25 10:32 UTC356
        }
c9effeac326aatagen2026-10-25 10:32 UTC357
    }
c9effeac326aatagen2026-10-25 10:32 UTC358
c9effeac326aatagen2026-10-25 10:32 UTC359
    async fn search(
c9effeac326aatagen2026-10-25 10:32 UTC360
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC361
        query: &str,
c9effeac326aatagen2026-10-25 10:32 UTC362
        scope: &SearchScope,
c9effeac326aatagen2026-10-25 10:32 UTC363
        cursor: Option<&str>,
c9effeac326aatagen2026-10-25 10:32 UTC364
        limit: u16,
74befbc72bafatagen2026-10-25 11:18 UTC365
        media_prefix: &str,
c9effeac326aatagen2026-10-25 10:32 UTC366
    ) -> Result<(SearchPage, Vec<MediaRegistration>), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC367
        if limit == 0 || limit > MAX_LIMIT {
c9effeac326aatagen2026-10-25 10:32 UTC368
            return Err(Error::new(
c9effeac326aatagen2026-10-25 10:32 UTC369
                ErrorKind::Invalid,
c9effeac326aatagen2026-10-25 10:32 UTC370
                "search limit must be between 1 and 50",
c9effeac326aatagen2026-10-25 10:32 UTC371
            ));
c9effeac326aatagen2026-10-25 10:32 UTC372
        }
c9effeac326aatagen2026-10-25 10:32 UTC373
        let before = match cursor {
c9effeac326aatagen2026-10-25 10:32 UTC374
            Some(cursor) => Some(
c9effeac326aatagen2026-10-25 10:32 UTC375
                decode_cursor(cursor)
c9effeac326aatagen2026-10-25 10:32 UTC376
                    .ok_or_else(|| Error::new(ErrorKind::Invalid, "search cursor is invalid"))?,
c9effeac326aatagen2026-10-25 10:32 UTC377
            ),
c9effeac326aatagen2026-10-25 10:32 UTC378
            None => None,
c9effeac326aatagen2026-10-25 10:32 UTC379
        };
c9effeac326aatagen2026-10-25 10:32 UTC380
        let rooms = self.scope_rooms(scope);
c9effeac326aatagen2026-10-25 10:32 UTC381
        let filter = Some(rooms.iter().map(ToString::to_string).collect());
c9effeac326aatagen2026-10-25 10:32 UTC382
        let (reply, response) = oneshot::channel();
c9effeac326aatagen2026-10-25 10:32 UTC383
        self.requests
c9effeac326aatagen2026-10-25 10:32 UTC384
            .send(Request::Search {
c9effeac326aatagen2026-10-25 10:32 UTC385
                query: query.to_owned(),
c9effeac326aatagen2026-10-25 10:32 UTC386
                rooms: filter,
c9effeac326aatagen2026-10-25 10:32 UTC387
                before,
c9effeac326aatagen2026-10-25 10:32 UTC388
                limit: usize::from(limit),
c9effeac326aatagen2026-10-25 10:32 UTC389
                coverage: rooms,
c9effeac326aatagen2026-10-25 10:32 UTC390
                reply,
c9effeac326aatagen2026-10-25 10:32 UTC391
            })
c9effeac326aatagen2026-10-25 10:32 UTC392
            .await
c9effeac326aatagen2026-10-25 10:32 UTC393
            .map_err(|_| Error::interrupted())?;
c9effeac326aatagen2026-10-25 10:32 UTC394
        let raw = response.await.map_err(|_| Error::interrupted())??;
c9effeac326aatagen2026-10-25 10:32 UTC395
        let next_cursor = raw
c9effeac326aatagen2026-10-25 10:32 UTC396
            .more
c9effeac326aatagen2026-10-25 10:32 UTC397
            .then(|| raw.hits.last().map(|hit| encode_cursor((hit.ts, hit.tie))))
c9effeac326aatagen2026-10-25 10:32 UTC398
            .flatten();
c9effeac326aatagen2026-10-25 10:32 UTC399
        let ignored = self.ignored_users().await;
c9effeac326aatagen2026-10-25 10:32 UTC400
        let current_user_id = self.client.user_id().map(ToOwned::to_owned);
74befbc72bafatagen2026-10-25 11:18 UTC401
        let terms = &raw.terms;
74befbc72bafatagen2026-10-25 11:18 UTC402
        let previews: Vec<_> = futures_util::stream::iter(
c9effeac326aatagen2026-10-25 10:32 UTC403
            raw.hits
c9effeac326aatagen2026-10-25 10:32 UTC404
                .into_iter()
74befbc72bafatagen2026-10-25 11:18 UTC405
                .filter(|hit| !ignored.iter().any(|user| user.as_str() == hit.sender)),
c9effeac326aatagen2026-10-25 10:32 UTC406
        )
74befbc72bafatagen2026-10-25 11:18 UTC407
        .map(|hit| self.preview(hit, current_user_id.clone(), terms, media_prefix))
74befbc72bafatagen2026-10-25 11:18 UTC408
        .buffered(PREVIEWS_AT_ONCE)
74befbc72bafatagen2026-10-25 11:18 UTC409
        .collect()
c9effeac326aatagen2026-10-25 10:32 UTC410
        .await;
c9effeac326aatagen2026-10-25 10:32 UTC411
        let mut hits = Vec::new();
c9effeac326aatagen2026-10-25 10:32 UTC412
        let mut media = Vec::new();
c9effeac326aatagen2026-10-25 10:32 UTC413
        for preview in previews {
c9effeac326aatagen2026-10-25 10:32 UTC414
            match preview {
c9effeac326aatagen2026-10-25 10:32 UTC415
                Preview::Shown(hit, registrations) => {
c9effeac326aatagen2026-10-25 10:32 UTC416
                    hits.push(hit);
c9effeac326aatagen2026-10-25 10:32 UTC417
                    media.extend(registrations);
c9effeac326aatagen2026-10-25 10:32 UTC418
                }
c9effeac326aatagen2026-10-25 10:32 UTC419
                Preview::Redacted(key) => {
c9effeac326aatagen2026-10-25 10:32 UTC420
                    let _ = self.requests.send(Request::Forget(key)).await;
c9effeac326aatagen2026-10-25 10:32 UTC421
                }
c9effeac326aatagen2026-10-25 10:32 UTC422
                Preview::Unavailable => {}
c9effeac326aatagen2026-10-25 10:32 UTC423
            }
c9effeac326aatagen2026-10-25 10:32 UTC424
        }
c9effeac326aatagen2026-10-25 10:32 UTC425
        let page = SearchPage {
c9effeac326aatagen2026-10-25 10:32 UTC426
            hits,
c9effeac326aatagen2026-10-25 10:32 UTC427
            next_cursor,
c9effeac326aatagen2026-10-25 10:32 UTC428
            coverage: raw.coverage,
c9effeac326aatagen2026-10-25 10:32 UTC429
        };
c9effeac326aatagen2026-10-25 10:32 UTC430
        Ok((page, media))
c9effeac326aatagen2026-10-25 10:32 UTC431
    }
c9effeac326aatagen2026-10-25 10:32 UTC432
c9effeac326aatagen2026-10-25 10:32 UTC433
    async fn ignored_users(&self) -> Vec<OwnedUserId> {
c9effeac326aatagen2026-10-25 10:32 UTC434
        self.client
c9effeac326aatagen2026-10-25 10:32 UTC435
            .account()
c9effeac326aatagen2026-10-25 10:32 UTC436
            .account_data::<IgnoredUserListEventContent>()
c9effeac326aatagen2026-10-25 10:32 UTC437
            .await
c9effeac326aatagen2026-10-25 10:32 UTC438
            .ok()
c9effeac326aatagen2026-10-25 10:32 UTC439
            .flatten()
c9effeac326aatagen2026-10-25 10:32 UTC440
            .and_then(|raw| raw.deserialize().ok())
c9effeac326aatagen2026-10-25 10:32 UTC441
            .map(|content| content.ignored_users.into_keys().collect())
c9effeac326aatagen2026-10-25 10:32 UTC442
            .unwrap_or_default()
c9effeac326aatagen2026-10-25 10:32 UTC443
    }
c9effeac326aatagen2026-10-25 10:32 UTC444
c9effeac326aatagen2026-10-25 10:32 UTC445
    async fn preview(
c9effeac326aatagen2026-10-25 10:32 UTC446
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC447
        hit: Hit,
c9effeac326aatagen2026-10-25 10:32 UTC448
        current_user_id: Option<OwnedUserId>,
c9effeac326aatagen2026-10-25 10:32 UTC449
        terms: &[String],
74befbc72bafatagen2026-10-25 11:18 UTC450
        media_prefix: &str,
c9effeac326aatagen2026-10-25 10:32 UTC451
    ) -> Preview {
c9effeac326aatagen2026-10-25 10:32 UTC452
        let (Ok(room_id), Ok(event_id)) = (
c9effeac326aatagen2026-10-25 10:32 UTC453
            OwnedRoomId::try_from(hit.room.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC454
            ruma::OwnedEventId::try_from(hit.key.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC455
        ) else {
c9effeac326aatagen2026-10-25 10:32 UTC456
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC457
        };
c9effeac326aatagen2026-10-25 10:32 UTC458
        let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC459
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC460
        };
74befbc72bafatagen2026-10-25 11:18 UTC461
        let Some((mut original, highlight)) = load(&room, &hit.key).await else {
c9effeac326aatagen2026-10-25 10:32 UTC462
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC463
        };
c9effeac326aatagen2026-10-25 10:32 UTC464
        if original.pointer("/unsigned/redacted_because").is_some() {
c9effeac326aatagen2026-10-25 10:32 UTC465
            return Preview::Redacted(hit.key);
c9effeac326aatagen2026-10-25 10:32 UTC466
        }
74befbc72bafatagen2026-10-25 11:18 UTC467
        // the index already settled which edit is newest, and a bundled one can be older
74befbc72bafatagen2026-10-25 11:18 UTC468
        if let Some(relations) = original
74befbc72bafatagen2026-10-25 11:18 UTC469
            .pointer_mut("/unsigned/m.relations")
74befbc72bafatagen2026-10-25 11:18 UTC470
            .and_then(Value::as_object_mut)
74befbc72bafatagen2026-10-25 11:18 UTC471
        {
74befbc72bafatagen2026-10-25 11:18 UTC472
            relations.remove("m.replace");
74befbc72bafatagen2026-10-25 11:18 UTC473
        }
c9effeac326aatagen2026-10-25 10:32 UTC474
        let mut values = vec![original];
c9effeac326aatagen2026-10-25 10:32 UTC475
        if hit.content != hit.key
c9effeac326aatagen2026-10-25 10:32 UTC476
            && let Some((edit, _)) = load(&room, &hit.content).await
c9effeac326aatagen2026-10-25 10:32 UTC477
        {
c9effeac326aatagen2026-10-25 10:32 UTC478
            values.push(edit);
c9effeac326aatagen2026-10-25 10:32 UTC479
        }
74befbc72bafatagen2026-10-25 11:18 UTC480
        let Some((event, registrations)) = context::preview(
74befbc72bafatagen2026-10-25 11:18 UTC481
            &room,
74befbc72bafatagen2026-10-25 11:18 UTC482
            current_user_id.as_deref(),
74befbc72bafatagen2026-10-25 11:18 UTC483
            &values,
74befbc72bafatagen2026-10-25 11:18 UTC484
            highlight,
74befbc72bafatagen2026-10-25 11:18 UTC485
            media_prefix,
74befbc72bafatagen2026-10-25 11:18 UTC486
        )
74befbc72bafatagen2026-10-25 11:18 UTC487
        .await
c9effeac326aatagen2026-10-25 10:32 UTC488
        else {
c9effeac326aatagen2026-10-25 10:32 UTC489
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC490
        };
c9effeac326aatagen2026-10-25 10:32 UTC491
        let text = match &event.content {
c9effeac326aatagen2026-10-25 10:32 UTC492
            crate::TimelineContent::Message(message) => Some(message.body.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC493
            crate::TimelineContent::Sticker { body, .. } => Some(body.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC494
            crate::TimelineContent::Poll { question } => Some(question.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC495
            _ => None,
c9effeac326aatagen2026-10-25 10:32 UTC496
        };
c9effeac326aatagen2026-10-25 10:32 UTC497
        let matches = text
74befbc72bafatagen2026-10-25 11:18 UTC498
            .map(|text| text::match_ranges(text, terms))
c9effeac326aatagen2026-10-25 10:32 UTC499
            .unwrap_or_default()
c9effeac326aatagen2026-10-25 10:32 UTC500
            .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC501
            .filter_map(|(start, end)| {
c9effeac326aatagen2026-10-25 10:32 UTC502
                Some(SearchMatch {
c9effeac326aatagen2026-10-25 10:32 UTC503
                    start: u32::try_from(start).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC504
                    end: u32::try_from(end).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC505
                })
c9effeac326aatagen2026-10-25 10:32 UTC506
            })
c9effeac326aatagen2026-10-25 10:32 UTC507
            .collect();
c9effeac326aatagen2026-10-25 10:32 UTC508
        Preview::Shown(
c9effeac326aatagen2026-10-25 10:32 UTC509
            SearchHit {
c9effeac326aatagen2026-10-25 10:32 UTC510
                room_id,
c9effeac326aatagen2026-10-25 10:32 UTC511
                event_id,
c9effeac326aatagen2026-10-25 10:32 UTC512
                timestamp_ms: event.timestamp_ms,
c9effeac326aatagen2026-10-25 10:32 UTC513
                event,
c9effeac326aatagen2026-10-25 10:32 UTC514
                matches,
c9effeac326aatagen2026-10-25 10:32 UTC515
            },
c9effeac326aatagen2026-10-25 10:32 UTC516
            registrations,
c9effeac326aatagen2026-10-25 10:32 UTC517
        )
c9effeac326aatagen2026-10-25 10:32 UTC518
    }
c9effeac326aatagen2026-10-25 10:32 UTC519
}
c9effeac326aatagen2026-10-25 10:32 UTC520
c9effeac326aatagen2026-10-25 10:32 UTC521
enum Preview {
c9effeac326aatagen2026-10-25 10:32 UTC522
    Shown(SearchHit, Vec<MediaRegistration>),
c9effeac326aatagen2026-10-25 10:32 UTC523
    Redacted(String),
c9effeac326aatagen2026-10-25 10:32 UTC524
    Unavailable,
c9effeac326aatagen2026-10-25 10:32 UTC525
}
c9effeac326aatagen2026-10-25 10:32 UTC526
c9effeac326aatagen2026-10-25 10:32 UTC527
/// open searches, oldest first, each owning the media of the hits it handed out
c9effeac326aatagen2026-10-25 10:32 UTC528
struct Sessions {
c9effeac326aatagen2026-10-25 10:32 UTC529
    open: Mutex<VecDeque<Session>>,
74befbc72bafatagen2026-10-25 11:18 UTC530
    events: EventSender,
74befbc72bafatagen2026-10-25 11:18 UTC531
}
74befbc72bafatagen2026-10-25 11:18 UTC532
74befbc72bafatagen2026-10-25 11:18 UTC533
/// media ids carry the search, so two owners of one event never share an entry
74befbc72bafatagen2026-10-25 11:18 UTC534
fn media_prefix(scope: &MediaScope) -> String {
74befbc72bafatagen2026-10-25 11:18 UTC535
    format!("{}#", scope.nonce())
c9effeac326aatagen2026-10-25 10:32 UTC536
}
c9effeac326aatagen2026-10-25 10:32 UTC537
c9effeac326aatagen2026-10-25 10:32 UTC538
struct Session {
c9effeac326aatagen2026-10-25 10:32 UTC539
    id: String,
c9effeac326aatagen2026-10-25 10:32 UTC540
    query: String,
c9effeac326aatagen2026-10-25 10:32 UTC541
    scope: SearchScope,
c9effeac326aatagen2026-10-25 10:32 UTC542
    media: MediaScope,
c9effeac326aatagen2026-10-25 10:32 UTC543
}
c9effeac326aatagen2026-10-25 10:32 UTC544
c9effeac326aatagen2026-10-25 10:32 UTC545
impl Sessions {
c9effeac326aatagen2026-10-25 10:32 UTC546
    async fn open(
c9effeac326aatagen2026-10-25 10:32 UTC547
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC548
        media: &MediaService,
c9effeac326aatagen2026-10-25 10:32 UTC549
        query: String,
c9effeac326aatagen2026-10-25 10:32 UTC550
        scope: SearchScope,
74befbc72bafatagen2026-10-25 11:18 UTC551
        media_scope: MediaScope,
c9effeac326aatagen2026-10-25 10:32 UTC552
        registrations: Vec<MediaRegistration>,
c9effeac326aatagen2026-10-25 10:32 UTC553
    ) -> String {
c9effeac326aatagen2026-10-25 10:32 UTC554
        let id = format!("search-{}", media_scope.nonce());
74befbc72bafatagen2026-10-25 11:18 UTC555
        let mut evicted = Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC556
        {
74befbc72bafatagen2026-10-25 11:18 UTC557
            let mut open = self.open.lock().await;
74befbc72bafatagen2026-10-25 11:18 UTC558
            media.register(&media_scope, registrations).await;
74befbc72bafatagen2026-10-25 11:18 UTC559
            open.push_back(Session {
74befbc72bafatagen2026-10-25 11:18 UTC560
                id: id.clone(),
74befbc72bafatagen2026-10-25 11:18 UTC561
                query,
74befbc72bafatagen2026-10-25 11:18 UTC562
                scope,
74befbc72bafatagen2026-10-25 11:18 UTC563
                media: media_scope,
74befbc72bafatagen2026-10-25 11:18 UTC564
            });
74befbc72bafatagen2026-10-25 11:18 UTC565
            while open.len() > MAX_SEARCHES {
74befbc72bafatagen2026-10-25 11:18 UTC566
                if let Some(oldest) = open.pop_front() {
74befbc72bafatagen2026-10-25 11:18 UTC567
                    media.release(&oldest.media).await;
74befbc72bafatagen2026-10-25 11:18 UTC568
                    evicted.push(oldest.id);
74befbc72bafatagen2026-10-25 11:18 UTC569
                }
74befbc72bafatagen2026-10-25 11:18 UTC570
            }
74befbc72bafatagen2026-10-25 11:18 UTC571
        }
74befbc72bafatagen2026-10-25 11:18 UTC572
        for search_id in evicted {
74befbc72bafatagen2026-10-25 11:18 UTC573
            let event = Event::SearchClosed {
74befbc72bafatagen2026-10-25 11:18 UTC574
                search_id,
74befbc72bafatagen2026-10-25 11:18 UTC575
                reason: SearchCloseReason::Evicted,
74befbc72bafatagen2026-10-25 11:18 UTC576
            };
74befbc72bafatagen2026-10-25 11:18 UTC577
            if self.events.send(event).await.is_err() {
74befbc72bafatagen2026-10-25 11:18 UTC578
                break;
c9effeac326aatagen2026-10-25 10:32 UTC579
            }
c9effeac326aatagen2026-10-25 10:32 UTC580
        }
c9effeac326aatagen2026-10-25 10:32 UTC581
        id
c9effeac326aatagen2026-10-25 10:32 UTC582
    }
c9effeac326aatagen2026-10-25 10:32 UTC583
74befbc72bafatagen2026-10-25 11:18 UTC584
    /// the query, scope and media prefix the search's pages use
74befbc72bafatagen2026-10-25 11:18 UTC585
    async fn get(&self, id: &str) -> Result<(String, SearchScope, String), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC586
        let open = self.open.lock().await;
74befbc72bafatagen2026-10-25 11:18 UTC587
        let session = open
74befbc72bafatagen2026-10-25 11:18 UTC588
            .iter()
74befbc72bafatagen2026-10-25 11:18 UTC589
            .find(|session| session.id == id)
74befbc72bafatagen2026-10-25 11:18 UTC590
            .ok_or_else(not_open)?;
74befbc72bafatagen2026-10-25 11:18 UTC591
        Ok((
74befbc72bafatagen2026-10-25 11:18 UTC592
            session.query.clone(),
74befbc72bafatagen2026-10-25 11:18 UTC593
            session.scope.clone(),
74befbc72bafatagen2026-10-25 11:18 UTC594
            media_prefix(&session.media),
74befbc72bafatagen2026-10-25 11:18 UTC595
        ))
c9effeac326aatagen2026-10-25 10:32 UTC596
    }
c9effeac326aatagen2026-10-25 10:32 UTC597
c9effeac326aatagen2026-10-25 10:32 UTC598
    /// under the lock, so a page that lands after its close registers nothing
c9effeac326aatagen2026-10-25 10:32 UTC599
    async fn register(
c9effeac326aatagen2026-10-25 10:32 UTC600
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC601
        media: &MediaService,
c9effeac326aatagen2026-10-25 10:32 UTC602
        id: &str,
c9effeac326aatagen2026-10-25 10:32 UTC603
        registrations: Vec<MediaRegistration>,
c9effeac326aatagen2026-10-25 10:32 UTC604
    ) -> Result<(), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC605
        let open = self.open.lock().await;
74befbc72bafatagen2026-10-25 11:18 UTC606
        let session = open
74befbc72bafatagen2026-10-25 11:18 UTC607
            .iter()
74befbc72bafatagen2026-10-25 11:18 UTC608
            .find(|session| session.id == id)
74befbc72bafatagen2026-10-25 11:18 UTC609
            .ok_or_else(not_open)?;
c9effeac326aatagen2026-10-25 10:32 UTC610
        media.register(&session.media, registrations).await;
c9effeac326aatagen2026-10-25 10:32 UTC611
        Ok(())
c9effeac326aatagen2026-10-25 10:32 UTC612
    }
c9effeac326aatagen2026-10-25 10:32 UTC613
c9effeac326aatagen2026-10-25 10:32 UTC614
    async fn close(&self, media: &MediaService, id: &str) -> Result<(), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC615
        let mut open = self.open.lock().await;
c9effeac326aatagen2026-10-25 10:32 UTC616
        let index = open
c9effeac326aatagen2026-10-25 10:32 UTC617
            .iter()
c9effeac326aatagen2026-10-25 10:32 UTC618
            .position(|session| session.id == id)
c9effeac326aatagen2026-10-25 10:32 UTC619
            .ok_or_else(not_open)?;
c9effeac326aatagen2026-10-25 10:32 UTC620
        if let Some(session) = open.remove(index) {
c9effeac326aatagen2026-10-25 10:32 UTC621
            media.release(&session.media).await;
c9effeac326aatagen2026-10-25 10:32 UTC622
        }
c9effeac326aatagen2026-10-25 10:32 UTC623
        Ok(())
c9effeac326aatagen2026-10-25 10:32 UTC624
    }
c9effeac326aatagen2026-10-25 10:32 UTC625
}
c9effeac326aatagen2026-10-25 10:32 UTC626
c9effeac326aatagen2026-10-25 10:32 UTC627
fn not_open() -> Error {
74befbc72bafatagen2026-10-25 11:18 UTC628
    Error::new(
74befbc72bafatagen2026-10-25 11:18 UTC629
        ErrorKind::SearchNotOpen,
74befbc72bafatagen2026-10-25 11:18 UTC630
        "search is closed or was never opened",
74befbc72bafatagen2026-10-25 11:18 UTC631
    )
c9effeac326aatagen2026-10-25 10:32 UTC632
}
c9effeac326aatagen2026-10-25 10:32 UTC633
c9effeac326aatagen2026-10-25 10:32 UTC634
async fn load(room: &Room, event_id: &str) -> Option<(Value, bool)> {
c9effeac326aatagen2026-10-25 10:32 UTC635
    let event_id = ruma::EventId::parse(event_id).ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC636
    let event = room
c9effeac326aatagen2026-10-25 10:32 UTC637
        .load_or_fetch_event(&event_id, None)
c9effeac326aatagen2026-10-25 10:32 UTC638
        .await
c9effeac326aatagen2026-10-25 10:32 UTC639
        .inspect_err(|error| tracing::debug!(%event_id, %error, "loading a search hit failed"))
c9effeac326aatagen2026-10-25 10:32 UTC640
        .ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC641
    let highlight = event
c9effeac326aatagen2026-10-25 10:32 UTC642
        .push_actions()
c9effeac326aatagen2026-10-25 10:32 UTC643
        .is_some_and(|actions| actions.iter().any(ruma::push::Action::is_highlight));
c9effeac326aatagen2026-10-25 10:32 UTC644
    Some((value_of(&event)?, highlight))
c9effeac326aatagen2026-10-25 10:32 UTC645
}
c9effeac326aatagen2026-10-25 10:32 UTC646
74befbc72bafatagen2026-10-25 11:18 UTC647
/// the original and every edit of it still around, so a redacted latest edit falls back to
74befbc72bafatagen2026-10-25 11:18 UTC648
/// the one before it rather than to the original
74befbc72bafatagen2026-10-25 11:18 UTC649
async fn load_with_edits(room: &Room, event_id: &str) -> Vec<Value> {
74befbc72bafatagen2026-10-25 11:18 UTC650
    let Ok(event_id) = ruma::EventId::parse(event_id) else {
74befbc72bafatagen2026-10-25 11:18 UTC651
        return Vec::new();
c9effeac326aatagen2026-10-25 10:32 UTC652
    };
74befbc72bafatagen2026-10-25 11:18 UTC653
    match room
74befbc72bafatagen2026-10-25 11:18 UTC654
        .load_or_fetch_event_with_relations(&event_id, Some(vec![RelationType::Replacement]), None)
74befbc72bafatagen2026-10-25 11:18 UTC655
        .await
74befbc72bafatagen2026-10-25 11:18 UTC656
    {
74befbc72bafatagen2026-10-25 11:18 UTC657
        Ok((original, edits)) => std::iter::once(original)
74befbc72bafatagen2026-10-25 11:18 UTC658
            .chain(edits)
74befbc72bafatagen2026-10-25 11:18 UTC659
            .filter_map(|event| value_of(&event))
74befbc72bafatagen2026-10-25 11:18 UTC660
            .collect(),
74befbc72bafatagen2026-10-25 11:18 UTC661
        Err(error) => {
74befbc72bafatagen2026-10-25 11:18 UTC662
            tracing::debug!(%event_id, %error, "reading an original again failed");
74befbc72bafatagen2026-10-25 11:18 UTC663
            Vec::new()
74befbc72bafatagen2026-10-25 11:18 UTC664
        }
74befbc72bafatagen2026-10-25 11:18 UTC665
    }
c9effeac326aatagen2026-10-25 10:32 UTC666
}
c9effeac326aatagen2026-10-25 10:32 UTC667
c9effeac326aatagen2026-10-25 10:32 UTC668
impl Service {
c9effeac326aatagen2026-10-25 10:32 UTC669
    pub(crate) fn new(
c9effeac326aatagen2026-10-25 10:32 UTC670
        client: Client,
c9effeac326aatagen2026-10-25 10:32 UTC671
        media: Arc<MediaService>,
c9effeac326aatagen2026-10-25 10:32 UTC672
        store: Option<Result<SearchStore, Error>>,
74befbc72bafatagen2026-10-25 11:18 UTC673
        events: EventSender,
c9effeac326aatagen2026-10-25 10:32 UTC674
        config: &EngineConfig,
c9effeac326aatagen2026-10-25 10:32 UTC675
    ) -> (Handle, Self) {
c9effeac326aatagen2026-10-25 10:32 UTC676
        let (sender, requests) = mpsc::channel(config.command_capacity.max(1));
74befbc72bafatagen2026-10-25 11:18 UTC677
        let persistence = match store {
74befbc72bafatagen2026-10-25 11:18 UTC678
            Some(Ok(store)) => Persistence::Store(store),
74befbc72bafatagen2026-10-25 11:18 UTC679
            Some(Err(error)) => {
74befbc72bafatagen2026-10-25 11:18 UTC680
                tracing::error!(%error, "search store unavailable, the index lives in memory");
74befbc72bafatagen2026-10-25 11:18 UTC681
                Persistence::Memory(SearchInMemory::OpenFailed)
74befbc72bafatagen2026-10-25 11:18 UTC682
            }
74befbc72bafatagen2026-10-25 11:18 UTC683
            None => Persistence::Memory(SearchInMemory::NoStore),
74befbc72bafatagen2026-10-25 11:18 UTC684
        };
c9effeac326aatagen2026-10-25 10:32 UTC685
        let service = Self {
c9effeac326aatagen2026-10-25 10:32 UTC686
            client: client.clone(),
74befbc72bafatagen2026-10-25 11:18 UTC687
            index: Box::new(KeywordIndex::new(matches!(
74befbc72bafatagen2026-10-25 11:18 UTC688
                persistence,
74befbc72bafatagen2026-10-25 11:18 UTC689
                Persistence::Store(_)
74befbc72bafatagen2026-10-25 11:18 UTC690
            ))),
74befbc72bafatagen2026-10-25 11:18 UTC691
            persistence,
74befbc72bafatagen2026-10-25 11:18 UTC692
            commit_failures: 0,
74befbc72bafatagen2026-10-25 11:18 UTC693
            orphans: Vec::new(),
c9effeac326aatagen2026-10-25 10:32 UTC694
            rooms: BTreeMap::new(),
74befbc72bafatagen2026-10-25 11:18 UTC695
            baselines: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC696
            pending: BTreeMap::new(),
74befbc72bafatagen2026-10-25 11:18 UTC697
            dirty_shards: BTreeSet::new(),
74befbc72bafatagen2026-10-25 11:18 UTC698
            decrypt_queue: VecDeque::new(),
74befbc72bafatagen2026-10-25 11:18 UTC699
            decrypting: 0,
c9effeac326aatagen2026-10-25 10:32 UTC700
            redactions: Redactions::default(),
c9effeac326aatagen2026-10-25 10:32 UTC701
            dirty_since: None,
c9effeac326aatagen2026-10-25 10:32 UTC702
            commit_after: 0,
c9effeac326aatagen2026-10-25 10:32 UTC703
            budget: Budget::from_config(config),
c9effeac326aatagen2026-10-25 10:32 UTC704
            requests,
c9effeac326aatagen2026-10-25 10:32 UTC705
            subscriptions: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC706
            updates: SelectAll::new(),
c9effeac326aatagen2026-10-25 10:32 UTC707
            work: FuturesUnordered::new(),
c9effeac326aatagen2026-10-25 10:32 UTC708
            pages: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC709
            heads: VecDeque::new(),
c9effeac326aatagen2026-10-25 10:32 UTC710
            headed: HashSet::new(),
c9effeac326aatagen2026-10-25 10:32 UTC711
            head_tokens: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC712
            backfilling: None,
c9effeac326aatagen2026-10-25 10:32 UTC713
            next_backfill: 0,
c9effeac326aatagen2026-10-25 10:32 UTC714
            failures: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC715
            turn: 0,
c9effeac326aatagen2026-10-25 10:32 UTC716
        };
c9effeac326aatagen2026-10-25 10:32 UTC717
        (
c9effeac326aatagen2026-10-25 10:32 UTC718
            Handle {
c9effeac326aatagen2026-10-25 10:32 UTC719
                requests: sender,
c9effeac326aatagen2026-10-25 10:32 UTC720
                client,
c9effeac326aatagen2026-10-25 10:32 UTC721
                media,
74befbc72bafatagen2026-10-25 11:18 UTC722
                sessions: Arc::new(Sessions {
74befbc72bafatagen2026-10-25 11:18 UTC723
                    open: Mutex::default(),
74befbc72bafatagen2026-10-25 11:18 UTC724
                    events,
74befbc72bafatagen2026-10-25 11:18 UTC725
                }),
c9effeac326aatagen2026-10-25 10:32 UTC726
            },
c9effeac326aatagen2026-10-25 10:32 UTC727
            service,
c9effeac326aatagen2026-10-25 10:32 UTC728
        )
c9effeac326aatagen2026-10-25 10:32 UTC729
    }
c9effeac326aatagen2026-10-25 10:32 UTC730
74befbc72bafatagen2026-10-25 11:18 UTC731
    fn in_memory(&self) -> Option<SearchInMemory> {
74befbc72bafatagen2026-10-25 11:18 UTC732
        match self.persistence {
74befbc72bafatagen2026-10-25 11:18 UTC733
            Persistence::Store(_) => None,
74befbc72bafatagen2026-10-25 11:18 UTC734
            Persistence::Memory(reason) => Some(reason),
74befbc72bafatagen2026-10-25 11:18 UTC735
        }
74befbc72bafatagen2026-10-25 11:18 UTC736
    }
74befbc72bafatagen2026-10-25 11:18 UTC737
74befbc72bafatagen2026-10-25 11:18 UTC738
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
74befbc72bafatagen2026-10-25 11:18 UTC739
    fn fall_back(&mut self, reason: SearchInMemory) {
74befbc72bafatagen2026-10-25 11:18 UTC740
        self.persistence = Persistence::Memory(reason);
74befbc72bafatagen2026-10-25 11:18 UTC741
        self.index.detach();
74befbc72bafatagen2026-10-25 11:18 UTC742
        self.orphans.clear();
c9effeac326aatagen2026-10-25 10:32 UTC743
    }
c9effeac326aatagen2026-10-25 10:32 UTC744
c9effeac326aatagen2026-10-25 10:32 UTC745
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC746
    async fn load(&mut self) {
74befbc72bafatagen2026-10-25 11:18 UTC747
        let Persistence::Store(store) = &self.persistence else {
c9effeac326aatagen2026-10-25 10:32 UTC748
            return;
c9effeac326aatagen2026-10-25 10:32 UTC749
        };
74befbc72bafatagen2026-10-25 11:18 UTC750
        let mut blobs = match store.load().await {
c9effeac326aatagen2026-10-25 10:32 UTC751
            Ok(blobs) => blobs,
c9effeac326aatagen2026-10-25 10:32 UTC752
            Err(error) => {
c9effeac326aatagen2026-10-25 10:32 UTC753
                tracing::error!(%error, "reading the search store failed, the index lives in memory");
74befbc72bafatagen2026-10-25 11:18 UTC754
                self.fall_back(SearchInMemory::ReadFailed);
c9effeac326aatagen2026-10-25 10:32 UTC755
                return;
c9effeac326aatagen2026-10-25 10:32 UTC756
            }
c9effeac326aatagen2026-10-25 10:32 UTC757
        };
c9effeac326aatagen2026-10-25 10:32 UTC758
        if blobs.is_empty() {
c9effeac326aatagen2026-10-25 10:32 UTC759
            return;
c9effeac326aatagen2026-10-25 10:32 UTC760
        }
74befbc72bafatagen2026-10-25 11:18 UTC761
        let restored = (|| {
74befbc72bafatagen2026-10-25 11:18 UTC762
            let meta: Value = serde_json::from_slice(&store.take(&mut blobs, "meta")?).ok()?;
74befbc72bafatagen2026-10-25 11:18 UTC763
            if meta["schema"].as_u64() != Some(SCHEMA) {
74befbc72bafatagen2026-10-25 11:18 UTC764
                return None;
74befbc72bafatagen2026-10-25 11:18 UTC765
            }
74befbc72bafatagen2026-10-25 11:18 UTC766
            let rooms: BTreeMap<String, RoomProgress> =
74befbc72bafatagen2026-10-25 11:18 UTC767
                serde_json::from_value(meta["rooms"].clone()).ok()?;
74befbc72bafatagen2026-10-25 11:18 UTC768
            let mut pending = BTreeMap::new();
74befbc72bafatagen2026-10-25 11:18 UTC769
            for shard in 0..UTD_SHARDS {
74befbc72bafatagen2026-10-25 11:18 UTC770
                if let Some(bytes) = store.take(&mut blobs, &format!("utd/{shard}")) {
74befbc72bafatagen2026-10-25 11:18 UTC771
                    let part: BTreeMap<String, Pending> = serde_json::from_slice(&bytes).ok()?;
74befbc72bafatagen2026-10-25 11:18 UTC772
                    pending.extend(part);
74befbc72bafatagen2026-10-25 11:18 UTC773
                }
74befbc72bafatagen2026-10-25 11:18 UTC774
            }
74befbc72bafatagen2026-10-25 11:18 UTC775
            let index = KeywordIndex::load(&meta["index"], |name| store.take(&mut blobs, name))
74befbc72bafatagen2026-10-25 11:18 UTC776
                .inspect_err(|error| tracing::warn!(%error, "search segments are unreadable"))
74befbc72bafatagen2026-10-25 11:18 UTC777
                .ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC778
            Some((index, rooms, pending))
74befbc72bafatagen2026-10-25 11:18 UTC779
        })();
c9effeac326aatagen2026-10-25 10:32 UTC780
        match restored {
c9effeac326aatagen2026-10-25 10:32 UTC781
            Some((index, rooms, pending)) => {
c9effeac326aatagen2026-10-25 10:32 UTC782
                self.index = Box::new(index);
74befbc72bafatagen2026-10-25 11:18 UTC783
                self.baselines = rooms
74befbc72bafatagen2026-10-25 11:18 UTC784
                    .iter()
74befbc72bafatagen2026-10-25 11:18 UTC785
                    .filter_map(|(room, progress)| Some((room.clone(), progress.newest?)))
74befbc72bafatagen2026-10-25 11:18 UTC786
                    .collect();
c9effeac326aatagen2026-10-25 10:32 UTC787
                self.rooms = rooms;
c9effeac326aatagen2026-10-25 10:32 UTC788
                self.pending = pending;
74befbc72bafatagen2026-10-25 11:18 UTC789
                self.orphans = blobs.leftover();
74befbc72bafatagen2026-10-25 11:18 UTC790
                if !self.orphans.is_empty() {
74befbc72bafatagen2026-10-25 11:18 UTC791
                    self.touch();
74befbc72bafatagen2026-10-25 11:18 UTC792
                }
c9effeac326aatagen2026-10-25 10:32 UTC793
            }
c9effeac326aatagen2026-10-25 10:32 UTC794
            None => {
c9effeac326aatagen2026-10-25 10:32 UTC795
                tracing::warn!("resetting an unreadable search index");
c9effeac326aatagen2026-10-25 10:32 UTC796
                if let Err(error) = store.reset().await {
c9effeac326aatagen2026-10-25 10:32 UTC797
                    tracing::error!(%error, "search store unavailable, the index lives in memory");
74befbc72bafatagen2026-10-25 11:18 UTC798
                    self.fall_back(SearchInMemory::ReadFailed);
c9effeac326aatagen2026-10-25 10:32 UTC799
                }
c9effeac326aatagen2026-10-25 10:32 UTC800
            }
c9effeac326aatagen2026-10-25 10:32 UTC801
        }
c9effeac326aatagen2026-10-25 10:32 UTC802
    }
c9effeac326aatagen2026-10-25 10:32 UTC803
c9effeac326aatagen2026-10-25 10:32 UTC804
    #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
74befbc72bafatagen2026-10-25 11:18 UTC805
    async fn load(&mut self) {}
c9effeac326aatagen2026-10-25 10:32 UTC806
c9effeac326aatagen2026-10-25 10:32 UTC807
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC808
    async fn commit(&mut self) {
74befbc72bafatagen2026-10-25 11:18 UTC809
        if !matches!(self.persistence, Persistence::Store(_)) {
c9effeac326aatagen2026-10-25 10:32 UTC810
            if let Err(error) = self.index.maintain() {
c9effeac326aatagen2026-10-25 10:32 UTC811
                tracing::warn!(%error, "maintaining the search index failed");
c9effeac326aatagen2026-10-25 10:32 UTC812
            }
c9effeac326aatagen2026-10-25 10:32 UTC813
            self.dirty_since = None;
74befbc72bafatagen2026-10-25 11:18 UTC814
            self.dirty_shards.clear();
c9effeac326aatagen2026-10-25 10:32 UTC815
            return;
c9effeac326aatagen2026-10-25 10:32 UTC816
        }
74befbc72bafatagen2026-10-25 11:18 UTC817
        let mut changes = match self.index.changes() {
c9effeac326aatagen2026-10-25 10:32 UTC818
            Ok(changes) => changes,
c9effeac326aatagen2026-10-25 10:32 UTC819
            Err(error) => {
c9effeac326aatagen2026-10-25 10:32 UTC820
                tracing::warn!(%error, "preparing a search commit failed");
74befbc72bafatagen2026-10-25 11:18 UTC821
                self.commit_failed();
c9effeac326aatagen2026-10-25 10:32 UTC822
                return;
c9effeac326aatagen2026-10-25 10:32 UTC823
            }
c9effeac326aatagen2026-10-25 10:32 UTC824
        };
74befbc72bafatagen2026-10-25 11:18 UTC825
        let mut puts = std::mem::take(&mut changes.puts);
74befbc72bafatagen2026-10-25 11:18 UTC826
        let mut deletes = changes.deletes.clone();
c9effeac326aatagen2026-10-25 10:32 UTC827
        let meta = serde_json::json!({
c9effeac326aatagen2026-10-25 10:32 UTC828
            "schema": SCHEMA,
74befbc72bafatagen2026-10-25 11:18 UTC829
            "index": changes.manifest,
c9effeac326aatagen2026-10-25 10:32 UTC830
            "rooms": self.rooms,
c9effeac326aatagen2026-10-25 10:32 UTC831
        });
c9effeac326aatagen2026-10-25 10:32 UTC832
        puts.push(("meta".to_owned(), meta.to_string().into_bytes()));
74befbc72bafatagen2026-10-25 11:18 UTC833
        for shard in &self.dirty_shards {
74befbc72bafatagen2026-10-25 11:18 UTC834
            let part: BTreeMap<&String, &Pending> = self
74befbc72bafatagen2026-10-25 11:18 UTC835
                .pending
74befbc72bafatagen2026-10-25 11:18 UTC836
                .iter()
74befbc72bafatagen2026-10-25 11:18 UTC837
                .filter(|(key, _)| self::shard(key) == *shard)
74befbc72bafatagen2026-10-25 11:18 UTC838
                .collect();
74befbc72bafatagen2026-10-25 11:18 UTC839
            let name = format!("utd/{shard}");
74befbc72bafatagen2026-10-25 11:18 UTC840
            if part.is_empty() {
74befbc72bafatagen2026-10-25 11:18 UTC841
                deletes.push(name);
74befbc72bafatagen2026-10-25 11:18 UTC842
            } else {
74befbc72bafatagen2026-10-25 11:18 UTC843
                puts.push((name, serde_json::to_vec(&part).unwrap_or_default()));
74befbc72bafatagen2026-10-25 11:18 UTC844
            }
c9effeac326aatagen2026-10-25 10:32 UTC845
        }
74befbc72bafatagen2026-10-25 11:18 UTC846
        let Persistence::Store(store) = &self.persistence else {
c9effeac326aatagen2026-10-25 10:32 UTC847
            return;
c9effeac326aatagen2026-10-25 10:32 UTC848
        };
74befbc72bafatagen2026-10-25 11:18 UTC849
        match store.commit(puts, deletes, self.orphans.clone()).await {
c9effeac326aatagen2026-10-25 10:32 UTC850
            Ok(()) => {
c9effeac326aatagen2026-10-25 10:32 UTC851
                self.index.committed(&changes);
74befbc72bafatagen2026-10-25 11:18 UTC852
                self.dirty_shards.clear();
74befbc72bafatagen2026-10-25 11:18 UTC853
                self.orphans.clear();
c9effeac326aatagen2026-10-25 10:32 UTC854
                self.dirty_since = None;
74befbc72bafatagen2026-10-25 11:18 UTC855
                self.commit_failures = 0;
c9effeac326aatagen2026-10-25 10:32 UTC856
            }
c9effeac326aatagen2026-10-25 10:32 UTC857
            Err(error) => {
74befbc72bafatagen2026-10-25 11:18 UTC858
                tracing::warn!(%error, "saving the search index failed");
74befbc72bafatagen2026-10-25 11:18 UTC859
                self.commit_failed();
c9effeac326aatagen2026-10-25 10:32 UTC860
            }
c9effeac326aatagen2026-10-25 10:32 UTC861
        }
c9effeac326aatagen2026-10-25 10:32 UTC862
    }
c9effeac326aatagen2026-10-25 10:32 UTC863
74befbc72bafatagen2026-10-25 11:18 UTC864
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
74befbc72bafatagen2026-10-25 11:18 UTC865
    fn commit_failed(&mut self) {
74befbc72bafatagen2026-10-25 11:18 UTC866
        self.commit_failures += 1;
74befbc72bafatagen2026-10-25 11:18 UTC867
        if self.commit_failures >= COMMIT_ATTEMPTS {
74befbc72bafatagen2026-10-25 11:18 UTC868
            tracing::error!("search commits keep failing, the index lives in memory from here");
74befbc72bafatagen2026-10-25 11:18 UTC869
            self.fall_back(SearchInMemory::CommitFailed);
74befbc72bafatagen2026-10-25 11:18 UTC870
            self.dirty_since = None;
74befbc72bafatagen2026-10-25 11:18 UTC871
            self.dirty_shards.clear();
74befbc72bafatagen2026-10-25 11:18 UTC872
        } else {
74befbc72bafatagen2026-10-25 11:18 UTC873
            self.commit_after = now() + COMMIT_RETRY_MS;
74befbc72bafatagen2026-10-25 11:18 UTC874
        }
74befbc72bafatagen2026-10-25 11:18 UTC875
    }
74befbc72bafatagen2026-10-25 11:18 UTC876
c9effeac326aatagen2026-10-25 10:32 UTC877
    #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
c9effeac326aatagen2026-10-25 10:32 UTC878
    async fn commit(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC879
        if let Err(error) = self.index.maintain() {
c9effeac326aatagen2026-10-25 10:32 UTC880
            tracing::warn!(%error, "maintaining the search index failed");
c9effeac326aatagen2026-10-25 10:32 UTC881
        }
c9effeac326aatagen2026-10-25 10:32 UTC882
        self.dirty_since = None;
74befbc72bafatagen2026-10-25 11:18 UTC883
        self.dirty_shards.clear();
c9effeac326aatagen2026-10-25 10:32 UTC884
    }
c9effeac326aatagen2026-10-25 10:32 UTC885
c9effeac326aatagen2026-10-25 10:32 UTC886
    fn touch(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC887
        self.dirty_since.get_or_insert_with(now);
c9effeac326aatagen2026-10-25 10:32 UTC888
    }
c9effeac326aatagen2026-10-25 10:32 UTC889
74befbc72bafatagen2026-10-25 11:18 UTC890
    fn unpend(&mut self, key: &str) {
74befbc72bafatagen2026-10-25 11:18 UTC891
        if self.pending.remove(key).is_some() {
74befbc72bafatagen2026-10-25 11:18 UTC892
            self.dirty_shards.insert(shard(key));
74befbc72bafatagen2026-10-25 11:18 UTC893
            self.touch();
74befbc72bafatagen2026-10-25 11:18 UTC894
        }
74befbc72bafatagen2026-10-25 11:18 UTC895
    }
74befbc72bafatagen2026-10-25 11:18 UTC896
c9effeac326aatagen2026-10-25 10:32 UTC897
    fn observe(&mut self, room: &OwnedRoomId, value: &Value) {
c9effeac326aatagen2026-10-25 10:32 UTC898
        let observed = feed::observe(value);
c9effeac326aatagen2026-10-25 10:32 UTC899
        let key = match &observed {
c9effeac326aatagen2026-10-25 10:32 UTC900
            Observed::Message { key, .. }
c9effeac326aatagen2026-10-25 10:32 UTC901
            | Observed::Redacted { key }
c9effeac326aatagen2026-10-25 10:32 UTC902
            | Observed::Encrypted { key, .. } => Some(key.clone()),
c9effeac326aatagen2026-10-25 10:32 UTC903
            Observed::Edit(edit) => Some(edit.id.clone()),
c9effeac326aatagen2026-10-25 10:32 UTC904
            Observed::Redaction { .. } | Observed::Other => None,
c9effeac326aatagen2026-10-25 10:32 UTC905
        };
c9effeac326aatagen2026-10-25 10:32 UTC906
        if let Observed::Encrypted {
c9effeac326aatagen2026-10-25 10:32 UTC907
            key,
c9effeac326aatagen2026-10-25 10:32 UTC908
            session_id,
c9effeac326aatagen2026-10-25 10:32 UTC909
            ts,
c9effeac326aatagen2026-10-25 10:32 UTC910
        } = &observed
c9effeac326aatagen2026-10-25 10:32 UTC911
        {
c9effeac326aatagen2026-10-25 10:32 UTC912
            if self.index.get(key).is_none() && !self.pending.contains_key(key) {
c9effeac326aatagen2026-10-25 10:32 UTC913
                self.pending.insert(
c9effeac326aatagen2026-10-25 10:32 UTC914
                    key.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC915
                    Pending {
c9effeac326aatagen2026-10-25 10:32 UTC916
                        room: room.to_string(),
c9effeac326aatagen2026-10-25 10:32 UTC917
                        session_id: session_id.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC918
                        ts: *ts,
c9effeac326aatagen2026-10-25 10:32 UTC919
                        raw: value.to_string(),
c9effeac326aatagen2026-10-25 10:32 UTC920
                    },
c9effeac326aatagen2026-10-25 10:32 UTC921
                );
74befbc72bafatagen2026-10-25 11:18 UTC922
                self.dirty_shards.insert(shard(key));
c9effeac326aatagen2026-10-25 10:32 UTC923
                while self.pending.len() > self.budget.max_pending {
c9effeac326aatagen2026-10-25 10:32 UTC924
                    let Some(oldest) = self
c9effeac326aatagen2026-10-25 10:32 UTC925
                        .pending
c9effeac326aatagen2026-10-25 10:32 UTC926
                        .iter()
c9effeac326aatagen2026-10-25 10:32 UTC927
                        .min_by_key(|(_, pending)| pending.ts)
c9effeac326aatagen2026-10-25 10:32 UTC928
                        .map(|(key, _)| key.clone())
c9effeac326aatagen2026-10-25 10:32 UTC929
                    else {
c9effeac326aatagen2026-10-25 10:32 UTC930
                        break;
c9effeac326aatagen2026-10-25 10:32 UTC931
                    };
74befbc72bafatagen2026-10-25 11:18 UTC932
                    self.unpend(&oldest);
c9effeac326aatagen2026-10-25 10:32 UTC933
                }
c9effeac326aatagen2026-10-25 10:32 UTC934
                self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC935
            }
c9effeac326aatagen2026-10-25 10:32 UTC936
            return;
c9effeac326aatagen2026-10-25 10:32 UTC937
        }
74befbc72bafatagen2026-10-25 11:18 UTC938
        if let Some(key) = key {
74befbc72bafatagen2026-10-25 11:18 UTC939
            self.unpend(&key);
c9effeac326aatagen2026-10-25 10:32 UTC940
        }
c9effeac326aatagen2026-10-25 10:32 UTC941
        match feed::apply(
c9effeac326aatagen2026-10-25 10:32 UTC942
            self.index.as_mut(),
c9effeac326aatagen2026-10-25 10:32 UTC943
            &mut self.redactions,
c9effeac326aatagen2026-10-25 10:32 UTC944
            room.as_str(),
c9effeac326aatagen2026-10-25 10:32 UTC945
            observed,
c9effeac326aatagen2026-10-25 10:32 UTC946
        ) {
c9effeac326aatagen2026-10-25 10:32 UTC947
            Effect::Indexed(ts) => {
c9effeac326aatagen2026-10-25 10:32 UTC948
                let progress = self.rooms.entry(room.to_string()).or_default();
c9effeac326aatagen2026-10-25 10:32 UTC949
                progress.oldest = Some(progress.oldest.map_or(ts, |oldest| oldest.min(ts)));
c9effeac326aatagen2026-10-25 10:32 UTC950
                progress.newest = Some(progress.newest.map_or(ts, |newest| newest.max(ts)));
c9effeac326aatagen2026-10-25 10:32 UTC951
                self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC952
            }
74befbc72bafatagen2026-10-25 11:18 UTC953
            Effect::Removed | Effect::Held => self.touch(),
c9effeac326aatagen2026-10-25 10:32 UTC954
            Effect::Refetch(key) => {
c9effeac326aatagen2026-10-25 10:32 UTC955
                self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC956
                let room_id = room.clone();
c9effeac326aatagen2026-10-25 10:32 UTC957
                let client = self.client.clone();
c9effeac326aatagen2026-10-25 10:32 UTC958
                self.work.push(Box::pin(async move {
74befbc72bafatagen2026-10-25 11:18 UTC959
                    let values = match client.get_room(&room_id) {
74befbc72bafatagen2026-10-25 11:18 UTC960
                        Some(room) => load_with_edits(&room, &key).await,
74befbc72bafatagen2026-10-25 11:18 UTC961
                        None => Vec::new(),
c9effeac326aatagen2026-10-25 10:32 UTC962
                    };
c9effeac326aatagen2026-10-25 10:32 UTC963
                    Work::Refetched {
c9effeac326aatagen2026-10-25 10:32 UTC964
                        room: room_id,
74befbc72bafatagen2026-10-25 11:18 UTC965
                        values,
c9effeac326aatagen2026-10-25 10:32 UTC966
                    }
c9effeac326aatagen2026-10-25 10:32 UTC967
                }));
c9effeac326aatagen2026-10-25 10:32 UTC968
            }
c9effeac326aatagen2026-10-25 10:32 UTC969
            Effect::Nothing => {}
c9effeac326aatagen2026-10-25 10:32 UTC970
        }
c9effeac326aatagen2026-10-25 10:32 UTC971
    }
c9effeac326aatagen2026-10-25 10:32 UTC972
c9effeac326aatagen2026-10-25 10:32 UTC973
    fn observe_all(&mut self, room: &OwnedRoomId, events: &[SdkTimelineEvent]) {
c9effeac326aatagen2026-10-25 10:32 UTC974
        for event in events {
c9effeac326aatagen2026-10-25 10:32 UTC975
            if let Some(value) = value_of(event) {
c9effeac326aatagen2026-10-25 10:32 UTC976
                self.observe(room, &value);
c9effeac326aatagen2026-10-25 10:32 UTC977
            }
c9effeac326aatagen2026-10-25 10:32 UTC978
        }
c9effeac326aatagen2026-10-25 10:32 UTC979
    }
c9effeac326aatagen2026-10-25 10:32 UTC980
c9effeac326aatagen2026-10-25 10:32 UTC981
    async fn subscribe(&mut self, room_id: OwnedRoomId) {
c9effeac326aatagen2026-10-25 10:32 UTC982
        if let Some((_, seen)) = self.subscriptions.get_mut(&room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC983
            *seen = now();
c9effeac326aatagen2026-10-25 10:32 UTC984
            return;
c9effeac326aatagen2026-10-25 10:32 UTC985
        }
c9effeac326aatagen2026-10-25 10:32 UTC986
        let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC987
            return;
c9effeac326aatagen2026-10-25 10:32 UTC988
        };
c9effeac326aatagen2026-10-25 10:32 UTC989
        if room.state() != RoomState::Joined {
c9effeac326aatagen2026-10-25 10:32 UTC990
            return;
c9effeac326aatagen2026-10-25 10:32 UTC991
        }
c9effeac326aatagen2026-10-25 10:32 UTC992
        let Ok((cache, drop_handles)) = room.event_cache().await else {
c9effeac326aatagen2026-10-25 10:32 UTC993
            return;
c9effeac326aatagen2026-10-25 10:32 UTC994
        };
c9effeac326aatagen2026-10-25 10:32 UTC995
        let Ok((initial, subscriber)) = cache.subscribe().await else {
c9effeac326aatagen2026-10-25 10:32 UTC996
            return;
c9effeac326aatagen2026-10-25 10:32 UTC997
        };
c9effeac326aatagen2026-10-25 10:32 UTC998
        self.observe_all(&room_id, &initial);
c9effeac326aatagen2026-10-25 10:32 UTC999
        let stream = updates(room_id.clone(), subscriber, drop_handles);
c9effeac326aatagen2026-10-25 10:32 UTC1000
        let (stream, abort) = abortable(stream);
c9effeac326aatagen2026-10-25 10:32 UTC1001
        self.updates.push(Box::pin(stream));
c9effeac326aatagen2026-10-25 10:32 UTC1002
        self.subscriptions.insert(room_id.clone(), (abort, now()));
74befbc72bafatagen2026-10-25 11:18 UTC1003
        let started = self
74befbc72bafatagen2026-10-25 11:18 UTC1004
            .rooms
74befbc72bafatagen2026-10-25 11:18 UTC1005
            .get(room_id.as_str())
74befbc72bafatagen2026-10-25 11:18 UTC1006
            .is_some_and(|progress| progress.started);
74befbc72bafatagen2026-10-25 11:18 UTC1007
        if started
74befbc72bafatagen2026-10-25 11:18 UTC1008
            && self.baselines.contains_key(room_id.as_str())
74befbc72bafatagen2026-10-25 11:18 UTC1009
            && self.headed.insert(room_id.clone())
74befbc72bafatagen2026-10-25 11:18 UTC1010
        {
c9effeac326aatagen2026-10-25 10:32 UTC1011
            self.heads.push_back(room_id);
c9effeac326aatagen2026-10-25 10:32 UTC1012
        }
c9effeac326aatagen2026-10-25 10:32 UTC1013
    }
c9effeac326aatagen2026-10-25 10:32 UTC1014
74befbc72bafatagen2026-10-25 11:18 UTC1015
    /// the generic channel holds 128 updates, and a missed one could be a room we never
74befbc72bafatagen2026-10-25 11:18 UTC1016
    /// follow, so a lag follows every joined room
74befbc72bafatagen2026-10-25 11:18 UTC1017
    async fn generic(&mut self, update: Result<OwnedRoomId, broadcast::error::RecvError>) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC1018
        match update {
74befbc72bafatagen2026-10-25 11:18 UTC1019
            Ok(room_id) => self.subscribe(room_id).await,
74befbc72bafatagen2026-10-25 11:18 UTC1020
            Err(broadcast::error::RecvError::Lagged(_)) => {
74befbc72bafatagen2026-10-25 11:18 UTC1021
                for room in self.client.joined_rooms() {
74befbc72bafatagen2026-10-25 11:18 UTC1022
                    self.subscribe(room.room_id().to_owned()).await;
74befbc72bafatagen2026-10-25 11:18 UTC1023
                }
74befbc72bafatagen2026-10-25 11:18 UTC1024
            }
74befbc72bafatagen2026-10-25 11:18 UTC1025
            Err(broadcast::error::RecvError::Closed) => return false,
74befbc72bafatagen2026-10-25 11:18 UTC1026
        }
74befbc72bafatagen2026-10-25 11:18 UTC1027
        true
74befbc72bafatagen2026-10-25 11:18 UTC1028
    }
74befbc72bafatagen2026-10-25 11:18 UTC1029
74befbc72bafatagen2026-10-25 11:18 UTC1030
    async fn room_update(&mut self, room_id: OwnedRoomId, update: RoomUpdate) {
74befbc72bafatagen2026-10-25 11:18 UTC1031
        match update {
74befbc72bafatagen2026-10-25 11:18 UTC1032
            RoomUpdate::Events(events) => {
74befbc72bafatagen2026-10-25 11:18 UTC1033
                if let Some((_, seen)) = self.subscriptions.get_mut(&room_id) {
74befbc72bafatagen2026-10-25 11:18 UTC1034
                    *seen = now();
74befbc72bafatagen2026-10-25 11:18 UTC1035
                }
74befbc72bafatagen2026-10-25 11:18 UTC1036
                self.observe_all(&room_id, &events);
74befbc72bafatagen2026-10-25 11:18 UTC1037
            }
74befbc72bafatagen2026-10-25 11:18 UTC1038
            RoomUpdate::Lagged => {
74befbc72bafatagen2026-10-25 11:18 UTC1039
                if let Some((abort, _)) = self.subscriptions.remove(&room_id) {
74befbc72bafatagen2026-10-25 11:18 UTC1040
                    abort.abort();
74befbc72bafatagen2026-10-25 11:18 UTC1041
                }
74befbc72bafatagen2026-10-25 11:18 UTC1042
                self.subscribe(room_id).await;
74befbc72bafatagen2026-10-25 11:18 UTC1043
            }
74befbc72bafatagen2026-10-25 11:18 UTC1044
            RoomUpdate::Closed => {
74befbc72bafatagen2026-10-25 11:18 UTC1045
                self.subscriptions.remove(&room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1046
            }
74befbc72bafatagen2026-10-25 11:18 UTC1047
        }
74befbc72bafatagen2026-10-25 11:18 UTC1048
    }
74befbc72bafatagen2026-10-25 11:18 UTC1049
74befbc72bafatagen2026-10-25 11:18 UTC1050
    fn left(&mut self, room_id: &OwnedRoomId) {
74befbc72bafatagen2026-10-25 11:18 UTC1051
        if let Some((abort, _)) = self.subscriptions.remove(room_id) {
74befbc72bafatagen2026-10-25 11:18 UTC1052
            abort.abort();
74befbc72bafatagen2026-10-25 11:18 UTC1053
        }
74befbc72bafatagen2026-10-25 11:18 UTC1054
        self.heads.retain(|head| head != room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1055
        self.head_tokens.remove(room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1056
        if let Some((backfilling, abort)) = &self.backfilling
74befbc72bafatagen2026-10-25 11:18 UTC1057
            && backfilling == room_id
74befbc72bafatagen2026-10-25 11:18 UTC1058
        {
74befbc72bafatagen2026-10-25 11:18 UTC1059
            abort.abort();
74befbc72bafatagen2026-10-25 11:18 UTC1060
        }
74befbc72bafatagen2026-10-25 11:18 UTC1061
    }
74befbc72bafatagen2026-10-25 11:18 UTC1062
c9effeac326aatagen2026-10-25 10:32 UTC1063
    fn drop_idle(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC1064
        let cutoff = now().saturating_sub(IDLE_SUBSCRIPTION_MS);
c9effeac326aatagen2026-10-25 10:32 UTC1065
        self.subscriptions.retain(|_, (abort, seen)| {
c9effeac326aatagen2026-10-25 10:32 UTC1066
            let keep = *seen >= cutoff;
c9effeac326aatagen2026-10-25 10:32 UTC1067
            if !keep {
c9effeac326aatagen2026-10-25 10:32 UTC1068
                abort.abort();
c9effeac326aatagen2026-10-25 10:32 UTC1069
            }
c9effeac326aatagen2026-10-25 10:32 UTC1070
            keep
c9effeac326aatagen2026-10-25 10:32 UTC1071
        });
c9effeac326aatagen2026-10-25 10:32 UTC1072
    }
c9effeac326aatagen2026-10-25 10:32 UTC1073
c9effeac326aatagen2026-10-25 10:32 UTC1074
    fn coverage(&self, rooms: &[OwnedRoomId]) -> SearchCoverage {
74befbc72bafatagen2026-10-25 11:18 UTC1075
        let scoped: HashSet<&str> = rooms.iter().map(|room| room.as_str()).collect();
74befbc72bafatagen2026-10-25 11:18 UTC1076
        let undecrypted = self
74befbc72bafatagen2026-10-25 11:18 UTC1077
            .pending
74befbc72bafatagen2026-10-25 11:18 UTC1078
            .values()
74befbc72bafatagen2026-10-25 11:18 UTC1079
            .filter(|pending| scoped.contains(pending.room.as_str()))
74befbc72bafatagen2026-10-25 11:18 UTC1080
            .count();
c9effeac326aatagen2026-10-25 10:32 UTC1081
        let rooms = rooms
c9effeac326aatagen2026-10-25 10:32 UTC1082
            .iter()
c9effeac326aatagen2026-10-25 10:32 UTC1083
            .map(|room_id| {
c9effeac326aatagen2026-10-25 10:32 UTC1084
                let progress = self
c9effeac326aatagen2026-10-25 10:32 UTC1085
                    .rooms
c9effeac326aatagen2026-10-25 10:32 UTC1086
                    .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC1087
                    .cloned()
c9effeac326aatagen2026-10-25 10:32 UTC1088
                    .unwrap_or_default();
c9effeac326aatagen2026-10-25 10:32 UTC1089
                let backfill = if progress.start_reached {
c9effeac326aatagen2026-10-25 10:32 UTC1090
                    SearchBackfill::Complete
c9effeac326aatagen2026-10-25 10:32 UTC1091
                } else if self.budget.pages_per_room == 0 {
c9effeac326aatagen2026-10-25 10:32 UTC1092
                    SearchBackfill::Disabled
74befbc72bafatagen2026-10-25 11:18 UTC1093
                } else if self
74befbc72bafatagen2026-10-25 11:18 UTC1094
                    .backfilling
74befbc72bafatagen2026-10-25 11:18 UTC1095
                    .as_ref()
74befbc72bafatagen2026-10-25 11:18 UTC1096
                    .is_some_and(|(backfilling, _)| backfilling == room_id)
c9effeac326aatagen2026-10-25 10:32 UTC1097
                {
74befbc72bafatagen2026-10-25 11:18 UTC1098
                    SearchBackfill::Running
74befbc72bafatagen2026-10-25 11:18 UTC1099
                } else if let Some(failure) = self.failures.get(room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC1100
                    SearchBackfill::Failed {
74befbc72bafatagen2026-10-25 11:18 UTC1101
                        message: failure.message.clone(),
74befbc72bafatagen2026-10-25 11:18 UTC1102
                        retry_at_ms: failure.retry_at_ms,
c9effeac326aatagen2026-10-25 10:32 UTC1103
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1104
                } else if self.pages.get(room_id).copied().unwrap_or(0)
c9effeac326aatagen2026-10-25 10:32 UTC1105
                    >= self.budget.pages_per_room
c9effeac326aatagen2026-10-25 10:32 UTC1106
                {
c9effeac326aatagen2026-10-25 10:32 UTC1107
                    SearchBackfill::Paused
c9effeac326aatagen2026-10-25 10:32 UTC1108
                } else {
c9effeac326aatagen2026-10-25 10:32 UTC1109
                    SearchBackfill::Waiting
c9effeac326aatagen2026-10-25 10:32 UTC1110
                };
c9effeac326aatagen2026-10-25 10:32 UTC1111
                RoomSearchCoverage {
c9effeac326aatagen2026-10-25 10:32 UTC1112
                    room_id: room_id.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC1113
                    oldest_indexed_ms: progress.oldest,
c9effeac326aatagen2026-10-25 10:32 UTC1114
                    newest_indexed_ms: progress.newest,
c9effeac326aatagen2026-10-25 10:32 UTC1115
                    start_reached: progress.start_reached,
c9effeac326aatagen2026-10-25 10:32 UTC1116
                    backfill,
c9effeac326aatagen2026-10-25 10:32 UTC1117
                }
c9effeac326aatagen2026-10-25 10:32 UTC1118
            })
c9effeac326aatagen2026-10-25 10:32 UTC1119
            .collect();
74befbc72bafatagen2026-10-25 11:18 UTC1120
        let in_memory = self.in_memory();
c9effeac326aatagen2026-10-25 10:32 UTC1121
        SearchCoverage {
c9effeac326aatagen2026-10-25 10:32 UTC1122
            rooms,
74befbc72bafatagen2026-10-25 11:18 UTC1123
            persistent: in_memory.is_none(),
74befbc72bafatagen2026-10-25 11:18 UTC1124
            in_memory,
74befbc72bafatagen2026-10-25 11:18 UTC1125
            undecrypted,
c9effeac326aatagen2026-10-25 10:32 UTC1126
        }
c9effeac326aatagen2026-10-25 10:32 UTC1127
    }
c9effeac326aatagen2026-10-25 10:32 UTC1128
74befbc72bafatagen2026-10-25 11:18 UTC1129
    fn spent(&self, room_id: &OwnedRoomId) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC1130
        self.pages.get(room_id).copied().unwrap_or(0) >= self.budget.pages_per_room
74befbc72bafatagen2026-10-25 11:18 UTC1131
    }
74befbc72bafatagen2026-10-25 11:18 UTC1132
74befbc72bafatagen2026-10-25 11:18 UTC1133
    fn blocked(&self, room_id: &OwnedRoomId, at: u64) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC1134
        self.failures
74befbc72bafatagen2026-10-25 11:18 UTC1135
            .get(room_id)
74befbc72bafatagen2026-10-25 11:18 UTC1136
            .is_some_and(|failure| failure.retry_at_ms > at)
74befbc72bafatagen2026-10-25 11:18 UTC1137
    }
74befbc72bafatagen2026-10-25 11:18 UTC1138
c9effeac326aatagen2026-10-25 10:32 UTC1139
    /// head passes first, so what arrived while we were away is found before anything older
c9effeac326aatagen2026-10-25 10:32 UTC1140
    fn schedule_backfill(&mut self) {
74befbc72bafatagen2026-10-25 11:18 UTC1141
        let at = now();
74befbc72bafatagen2026-10-25 11:18 UTC1142
        if self.backfilling.is_some() || self.budget.pages_per_room == 0 || at < self.next_backfill
c9effeac326aatagen2026-10-25 10:32 UTC1143
        {
c9effeac326aatagen2026-10-25 10:32 UTC1144
            return;
c9effeac326aatagen2026-10-25 10:32 UTC1145
        }
c9effeac326aatagen2026-10-25 10:32 UTC1146
        let mut chosen = None;
74befbc72bafatagen2026-10-25 11:18 UTC1147
        for _ in 0..self.heads.len() {
74befbc72bafatagen2026-10-25 11:18 UTC1148
            let Some(room_id) = self.heads.pop_front() else {
c9effeac326aatagen2026-10-25 10:32 UTC1149
                break;
74befbc72bafatagen2026-10-25 11:18 UTC1150
            };
74befbc72bafatagen2026-10-25 11:18 UTC1151
            if self.spent(&room_id) {
74befbc72bafatagen2026-10-25 11:18 UTC1152
                self.head_tokens.remove(&room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1153
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC1154
            }
74befbc72bafatagen2026-10-25 11:18 UTC1155
            if self.blocked(&room_id, at) {
74befbc72bafatagen2026-10-25 11:18 UTC1156
                self.heads.push_back(room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1157
                continue;
74befbc72bafatagen2026-10-25 11:18 UTC1158
            }
74befbc72bafatagen2026-10-25 11:18 UTC1159
            chosen = Some((room_id, true));
74befbc72bafatagen2026-10-25 11:18 UTC1160
            break;
c9effeac326aatagen2026-10-25 10:32 UTC1161
        }
c9effeac326aatagen2026-10-25 10:32 UTC1162
        if chosen.is_none() {
c9effeac326aatagen2026-10-25 10:32 UTC1163
            let mut joined: Vec<_> = self
c9effeac326aatagen2026-10-25 10:32 UTC1164
                .client
c9effeac326aatagen2026-10-25 10:32 UTC1165
                .joined_rooms()
c9effeac326aatagen2026-10-25 10:32 UTC1166
                .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC1167
                .map(|room| room.room_id().to_owned())
c9effeac326aatagen2026-10-25 10:32 UTC1168
                .collect();
c9effeac326aatagen2026-10-25 10:32 UTC1169
            joined.sort();
c9effeac326aatagen2026-10-25 10:32 UTC1170
            let count = joined.len();
c9effeac326aatagen2026-10-25 10:32 UTC1171
            for offset in 0..count {
c9effeac326aatagen2026-10-25 10:32 UTC1172
                let room_id = &joined[(self.turn + offset) % count];
c9effeac326aatagen2026-10-25 10:32 UTC1173
                let done = self
c9effeac326aatagen2026-10-25 10:32 UTC1174
                    .rooms
c9effeac326aatagen2026-10-25 10:32 UTC1175
                    .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC1176
                    .is_some_and(|progress| progress.start_reached);
74befbc72bafatagen2026-10-25 11:18 UTC1177
                if !done && !self.spent(room_id) && !self.blocked(room_id, at) {
c9effeac326aatagen2026-10-25 10:32 UTC1178
                    self.turn = (self.turn + offset + 1) % count;
c9effeac326aatagen2026-10-25 10:32 UTC1179
                    chosen = Some((room_id.clone(), false));
c9effeac326aatagen2026-10-25 10:32 UTC1180
                    break;
c9effeac326aatagen2026-10-25 10:32 UTC1181
                }
c9effeac326aatagen2026-10-25 10:32 UTC1182
            }
c9effeac326aatagen2026-10-25 10:32 UTC1183
        }
c9effeac326aatagen2026-10-25 10:32 UTC1184
        let Some((room_id, head)) = chosen else {
c9effeac326aatagen2026-10-25 10:32 UTC1185
            return;
c9effeac326aatagen2026-10-25 10:32 UTC1186
        };
c9effeac326aatagen2026-10-25 10:32 UTC1187
        let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC1188
            return;
c9effeac326aatagen2026-10-25 10:32 UTC1189
        };
c9effeac326aatagen2026-10-25 10:32 UTC1190
        let from = if head {
c9effeac326aatagen2026-10-25 10:32 UTC1191
            self.head_tokens.get(&room_id).cloned()
c9effeac326aatagen2026-10-25 10:32 UTC1192
        } else {
c9effeac326aatagen2026-10-25 10:32 UTC1193
            self.rooms
c9effeac326aatagen2026-10-25 10:32 UTC1194
                .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC1195
                .and_then(|progress| progress.tail.clone())
c9effeac326aatagen2026-10-25 10:32 UTC1196
        };
c9effeac326aatagen2026-10-25 10:32 UTC1197
        *self.pages.entry(room_id.clone()).or_default() += 1;
c9effeac326aatagen2026-10-25 10:32 UTC1198
        let mut options = MessagesOptions::backward();
74befbc72bafatagen2026-10-25 11:18 UTC1199
        options.from = from.clone();
c9effeac326aatagen2026-10-25 10:32 UTC1200
        options.limit = UInt::from(self.budget.page_size);
74befbc72bafatagen2026-10-25 11:18 UTC1201
        let mut filter = RoomEventFilter::default();
74befbc72bafatagen2026-10-25 11:18 UTC1202
        filter.types = Some(
74befbc72bafatagen2026-10-25 11:18 UTC1203
            BACKFILL_TYPES
74befbc72bafatagen2026-10-25 11:18 UTC1204
                .iter()
74befbc72bafatagen2026-10-25 11:18 UTC1205
                .map(|kind| (*kind).to_owned())
74befbc72bafatagen2026-10-25 11:18 UTC1206
                .collect(),
74befbc72bafatagen2026-10-25 11:18 UTC1207
        );
74befbc72bafatagen2026-10-25 11:18 UTC1208
        options.filter = filter;
74befbc72bafatagen2026-10-25 11:18 UTC1209
        let (abort, registration) = AbortHandle::new_pair();
74befbc72bafatagen2026-10-25 11:18 UTC1210
        self.backfilling = Some((room_id.clone(), abort));
74befbc72bafatagen2026-10-25 11:18 UTC1211
        let page = Abortable::new(
74befbc72bafatagen2026-10-25 11:18 UTC1212
            async move {
74befbc72bafatagen2026-10-25 11:18 UTC1213
                room.messages(options)
74befbc72bafatagen2026-10-25 11:18 UTC1214
                    .await
74befbc72bafatagen2026-10-25 11:18 UTC1215
                    .map_err(|error| Error::from_sdk("backfilling search history failed", error))
74befbc72bafatagen2026-10-25 11:18 UTC1216
            },
74befbc72bafatagen2026-10-25 11:18 UTC1217
            registration,
74befbc72bafatagen2026-10-25 11:18 UTC1218
        );
c9effeac326aatagen2026-10-25 10:32 UTC1219
        self.work.push(Box::pin(async move {
74befbc72bafatagen2026-10-25 11:18 UTC1220
            match page.await {
74befbc72bafatagen2026-10-25 11:18 UTC1221
                Ok(result) => Work::Backfill {
74befbc72bafatagen2026-10-25 11:18 UTC1222
                    room: room_id,
74befbc72bafatagen2026-10-25 11:18 UTC1223
                    head,
74befbc72bafatagen2026-10-25 11:18 UTC1224
                    from,
74befbc72bafatagen2026-10-25 11:18 UTC1225
                    result,
74befbc72bafatagen2026-10-25 11:18 UTC1226
                },
74befbc72bafatagen2026-10-25 11:18 UTC1227
                Err(_) => Work::BackfillCancelled,
c9effeac326aatagen2026-10-25 10:32 UTC1228
            }
c9effeac326aatagen2026-10-25 10:32 UTC1229
        }));
c9effeac326aatagen2026-10-25 10:32 UTC1230
    }
c9effeac326aatagen2026-10-25 10:32 UTC1231
74befbc72bafatagen2026-10-25 11:18 UTC1232
    fn fail_backfill(&mut self, room_id: OwnedRoomId, message: String, delay_ms: u64) {
74befbc72bafatagen2026-10-25 11:18 UTC1233
        self.failures.insert(
74befbc72bafatagen2026-10-25 11:18 UTC1234
            room_id,
74befbc72bafatagen2026-10-25 11:18 UTC1235
            Failure {
74befbc72bafatagen2026-10-25 11:18 UTC1236
                message,
74befbc72bafatagen2026-10-25 11:18 UTC1237
                retry_at_ms: now() + delay_ms,
74befbc72bafatagen2026-10-25 11:18 UTC1238
                delay_ms,
74befbc72bafatagen2026-10-25 11:18 UTC1239
            },
74befbc72bafatagen2026-10-25 11:18 UTC1240
        );
74befbc72bafatagen2026-10-25 11:18 UTC1241
    }
74befbc72bafatagen2026-10-25 11:18 UTC1242
c9effeac326aatagen2026-10-25 10:32 UTC1243
    fn backfilled(
c9effeac326aatagen2026-10-25 10:32 UTC1244
        &mut self,
c9effeac326aatagen2026-10-25 10:32 UTC1245
        room_id: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC1246
        head: bool,
74befbc72bafatagen2026-10-25 11:18 UTC1247
        from: Option<String>,
74befbc72bafatagen2026-10-25 11:18 UTC1248
        result: Result<Messages, Error>,
c9effeac326aatagen2026-10-25 10:32 UTC1249
    ) {
c9effeac326aatagen2026-10-25 10:32 UTC1250
        self.backfilling = None;
74befbc72bafatagen2026-10-25 11:18 UTC1251
        self.next_backfill = now() + self.budget.interval_ms;
c9effeac326aatagen2026-10-25 10:32 UTC1252
        let messages = match result {
c9effeac326aatagen2026-10-25 10:32 UTC1253
            Ok(messages) => messages,
74befbc72bafatagen2026-10-25 11:18 UTC1254
            Err(error) if from.is_some() && token_rejected(&error) => {
74befbc72bafatagen2026-10-25 11:18 UTC1255
                if head {
74befbc72bafatagen2026-10-25 11:18 UTC1256
                    self.head_tokens.remove(&room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1257
                } else {
74befbc72bafatagen2026-10-25 11:18 UTC1258
                    let progress = self.rooms.entry(room_id.to_string()).or_default();
74befbc72bafatagen2026-10-25 11:18 UTC1259
                    progress.tail = None;
74befbc72bafatagen2026-10-25 11:18 UTC1260
                    progress.started = false;
74befbc72bafatagen2026-10-25 11:18 UTC1261
                    self.touch();
74befbc72bafatagen2026-10-25 11:18 UTC1262
                }
74befbc72bafatagen2026-10-25 11:18 UTC1263
                let message = format!("{}, starting again from the newest", error.message);
74befbc72bafatagen2026-10-25 11:18 UTC1264
                self.fail_backfill(room_id, message, self.budget.interval_ms);
74befbc72bafatagen2026-10-25 11:18 UTC1265
                return;
74befbc72bafatagen2026-10-25 11:18 UTC1266
            }
c9effeac326aatagen2026-10-25 10:32 UTC1267
            Err(error) => {
74befbc72bafatagen2026-10-25 11:18 UTC1268
                let delay_ms = self
74befbc72bafatagen2026-10-25 11:18 UTC1269
                    .failures
74befbc72bafatagen2026-10-25 11:18 UTC1270
                    .get(&room_id)
74befbc72bafatagen2026-10-25 11:18 UTC1271
                    .map_or(BACKFILL_RETRY_MS, |failure| {
74befbc72bafatagen2026-10-25 11:18 UTC1272
                        (failure.delay_ms * 2).min(BACKFILL_RETRY_MAX_MS)
74befbc72bafatagen2026-10-25 11:18 UTC1273
                    });
74befbc72bafatagen2026-10-25 11:18 UTC1274
                self.fail_backfill(room_id, error.message.clone(), delay_ms);
c9effeac326aatagen2026-10-25 10:32 UTC1275
                return;
c9effeac326aatagen2026-10-25 10:32 UTC1276
            }
c9effeac326aatagen2026-10-25 10:32 UTC1277
        };
c9effeac326aatagen2026-10-25 10:32 UTC1278
        self.failures.remove(&room_id);
74befbc72bafatagen2026-10-25 11:18 UTC1279
        let baseline = self.baselines.get(room_id.as_str()).copied();
c9effeac326aatagen2026-10-25 10:32 UTC1280
        let mut overlapped = false;
c9effeac326aatagen2026-10-25 10:32 UTC1281
        for event in &messages.chunk {
c9effeac326aatagen2026-10-25 10:32 UTC1282
            let Some(value) = value_of(event) else {
c9effeac326aatagen2026-10-25 10:32 UTC1283
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC1284
            };
74befbc72bafatagen2026-10-25 11:18 UTC1285
            if head && head_overlap(self.index.as_ref(), baseline, &value) {
c9effeac326aatagen2026-10-25 10:32 UTC1286
                overlapped = true;
c9effeac326aatagen2026-10-25 10:32 UTC1287
            }
c9effeac326aatagen2026-10-25 10:32 UTC1288
            self.observe(&room_id, &value);
c9effeac326aatagen2026-10-25 10:32 UTC1289
        }
74befbc72bafatagen2026-10-25 11:18 UTC1290
        let ended = messages.end.is_none() || (messages.chunk.is_empty() && messages.end == from);
c9effeac326aatagen2026-10-25 10:32 UTC1291
        if head {
c9effeac326aatagen2026-10-25 10:32 UTC1292
            match messages.end {
c9effeac326aatagen2026-10-25 10:32 UTC1293
                Some(end) if !overlapped && !ended => {
c9effeac326aatagen2026-10-25 10:32 UTC1294
                    self.head_tokens.insert(room_id.clone(), end);
c9effeac326aatagen2026-10-25 10:32 UTC1295
                    self.heads.push_front(room_id);
c9effeac326aatagen2026-10-25 10:32 UTC1296
                }
c9effeac326aatagen2026-10-25 10:32 UTC1297
                _ => {
c9effeac326aatagen2026-10-25 10:32 UTC1298
                    self.head_tokens.remove(&room_id);
c9effeac326aatagen2026-10-25 10:32 UTC1299
                }
c9effeac326aatagen2026-10-25 10:32 UTC1300
            }
c9effeac326aatagen2026-10-25 10:32 UTC1301
            return;
c9effeac326aatagen2026-10-25 10:32 UTC1302
        }
c9effeac326aatagen2026-10-25 10:32 UTC1303
        let progress = self.rooms.entry(room_id.to_string()).or_default();
c9effeac326aatagen2026-10-25 10:32 UTC1304
        progress.started = true;
c9effeac326aatagen2026-10-25 10:32 UTC1305
        progress.tail = messages.end;
c9effeac326aatagen2026-10-25 10:32 UTC1306
        if ended {
c9effeac326aatagen2026-10-25 10:32 UTC1307
            progress.start_reached = true;
c9effeac326aatagen2026-10-25 10:32 UTC1308
            progress.tail = None;
c9effeac326aatagen2026-10-25 10:32 UTC1309
        }
c9effeac326aatagen2026-10-25 10:32 UTC1310
        self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC1311
    }
c9effeac326aatagen2026-10-25 10:32 UTC1312
c9effeac326aatagen2026-10-25 10:32 UTC1313
    fn retry_pending(&mut self, room_id: Option<&OwnedRoomId>, session_id: Option<&str>) {
74befbc72bafatagen2026-10-25 11:18 UTC1314
        let queued: HashSet<String> = self.decrypt_queue.iter().cloned().collect();
c9effeac326aatagen2026-10-25 10:32 UTC1315
        let due: Vec<_> = self
c9effeac326aatagen2026-10-25 10:32 UTC1316
            .pending
c9effeac326aatagen2026-10-25 10:32 UTC1317
            .iter()
74befbc72bafatagen2026-10-25 11:18 UTC1318
            .filter(|(key, pending)| {
74befbc72bafatagen2026-10-25 11:18 UTC1319
                !queued.contains(*key)
74befbc72bafatagen2026-10-25 11:18 UTC1320
                    && room_id.is_none_or(|room_id| pending.room == room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC1321
                    && session_id.is_none_or(|session_id| pending.session_id == session_id)
c9effeac326aatagen2026-10-25 10:32 UTC1322
            })
74befbc72bafatagen2026-10-25 11:18 UTC1323
            .map(|(key, _)| key.clone())
c9effeac326aatagen2026-10-25 10:32 UTC1324
            .collect();
74befbc72bafatagen2026-10-25 11:18 UTC1325
        self.decrypt_queue.extend(due);
74befbc72bafatagen2026-10-25 11:18 UTC1326
        self.pump_decrypts();
74befbc72bafatagen2026-10-25 11:18 UTC1327
    }
74befbc72bafatagen2026-10-25 11:18 UTC1328
74befbc72bafatagen2026-10-25 11:18 UTC1329
    fn pump_decrypts(&mut self) {
74befbc72bafatagen2026-10-25 11:18 UTC1330
        while self.decrypting < DECRYPTS_AT_ONCE {
74befbc72bafatagen2026-10-25 11:18 UTC1331
            let Some(key) = self.decrypt_queue.pop_front() else {
74befbc72bafatagen2026-10-25 11:18 UTC1332
                return;
74befbc72bafatagen2026-10-25 11:18 UTC1333
            };
74befbc72bafatagen2026-10-25 11:18 UTC1334
            let Some(pending) = self.pending.get(&key).cloned() else {
74befbc72bafatagen2026-10-25 11:18 UTC1335
                continue;
74befbc72bafatagen2026-10-25 11:18 UTC1336
            };
c9effeac326aatagen2026-10-25 10:32 UTC1337
            let Ok(room_id) = OwnedRoomId::try_from(pending.room.as_str()) else {
c9effeac326aatagen2026-10-25 10:32 UTC1338
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC1339
            };
c9effeac326aatagen2026-10-25 10:32 UTC1340
            let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC1341
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC1342
            };
74befbc72bafatagen2026-10-25 11:18 UTC1343
            self.decrypting += 1;
c9effeac326aatagen2026-10-25 10:32 UTC1344
            self.work.push(Box::pin(async move {
c9effeac326aatagen2026-10-25 10:32 UTC1345
                let value = async {
c9effeac326aatagen2026-10-25 10:32 UTC1346
                    let raw: Raw<OriginalSyncRoomEncryptedEvent> =
c9effeac326aatagen2026-10-25 10:32 UTC1347
                        Raw::from_json_string(pending.raw).ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC1348
                    let event = room.decrypt_event(&raw, None).await.ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC1349
                    value_of(&event).filter(|value| value["type"] != "m.room.encrypted")
c9effeac326aatagen2026-10-25 10:32 UTC1350
                }
c9effeac326aatagen2026-10-25 10:32 UTC1351
                .await;
c9effeac326aatagen2026-10-25 10:32 UTC1352
                Work::Decrypted {
c9effeac326aatagen2026-10-25 10:32 UTC1353
                    key,
c9effeac326aatagen2026-10-25 10:32 UTC1354
                    room: room_id,
c9effeac326aatagen2026-10-25 10:32 UTC1355
                    value,
c9effeac326aatagen2026-10-25 10:32 UTC1356
                }
c9effeac326aatagen2026-10-25 10:32 UTC1357
            }));
c9effeac326aatagen2026-10-25 10:32 UTC1358
        }
c9effeac326aatagen2026-10-25 10:32 UTC1359
    }
c9effeac326aatagen2026-10-25 10:32 UTC1360
c9effeac326aatagen2026-10-25 10:32 UTC1361
    fn complete(&mut self, work: Work) {
c9effeac326aatagen2026-10-25 10:32 UTC1362
        match work {
74befbc72bafatagen2026-10-25 11:18 UTC1363
            Work::Backfill {
74befbc72bafatagen2026-10-25 11:18 UTC1364
                room,
74befbc72bafatagen2026-10-25 11:18 UTC1365
                head,
74befbc72bafatagen2026-10-25 11:18 UTC1366
                from,
74befbc72bafatagen2026-10-25 11:18 UTC1367
                result,
74befbc72bafatagen2026-10-25 11:18 UTC1368
            } => self.backfilled(room, head, from, result),
74befbc72bafatagen2026-10-25 11:18 UTC1369
            Work::BackfillCancelled => {
74befbc72bafatagen2026-10-25 11:18 UTC1370
                self.backfilling = None;
74befbc72bafatagen2026-10-25 11:18 UTC1371
                self.next_backfill = now() + self.budget.interval_ms;
74befbc72bafatagen2026-10-25 11:18 UTC1372
            }
c9effeac326aatagen2026-10-25 10:32 UTC1373
            Work::Decrypted { key, room, value } => {
74befbc72bafatagen2026-10-25 11:18 UTC1374
                self.decrypting -= 1;
c9effeac326aatagen2026-10-25 10:32 UTC1375
                if let Some(value) = value {
74befbc72bafatagen2026-10-25 11:18 UTC1376
                    self.unpend(&key);
c9effeac326aatagen2026-10-25 10:32 UTC1377
                    self.observe(&room, &value);
c9effeac326aatagen2026-10-25 10:32 UTC1378
                }
74befbc72bafatagen2026-10-25 11:18 UTC1379
                self.pump_decrypts();
c9effeac326aatagen2026-10-25 10:32 UTC1380
            }
74befbc72bafatagen2026-10-25 11:18 UTC1381
            Work::Refetched { room, values } => {
74befbc72bafatagen2026-10-25 11:18 UTC1382
                for value in values {
c9effeac326aatagen2026-10-25 10:32 UTC1383
                    self.observe(&room, &value);
c9effeac326aatagen2026-10-25 10:32 UTC1384
                }
c9effeac326aatagen2026-10-25 10:32 UTC1385
            }
c9effeac326aatagen2026-10-25 10:32 UTC1386
        }
c9effeac326aatagen2026-10-25 10:32 UTC1387
    }
c9effeac326aatagen2026-10-25 10:32 UTC1388
c9effeac326aatagen2026-10-25 10:32 UTC1389
    fn search(
c9effeac326aatagen2026-10-25 10:32 UTC1390
        &mut self,
c9effeac326aatagen2026-10-25 10:32 UTC1391
        query: &str,
c9effeac326aatagen2026-10-25 10:32 UTC1392
        rooms: Option<&[String]>,
c9effeac326aatagen2026-10-25 10:32 UTC1393
        before: Option<(u64, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC1394
        limit: usize,
c9effeac326aatagen2026-10-25 10:32 UTC1395
        coverage: &[OwnedRoomId],
c9effeac326aatagen2026-10-25 10:32 UTC1396
    ) -> Result<RawPage, Error> {
c9effeac326aatagen2026-10-25 10:32 UTC1397
        let (mut hits, terms) = self.index.search(query, rooms, before, limit + 1)?;
c9effeac326aatagen2026-10-25 10:32 UTC1398
        let more = hits.len() > limit;
c9effeac326aatagen2026-10-25 10:32 UTC1399
        hits.truncate(limit);
c9effeac326aatagen2026-10-25 10:32 UTC1400
        Ok(RawPage {
c9effeac326aatagen2026-10-25 10:32 UTC1401
            hits,
c9effeac326aatagen2026-10-25 10:32 UTC1402
            terms,
c9effeac326aatagen2026-10-25 10:32 UTC1403
            more,
c9effeac326aatagen2026-10-25 10:32 UTC1404
            coverage: self.coverage(coverage),
c9effeac326aatagen2026-10-25 10:32 UTC1405
        })
c9effeac326aatagen2026-10-25 10:32 UTC1406
    }
c9effeac326aatagen2026-10-25 10:32 UTC1407
c9effeac326aatagen2026-10-25 10:32 UTC1408
    pub(crate) async fn run(mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC1409
        let client = self.client.clone();
c9effeac326aatagen2026-10-25 10:32 UTC1410
        let mut generic = client.event_cache().subscribe_to_room_generic_updates();
74befbc72bafatagen2026-10-25 11:18 UTC1411
        let mut room_info = client.room_info_notable_update_receiver();
74befbc72bafatagen2026-10-25 11:18 UTC1412
        self.load().await;
c9effeac326aatagen2026-10-25 10:32 UTC1413
        let mut keys = client
c9effeac326aatagen2026-10-25 10:32 UTC1414
            .encryption()
c9effeac326aatagen2026-10-25 10:32 UTC1415
            .room_keys_received_stream()
c9effeac326aatagen2026-10-25 10:32 UTC1416
            .await
c9effeac326aatagen2026-10-25 10:32 UTC1417
            .map(Box::pin);
c9effeac326aatagen2026-10-25 10:32 UTC1418
        self.retry_pending(None, None);
c9effeac326aatagen2026-10-25 10:32 UTC1419
        let mut tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
c9effeac326aatagen2026-10-25 10:32 UTC1420
        loop {
c9effeac326aatagen2026-10-25 10:32 UTC1421
            self.schedule_backfill();
c9effeac326aatagen2026-10-25 10:32 UTC1422
            tokio::select! {
c9effeac326aatagen2026-10-25 10:32 UTC1423
                biased;
c9effeac326aatagen2026-10-25 10:32 UTC1424
                request = self.requests.recv() => match request {
c9effeac326aatagen2026-10-25 10:32 UTC1425
                    Some(Request::Stop(reply)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1426
                        if self.dirty_since.is_some() || self.index.buffered() > 0 {
c9effeac326aatagen2026-10-25 10:32 UTC1427
                            self.commit().await;
c9effeac326aatagen2026-10-25 10:32 UTC1428
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1429
                        for (abort, _) in self.subscriptions.values() {
c9effeac326aatagen2026-10-25 10:32 UTC1430
                            abort.abort();
c9effeac326aatagen2026-10-25 10:32 UTC1431
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1432
                        let _ = reply.send(());
c9effeac326aatagen2026-10-25 10:32 UTC1433
                        return;
c9effeac326aatagen2026-10-25 10:32 UTC1434
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1435
                    Some(Request::Search { query, rooms, before, limit, coverage, reply }) => {
c9effeac326aatagen2026-10-25 10:32 UTC1436
                        let _ = reply.send(self.search(&query, rooms.as_deref(), before, limit, &coverage));
c9effeac326aatagen2026-10-25 10:32 UTC1437
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1438
                    Some(Request::Coverage(rooms, reply)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1439
                        let _ = reply.send(self.coverage(&rooms));
c9effeac326aatagen2026-10-25 10:32 UTC1440
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1441
                    Some(Request::Forget(key)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1442
                        let observed = Observed::Redacted { key };
c9effeac326aatagen2026-10-25 10:32 UTC1443
                        if feed::apply(self.index.as_mut(), &mut self.redactions, "", observed) == Effect::Removed {
c9effeac326aatagen2026-10-25 10:32 UTC1444
                            self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC1445
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1446
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1447
                    None => return,
c9effeac326aatagen2026-10-25 10:32 UTC1448
                },
c9effeac326aatagen2026-10-25 10:32 UTC1449
                Some(work) = self.work.next(), if !self.work.is_empty() => self.complete(work),
74befbc72bafatagen2026-10-25 11:18 UTC1450
                Some((room_id, update)) = self.updates.next(), if !self.updates.is_empty() => {
74befbc72bafatagen2026-10-25 11:18 UTC1451
                    self.room_update(room_id, update).await;
74befbc72bafatagen2026-10-25 11:18 UTC1452
                }
74befbc72bafatagen2026-10-25 11:18 UTC1453
                update = generic.recv() => {
74befbc72bafatagen2026-10-25 11:18 UTC1454
                    if !self.generic(update.map(|update| update.room_id)).await {
74befbc72bafatagen2026-10-25 11:18 UTC1455
                        return;
c9effeac326aatagen2026-10-25 10:32 UTC1456
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1457
                }
c9effeac326aatagen2026-10-25 10:32 UTC1458
                Some(update) = async { keys.as_mut()?.next().await }, if keys.is_some() => match update {
c9effeac326aatagen2026-10-25 10:32 UTC1459
                    Ok(infos) => {
c9effeac326aatagen2026-10-25 10:32 UTC1460
                        for info in infos {
c9effeac326aatagen2026-10-25 10:32 UTC1461
                            self.retry_pending(Some(&info.room_id), Some(&info.session_id));
c9effeac326aatagen2026-10-25 10:32 UTC1462
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1463
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1464
                    Err(_) => self.retry_pending(None, None),
c9effeac326aatagen2026-10-25 10:32 UTC1465
                },
c9effeac326aatagen2026-10-25 10:32 UTC1466
                update = room_info.recv() => match update {
c9effeac326aatagen2026-10-25 10:32 UTC1467
                    Ok(update) if update.reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP) => {
c9effeac326aatagen2026-10-25 10:32 UTC1468
                        let left = self.client.get_room(&update.room_id).is_none_or(|room| room.state() != RoomState::Joined);
74befbc72bafatagen2026-10-25 11:18 UTC1469
                        if left {
74befbc72bafatagen2026-10-25 11:18 UTC1470
                            self.left(&update.room_id);
c9effeac326aatagen2026-10-25 10:32 UTC1471
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1472
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1473
                    Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
c9effeac326aatagen2026-10-25 10:32 UTC1474
                    Err(broadcast::error::RecvError::Closed) => return,
c9effeac326aatagen2026-10-25 10:32 UTC1475
                },
c9effeac326aatagen2026-10-25 10:32 UTC1476
                () = &mut tick => {
c9effeac326aatagen2026-10-25 10:32 UTC1477
                    tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
c9effeac326aatagen2026-10-25 10:32 UTC1478
                    self.drop_idle();
c9effeac326aatagen2026-10-25 10:32 UTC1479
                    let due = self.dirty_since.is_some_and(|since| now() >= since + COMMIT_AFTER_MS)
c9effeac326aatagen2026-10-25 10:32 UTC1480
                        || self.index.buffered() >= COMMIT_BUFFERED;
c9effeac326aatagen2026-10-25 10:32 UTC1481
                    if due && now() >= self.commit_after {
c9effeac326aatagen2026-10-25 10:32 UTC1482
                        self.commit().await;
c9effeac326aatagen2026-10-25 10:32 UTC1483
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1484
                }
c9effeac326aatagen2026-10-25 10:32 UTC1485
            }
c9effeac326aatagen2026-10-25 10:32 UTC1486
        }
c9effeac326aatagen2026-10-25 10:32 UTC1487
    }
c9effeac326aatagen2026-10-25 10:32 UTC1488
}
c9effeac326aatagen2026-10-25 10:32 UTC1489
74befbc72bafatagen2026-10-25 11:18 UTC1490
/// a head pass stops at what this engine found already indexed, and live events indexed
74befbc72bafatagen2026-10-25 11:18 UTC1491
/// since it started don't count, or the first page would always stop it
74befbc72bafatagen2026-10-25 11:18 UTC1492
fn head_overlap(index: &dyn MessageIndex, baseline: Option<u64>, value: &Value) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC1493
    let (Some(baseline), Some(key), Some(ts)) = (
74befbc72bafatagen2026-10-25 11:18 UTC1494
        baseline,
74befbc72bafatagen2026-10-25 11:18 UTC1495
        value.get("event_id").and_then(Value::as_str),
74befbc72bafatagen2026-10-25 11:18 UTC1496
        value.get("origin_server_ts").and_then(Value::as_u64),
74befbc72bafatagen2026-10-25 11:18 UTC1497
    ) else {
74befbc72bafatagen2026-10-25 11:18 UTC1498
        return false;
74befbc72bafatagen2026-10-25 11:18 UTC1499
    };
74befbc72bafatagen2026-10-25 11:18 UTC1500
    ts <= baseline && index.get(key).is_some_and(|doc| !doc.provisional)
74befbc72bafatagen2026-10-25 11:18 UTC1501
}
74befbc72bafatagen2026-10-25 11:18 UTC1502
c9effeac326aatagen2026-10-25 10:32 UTC1503
fn updates(
c9effeac326aatagen2026-10-25 10:32 UTC1504
    room_id: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC1505
    subscriber: RoomEventCacheSubscriber,
c9effeac326aatagen2026-10-25 10:32 UTC1506
    drop_handles: Arc<matrix_sdk::event_cache::EventCacheDropHandles>,
74befbc72bafatagen2026-10-25 11:18 UTC1507
) -> BoxStream<(OwnedRoomId, RoomUpdate)> {
c9effeac326aatagen2026-10-25 10:32 UTC1508
    Box::pin(futures_util::stream::unfold(
74befbc72bafatagen2026-10-25 11:18 UTC1509
        Some((subscriber, drop_handles)),
74befbc72bafatagen2026-10-25 11:18 UTC1510
        move |state| {
c9effeac326aatagen2026-10-25 10:32 UTC1511
            let room_id = room_id.clone();
c9effeac326aatagen2026-10-25 10:32 UTC1512
            async move {
74befbc72bafatagen2026-10-25 11:18 UTC1513
                let (mut subscriber, drop_handles) = state?;
c9effeac326aatagen2026-10-25 10:32 UTC1514
                loop {
c9effeac326aatagen2026-10-25 10:32 UTC1515
                    match subscriber.recv().await {
c9effeac326aatagen2026-10-25 10:32 UTC1516
                        Ok(RoomEventCacheUpdate::UpdateTimelineEvents(update)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1517
                            let events = update.diffs.into_iter().flat_map(added).collect();
74befbc72bafatagen2026-10-25 11:18 UTC1518
                            return Some((
74befbc72bafatagen2026-10-25 11:18 UTC1519
                                (room_id, RoomUpdate::Events(events)),
74befbc72bafatagen2026-10-25 11:18 UTC1520
                                Some((subscriber, drop_handles)),
74befbc72bafatagen2026-10-25 11:18 UTC1521
                            ));
c9effeac326aatagen2026-10-25 10:32 UTC1522
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1523
                        Ok(_) => {}
c9effeac326aatagen2026-10-25 10:32 UTC1524
                        Err(broadcast::error::RecvError::Lagged(_)) => {
74befbc72bafatagen2026-10-25 11:18 UTC1525
                            return Some((
74befbc72bafatagen2026-10-25 11:18 UTC1526
                                (room_id, RoomUpdate::Lagged),
74befbc72bafatagen2026-10-25 11:18 UTC1527
                                Some((subscriber, drop_handles)),
74befbc72bafatagen2026-10-25 11:18 UTC1528
                            ));
74befbc72bafatagen2026-10-25 11:18 UTC1529
                        }
74befbc72bafatagen2026-10-25 11:18 UTC1530
                        Err(broadcast::error::RecvError::Closed) => {
74befbc72bafatagen2026-10-25 11:18 UTC1531
                            return Some(((room_id, RoomUpdate::Closed), None));
c9effeac326aatagen2026-10-25 10:32 UTC1532
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1533
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1534
                }
c9effeac326aatagen2026-10-25 10:32 UTC1535
            }
c9effeac326aatagen2026-10-25 10:32 UTC1536
        },
c9effeac326aatagen2026-10-25 10:32 UTC1537
    ))
c9effeac326aatagen2026-10-25 10:32 UTC1538
}
c9effeac326aatagen2026-10-25 10:32 UTC1539
c9effeac326aatagen2026-10-25 10:32 UTC1540
fn added(diff: VectorDiff<SdkTimelineEvent>) -> Vec<SdkTimelineEvent> {
c9effeac326aatagen2026-10-25 10:32 UTC1541
    match diff {
c9effeac326aatagen2026-10-25 10:32 UTC1542
        VectorDiff::Append { values } | VectorDiff::Reset { values } => {
c9effeac326aatagen2026-10-25 10:32 UTC1543
            values.into_iter().collect()
c9effeac326aatagen2026-10-25 10:32 UTC1544
        }
c9effeac326aatagen2026-10-25 10:32 UTC1545
        VectorDiff::PushFront { value }
c9effeac326aatagen2026-10-25 10:32 UTC1546
        | VectorDiff::PushBack { value }
c9effeac326aatagen2026-10-25 10:32 UTC1547
        | VectorDiff::Insert { value, .. }
c9effeac326aatagen2026-10-25 10:32 UTC1548
        | VectorDiff::Set { value, .. } => vec![value],
c9effeac326aatagen2026-10-25 10:32 UTC1549
        VectorDiff::Clear
c9effeac326aatagen2026-10-25 10:32 UTC1550
        | VectorDiff::PopFront
c9effeac326aatagen2026-10-25 10:32 UTC1551
        | VectorDiff::PopBack
c9effeac326aatagen2026-10-25 10:32 UTC1552
        | VectorDiff::Remove { .. }
c9effeac326aatagen2026-10-25 10:32 UTC1553
        | VectorDiff::Truncate { .. } => Vec::new(),
c9effeac326aatagen2026-10-25 10:32 UTC1554
    }
c9effeac326aatagen2026-10-25 10:32 UTC1555
}
74befbc72bafatagen2026-10-25 11:18 UTC1556
74befbc72bafatagen2026-10-25 11:18 UTC1557
#[cfg(all(test, feature = "sqlite"))]
74befbc72bafatagen2026-10-25 11:18 UTC1558
#[path = "service_tests.rs"]
74befbc72bafatagen2026-10-25 11:18 UTC1559
mod tests;