atagen

macltok

public
Star0
Watch0
cff1b0a4eb30290a6fe32fea825c816b696fe4f4
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;
c9effeac326aatagen2026-10-25 10:32 UTC6
c9effeac326aatagen2026-10-25 10:32 UTC7
use std::{
c9effeac326aatagen2026-10-25 10:32 UTC8
    collections::{BTreeMap, HashMap, HashSet, VecDeque},
c9effeac326aatagen2026-10-25 10:32 UTC9
    sync::Arc,
c9effeac326aatagen2026-10-25 10:32 UTC10
    time::Duration,
c9effeac326aatagen2026-10-25 10:32 UTC11
};
c9effeac326aatagen2026-10-25 10:32 UTC12
c9effeac326aatagen2026-10-25 10:32 UTC13
use futures_util::{
c9effeac326aatagen2026-10-25 10:32 UTC14
    StreamExt,
c9effeac326aatagen2026-10-25 10:32 UTC15
    future::join_all,
c9effeac326aatagen2026-10-25 10:32 UTC16
    stream::{AbortHandle, FuturesUnordered, SelectAll, abortable},
c9effeac326aatagen2026-10-25 10:32 UTC17
};
c9effeac326aatagen2026-10-25 10:32 UTC18
use matrix_sdk::{
c9effeac326aatagen2026-10-25 10:32 UTC19
    Client, Room, RoomState, SendOutsideWasm,
c9effeac326aatagen2026-10-25 10:32 UTC20
    deserialized_responses::TimelineEvent as SdkTimelineEvent,
c9effeac326aatagen2026-10-25 10:32 UTC21
    event_cache::{RoomEventCacheSubscriber, RoomEventCacheUpdate},
c9effeac326aatagen2026-10-25 10:32 UTC22
    room::MessagesOptions,
c9effeac326aatagen2026-10-25 10:32 UTC23
};
c9effeac326aatagen2026-10-25 10:32 UTC24
use matrix_sdk_base::RoomInfoNotableUpdateReasons;
c9effeac326aatagen2026-10-25 10:32 UTC25
use matrix_sdk_ui::eyeball_im::VectorDiff;
c9effeac326aatagen2026-10-25 10:32 UTC26
use ruma::{
c9effeac326aatagen2026-10-25 10:32 UTC27
    OwnedRoomId, OwnedUserId, UInt,
c9effeac326aatagen2026-10-25 10:32 UTC28
    events::{
c9effeac326aatagen2026-10-25 10:32 UTC29
        ignored_user_list::IgnoredUserListEventContent,
c9effeac326aatagen2026-10-25 10:32 UTC30
        room::encrypted::OriginalSyncRoomEncryptedEvent,
c9effeac326aatagen2026-10-25 10:32 UTC31
    },
c9effeac326aatagen2026-10-25 10:32 UTC32
    serde::Raw,
c9effeac326aatagen2026-10-25 10:32 UTC33
};
c9effeac326aatagen2026-10-25 10:32 UTC34
use serde_json::Value;
c9effeac326aatagen2026-10-25 10:32 UTC35
use tokio::sync::{Mutex, broadcast, mpsc, oneshot};
c9effeac326aatagen2026-10-25 10:32 UTC36
c9effeac326aatagen2026-10-25 10:32 UTC37
use self::{
c9effeac326aatagen2026-10-25 10:32 UTC38
    feed::{Effect, Observed, Redactions},
c9effeac326aatagen2026-10-25 10:32 UTC39
    index::{Changes, Doc, Hit, KeywordIndex},
c9effeac326aatagen2026-10-25 10:32 UTC40
};
c9effeac326aatagen2026-10-25 10:32 UTC41
use crate::{
c9effeac326aatagen2026-10-25 10:32 UTC42
    CommandOutput, EngineConfig, Error, ErrorKind, RoomSearchCoverage, SearchBackfill,
c9effeac326aatagen2026-10-25 10:32 UTC43
    SearchCommand, SearchCoverage, SearchHit, SearchMatch, SearchPage, SearchScope, SearchSession,
c9effeac326aatagen2026-10-25 10:32 UTC44
    engine::{MediaService, SearchStore, context, executor::EngineBoxFuture},
c9effeac326aatagen2026-10-25 10:32 UTC45
    normalise::{MediaRegistration, MediaScope},
c9effeac326aatagen2026-10-25 10:32 UTC46
};
c9effeac326aatagen2026-10-25 10:32 UTC47
c9effeac326aatagen2026-10-25 10:32 UTC48
const MAX_LIMIT: u16 = 50;
c9effeac326aatagen2026-10-25 10:32 UTC49
/// a consumer that never closes its searches still holds at most this many
c9effeac326aatagen2026-10-25 10:32 UTC50
const MAX_SEARCHES: usize = 8;
c9effeac326aatagen2026-10-25 10:32 UTC51
const COMMIT_AFTER_MS: u64 = 5_000;
c9effeac326aatagen2026-10-25 10:32 UTC52
const COMMIT_BUFFERED: usize = 2_000;
c9effeac326aatagen2026-10-25 10:32 UTC53
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC54
const COMMIT_RETRY_MS: u64 = 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC55
const IDLE_SUBSCRIPTION_MS: u64 = 5 * 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC56
const BACKFILL_RETRY_MS: u64 = 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC57
const BACKFILL_RETRY_MAX_MS: u64 = 15 * 60_000;
c9effeac326aatagen2026-10-25 10:32 UTC58
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC59
const SCHEMA: u64 = 1;
c9effeac326aatagen2026-10-25 10:32 UTC60
c9effeac326aatagen2026-10-25 10:32 UTC61
#[cfg(not(target_family = "wasm"))]
c9effeac326aatagen2026-10-25 10:32 UTC62
type BoxStream<T> = futures_util::stream::BoxStream<'static, T>;
c9effeac326aatagen2026-10-25 10:32 UTC63
#[cfg(target_family = "wasm")]
c9effeac326aatagen2026-10-25 10:32 UTC64
type BoxStream<T> = futures_util::stream::LocalBoxStream<'static, T>;
c9effeac326aatagen2026-10-25 10:32 UTC65
c9effeac326aatagen2026-10-25 10:32 UTC66
/// a local index of message text. keyword search over tantivy is the only one today, an
c9effeac326aatagen2026-10-25 10:32 UTC67
/// embedding index can stand beside it later without the public api noticing
c9effeac326aatagen2026-10-25 10:32 UTC68
#[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
c9effeac326aatagen2026-10-25 10:32 UTC69
pub(crate) trait MessageIndex: SendOutsideWasm {
c9effeac326aatagen2026-10-25 10:32 UTC70
    fn get(&self, key: &str) -> Option<Doc>;
c9effeac326aatagen2026-10-25 10:32 UTC71
    fn by_content(&self, content: &str) -> Option<Doc>;
c9effeac326aatagen2026-10-25 10:32 UTC72
    /// replaces whatever `doc.key` held
c9effeac326aatagen2026-10-25 10:32 UTC73
    fn put(&mut self, doc: Doc);
c9effeac326aatagen2026-10-25 10:32 UTC74
    fn remove(&mut self, key: &str) -> bool;
c9effeac326aatagen2026-10-25 10:32 UTC75
    fn buffered(&self) -> usize;
c9effeac326aatagen2026-10-25 10:32 UTC76
    /// newest first, strictly older than `before`. also hands back the analysed query terms
c9effeac326aatagen2026-10-25 10:32 UTC77
    fn search(
c9effeac326aatagen2026-10-25 10:32 UTC78
        &mut self,
c9effeac326aatagen2026-10-25 10:32 UTC79
        query: &str,
c9effeac326aatagen2026-10-25 10:32 UTC80
        rooms: Option<&[String]>,
c9effeac326aatagen2026-10-25 10:32 UTC81
        before: Option<(u64, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC82
        limit: usize,
c9effeac326aatagen2026-10-25 10:32 UTC83
    ) -> Result<(Vec<Hit>, Vec<String>), Error>;
c9effeac326aatagen2026-10-25 10:32 UTC84
    fn maintain(&mut self) -> Result<(), Error>;
c9effeac326aatagen2026-10-25 10:32 UTC85
    fn changes(&mut self) -> Result<Changes, Error>;
c9effeac326aatagen2026-10-25 10:32 UTC86
    fn committed(&mut self, changes: &Changes);
c9effeac326aatagen2026-10-25 10:32 UTC87
    fn manifest(&self) -> Value;
c9effeac326aatagen2026-10-25 10:32 UTC88
}
c9effeac326aatagen2026-10-25 10:32 UTC89
c9effeac326aatagen2026-10-25 10:32 UTC90
#[derive(Clone, Debug, Default)]
c9effeac326aatagen2026-10-25 10:32 UTC91
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
c9effeac326aatagen2026-10-25 10:32 UTC92
struct RoomProgress {
c9effeac326aatagen2026-10-25 10:32 UTC93
    oldest: Option<u64>,
c9effeac326aatagen2026-10-25 10:32 UTC94
    newest: Option<u64>,
c9effeac326aatagen2026-10-25 10:32 UTC95
    /// where the next backfill page starts, `None` before the first one
c9effeac326aatagen2026-10-25 10:32 UTC96
    tail: Option<String>,
c9effeac326aatagen2026-10-25 10:32 UTC97
    started: bool,
c9effeac326aatagen2026-10-25 10:32 UTC98
    start_reached: bool,
c9effeac326aatagen2026-10-25 10:32 UTC99
}
c9effeac326aatagen2026-10-25 10:32 UTC100
c9effeac326aatagen2026-10-25 10:32 UTC101
#[derive(Clone, Debug)]
c9effeac326aatagen2026-10-25 10:32 UTC102
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
c9effeac326aatagen2026-10-25 10:32 UTC103
struct Pending {
c9effeac326aatagen2026-10-25 10:32 UTC104
    room: String,
c9effeac326aatagen2026-10-25 10:32 UTC105
    session_id: String,
c9effeac326aatagen2026-10-25 10:32 UTC106
    ts: u64,
c9effeac326aatagen2026-10-25 10:32 UTC107
    raw: String,
c9effeac326aatagen2026-10-25 10:32 UTC108
}
c9effeac326aatagen2026-10-25 10:32 UTC109
c9effeac326aatagen2026-10-25 10:32 UTC110
#[derive(Clone, Copy)]
c9effeac326aatagen2026-10-25 10:32 UTC111
struct Budget {
c9effeac326aatagen2026-10-25 10:32 UTC112
    pages_per_room: u16,
c9effeac326aatagen2026-10-25 10:32 UTC113
    page_size: u16,
c9effeac326aatagen2026-10-25 10:32 UTC114
    interval_ms: u64,
c9effeac326aatagen2026-10-25 10:32 UTC115
    max_pending: usize,
c9effeac326aatagen2026-10-25 10:32 UTC116
}
c9effeac326aatagen2026-10-25 10:32 UTC117
c9effeac326aatagen2026-10-25 10:32 UTC118
impl Budget {
c9effeac326aatagen2026-10-25 10:32 UTC119
    fn from_config(config: &EngineConfig) -> Self {
c9effeac326aatagen2026-10-25 10:32 UTC120
        Self {
c9effeac326aatagen2026-10-25 10:32 UTC121
            pages_per_room: config.search_backfill_pages_per_room,
c9effeac326aatagen2026-10-25 10:32 UTC122
            page_size: config.search_backfill_page_size.max(1),
c9effeac326aatagen2026-10-25 10:32 UTC123
            interval_ms: config.search_backfill_interval_ms,
c9effeac326aatagen2026-10-25 10:32 UTC124
            max_pending: config.max_search_undecrypted,
c9effeac326aatagen2026-10-25 10:32 UTC125
        }
c9effeac326aatagen2026-10-25 10:32 UTC126
    }
c9effeac326aatagen2026-10-25 10:32 UTC127
}
c9effeac326aatagen2026-10-25 10:32 UTC128
c9effeac326aatagen2026-10-25 10:32 UTC129
struct RawPage {
c9effeac326aatagen2026-10-25 10:32 UTC130
    hits: Vec<Hit>,
c9effeac326aatagen2026-10-25 10:32 UTC131
    terms: Vec<String>,
c9effeac326aatagen2026-10-25 10:32 UTC132
    more: bool,
c9effeac326aatagen2026-10-25 10:32 UTC133
    coverage: SearchCoverage,
c9effeac326aatagen2026-10-25 10:32 UTC134
}
c9effeac326aatagen2026-10-25 10:32 UTC135
c9effeac326aatagen2026-10-25 10:32 UTC136
enum Request {
c9effeac326aatagen2026-10-25 10:32 UTC137
    Search {
c9effeac326aatagen2026-10-25 10:32 UTC138
        query: String,
c9effeac326aatagen2026-10-25 10:32 UTC139
        rooms: Option<Vec<String>>,
c9effeac326aatagen2026-10-25 10:32 UTC140
        before: Option<(u64, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC141
        limit: usize,
c9effeac326aatagen2026-10-25 10:32 UTC142
        coverage: Vec<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC143
        reply: oneshot::Sender<Result<RawPage, Error>>,
c9effeac326aatagen2026-10-25 10:32 UTC144
    },
c9effeac326aatagen2026-10-25 10:32 UTC145
    Coverage(Vec<OwnedRoomId>, oneshot::Sender<SearchCoverage>),
c9effeac326aatagen2026-10-25 10:32 UTC146
    /// a hit that turned out redacted when its preview was read
c9effeac326aatagen2026-10-25 10:32 UTC147
    Forget(String),
c9effeac326aatagen2026-10-25 10:32 UTC148
    Stop(oneshot::Sender<()>),
c9effeac326aatagen2026-10-25 10:32 UTC149
}
c9effeac326aatagen2026-10-25 10:32 UTC150
c9effeac326aatagen2026-10-25 10:32 UTC151
#[derive(Clone)]
c9effeac326aatagen2026-10-25 10:32 UTC152
pub(crate) struct Handle {
c9effeac326aatagen2026-10-25 10:32 UTC153
    requests: mpsc::Sender<Request>,
c9effeac326aatagen2026-10-25 10:32 UTC154
    client: Client,
c9effeac326aatagen2026-10-25 10:32 UTC155
    media: Arc<MediaService>,
c9effeac326aatagen2026-10-25 10:32 UTC156
    sessions: Arc<Sessions>,
c9effeac326aatagen2026-10-25 10:32 UTC157
}
c9effeac326aatagen2026-10-25 10:32 UTC158
c9effeac326aatagen2026-10-25 10:32 UTC159
enum Work {
c9effeac326aatagen2026-10-25 10:32 UTC160
    Backfill {
c9effeac326aatagen2026-10-25 10:32 UTC161
        room: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC162
        head: bool,
c9effeac326aatagen2026-10-25 10:32 UTC163
        result: Result<matrix_sdk::room::Messages, Error>,
c9effeac326aatagen2026-10-25 10:32 UTC164
    },
c9effeac326aatagen2026-10-25 10:32 UTC165
    Decrypted {
c9effeac326aatagen2026-10-25 10:32 UTC166
        key: String,
c9effeac326aatagen2026-10-25 10:32 UTC167
        room: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC168
        value: Option<Value>,
c9effeac326aatagen2026-10-25 10:32 UTC169
    },
c9effeac326aatagen2026-10-25 10:32 UTC170
    Refetched {
c9effeac326aatagen2026-10-25 10:32 UTC171
        room: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC172
        value: Option<Value>,
c9effeac326aatagen2026-10-25 10:32 UTC173
    },
c9effeac326aatagen2026-10-25 10:32 UTC174
}
c9effeac326aatagen2026-10-25 10:32 UTC175
c9effeac326aatagen2026-10-25 10:32 UTC176
struct Failure {
c9effeac326aatagen2026-10-25 10:32 UTC177
    message: String,
c9effeac326aatagen2026-10-25 10:32 UTC178
    retry_at_ms: u64,
c9effeac326aatagen2026-10-25 10:32 UTC179
}
c9effeac326aatagen2026-10-25 10:32 UTC180
c9effeac326aatagen2026-10-25 10:32 UTC181
pub(crate) struct Service {
c9effeac326aatagen2026-10-25 10:32 UTC182
    client: Client,
c9effeac326aatagen2026-10-25 10:32 UTC183
    index: Box<dyn MessageIndex>,
c9effeac326aatagen2026-10-25 10:32 UTC184
    store: Option<Result<SearchStore, Error>>,
c9effeac326aatagen2026-10-25 10:32 UTC185
    rooms: BTreeMap<String, RoomProgress>,
c9effeac326aatagen2026-10-25 10:32 UTC186
    pending: BTreeMap<String, Pending>,
c9effeac326aatagen2026-10-25 10:32 UTC187
    pending_dirty: bool,
c9effeac326aatagen2026-10-25 10:32 UTC188
    redactions: Redactions,
c9effeac326aatagen2026-10-25 10:32 UTC189
    dirty_since: Option<u64>,
c9effeac326aatagen2026-10-25 10:32 UTC190
    commit_after: u64,
c9effeac326aatagen2026-10-25 10:32 UTC191
    budget: Budget,
c9effeac326aatagen2026-10-25 10:32 UTC192
    requests: mpsc::Receiver<Request>,
c9effeac326aatagen2026-10-25 10:32 UTC193
    subscriptions: HashMap<OwnedRoomId, (AbortHandle, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC194
    updates: SelectAll<BoxStream<(OwnedRoomId, Option<Vec<SdkTimelineEvent>>)>>,
c9effeac326aatagen2026-10-25 10:32 UTC195
    work: FuturesUnordered<EngineBoxFuture<'static, Work>>,
c9effeac326aatagen2026-10-25 10:32 UTC196
    pages: HashMap<OwnedRoomId, u16>,
c9effeac326aatagen2026-10-25 10:32 UTC197
    heads: VecDeque<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC198
    headed: HashSet<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC199
    head_tokens: HashMap<OwnedRoomId, String>,
c9effeac326aatagen2026-10-25 10:32 UTC200
    backfilling: Option<OwnedRoomId>,
c9effeac326aatagen2026-10-25 10:32 UTC201
    next_backfill: u64,
c9effeac326aatagen2026-10-25 10:32 UTC202
    retry_delay: u64,
c9effeac326aatagen2026-10-25 10:32 UTC203
    failures: HashMap<OwnedRoomId, Failure>,
c9effeac326aatagen2026-10-25 10:32 UTC204
    turn: usize,
c9effeac326aatagen2026-10-25 10:32 UTC205
}
c9effeac326aatagen2026-10-25 10:32 UTC206
c9effeac326aatagen2026-10-25 10:32 UTC207
fn now() -> u64 {
c9effeac326aatagen2026-10-25 10:32 UTC208
    ruma::MilliSecondsSinceUnixEpoch::now().get().into()
c9effeac326aatagen2026-10-25 10:32 UTC209
}
c9effeac326aatagen2026-10-25 10:32 UTC210
c9effeac326aatagen2026-10-25 10:32 UTC211
fn value_of(event: &SdkTimelineEvent) -> Option<Value> {
c9effeac326aatagen2026-10-25 10:32 UTC212
    serde_json::from_str(event.raw().json().get()).ok()
c9effeac326aatagen2026-10-25 10:32 UTC213
}
c9effeac326aatagen2026-10-25 10:32 UTC214
c9effeac326aatagen2026-10-25 10:32 UTC215
fn encode_cursor((ts, tie): (u64, u64)) -> String {
c9effeac326aatagen2026-10-25 10:32 UTC216
    format!("{ts:x}.{tie:x}")
c9effeac326aatagen2026-10-25 10:32 UTC217
}
c9effeac326aatagen2026-10-25 10:32 UTC218
c9effeac326aatagen2026-10-25 10:32 UTC219
fn decode_cursor(cursor: &str) -> Option<(u64, u64)> {
c9effeac326aatagen2026-10-25 10:32 UTC220
    let (ts, tie) = cursor.split_once('.')?;
c9effeac326aatagen2026-10-25 10:32 UTC221
    Some((
c9effeac326aatagen2026-10-25 10:32 UTC222
        u64::from_str_radix(ts, 16).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC223
        u64::from_str_radix(tie, 16).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC224
    ))
c9effeac326aatagen2026-10-25 10:32 UTC225
}
c9effeac326aatagen2026-10-25 10:32 UTC226
c9effeac326aatagen2026-10-25 10:32 UTC227
impl Handle {
c9effeac326aatagen2026-10-25 10:32 UTC228
    pub(crate) async fn execute(&self, command: SearchCommand) -> Result<CommandOutput, Error> {
c9effeac326aatagen2026-10-25 10:32 UTC229
        match command {
c9effeac326aatagen2026-10-25 10:32 UTC230
            SearchCommand::Search {
c9effeac326aatagen2026-10-25 10:32 UTC231
                query,
c9effeac326aatagen2026-10-25 10:32 UTC232
                scope,
c9effeac326aatagen2026-10-25 10:32 UTC233
                limit,
c9effeac326aatagen2026-10-25 10:32 UTC234
            } => {
c9effeac326aatagen2026-10-25 10:32 UTC235
                let (page, media) = self.search(&query, &scope, None, limit).await?;
c9effeac326aatagen2026-10-25 10:32 UTC236
                let search_id = self.sessions.open(&self.media, query, scope, media).await;
c9effeac326aatagen2026-10-25 10:32 UTC237
                Ok(CommandOutput::SearchSession(SearchSession { search_id, page }))
c9effeac326aatagen2026-10-25 10:32 UTC238
            }
c9effeac326aatagen2026-10-25 10:32 UTC239
            SearchCommand::More {
c9effeac326aatagen2026-10-25 10:32 UTC240
                search_id,
c9effeac326aatagen2026-10-25 10:32 UTC241
                cursor,
c9effeac326aatagen2026-10-25 10:32 UTC242
                limit,
c9effeac326aatagen2026-10-25 10:32 UTC243
            } => {
c9effeac326aatagen2026-10-25 10:32 UTC244
                let (query, scope) = self.sessions.get(&search_id).await?;
c9effeac326aatagen2026-10-25 10:32 UTC245
                let (page, media) = self.search(&query, &scope, Some(cursor.as_str()), limit).await?;
c9effeac326aatagen2026-10-25 10:32 UTC246
                self.sessions.register(&self.media, &search_id, media).await?;
c9effeac326aatagen2026-10-25 10:32 UTC247
                Ok(CommandOutput::SearchPage(page))
c9effeac326aatagen2026-10-25 10:32 UTC248
            }
c9effeac326aatagen2026-10-25 10:32 UTC249
            SearchCommand::Close { search_id } => {
c9effeac326aatagen2026-10-25 10:32 UTC250
                self.sessions.close(&self.media, &search_id).await?;
c9effeac326aatagen2026-10-25 10:32 UTC251
                Ok(CommandOutput::Done)
c9effeac326aatagen2026-10-25 10:32 UTC252
            }
c9effeac326aatagen2026-10-25 10:32 UTC253
            SearchCommand::Coverage(scope) => {
c9effeac326aatagen2026-10-25 10:32 UTC254
                let (reply, response) = oneshot::channel();
c9effeac326aatagen2026-10-25 10:32 UTC255
                self.requests
c9effeac326aatagen2026-10-25 10:32 UTC256
                    .send(Request::Coverage(self.scope_rooms(&scope), reply))
c9effeac326aatagen2026-10-25 10:32 UTC257
                    .await
c9effeac326aatagen2026-10-25 10:32 UTC258
                    .map_err(|_| Error::interrupted())?;
c9effeac326aatagen2026-10-25 10:32 UTC259
                Ok(CommandOutput::SearchCoverage(
c9effeac326aatagen2026-10-25 10:32 UTC260
                    response.await.map_err(|_| Error::interrupted())?,
c9effeac326aatagen2026-10-25 10:32 UTC261
                ))
c9effeac326aatagen2026-10-25 10:32 UTC262
            }
c9effeac326aatagen2026-10-25 10:32 UTC263
        }
c9effeac326aatagen2026-10-25 10:32 UTC264
    }
c9effeac326aatagen2026-10-25 10:32 UTC265
c9effeac326aatagen2026-10-25 10:32 UTC266
    /// the media service is cleared beside this, so nothing is released one by one
c9effeac326aatagen2026-10-25 10:32 UTC267
    pub(crate) async fn stop(&self) {
c9effeac326aatagen2026-10-25 10:32 UTC268
        let (reply, response) = oneshot::channel();
c9effeac326aatagen2026-10-25 10:32 UTC269
        if self.requests.send(Request::Stop(reply)).await.is_ok() {
c9effeac326aatagen2026-10-25 10:32 UTC270
            let _ = response.await;
c9effeac326aatagen2026-10-25 10:32 UTC271
        }
c9effeac326aatagen2026-10-25 10:32 UTC272
        self.sessions.open.lock().await.clear();
c9effeac326aatagen2026-10-25 10:32 UTC273
    }
c9effeac326aatagen2026-10-25 10:32 UTC274
c9effeac326aatagen2026-10-25 10:32 UTC275
    fn scope_rooms(&self, scope: &SearchScope) -> Vec<OwnedRoomId> {
c9effeac326aatagen2026-10-25 10:32 UTC276
        match scope {
c9effeac326aatagen2026-10-25 10:32 UTC277
            SearchScope::Room(room_id) => vec![room_id.clone()],
c9effeac326aatagen2026-10-25 10:32 UTC278
            SearchScope::Rooms(room_ids) => room_ids.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC279
            SearchScope::All => self
c9effeac326aatagen2026-10-25 10:32 UTC280
                .client
c9effeac326aatagen2026-10-25 10:32 UTC281
                .joined_rooms()
c9effeac326aatagen2026-10-25 10:32 UTC282
                .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC283
                .map(|room| room.room_id().to_owned())
c9effeac326aatagen2026-10-25 10:32 UTC284
                .collect(),
c9effeac326aatagen2026-10-25 10:32 UTC285
        }
c9effeac326aatagen2026-10-25 10:32 UTC286
    }
c9effeac326aatagen2026-10-25 10:32 UTC287
c9effeac326aatagen2026-10-25 10:32 UTC288
    async fn search(
c9effeac326aatagen2026-10-25 10:32 UTC289
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC290
        query: &str,
c9effeac326aatagen2026-10-25 10:32 UTC291
        scope: &SearchScope,
c9effeac326aatagen2026-10-25 10:32 UTC292
        cursor: Option<&str>,
c9effeac326aatagen2026-10-25 10:32 UTC293
        limit: u16,
c9effeac326aatagen2026-10-25 10:32 UTC294
    ) -> Result<(SearchPage, Vec<MediaRegistration>), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC295
        if limit == 0 || limit > MAX_LIMIT {
c9effeac326aatagen2026-10-25 10:32 UTC296
            return Err(Error::new(
c9effeac326aatagen2026-10-25 10:32 UTC297
                ErrorKind::Invalid,
c9effeac326aatagen2026-10-25 10:32 UTC298
                "search limit must be between 1 and 50",
c9effeac326aatagen2026-10-25 10:32 UTC299
            ));
c9effeac326aatagen2026-10-25 10:32 UTC300
        }
c9effeac326aatagen2026-10-25 10:32 UTC301
        if query.trim().is_empty() {
c9effeac326aatagen2026-10-25 10:32 UTC302
            return Err(Error::new(ErrorKind::Invalid, "search query is empty"));
c9effeac326aatagen2026-10-25 10:32 UTC303
        }
c9effeac326aatagen2026-10-25 10:32 UTC304
        let before = match cursor {
c9effeac326aatagen2026-10-25 10:32 UTC305
            Some(cursor) => Some(
c9effeac326aatagen2026-10-25 10:32 UTC306
                decode_cursor(cursor)
c9effeac326aatagen2026-10-25 10:32 UTC307
                    .ok_or_else(|| Error::new(ErrorKind::Invalid, "search cursor is invalid"))?,
c9effeac326aatagen2026-10-25 10:32 UTC308
            ),
c9effeac326aatagen2026-10-25 10:32 UTC309
            None => None,
c9effeac326aatagen2026-10-25 10:32 UTC310
        };
c9effeac326aatagen2026-10-25 10:32 UTC311
        let rooms = self.scope_rooms(scope);
c9effeac326aatagen2026-10-25 10:32 UTC312
        let filter = Some(rooms.iter().map(ToString::to_string).collect());
c9effeac326aatagen2026-10-25 10:32 UTC313
        let (reply, response) = oneshot::channel();
c9effeac326aatagen2026-10-25 10:32 UTC314
        self.requests
c9effeac326aatagen2026-10-25 10:32 UTC315
            .send(Request::Search {
c9effeac326aatagen2026-10-25 10:32 UTC316
                query: query.to_owned(),
c9effeac326aatagen2026-10-25 10:32 UTC317
                rooms: filter,
c9effeac326aatagen2026-10-25 10:32 UTC318
                before,
c9effeac326aatagen2026-10-25 10:32 UTC319
                limit: usize::from(limit),
c9effeac326aatagen2026-10-25 10:32 UTC320
                coverage: rooms,
c9effeac326aatagen2026-10-25 10:32 UTC321
                reply,
c9effeac326aatagen2026-10-25 10:32 UTC322
            })
c9effeac326aatagen2026-10-25 10:32 UTC323
            .await
c9effeac326aatagen2026-10-25 10:32 UTC324
            .map_err(|_| Error::interrupted())?;
c9effeac326aatagen2026-10-25 10:32 UTC325
        let raw = response.await.map_err(|_| Error::interrupted())??;
c9effeac326aatagen2026-10-25 10:32 UTC326
        let next_cursor = raw
c9effeac326aatagen2026-10-25 10:32 UTC327
            .more
c9effeac326aatagen2026-10-25 10:32 UTC328
            .then(|| raw.hits.last().map(|hit| encode_cursor((hit.ts, hit.tie))))
c9effeac326aatagen2026-10-25 10:32 UTC329
            .flatten();
c9effeac326aatagen2026-10-25 10:32 UTC330
        let ignored = self.ignored_users().await;
c9effeac326aatagen2026-10-25 10:32 UTC331
        let current_user_id = self.client.user_id().map(ToOwned::to_owned);
c9effeac326aatagen2026-10-25 10:32 UTC332
        let previews = join_all(
c9effeac326aatagen2026-10-25 10:32 UTC333
            raw.hits
c9effeac326aatagen2026-10-25 10:32 UTC334
                .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC335
                .filter(|hit| !ignored.iter().any(|user| user.as_str() == hit.sender))
c9effeac326aatagen2026-10-25 10:32 UTC336
                .map(|hit| self.preview(hit, current_user_id.clone(), &raw.terms)),
c9effeac326aatagen2026-10-25 10:32 UTC337
        )
c9effeac326aatagen2026-10-25 10:32 UTC338
        .await;
c9effeac326aatagen2026-10-25 10:32 UTC339
        let mut hits = Vec::new();
c9effeac326aatagen2026-10-25 10:32 UTC340
        let mut media = Vec::new();
c9effeac326aatagen2026-10-25 10:32 UTC341
        for preview in previews {
c9effeac326aatagen2026-10-25 10:32 UTC342
            match preview {
c9effeac326aatagen2026-10-25 10:32 UTC343
                Preview::Shown(hit, registrations) => {
c9effeac326aatagen2026-10-25 10:32 UTC344
                    hits.push(hit);
c9effeac326aatagen2026-10-25 10:32 UTC345
                    media.extend(registrations);
c9effeac326aatagen2026-10-25 10:32 UTC346
                }
c9effeac326aatagen2026-10-25 10:32 UTC347
                Preview::Redacted(key) => {
c9effeac326aatagen2026-10-25 10:32 UTC348
                    let _ = self.requests.send(Request::Forget(key)).await;
c9effeac326aatagen2026-10-25 10:32 UTC349
                }
c9effeac326aatagen2026-10-25 10:32 UTC350
                Preview::Unavailable => {}
c9effeac326aatagen2026-10-25 10:32 UTC351
            }
c9effeac326aatagen2026-10-25 10:32 UTC352
        }
c9effeac326aatagen2026-10-25 10:32 UTC353
        let page = SearchPage {
c9effeac326aatagen2026-10-25 10:32 UTC354
            hits,
c9effeac326aatagen2026-10-25 10:32 UTC355
            next_cursor,
c9effeac326aatagen2026-10-25 10:32 UTC356
            coverage: raw.coverage,
c9effeac326aatagen2026-10-25 10:32 UTC357
        };
c9effeac326aatagen2026-10-25 10:32 UTC358
        Ok((page, media))
c9effeac326aatagen2026-10-25 10:32 UTC359
    }
c9effeac326aatagen2026-10-25 10:32 UTC360
c9effeac326aatagen2026-10-25 10:32 UTC361
    async fn ignored_users(&self) -> Vec<OwnedUserId> {
c9effeac326aatagen2026-10-25 10:32 UTC362
        self.client
c9effeac326aatagen2026-10-25 10:32 UTC363
            .account()
c9effeac326aatagen2026-10-25 10:32 UTC364
            .account_data::<IgnoredUserListEventContent>()
c9effeac326aatagen2026-10-25 10:32 UTC365
            .await
c9effeac326aatagen2026-10-25 10:32 UTC366
            .ok()
c9effeac326aatagen2026-10-25 10:32 UTC367
            .flatten()
c9effeac326aatagen2026-10-25 10:32 UTC368
            .and_then(|raw| raw.deserialize().ok())
c9effeac326aatagen2026-10-25 10:32 UTC369
            .map(|content| content.ignored_users.into_keys().collect())
c9effeac326aatagen2026-10-25 10:32 UTC370
            .unwrap_or_default()
c9effeac326aatagen2026-10-25 10:32 UTC371
    }
c9effeac326aatagen2026-10-25 10:32 UTC372
c9effeac326aatagen2026-10-25 10:32 UTC373
    async fn preview(
c9effeac326aatagen2026-10-25 10:32 UTC374
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC375
        hit: Hit,
c9effeac326aatagen2026-10-25 10:32 UTC376
        current_user_id: Option<OwnedUserId>,
c9effeac326aatagen2026-10-25 10:32 UTC377
        terms: &[String],
c9effeac326aatagen2026-10-25 10:32 UTC378
    ) -> Preview {
c9effeac326aatagen2026-10-25 10:32 UTC379
        let (Ok(room_id), Ok(event_id)) = (
c9effeac326aatagen2026-10-25 10:32 UTC380
            OwnedRoomId::try_from(hit.room.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC381
            ruma::OwnedEventId::try_from(hit.key.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC382
        ) else {
c9effeac326aatagen2026-10-25 10:32 UTC383
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC384
        };
c9effeac326aatagen2026-10-25 10:32 UTC385
        let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC386
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC387
        };
c9effeac326aatagen2026-10-25 10:32 UTC388
        let Some((original, highlight)) = load(&room, &hit.key).await else {
c9effeac326aatagen2026-10-25 10:32 UTC389
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC390
        };
c9effeac326aatagen2026-10-25 10:32 UTC391
        if original.pointer("/unsigned/redacted_because").is_some() {
c9effeac326aatagen2026-10-25 10:32 UTC392
            return Preview::Redacted(hit.key);
c9effeac326aatagen2026-10-25 10:32 UTC393
        }
c9effeac326aatagen2026-10-25 10:32 UTC394
        let mut values = vec![original];
c9effeac326aatagen2026-10-25 10:32 UTC395
        if hit.content != hit.key
c9effeac326aatagen2026-10-25 10:32 UTC396
            && let Some((edit, _)) = load(&room, &hit.content).await
c9effeac326aatagen2026-10-25 10:32 UTC397
        {
c9effeac326aatagen2026-10-25 10:32 UTC398
            values.push(edit);
c9effeac326aatagen2026-10-25 10:32 UTC399
        }
c9effeac326aatagen2026-10-25 10:32 UTC400
        let Some((event, registrations)) =
c9effeac326aatagen2026-10-25 10:32 UTC401
            context::preview(&room, current_user_id.as_deref(), &values, highlight).await
c9effeac326aatagen2026-10-25 10:32 UTC402
        else {
c9effeac326aatagen2026-10-25 10:32 UTC403
            return Preview::Unavailable;
c9effeac326aatagen2026-10-25 10:32 UTC404
        };
c9effeac326aatagen2026-10-25 10:32 UTC405
        let text = match &event.content {
c9effeac326aatagen2026-10-25 10:32 UTC406
            crate::TimelineContent::Message(message) => Some(message.body.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC407
            crate::TimelineContent::Sticker { body, .. } => Some(body.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC408
            crate::TimelineContent::Poll { question } => Some(question.as_str()),
c9effeac326aatagen2026-10-25 10:32 UTC409
            _ => None,
c9effeac326aatagen2026-10-25 10:32 UTC410
        };
c9effeac326aatagen2026-10-25 10:32 UTC411
        let matches = text
c9effeac326aatagen2026-10-25 10:32 UTC412
            .map(|text| index::match_ranges(text, terms))
c9effeac326aatagen2026-10-25 10:32 UTC413
            .unwrap_or_default()
c9effeac326aatagen2026-10-25 10:32 UTC414
            .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC415
            .filter_map(|(start, end)| {
c9effeac326aatagen2026-10-25 10:32 UTC416
                Some(SearchMatch {
c9effeac326aatagen2026-10-25 10:32 UTC417
                    start: u32::try_from(start).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC418
                    end: u32::try_from(end).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC419
                })
c9effeac326aatagen2026-10-25 10:32 UTC420
            })
c9effeac326aatagen2026-10-25 10:32 UTC421
            .collect();
c9effeac326aatagen2026-10-25 10:32 UTC422
        Preview::Shown(
c9effeac326aatagen2026-10-25 10:32 UTC423
            SearchHit {
c9effeac326aatagen2026-10-25 10:32 UTC424
                room_id,
c9effeac326aatagen2026-10-25 10:32 UTC425
                event_id,
c9effeac326aatagen2026-10-25 10:32 UTC426
                timestamp_ms: event.timestamp_ms,
c9effeac326aatagen2026-10-25 10:32 UTC427
                event,
c9effeac326aatagen2026-10-25 10:32 UTC428
                matches,
c9effeac326aatagen2026-10-25 10:32 UTC429
            },
c9effeac326aatagen2026-10-25 10:32 UTC430
            registrations,
c9effeac326aatagen2026-10-25 10:32 UTC431
        )
c9effeac326aatagen2026-10-25 10:32 UTC432
    }
c9effeac326aatagen2026-10-25 10:32 UTC433
}
c9effeac326aatagen2026-10-25 10:32 UTC434
c9effeac326aatagen2026-10-25 10:32 UTC435
enum Preview {
c9effeac326aatagen2026-10-25 10:32 UTC436
    Shown(SearchHit, Vec<MediaRegistration>),
c9effeac326aatagen2026-10-25 10:32 UTC437
    Redacted(String),
c9effeac326aatagen2026-10-25 10:32 UTC438
    Unavailable,
c9effeac326aatagen2026-10-25 10:32 UTC439
}
c9effeac326aatagen2026-10-25 10:32 UTC440
c9effeac326aatagen2026-10-25 10:32 UTC441
/// open searches, oldest first, each owning the media of the hits it handed out
c9effeac326aatagen2026-10-25 10:32 UTC442
#[derive(Default)]
c9effeac326aatagen2026-10-25 10:32 UTC443
struct Sessions {
c9effeac326aatagen2026-10-25 10:32 UTC444
    open: Mutex<VecDeque<Session>>,
c9effeac326aatagen2026-10-25 10:32 UTC445
}
c9effeac326aatagen2026-10-25 10:32 UTC446
c9effeac326aatagen2026-10-25 10:32 UTC447
struct Session {
c9effeac326aatagen2026-10-25 10:32 UTC448
    id: String,
c9effeac326aatagen2026-10-25 10:32 UTC449
    query: String,
c9effeac326aatagen2026-10-25 10:32 UTC450
    scope: SearchScope,
c9effeac326aatagen2026-10-25 10:32 UTC451
    media: MediaScope,
c9effeac326aatagen2026-10-25 10:32 UTC452
}
c9effeac326aatagen2026-10-25 10:32 UTC453
c9effeac326aatagen2026-10-25 10:32 UTC454
impl Sessions {
c9effeac326aatagen2026-10-25 10:32 UTC455
    async fn open(
c9effeac326aatagen2026-10-25 10:32 UTC456
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC457
        media: &MediaService,
c9effeac326aatagen2026-10-25 10:32 UTC458
        query: String,
c9effeac326aatagen2026-10-25 10:32 UTC459
        scope: SearchScope,
c9effeac326aatagen2026-10-25 10:32 UTC460
        registrations: Vec<MediaRegistration>,
c9effeac326aatagen2026-10-25 10:32 UTC461
    ) -> String {
c9effeac326aatagen2026-10-25 10:32 UTC462
        let media_scope = media.context_scope();
c9effeac326aatagen2026-10-25 10:32 UTC463
        let id = format!("search-{}", media_scope.nonce());
c9effeac326aatagen2026-10-25 10:32 UTC464
        let mut open = self.open.lock().await;
c9effeac326aatagen2026-10-25 10:32 UTC465
        media.register(&media_scope, registrations).await;
c9effeac326aatagen2026-10-25 10:32 UTC466
        open.push_back(Session {
c9effeac326aatagen2026-10-25 10:32 UTC467
            id: id.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC468
            query,
c9effeac326aatagen2026-10-25 10:32 UTC469
            scope,
c9effeac326aatagen2026-10-25 10:32 UTC470
            media: media_scope,
c9effeac326aatagen2026-10-25 10:32 UTC471
        });
c9effeac326aatagen2026-10-25 10:32 UTC472
        while open.len() > MAX_SEARCHES {
c9effeac326aatagen2026-10-25 10:32 UTC473
            if let Some(oldest) = open.pop_front() {
c9effeac326aatagen2026-10-25 10:32 UTC474
                media.release(&oldest.media).await;
c9effeac326aatagen2026-10-25 10:32 UTC475
            }
c9effeac326aatagen2026-10-25 10:32 UTC476
        }
c9effeac326aatagen2026-10-25 10:32 UTC477
        id
c9effeac326aatagen2026-10-25 10:32 UTC478
    }
c9effeac326aatagen2026-10-25 10:32 UTC479
c9effeac326aatagen2026-10-25 10:32 UTC480
    async fn get(&self, id: &str) -> Result<(String, SearchScope), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC481
        let open = self.open.lock().await;
c9effeac326aatagen2026-10-25 10:32 UTC482
        let session = open.iter().find(|session| session.id == id).ok_or_else(not_open)?;
c9effeac326aatagen2026-10-25 10:32 UTC483
        Ok((session.query.clone(), session.scope.clone()))
c9effeac326aatagen2026-10-25 10:32 UTC484
    }
c9effeac326aatagen2026-10-25 10:32 UTC485
c9effeac326aatagen2026-10-25 10:32 UTC486
    /// under the lock, so a page that lands after its close registers nothing
c9effeac326aatagen2026-10-25 10:32 UTC487
    async fn register(
c9effeac326aatagen2026-10-25 10:32 UTC488
        &self,
c9effeac326aatagen2026-10-25 10:32 UTC489
        media: &MediaService,
c9effeac326aatagen2026-10-25 10:32 UTC490
        id: &str,
c9effeac326aatagen2026-10-25 10:32 UTC491
        registrations: Vec<MediaRegistration>,
c9effeac326aatagen2026-10-25 10:32 UTC492
    ) -> Result<(), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC493
        let open = self.open.lock().await;
c9effeac326aatagen2026-10-25 10:32 UTC494
        let session = open.iter().find(|session| session.id == id).ok_or_else(not_open)?;
c9effeac326aatagen2026-10-25 10:32 UTC495
        media.register(&session.media, registrations).await;
c9effeac326aatagen2026-10-25 10:32 UTC496
        Ok(())
c9effeac326aatagen2026-10-25 10:32 UTC497
    }
c9effeac326aatagen2026-10-25 10:32 UTC498
c9effeac326aatagen2026-10-25 10:32 UTC499
    async fn close(&self, media: &MediaService, id: &str) -> Result<(), Error> {
c9effeac326aatagen2026-10-25 10:32 UTC500
        let mut open = self.open.lock().await;
c9effeac326aatagen2026-10-25 10:32 UTC501
        let index = open
c9effeac326aatagen2026-10-25 10:32 UTC502
            .iter()
c9effeac326aatagen2026-10-25 10:32 UTC503
            .position(|session| session.id == id)
c9effeac326aatagen2026-10-25 10:32 UTC504
            .ok_or_else(not_open)?;
c9effeac326aatagen2026-10-25 10:32 UTC505
        if let Some(session) = open.remove(index) {
c9effeac326aatagen2026-10-25 10:32 UTC506
            media.release(&session.media).await;
c9effeac326aatagen2026-10-25 10:32 UTC507
        }
c9effeac326aatagen2026-10-25 10:32 UTC508
        Ok(())
c9effeac326aatagen2026-10-25 10:32 UTC509
    }
c9effeac326aatagen2026-10-25 10:32 UTC510
}
c9effeac326aatagen2026-10-25 10:32 UTC511
c9effeac326aatagen2026-10-25 10:32 UTC512
fn not_open() -> Error {
c9effeac326aatagen2026-10-25 10:32 UTC513
    Error::new(ErrorKind::SearchNotOpen, "search is closed or was never opened")
c9effeac326aatagen2026-10-25 10:32 UTC514
}
c9effeac326aatagen2026-10-25 10:32 UTC515
c9effeac326aatagen2026-10-25 10:32 UTC516
async fn load(room: &Room, event_id: &str) -> Option<(Value, bool)> {
c9effeac326aatagen2026-10-25 10:32 UTC517
    let event_id = ruma::EventId::parse(event_id).ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC518
    let event = room
c9effeac326aatagen2026-10-25 10:32 UTC519
        .load_or_fetch_event(&event_id, None)
c9effeac326aatagen2026-10-25 10:32 UTC520
        .await
c9effeac326aatagen2026-10-25 10:32 UTC521
        .inspect_err(|error| tracing::debug!(%event_id, %error, "loading a search hit failed"))
c9effeac326aatagen2026-10-25 10:32 UTC522
        .ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC523
    let highlight = event
c9effeac326aatagen2026-10-25 10:32 UTC524
        .push_actions()
c9effeac326aatagen2026-10-25 10:32 UTC525
        .is_some_and(|actions| actions.iter().any(ruma::push::Action::is_highlight));
c9effeac326aatagen2026-10-25 10:32 UTC526
    Some((value_of(&event)?, highlight))
c9effeac326aatagen2026-10-25 10:32 UTC527
}
c9effeac326aatagen2026-10-25 10:32 UTC528
c9effeac326aatagen2026-10-25 10:32 UTC529
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC530
fn decode_meta(
c9effeac326aatagen2026-10-25 10:32 UTC531
    store: &SearchStore,
c9effeac326aatagen2026-10-25 10:32 UTC532
    blobs: &BTreeMap<String, Vec<u8>>,
c9effeac326aatagen2026-10-25 10:32 UTC533
) -> Option<(
c9effeac326aatagen2026-10-25 10:32 UTC534
    Value,
c9effeac326aatagen2026-10-25 10:32 UTC535
    BTreeMap<String, RoomProgress>,
c9effeac326aatagen2026-10-25 10:32 UTC536
    BTreeMap<String, Pending>,
c9effeac326aatagen2026-10-25 10:32 UTC537
)> {
c9effeac326aatagen2026-10-25 10:32 UTC538
    let meta: Value = serde_json::from_slice(&store.open_blob("meta", blobs.get("meta")?)?).ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC539
    if meta["schema"].as_u64() != Some(SCHEMA) {
c9effeac326aatagen2026-10-25 10:32 UTC540
        return None;
c9effeac326aatagen2026-10-25 10:32 UTC541
    }
c9effeac326aatagen2026-10-25 10:32 UTC542
    let rooms = serde_json::from_value(meta["rooms"].clone()).ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC543
    let pending = match blobs.get("utd") {
c9effeac326aatagen2026-10-25 10:32 UTC544
        Some(sealed) => serde_json::from_slice(&store.open_blob("utd", sealed)?).ok()?,
c9effeac326aatagen2026-10-25 10:32 UTC545
        None => BTreeMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC546
    };
c9effeac326aatagen2026-10-25 10:32 UTC547
    Some((meta["index"].clone(), rooms, pending))
c9effeac326aatagen2026-10-25 10:32 UTC548
}
c9effeac326aatagen2026-10-25 10:32 UTC549
c9effeac326aatagen2026-10-25 10:32 UTC550
impl Service {
c9effeac326aatagen2026-10-25 10:32 UTC551
    pub(crate) fn new(
c9effeac326aatagen2026-10-25 10:32 UTC552
        client: Client,
c9effeac326aatagen2026-10-25 10:32 UTC553
        media: Arc<MediaService>,
c9effeac326aatagen2026-10-25 10:32 UTC554
        store: Option<Result<SearchStore, Error>>,
c9effeac326aatagen2026-10-25 10:32 UTC555
        config: &EngineConfig,
c9effeac326aatagen2026-10-25 10:32 UTC556
    ) -> (Handle, Self) {
c9effeac326aatagen2026-10-25 10:32 UTC557
        let (sender, requests) = mpsc::channel(config.command_capacity.max(1));
c9effeac326aatagen2026-10-25 10:32 UTC558
        let service = Self {
c9effeac326aatagen2026-10-25 10:32 UTC559
            client: client.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC560
            index: Box::new(KeywordIndex::new(store.as_ref().is_some_and(Result::is_ok))),
c9effeac326aatagen2026-10-25 10:32 UTC561
            store,
c9effeac326aatagen2026-10-25 10:32 UTC562
            rooms: BTreeMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC563
            pending: BTreeMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC564
            pending_dirty: false,
c9effeac326aatagen2026-10-25 10:32 UTC565
            redactions: Redactions::default(),
c9effeac326aatagen2026-10-25 10:32 UTC566
            dirty_since: None,
c9effeac326aatagen2026-10-25 10:32 UTC567
            commit_after: 0,
c9effeac326aatagen2026-10-25 10:32 UTC568
            budget: Budget::from_config(config),
c9effeac326aatagen2026-10-25 10:32 UTC569
            requests,
c9effeac326aatagen2026-10-25 10:32 UTC570
            subscriptions: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC571
            updates: SelectAll::new(),
c9effeac326aatagen2026-10-25 10:32 UTC572
            work: FuturesUnordered::new(),
c9effeac326aatagen2026-10-25 10:32 UTC573
            pages: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC574
            heads: VecDeque::new(),
c9effeac326aatagen2026-10-25 10:32 UTC575
            headed: HashSet::new(),
c9effeac326aatagen2026-10-25 10:32 UTC576
            head_tokens: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC577
            backfilling: None,
c9effeac326aatagen2026-10-25 10:32 UTC578
            next_backfill: 0,
c9effeac326aatagen2026-10-25 10:32 UTC579
            retry_delay: BACKFILL_RETRY_MS,
c9effeac326aatagen2026-10-25 10:32 UTC580
            failures: HashMap::new(),
c9effeac326aatagen2026-10-25 10:32 UTC581
            turn: 0,
c9effeac326aatagen2026-10-25 10:32 UTC582
        };
c9effeac326aatagen2026-10-25 10:32 UTC583
        (
c9effeac326aatagen2026-10-25 10:32 UTC584
            Handle {
c9effeac326aatagen2026-10-25 10:32 UTC585
                requests: sender,
c9effeac326aatagen2026-10-25 10:32 UTC586
                client,
c9effeac326aatagen2026-10-25 10:32 UTC587
                media,
c9effeac326aatagen2026-10-25 10:32 UTC588
                sessions: Arc::default(),
c9effeac326aatagen2026-10-25 10:32 UTC589
            },
c9effeac326aatagen2026-10-25 10:32 UTC590
            service,
c9effeac326aatagen2026-10-25 10:32 UTC591
        )
c9effeac326aatagen2026-10-25 10:32 UTC592
    }
c9effeac326aatagen2026-10-25 10:32 UTC593
c9effeac326aatagen2026-10-25 10:32 UTC594
    fn persistent(&self) -> bool {
c9effeac326aatagen2026-10-25 10:32 UTC595
        matches!(self.store, Some(Ok(_)))
c9effeac326aatagen2026-10-25 10:32 UTC596
    }
c9effeac326aatagen2026-10-25 10:32 UTC597
c9effeac326aatagen2026-10-25 10:32 UTC598
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC599
    async fn load(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC600
        let Some(Ok(store)) = &self.store else {
c9effeac326aatagen2026-10-25 10:32 UTC601
            if let Some(Err(error)) = &self.store {
c9effeac326aatagen2026-10-25 10:32 UTC602
                tracing::error!(%error, "search store unavailable, the index lives in memory");
c9effeac326aatagen2026-10-25 10:32 UTC603
            }
c9effeac326aatagen2026-10-25 10:32 UTC604
            return;
c9effeac326aatagen2026-10-25 10:32 UTC605
        };
c9effeac326aatagen2026-10-25 10:32 UTC606
        let blobs = match store.load().await {
c9effeac326aatagen2026-10-25 10:32 UTC607
            Ok(blobs) => blobs,
c9effeac326aatagen2026-10-25 10:32 UTC608
            Err(error) => {
c9effeac326aatagen2026-10-25 10:32 UTC609
                tracing::error!(%error, "reading the search store failed, the index lives in memory");
c9effeac326aatagen2026-10-25 10:32 UTC610
                self.store = Some(Err(error));
c9effeac326aatagen2026-10-25 10:32 UTC611
                self.index = Box::new(KeywordIndex::new(false));
c9effeac326aatagen2026-10-25 10:32 UTC612
                return;
c9effeac326aatagen2026-10-25 10:32 UTC613
            }
c9effeac326aatagen2026-10-25 10:32 UTC614
        };
c9effeac326aatagen2026-10-25 10:32 UTC615
        if blobs.is_empty() {
c9effeac326aatagen2026-10-25 10:32 UTC616
            return;
c9effeac326aatagen2026-10-25 10:32 UTC617
        }
c9effeac326aatagen2026-10-25 10:32 UTC618
        let restored = decode_meta(store, &blobs).and_then(|(manifest, rooms, pending)| {
c9effeac326aatagen2026-10-25 10:32 UTC619
            let index = KeywordIndex::load(&manifest, |key| {
c9effeac326aatagen2026-10-25 10:32 UTC620
                blobs
c9effeac326aatagen2026-10-25 10:32 UTC621
                    .get(key)
c9effeac326aatagen2026-10-25 10:32 UTC622
                    .and_then(|sealed| store.open_blob(key, sealed))
c9effeac326aatagen2026-10-25 10:32 UTC623
            })
c9effeac326aatagen2026-10-25 10:32 UTC624
            .inspect_err(|error| tracing::warn!(%error, "search segments are unreadable"))
c9effeac326aatagen2026-10-25 10:32 UTC625
            .ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC626
            Some((index, rooms, pending))
c9effeac326aatagen2026-10-25 10:32 UTC627
        });
c9effeac326aatagen2026-10-25 10:32 UTC628
        drop(blobs);
c9effeac326aatagen2026-10-25 10:32 UTC629
        match restored {
c9effeac326aatagen2026-10-25 10:32 UTC630
            Some((index, rooms, pending)) => {
c9effeac326aatagen2026-10-25 10:32 UTC631
                self.index = Box::new(index);
c9effeac326aatagen2026-10-25 10:32 UTC632
                self.rooms = rooms;
c9effeac326aatagen2026-10-25 10:32 UTC633
                self.pending = pending;
c9effeac326aatagen2026-10-25 10:32 UTC634
            }
c9effeac326aatagen2026-10-25 10:32 UTC635
            None => {
c9effeac326aatagen2026-10-25 10:32 UTC636
                tracing::warn!("resetting an unreadable search index");
c9effeac326aatagen2026-10-25 10:32 UTC637
                if let Err(error) = store.reset().await {
c9effeac326aatagen2026-10-25 10:32 UTC638
                    tracing::error!(%error, "search store unavailable, the index lives in memory");
c9effeac326aatagen2026-10-25 10:32 UTC639
                    self.store = Some(Err(error));
c9effeac326aatagen2026-10-25 10:32 UTC640
                    self.index = Box::new(KeywordIndex::new(false));
c9effeac326aatagen2026-10-25 10:32 UTC641
                }
c9effeac326aatagen2026-10-25 10:32 UTC642
            }
c9effeac326aatagen2026-10-25 10:32 UTC643
        }
c9effeac326aatagen2026-10-25 10:32 UTC644
    }
c9effeac326aatagen2026-10-25 10:32 UTC645
c9effeac326aatagen2026-10-25 10:32 UTC646
    #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
c9effeac326aatagen2026-10-25 10:32 UTC647
    async fn load(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC648
        if let Some(Err(error)) = &self.store {
c9effeac326aatagen2026-10-25 10:32 UTC649
            tracing::error!(%error, "search store unavailable, the index lives in memory");
c9effeac326aatagen2026-10-25 10:32 UTC650
        }
c9effeac326aatagen2026-10-25 10:32 UTC651
    }
c9effeac326aatagen2026-10-25 10:32 UTC652
c9effeac326aatagen2026-10-25 10:32 UTC653
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
c9effeac326aatagen2026-10-25 10:32 UTC654
    async fn commit(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC655
        if !self.persistent() {
c9effeac326aatagen2026-10-25 10:32 UTC656
            if let Err(error) = self.index.maintain() {
c9effeac326aatagen2026-10-25 10:32 UTC657
                tracing::warn!(%error, "maintaining the search index failed");
c9effeac326aatagen2026-10-25 10:32 UTC658
            }
c9effeac326aatagen2026-10-25 10:32 UTC659
            self.dirty_since = None;
c9effeac326aatagen2026-10-25 10:32 UTC660
            return;
c9effeac326aatagen2026-10-25 10:32 UTC661
        }
c9effeac326aatagen2026-10-25 10:32 UTC662
        let changes = match self.index.changes() {
c9effeac326aatagen2026-10-25 10:32 UTC663
            Ok(changes) => changes,
c9effeac326aatagen2026-10-25 10:32 UTC664
            Err(error) => {
c9effeac326aatagen2026-10-25 10:32 UTC665
                tracing::warn!(%error, "preparing a search commit failed");
c9effeac326aatagen2026-10-25 10:32 UTC666
                self.commit_after = now() + COMMIT_RETRY_MS;
c9effeac326aatagen2026-10-25 10:32 UTC667
                return;
c9effeac326aatagen2026-10-25 10:32 UTC668
            }
c9effeac326aatagen2026-10-25 10:32 UTC669
        };
c9effeac326aatagen2026-10-25 10:32 UTC670
        let mut puts = changes.puts.clone();
c9effeac326aatagen2026-10-25 10:32 UTC671
        let meta = serde_json::json!({
c9effeac326aatagen2026-10-25 10:32 UTC672
            "schema": SCHEMA,
c9effeac326aatagen2026-10-25 10:32 UTC673
            "index": self.index.manifest(),
c9effeac326aatagen2026-10-25 10:32 UTC674
            "rooms": self.rooms,
c9effeac326aatagen2026-10-25 10:32 UTC675
        });
c9effeac326aatagen2026-10-25 10:32 UTC676
        puts.push(("meta".to_owned(), meta.to_string().into_bytes()));
c9effeac326aatagen2026-10-25 10:32 UTC677
        if self.pending_dirty {
c9effeac326aatagen2026-10-25 10:32 UTC678
            puts.push((
c9effeac326aatagen2026-10-25 10:32 UTC679
                "utd".to_owned(),
c9effeac326aatagen2026-10-25 10:32 UTC680
                serde_json::to_vec(&self.pending).unwrap_or_default(),
c9effeac326aatagen2026-10-25 10:32 UTC681
            ));
c9effeac326aatagen2026-10-25 10:32 UTC682
        }
c9effeac326aatagen2026-10-25 10:32 UTC683
        let Some(Ok(store)) = &self.store else {
c9effeac326aatagen2026-10-25 10:32 UTC684
            return;
c9effeac326aatagen2026-10-25 10:32 UTC685
        };
c9effeac326aatagen2026-10-25 10:32 UTC686
        match store.commit(puts, changes.deletes.clone()).await {
c9effeac326aatagen2026-10-25 10:32 UTC687
            Ok(()) => {
c9effeac326aatagen2026-10-25 10:32 UTC688
                self.index.committed(&changes);
c9effeac326aatagen2026-10-25 10:32 UTC689
                self.pending_dirty = false;
c9effeac326aatagen2026-10-25 10:32 UTC690
                self.dirty_since = None;
c9effeac326aatagen2026-10-25 10:32 UTC691
            }
c9effeac326aatagen2026-10-25 10:32 UTC692
            Err(error) => {
c9effeac326aatagen2026-10-25 10:32 UTC693
                tracing::warn!(%error, "saving the search index failed, retrying later");
c9effeac326aatagen2026-10-25 10:32 UTC694
                self.commit_after = now() + COMMIT_RETRY_MS;
c9effeac326aatagen2026-10-25 10:32 UTC695
            }
c9effeac326aatagen2026-10-25 10:32 UTC696
        }
c9effeac326aatagen2026-10-25 10:32 UTC697
    }
c9effeac326aatagen2026-10-25 10:32 UTC698
c9effeac326aatagen2026-10-25 10:32 UTC699
    #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
c9effeac326aatagen2026-10-25 10:32 UTC700
    async fn commit(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC701
        if let Err(error) = self.index.maintain() {
c9effeac326aatagen2026-10-25 10:32 UTC702
            tracing::warn!(%error, "maintaining the search index failed");
c9effeac326aatagen2026-10-25 10:32 UTC703
        }
c9effeac326aatagen2026-10-25 10:32 UTC704
        self.dirty_since = None;
c9effeac326aatagen2026-10-25 10:32 UTC705
    }
c9effeac326aatagen2026-10-25 10:32 UTC706
c9effeac326aatagen2026-10-25 10:32 UTC707
    fn touch(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC708
        self.dirty_since.get_or_insert_with(now);
c9effeac326aatagen2026-10-25 10:32 UTC709
    }
c9effeac326aatagen2026-10-25 10:32 UTC710
c9effeac326aatagen2026-10-25 10:32 UTC711
    fn observe(&mut self, room: &OwnedRoomId, value: &Value) {
c9effeac326aatagen2026-10-25 10:32 UTC712
        let observed = feed::observe(value);
c9effeac326aatagen2026-10-25 10:32 UTC713
        let key = match &observed {
c9effeac326aatagen2026-10-25 10:32 UTC714
            Observed::Message { key, .. }
c9effeac326aatagen2026-10-25 10:32 UTC715
            | Observed::Redacted { key }
c9effeac326aatagen2026-10-25 10:32 UTC716
            | Observed::Encrypted { key, .. } => Some(key.clone()),
c9effeac326aatagen2026-10-25 10:32 UTC717
            Observed::Edit(edit) => Some(edit.id.clone()),
c9effeac326aatagen2026-10-25 10:32 UTC718
            Observed::Redaction { .. } | Observed::Other => None,
c9effeac326aatagen2026-10-25 10:32 UTC719
        };
c9effeac326aatagen2026-10-25 10:32 UTC720
        if let Observed::Encrypted {
c9effeac326aatagen2026-10-25 10:32 UTC721
            key,
c9effeac326aatagen2026-10-25 10:32 UTC722
            session_id,
c9effeac326aatagen2026-10-25 10:32 UTC723
            ts,
c9effeac326aatagen2026-10-25 10:32 UTC724
        } = &observed
c9effeac326aatagen2026-10-25 10:32 UTC725
        {
c9effeac326aatagen2026-10-25 10:32 UTC726
            if self.index.get(key).is_none() && !self.pending.contains_key(key) {
c9effeac326aatagen2026-10-25 10:32 UTC727
                self.pending.insert(
c9effeac326aatagen2026-10-25 10:32 UTC728
                    key.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC729
                    Pending {
c9effeac326aatagen2026-10-25 10:32 UTC730
                        room: room.to_string(),
c9effeac326aatagen2026-10-25 10:32 UTC731
                        session_id: session_id.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC732
                        ts: *ts,
c9effeac326aatagen2026-10-25 10:32 UTC733
                        raw: value.to_string(),
c9effeac326aatagen2026-10-25 10:32 UTC734
                    },
c9effeac326aatagen2026-10-25 10:32 UTC735
                );
c9effeac326aatagen2026-10-25 10:32 UTC736
                while self.pending.len() > self.budget.max_pending {
c9effeac326aatagen2026-10-25 10:32 UTC737
                    let Some(oldest) = self
c9effeac326aatagen2026-10-25 10:32 UTC738
                        .pending
c9effeac326aatagen2026-10-25 10:32 UTC739
                        .iter()
c9effeac326aatagen2026-10-25 10:32 UTC740
                        .min_by_key(|(_, pending)| pending.ts)
c9effeac326aatagen2026-10-25 10:32 UTC741
                        .map(|(key, _)| key.clone())
c9effeac326aatagen2026-10-25 10:32 UTC742
                    else {
c9effeac326aatagen2026-10-25 10:32 UTC743
                        break;
c9effeac326aatagen2026-10-25 10:32 UTC744
                    };
c9effeac326aatagen2026-10-25 10:32 UTC745
                    self.pending.remove(&oldest);
c9effeac326aatagen2026-10-25 10:32 UTC746
                }
c9effeac326aatagen2026-10-25 10:32 UTC747
                self.pending_dirty = true;
c9effeac326aatagen2026-10-25 10:32 UTC748
                self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC749
            }
c9effeac326aatagen2026-10-25 10:32 UTC750
            return;
c9effeac326aatagen2026-10-25 10:32 UTC751
        }
c9effeac326aatagen2026-10-25 10:32 UTC752
        if let Some(key) = key
c9effeac326aatagen2026-10-25 10:32 UTC753
            && self.pending.remove(&key).is_some()
c9effeac326aatagen2026-10-25 10:32 UTC754
        {
c9effeac326aatagen2026-10-25 10:32 UTC755
            self.pending_dirty = true;
c9effeac326aatagen2026-10-25 10:32 UTC756
            self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC757
        }
c9effeac326aatagen2026-10-25 10:32 UTC758
        match feed::apply(
c9effeac326aatagen2026-10-25 10:32 UTC759
            self.index.as_mut(),
c9effeac326aatagen2026-10-25 10:32 UTC760
            &mut self.redactions,
c9effeac326aatagen2026-10-25 10:32 UTC761
            room.as_str(),
c9effeac326aatagen2026-10-25 10:32 UTC762
            observed,
c9effeac326aatagen2026-10-25 10:32 UTC763
        ) {
c9effeac326aatagen2026-10-25 10:32 UTC764
            Effect::Indexed(ts) => {
c9effeac326aatagen2026-10-25 10:32 UTC765
                let progress = self.rooms.entry(room.to_string()).or_default();
c9effeac326aatagen2026-10-25 10:32 UTC766
                progress.oldest = Some(progress.oldest.map_or(ts, |oldest| oldest.min(ts)));
c9effeac326aatagen2026-10-25 10:32 UTC767
                progress.newest = Some(progress.newest.map_or(ts, |newest| newest.max(ts)));
c9effeac326aatagen2026-10-25 10:32 UTC768
                self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC769
            }
c9effeac326aatagen2026-10-25 10:32 UTC770
            Effect::Removed => self.touch(),
c9effeac326aatagen2026-10-25 10:32 UTC771
            Effect::Refetch(key) => {
c9effeac326aatagen2026-10-25 10:32 UTC772
                self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC773
                let room_id = room.clone();
c9effeac326aatagen2026-10-25 10:32 UTC774
                let client = self.client.clone();
c9effeac326aatagen2026-10-25 10:32 UTC775
                self.work.push(Box::pin(async move {
c9effeac326aatagen2026-10-25 10:32 UTC776
                    let value = match client.get_room(&room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC777
                        Some(room) => load(&room, &key).await.map(|(value, _)| value),
c9effeac326aatagen2026-10-25 10:32 UTC778
                        None => None,
c9effeac326aatagen2026-10-25 10:32 UTC779
                    };
c9effeac326aatagen2026-10-25 10:32 UTC780
                    Work::Refetched {
c9effeac326aatagen2026-10-25 10:32 UTC781
                        room: room_id,
c9effeac326aatagen2026-10-25 10:32 UTC782
                        value,
c9effeac326aatagen2026-10-25 10:32 UTC783
                    }
c9effeac326aatagen2026-10-25 10:32 UTC784
                }));
c9effeac326aatagen2026-10-25 10:32 UTC785
            }
c9effeac326aatagen2026-10-25 10:32 UTC786
            Effect::Nothing => {}
c9effeac326aatagen2026-10-25 10:32 UTC787
        }
c9effeac326aatagen2026-10-25 10:32 UTC788
    }
c9effeac326aatagen2026-10-25 10:32 UTC789
c9effeac326aatagen2026-10-25 10:32 UTC790
    fn observe_all(&mut self, room: &OwnedRoomId, events: &[SdkTimelineEvent]) {
c9effeac326aatagen2026-10-25 10:32 UTC791
        for event in events {
c9effeac326aatagen2026-10-25 10:32 UTC792
            if let Some(value) = value_of(event) {
c9effeac326aatagen2026-10-25 10:32 UTC793
                self.observe(room, &value);
c9effeac326aatagen2026-10-25 10:32 UTC794
            }
c9effeac326aatagen2026-10-25 10:32 UTC795
        }
c9effeac326aatagen2026-10-25 10:32 UTC796
    }
c9effeac326aatagen2026-10-25 10:32 UTC797
c9effeac326aatagen2026-10-25 10:32 UTC798
    async fn subscribe(&mut self, room_id: OwnedRoomId) {
c9effeac326aatagen2026-10-25 10:32 UTC799
        if let Some((_, seen)) = self.subscriptions.get_mut(&room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC800
            *seen = now();
c9effeac326aatagen2026-10-25 10:32 UTC801
            return;
c9effeac326aatagen2026-10-25 10:32 UTC802
        }
c9effeac326aatagen2026-10-25 10:32 UTC803
        let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC804
            return;
c9effeac326aatagen2026-10-25 10:32 UTC805
        };
c9effeac326aatagen2026-10-25 10:32 UTC806
        if room.state() != RoomState::Joined {
c9effeac326aatagen2026-10-25 10:32 UTC807
            return;
c9effeac326aatagen2026-10-25 10:32 UTC808
        }
c9effeac326aatagen2026-10-25 10:32 UTC809
        let Ok((cache, drop_handles)) = room.event_cache().await else {
c9effeac326aatagen2026-10-25 10:32 UTC810
            return;
c9effeac326aatagen2026-10-25 10:32 UTC811
        };
c9effeac326aatagen2026-10-25 10:32 UTC812
        let Ok((initial, subscriber)) = cache.subscribe().await else {
c9effeac326aatagen2026-10-25 10:32 UTC813
            return;
c9effeac326aatagen2026-10-25 10:32 UTC814
        };
c9effeac326aatagen2026-10-25 10:32 UTC815
        self.observe_all(&room_id, &initial);
c9effeac326aatagen2026-10-25 10:32 UTC816
        let stream = updates(room_id.clone(), subscriber, drop_handles);
c9effeac326aatagen2026-10-25 10:32 UTC817
        let (stream, abort) = abortable(stream);
c9effeac326aatagen2026-10-25 10:32 UTC818
        self.updates.push(Box::pin(stream));
c9effeac326aatagen2026-10-25 10:32 UTC819
        self.subscriptions.insert(room_id.clone(), (abort, now()));
c9effeac326aatagen2026-10-25 10:32 UTC820
        if self.headed.insert(room_id.clone()) {
c9effeac326aatagen2026-10-25 10:32 UTC821
            self.heads.push_back(room_id);
c9effeac326aatagen2026-10-25 10:32 UTC822
        }
c9effeac326aatagen2026-10-25 10:32 UTC823
    }
c9effeac326aatagen2026-10-25 10:32 UTC824
c9effeac326aatagen2026-10-25 10:32 UTC825
    fn drop_idle(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC826
        let cutoff = now().saturating_sub(IDLE_SUBSCRIPTION_MS);
c9effeac326aatagen2026-10-25 10:32 UTC827
        self.subscriptions.retain(|_, (abort, seen)| {
c9effeac326aatagen2026-10-25 10:32 UTC828
            let keep = *seen >= cutoff;
c9effeac326aatagen2026-10-25 10:32 UTC829
            if !keep {
c9effeac326aatagen2026-10-25 10:32 UTC830
                abort.abort();
c9effeac326aatagen2026-10-25 10:32 UTC831
            }
c9effeac326aatagen2026-10-25 10:32 UTC832
            keep
c9effeac326aatagen2026-10-25 10:32 UTC833
        });
c9effeac326aatagen2026-10-25 10:32 UTC834
    }
c9effeac326aatagen2026-10-25 10:32 UTC835
c9effeac326aatagen2026-10-25 10:32 UTC836
    fn coverage(&self, rooms: &[OwnedRoomId]) -> SearchCoverage {
c9effeac326aatagen2026-10-25 10:32 UTC837
        let rooms = rooms
c9effeac326aatagen2026-10-25 10:32 UTC838
            .iter()
c9effeac326aatagen2026-10-25 10:32 UTC839
            .map(|room_id| {
c9effeac326aatagen2026-10-25 10:32 UTC840
                let progress = self
c9effeac326aatagen2026-10-25 10:32 UTC841
                    .rooms
c9effeac326aatagen2026-10-25 10:32 UTC842
                    .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC843
                    .cloned()
c9effeac326aatagen2026-10-25 10:32 UTC844
                    .unwrap_or_default();
c9effeac326aatagen2026-10-25 10:32 UTC845
                let backfill = if progress.start_reached {
c9effeac326aatagen2026-10-25 10:32 UTC846
                    SearchBackfill::Complete
c9effeac326aatagen2026-10-25 10:32 UTC847
                } else if self.budget.pages_per_room == 0 {
c9effeac326aatagen2026-10-25 10:32 UTC848
                    SearchBackfill::Disabled
c9effeac326aatagen2026-10-25 10:32 UTC849
                } else if self.backfilling.as_ref() == Some(room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC850
                    SearchBackfill::Running
c9effeac326aatagen2026-10-25 10:32 UTC851
                } else if let Some(Failure {
c9effeac326aatagen2026-10-25 10:32 UTC852
                    message,
c9effeac326aatagen2026-10-25 10:32 UTC853
                    retry_at_ms,
c9effeac326aatagen2026-10-25 10:32 UTC854
                }) = self.failures.get(room_id)
c9effeac326aatagen2026-10-25 10:32 UTC855
                {
c9effeac326aatagen2026-10-25 10:32 UTC856
                    SearchBackfill::Failed {
c9effeac326aatagen2026-10-25 10:32 UTC857
                        message: message.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC858
                        retry_at_ms: *retry_at_ms,
c9effeac326aatagen2026-10-25 10:32 UTC859
                    }
c9effeac326aatagen2026-10-25 10:32 UTC860
                } else if self.pages.get(room_id).copied().unwrap_or(0)
c9effeac326aatagen2026-10-25 10:32 UTC861
                    >= self.budget.pages_per_room
c9effeac326aatagen2026-10-25 10:32 UTC862
                {
c9effeac326aatagen2026-10-25 10:32 UTC863
                    SearchBackfill::Paused
c9effeac326aatagen2026-10-25 10:32 UTC864
                } else {
c9effeac326aatagen2026-10-25 10:32 UTC865
                    SearchBackfill::Waiting
c9effeac326aatagen2026-10-25 10:32 UTC866
                };
c9effeac326aatagen2026-10-25 10:32 UTC867
                RoomSearchCoverage {
c9effeac326aatagen2026-10-25 10:32 UTC868
                    room_id: room_id.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC869
                    oldest_indexed_ms: progress.oldest,
c9effeac326aatagen2026-10-25 10:32 UTC870
                    newest_indexed_ms: progress.newest,
c9effeac326aatagen2026-10-25 10:32 UTC871
                    start_reached: progress.start_reached,
c9effeac326aatagen2026-10-25 10:32 UTC872
                    backfill,
c9effeac326aatagen2026-10-25 10:32 UTC873
                }
c9effeac326aatagen2026-10-25 10:32 UTC874
            })
c9effeac326aatagen2026-10-25 10:32 UTC875
            .collect();
c9effeac326aatagen2026-10-25 10:32 UTC876
        SearchCoverage {
c9effeac326aatagen2026-10-25 10:32 UTC877
            rooms,
c9effeac326aatagen2026-10-25 10:32 UTC878
            persistent: self.persistent(),
c9effeac326aatagen2026-10-25 10:32 UTC879
            undecrypted: self.pending.len(),
c9effeac326aatagen2026-10-25 10:32 UTC880
        }
c9effeac326aatagen2026-10-25 10:32 UTC881
    }
c9effeac326aatagen2026-10-25 10:32 UTC882
c9effeac326aatagen2026-10-25 10:32 UTC883
    /// head passes first, so what arrived while we were away is found before anything older
c9effeac326aatagen2026-10-25 10:32 UTC884
    fn schedule_backfill(&mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC885
        if self.backfilling.is_some()
c9effeac326aatagen2026-10-25 10:32 UTC886
            || self.budget.pages_per_room == 0
c9effeac326aatagen2026-10-25 10:32 UTC887
            || now() < self.next_backfill
c9effeac326aatagen2026-10-25 10:32 UTC888
        {
c9effeac326aatagen2026-10-25 10:32 UTC889
            return;
c9effeac326aatagen2026-10-25 10:32 UTC890
        }
c9effeac326aatagen2026-10-25 10:32 UTC891
        let budget = self.budget.pages_per_room;
c9effeac326aatagen2026-10-25 10:32 UTC892
        let spent = |pages: &HashMap<OwnedRoomId, u16>, room: &OwnedRoomId| {
c9effeac326aatagen2026-10-25 10:32 UTC893
            pages.get(room).copied().unwrap_or(0) >= budget
c9effeac326aatagen2026-10-25 10:32 UTC894
        };
c9effeac326aatagen2026-10-25 10:32 UTC895
        let blocked = |failures: &HashMap<OwnedRoomId, Failure>, room: &OwnedRoomId| {
c9effeac326aatagen2026-10-25 10:32 UTC896
            failures
c9effeac326aatagen2026-10-25 10:32 UTC897
                .get(room)
c9effeac326aatagen2026-10-25 10:32 UTC898
                .is_some_and(|failure| failure.retry_at_ms > now())
c9effeac326aatagen2026-10-25 10:32 UTC899
        };
c9effeac326aatagen2026-10-25 10:32 UTC900
        let mut chosen = None;
c9effeac326aatagen2026-10-25 10:32 UTC901
        while let Some(room_id) = self.heads.pop_front() {
c9effeac326aatagen2026-10-25 10:32 UTC902
            let started = self
c9effeac326aatagen2026-10-25 10:32 UTC903
                .rooms
c9effeac326aatagen2026-10-25 10:32 UTC904
                .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC905
                .is_some_and(|progress| progress.started);
c9effeac326aatagen2026-10-25 10:32 UTC906
            if started && !spent(&self.pages, &room_id) && !blocked(&self.failures, &room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC907
                chosen = Some((room_id, true));
c9effeac326aatagen2026-10-25 10:32 UTC908
                break;
c9effeac326aatagen2026-10-25 10:32 UTC909
            }
c9effeac326aatagen2026-10-25 10:32 UTC910
        }
c9effeac326aatagen2026-10-25 10:32 UTC911
        if chosen.is_none() {
c9effeac326aatagen2026-10-25 10:32 UTC912
            let mut joined: Vec<_> = self
c9effeac326aatagen2026-10-25 10:32 UTC913
                .client
c9effeac326aatagen2026-10-25 10:32 UTC914
                .joined_rooms()
c9effeac326aatagen2026-10-25 10:32 UTC915
                .into_iter()
c9effeac326aatagen2026-10-25 10:32 UTC916
                .map(|room| room.room_id().to_owned())
c9effeac326aatagen2026-10-25 10:32 UTC917
                .collect();
c9effeac326aatagen2026-10-25 10:32 UTC918
            joined.sort();
c9effeac326aatagen2026-10-25 10:32 UTC919
            let count = joined.len();
c9effeac326aatagen2026-10-25 10:32 UTC920
            for offset in 0..count {
c9effeac326aatagen2026-10-25 10:32 UTC921
                let room_id = &joined[(self.turn + offset) % count];
c9effeac326aatagen2026-10-25 10:32 UTC922
                let done = self
c9effeac326aatagen2026-10-25 10:32 UTC923
                    .rooms
c9effeac326aatagen2026-10-25 10:32 UTC924
                    .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC925
                    .is_some_and(|progress| progress.start_reached);
c9effeac326aatagen2026-10-25 10:32 UTC926
                if !done && !spent(&self.pages, room_id) && !blocked(&self.failures, room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC927
                    self.turn = (self.turn + offset + 1) % count;
c9effeac326aatagen2026-10-25 10:32 UTC928
                    chosen = Some((room_id.clone(), false));
c9effeac326aatagen2026-10-25 10:32 UTC929
                    break;
c9effeac326aatagen2026-10-25 10:32 UTC930
                }
c9effeac326aatagen2026-10-25 10:32 UTC931
            }
c9effeac326aatagen2026-10-25 10:32 UTC932
        }
c9effeac326aatagen2026-10-25 10:32 UTC933
        let Some((room_id, head)) = chosen else {
c9effeac326aatagen2026-10-25 10:32 UTC934
            return;
c9effeac326aatagen2026-10-25 10:32 UTC935
        };
c9effeac326aatagen2026-10-25 10:32 UTC936
        let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC937
            return;
c9effeac326aatagen2026-10-25 10:32 UTC938
        };
c9effeac326aatagen2026-10-25 10:32 UTC939
        let from = if head {
c9effeac326aatagen2026-10-25 10:32 UTC940
            self.head_tokens.get(&room_id).cloned()
c9effeac326aatagen2026-10-25 10:32 UTC941
        } else {
c9effeac326aatagen2026-10-25 10:32 UTC942
            self.rooms
c9effeac326aatagen2026-10-25 10:32 UTC943
                .get(room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC944
                .and_then(|progress| progress.tail.clone())
c9effeac326aatagen2026-10-25 10:32 UTC945
        };
c9effeac326aatagen2026-10-25 10:32 UTC946
        *self.pages.entry(room_id.clone()).or_default() += 1;
c9effeac326aatagen2026-10-25 10:32 UTC947
        self.backfilling = Some(room_id.clone());
c9effeac326aatagen2026-10-25 10:32 UTC948
        let mut options = MessagesOptions::backward();
c9effeac326aatagen2026-10-25 10:32 UTC949
        options.from = from;
c9effeac326aatagen2026-10-25 10:32 UTC950
        options.limit = UInt::from(self.budget.page_size);
c9effeac326aatagen2026-10-25 10:32 UTC951
        self.work.push(Box::pin(async move {
c9effeac326aatagen2026-10-25 10:32 UTC952
            let result = room
c9effeac326aatagen2026-10-25 10:32 UTC953
                .messages(options)
c9effeac326aatagen2026-10-25 10:32 UTC954
                .await
c9effeac326aatagen2026-10-25 10:32 UTC955
                .map_err(|error| Error::from_sdk("backfilling search history failed", error));
c9effeac326aatagen2026-10-25 10:32 UTC956
            Work::Backfill {
c9effeac326aatagen2026-10-25 10:32 UTC957
                room: room_id,
c9effeac326aatagen2026-10-25 10:32 UTC958
                head,
c9effeac326aatagen2026-10-25 10:32 UTC959
                result,
c9effeac326aatagen2026-10-25 10:32 UTC960
            }
c9effeac326aatagen2026-10-25 10:32 UTC961
        }));
c9effeac326aatagen2026-10-25 10:32 UTC962
    }
c9effeac326aatagen2026-10-25 10:32 UTC963
c9effeac326aatagen2026-10-25 10:32 UTC964
    fn backfilled(
c9effeac326aatagen2026-10-25 10:32 UTC965
        &mut self,
c9effeac326aatagen2026-10-25 10:32 UTC966
        room_id: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC967
        head: bool,
c9effeac326aatagen2026-10-25 10:32 UTC968
        result: Result<matrix_sdk::room::Messages, Error>,
c9effeac326aatagen2026-10-25 10:32 UTC969
    ) {
c9effeac326aatagen2026-10-25 10:32 UTC970
        self.backfilling = None;
c9effeac326aatagen2026-10-25 10:32 UTC971
        let messages = match result {
c9effeac326aatagen2026-10-25 10:32 UTC972
            Ok(messages) => messages,
c9effeac326aatagen2026-10-25 10:32 UTC973
            Err(error) => {
c9effeac326aatagen2026-10-25 10:32 UTC974
                let retry_at_ms = now() + self.retry_delay;
c9effeac326aatagen2026-10-25 10:32 UTC975
                self.next_backfill = retry_at_ms;
c9effeac326aatagen2026-10-25 10:32 UTC976
                self.retry_delay = (self.retry_delay * 2).min(BACKFILL_RETRY_MAX_MS);
c9effeac326aatagen2026-10-25 10:32 UTC977
                self.failures.insert(
c9effeac326aatagen2026-10-25 10:32 UTC978
                    room_id,
c9effeac326aatagen2026-10-25 10:32 UTC979
                    Failure {
c9effeac326aatagen2026-10-25 10:32 UTC980
                        message: error.message.clone(),
c9effeac326aatagen2026-10-25 10:32 UTC981
                        retry_at_ms,
c9effeac326aatagen2026-10-25 10:32 UTC982
                    },
c9effeac326aatagen2026-10-25 10:32 UTC983
                );
c9effeac326aatagen2026-10-25 10:32 UTC984
                return;
c9effeac326aatagen2026-10-25 10:32 UTC985
            }
c9effeac326aatagen2026-10-25 10:32 UTC986
        };
c9effeac326aatagen2026-10-25 10:32 UTC987
        self.retry_delay = BACKFILL_RETRY_MS;
c9effeac326aatagen2026-10-25 10:32 UTC988
        self.failures.remove(&room_id);
c9effeac326aatagen2026-10-25 10:32 UTC989
        self.next_backfill = now() + self.budget.interval_ms;
c9effeac326aatagen2026-10-25 10:32 UTC990
        let mut overlapped = false;
c9effeac326aatagen2026-10-25 10:32 UTC991
        for event in &messages.chunk {
c9effeac326aatagen2026-10-25 10:32 UTC992
            let Some(value) = value_of(event) else {
c9effeac326aatagen2026-10-25 10:32 UTC993
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC994
            };
c9effeac326aatagen2026-10-25 10:32 UTC995
            if head
c9effeac326aatagen2026-10-25 10:32 UTC996
                && let Some(key) = value.get("event_id").and_then(Value::as_str)
c9effeac326aatagen2026-10-25 10:32 UTC997
                && self.index.get(key).is_some_and(|doc| !doc.provisional)
c9effeac326aatagen2026-10-25 10:32 UTC998
            {
c9effeac326aatagen2026-10-25 10:32 UTC999
                overlapped = true;
c9effeac326aatagen2026-10-25 10:32 UTC1000
            }
c9effeac326aatagen2026-10-25 10:32 UTC1001
            self.observe(&room_id, &value);
c9effeac326aatagen2026-10-25 10:32 UTC1002
        }
c9effeac326aatagen2026-10-25 10:32 UTC1003
        let ended = messages.end.is_none() || messages.chunk.is_empty();
c9effeac326aatagen2026-10-25 10:32 UTC1004
        if head {
c9effeac326aatagen2026-10-25 10:32 UTC1005
            match messages.end {
c9effeac326aatagen2026-10-25 10:32 UTC1006
                Some(end) if !overlapped && !ended => {
c9effeac326aatagen2026-10-25 10:32 UTC1007
                    self.head_tokens.insert(room_id.clone(), end);
c9effeac326aatagen2026-10-25 10:32 UTC1008
                    self.heads.push_front(room_id);
c9effeac326aatagen2026-10-25 10:32 UTC1009
                }
c9effeac326aatagen2026-10-25 10:32 UTC1010
                _ => {
c9effeac326aatagen2026-10-25 10:32 UTC1011
                    self.head_tokens.remove(&room_id);
c9effeac326aatagen2026-10-25 10:32 UTC1012
                }
c9effeac326aatagen2026-10-25 10:32 UTC1013
            }
c9effeac326aatagen2026-10-25 10:32 UTC1014
            return;
c9effeac326aatagen2026-10-25 10:32 UTC1015
        }
c9effeac326aatagen2026-10-25 10:32 UTC1016
        let progress = self.rooms.entry(room_id.to_string()).or_default();
c9effeac326aatagen2026-10-25 10:32 UTC1017
        progress.started = true;
c9effeac326aatagen2026-10-25 10:32 UTC1018
        progress.tail = messages.end;
c9effeac326aatagen2026-10-25 10:32 UTC1019
        if ended {
c9effeac326aatagen2026-10-25 10:32 UTC1020
            progress.start_reached = true;
c9effeac326aatagen2026-10-25 10:32 UTC1021
            progress.tail = None;
c9effeac326aatagen2026-10-25 10:32 UTC1022
        }
c9effeac326aatagen2026-10-25 10:32 UTC1023
        self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC1024
    }
c9effeac326aatagen2026-10-25 10:32 UTC1025
c9effeac326aatagen2026-10-25 10:32 UTC1026
    fn retry_pending(&mut self, room_id: Option<&OwnedRoomId>, session_id: Option<&str>) {
c9effeac326aatagen2026-10-25 10:32 UTC1027
        let due: Vec<_> = self
c9effeac326aatagen2026-10-25 10:32 UTC1028
            .pending
c9effeac326aatagen2026-10-25 10:32 UTC1029
            .iter()
c9effeac326aatagen2026-10-25 10:32 UTC1030
            .filter(|(_, pending)| {
c9effeac326aatagen2026-10-25 10:32 UTC1031
                room_id.is_none_or(|room_id| pending.room == room_id.as_str())
c9effeac326aatagen2026-10-25 10:32 UTC1032
                    && session_id.is_none_or(|session_id| pending.session_id == session_id)
c9effeac326aatagen2026-10-25 10:32 UTC1033
            })
c9effeac326aatagen2026-10-25 10:32 UTC1034
            .map(|(key, pending)| (key.clone(), pending.clone()))
c9effeac326aatagen2026-10-25 10:32 UTC1035
            .collect();
c9effeac326aatagen2026-10-25 10:32 UTC1036
        for (key, pending) in due {
c9effeac326aatagen2026-10-25 10:32 UTC1037
            let Ok(room_id) = OwnedRoomId::try_from(pending.room.as_str()) else {
c9effeac326aatagen2026-10-25 10:32 UTC1038
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC1039
            };
c9effeac326aatagen2026-10-25 10:32 UTC1040
            let Some(room) = self.client.get_room(&room_id) else {
c9effeac326aatagen2026-10-25 10:32 UTC1041
                continue;
c9effeac326aatagen2026-10-25 10:32 UTC1042
            };
c9effeac326aatagen2026-10-25 10:32 UTC1043
            self.work.push(Box::pin(async move {
c9effeac326aatagen2026-10-25 10:32 UTC1044
                let value = async {
c9effeac326aatagen2026-10-25 10:32 UTC1045
                    let raw: Raw<OriginalSyncRoomEncryptedEvent> =
c9effeac326aatagen2026-10-25 10:32 UTC1046
                        Raw::from_json_string(pending.raw).ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC1047
                    let event = room.decrypt_event(&raw, None).await.ok()?;
c9effeac326aatagen2026-10-25 10:32 UTC1048
                    value_of(&event).filter(|value| value["type"] != "m.room.encrypted")
c9effeac326aatagen2026-10-25 10:32 UTC1049
                }
c9effeac326aatagen2026-10-25 10:32 UTC1050
                .await;
c9effeac326aatagen2026-10-25 10:32 UTC1051
                Work::Decrypted {
c9effeac326aatagen2026-10-25 10:32 UTC1052
                    key,
c9effeac326aatagen2026-10-25 10:32 UTC1053
                    room: room_id,
c9effeac326aatagen2026-10-25 10:32 UTC1054
                    value,
c9effeac326aatagen2026-10-25 10:32 UTC1055
                }
c9effeac326aatagen2026-10-25 10:32 UTC1056
            }));
c9effeac326aatagen2026-10-25 10:32 UTC1057
        }
c9effeac326aatagen2026-10-25 10:32 UTC1058
    }
c9effeac326aatagen2026-10-25 10:32 UTC1059
c9effeac326aatagen2026-10-25 10:32 UTC1060
    fn complete(&mut self, work: Work) {
c9effeac326aatagen2026-10-25 10:32 UTC1061
        match work {
c9effeac326aatagen2026-10-25 10:32 UTC1062
            Work::Backfill { room, head, result } => self.backfilled(room, head, result),
c9effeac326aatagen2026-10-25 10:32 UTC1063
            Work::Decrypted { key, room, value } => {
c9effeac326aatagen2026-10-25 10:32 UTC1064
                if let Some(value) = value {
c9effeac326aatagen2026-10-25 10:32 UTC1065
                    self.pending.remove(&key);
c9effeac326aatagen2026-10-25 10:32 UTC1066
                    self.pending_dirty = true;
c9effeac326aatagen2026-10-25 10:32 UTC1067
                    self.observe(&room, &value);
c9effeac326aatagen2026-10-25 10:32 UTC1068
                    self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC1069
                }
c9effeac326aatagen2026-10-25 10:32 UTC1070
            }
c9effeac326aatagen2026-10-25 10:32 UTC1071
            Work::Refetched { room, value } => {
c9effeac326aatagen2026-10-25 10:32 UTC1072
                if let Some(value) = value {
c9effeac326aatagen2026-10-25 10:32 UTC1073
                    self.observe(&room, &value);
c9effeac326aatagen2026-10-25 10:32 UTC1074
                }
c9effeac326aatagen2026-10-25 10:32 UTC1075
            }
c9effeac326aatagen2026-10-25 10:32 UTC1076
        }
c9effeac326aatagen2026-10-25 10:32 UTC1077
    }
c9effeac326aatagen2026-10-25 10:32 UTC1078
c9effeac326aatagen2026-10-25 10:32 UTC1079
    fn search(
c9effeac326aatagen2026-10-25 10:32 UTC1080
        &mut self,
c9effeac326aatagen2026-10-25 10:32 UTC1081
        query: &str,
c9effeac326aatagen2026-10-25 10:32 UTC1082
        rooms: Option<&[String]>,
c9effeac326aatagen2026-10-25 10:32 UTC1083
        before: Option<(u64, u64)>,
c9effeac326aatagen2026-10-25 10:32 UTC1084
        limit: usize,
c9effeac326aatagen2026-10-25 10:32 UTC1085
        coverage: &[OwnedRoomId],
c9effeac326aatagen2026-10-25 10:32 UTC1086
    ) -> Result<RawPage, Error> {
c9effeac326aatagen2026-10-25 10:32 UTC1087
        let (mut hits, terms) = self.index.search(query, rooms, before, limit + 1)?;
c9effeac326aatagen2026-10-25 10:32 UTC1088
        let more = hits.len() > limit;
c9effeac326aatagen2026-10-25 10:32 UTC1089
        hits.truncate(limit);
c9effeac326aatagen2026-10-25 10:32 UTC1090
        Ok(RawPage {
c9effeac326aatagen2026-10-25 10:32 UTC1091
            hits,
c9effeac326aatagen2026-10-25 10:32 UTC1092
            terms,
c9effeac326aatagen2026-10-25 10:32 UTC1093
            more,
c9effeac326aatagen2026-10-25 10:32 UTC1094
            coverage: self.coverage(coverage),
c9effeac326aatagen2026-10-25 10:32 UTC1095
        })
c9effeac326aatagen2026-10-25 10:32 UTC1096
    }
c9effeac326aatagen2026-10-25 10:32 UTC1097
c9effeac326aatagen2026-10-25 10:32 UTC1098
    pub(crate) async fn run(mut self) {
c9effeac326aatagen2026-10-25 10:32 UTC1099
        self.load().await;
c9effeac326aatagen2026-10-25 10:32 UTC1100
        let client = self.client.clone();
c9effeac326aatagen2026-10-25 10:32 UTC1101
        let mut generic = client.event_cache().subscribe_to_room_generic_updates();
c9effeac326aatagen2026-10-25 10:32 UTC1102
        let mut keys = client
c9effeac326aatagen2026-10-25 10:32 UTC1103
            .encryption()
c9effeac326aatagen2026-10-25 10:32 UTC1104
            .room_keys_received_stream()
c9effeac326aatagen2026-10-25 10:32 UTC1105
            .await
c9effeac326aatagen2026-10-25 10:32 UTC1106
            .map(Box::pin);
c9effeac326aatagen2026-10-25 10:32 UTC1107
        let mut room_info = client.room_info_notable_update_receiver();
c9effeac326aatagen2026-10-25 10:32 UTC1108
        self.retry_pending(None, None);
c9effeac326aatagen2026-10-25 10:32 UTC1109
        let mut tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
c9effeac326aatagen2026-10-25 10:32 UTC1110
        loop {
c9effeac326aatagen2026-10-25 10:32 UTC1111
            self.schedule_backfill();
c9effeac326aatagen2026-10-25 10:32 UTC1112
            tokio::select! {
c9effeac326aatagen2026-10-25 10:32 UTC1113
                biased;
c9effeac326aatagen2026-10-25 10:32 UTC1114
                request = self.requests.recv() => match request {
c9effeac326aatagen2026-10-25 10:32 UTC1115
                    Some(Request::Stop(reply)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1116
                        if self.dirty_since.is_some() || self.index.buffered() > 0 {
c9effeac326aatagen2026-10-25 10:32 UTC1117
                            self.commit().await;
c9effeac326aatagen2026-10-25 10:32 UTC1118
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1119
                        for (abort, _) in self.subscriptions.values() {
c9effeac326aatagen2026-10-25 10:32 UTC1120
                            abort.abort();
c9effeac326aatagen2026-10-25 10:32 UTC1121
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1122
                        let _ = reply.send(());
c9effeac326aatagen2026-10-25 10:32 UTC1123
                        return;
c9effeac326aatagen2026-10-25 10:32 UTC1124
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1125
                    Some(Request::Search { query, rooms, before, limit, coverage, reply }) => {
c9effeac326aatagen2026-10-25 10:32 UTC1126
                        let _ = reply.send(self.search(&query, rooms.as_deref(), before, limit, &coverage));
c9effeac326aatagen2026-10-25 10:32 UTC1127
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1128
                    Some(Request::Coverage(rooms, reply)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1129
                        let _ = reply.send(self.coverage(&rooms));
c9effeac326aatagen2026-10-25 10:32 UTC1130
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1131
                    Some(Request::Forget(key)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1132
                        let observed = Observed::Redacted { key };
c9effeac326aatagen2026-10-25 10:32 UTC1133
                        if feed::apply(self.index.as_mut(), &mut self.redactions, "", observed) == Effect::Removed {
c9effeac326aatagen2026-10-25 10:32 UTC1134
                            self.touch();
c9effeac326aatagen2026-10-25 10:32 UTC1135
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1136
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1137
                    None => return,
c9effeac326aatagen2026-10-25 10:32 UTC1138
                },
c9effeac326aatagen2026-10-25 10:32 UTC1139
                Some(work) = self.work.next(), if !self.work.is_empty() => self.complete(work),
c9effeac326aatagen2026-10-25 10:32 UTC1140
                Some((room_id, events)) = self.updates.next(), if !self.updates.is_empty() => {
c9effeac326aatagen2026-10-25 10:32 UTC1141
                    if let Some((_, seen)) = self.subscriptions.get_mut(&room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC1142
                        *seen = now();
c9effeac326aatagen2026-10-25 10:32 UTC1143
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1144
                    match events {
c9effeac326aatagen2026-10-25 10:32 UTC1145
                        Some(events) => self.observe_all(&room_id, &events),
c9effeac326aatagen2026-10-25 10:32 UTC1146
                        None => {
c9effeac326aatagen2026-10-25 10:32 UTC1147
                            if let Some((abort, _)) = self.subscriptions.remove(&room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC1148
                                abort.abort();
c9effeac326aatagen2026-10-25 10:32 UTC1149
                            }
c9effeac326aatagen2026-10-25 10:32 UTC1150
                            self.subscribe(room_id).await;
c9effeac326aatagen2026-10-25 10:32 UTC1151
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1152
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1153
                }
c9effeac326aatagen2026-10-25 10:32 UTC1154
                update = generic.recv() => match update {
c9effeac326aatagen2026-10-25 10:32 UTC1155
                    Ok(update) => self.subscribe(update.room_id).await,
c9effeac326aatagen2026-10-25 10:32 UTC1156
                    Err(broadcast::error::RecvError::Lagged(_)) => {}
c9effeac326aatagen2026-10-25 10:32 UTC1157
                    Err(broadcast::error::RecvError::Closed) => return,
c9effeac326aatagen2026-10-25 10:32 UTC1158
                },
c9effeac326aatagen2026-10-25 10:32 UTC1159
                Some(update) = async { keys.as_mut()?.next().await }, if keys.is_some() => match update {
c9effeac326aatagen2026-10-25 10:32 UTC1160
                    Ok(infos) => {
c9effeac326aatagen2026-10-25 10:32 UTC1161
                        for info in infos {
c9effeac326aatagen2026-10-25 10:32 UTC1162
                            self.retry_pending(Some(&info.room_id), Some(&info.session_id));
c9effeac326aatagen2026-10-25 10:32 UTC1163
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1164
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1165
                    Err(_) => self.retry_pending(None, None),
c9effeac326aatagen2026-10-25 10:32 UTC1166
                },
c9effeac326aatagen2026-10-25 10:32 UTC1167
                update = room_info.recv() => match update {
c9effeac326aatagen2026-10-25 10:32 UTC1168
                    Ok(update) if update.reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP) => {
c9effeac326aatagen2026-10-25 10:32 UTC1169
                        let left = self.client.get_room(&update.room_id).is_none_or(|room| room.state() != RoomState::Joined);
c9effeac326aatagen2026-10-25 10:32 UTC1170
                        if left && let Some((abort, _)) = self.subscriptions.remove(&update.room_id) {
c9effeac326aatagen2026-10-25 10:32 UTC1171
                            abort.abort();
c9effeac326aatagen2026-10-25 10:32 UTC1172
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1173
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1174
                    Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
c9effeac326aatagen2026-10-25 10:32 UTC1175
                    Err(broadcast::error::RecvError::Closed) => return,
c9effeac326aatagen2026-10-25 10:32 UTC1176
                },
c9effeac326aatagen2026-10-25 10:32 UTC1177
                () = &mut tick => {
c9effeac326aatagen2026-10-25 10:32 UTC1178
                    tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
c9effeac326aatagen2026-10-25 10:32 UTC1179
                    self.drop_idle();
c9effeac326aatagen2026-10-25 10:32 UTC1180
                    let due = self.dirty_since.is_some_and(|since| now() >= since + COMMIT_AFTER_MS)
c9effeac326aatagen2026-10-25 10:32 UTC1181
                        || self.index.buffered() >= COMMIT_BUFFERED;
c9effeac326aatagen2026-10-25 10:32 UTC1182
                    if due && now() >= self.commit_after {
c9effeac326aatagen2026-10-25 10:32 UTC1183
                        self.commit().await;
c9effeac326aatagen2026-10-25 10:32 UTC1184
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1185
                }
c9effeac326aatagen2026-10-25 10:32 UTC1186
            }
c9effeac326aatagen2026-10-25 10:32 UTC1187
        }
c9effeac326aatagen2026-10-25 10:32 UTC1188
    }
c9effeac326aatagen2026-10-25 10:32 UTC1189
}
c9effeac326aatagen2026-10-25 10:32 UTC1190
c9effeac326aatagen2026-10-25 10:32 UTC1191
fn updates(
c9effeac326aatagen2026-10-25 10:32 UTC1192
    room_id: OwnedRoomId,
c9effeac326aatagen2026-10-25 10:32 UTC1193
    subscriber: RoomEventCacheSubscriber,
c9effeac326aatagen2026-10-25 10:32 UTC1194
    drop_handles: Arc<matrix_sdk::event_cache::EventCacheDropHandles>,
c9effeac326aatagen2026-10-25 10:32 UTC1195
) -> BoxStream<(OwnedRoomId, Option<Vec<SdkTimelineEvent>>)> {
c9effeac326aatagen2026-10-25 10:32 UTC1196
    Box::pin(futures_util::stream::unfold(
c9effeac326aatagen2026-10-25 10:32 UTC1197
        (subscriber, drop_handles),
c9effeac326aatagen2026-10-25 10:32 UTC1198
        move |(mut subscriber, drop_handles)| {
c9effeac326aatagen2026-10-25 10:32 UTC1199
            let room_id = room_id.clone();
c9effeac326aatagen2026-10-25 10:32 UTC1200
            async move {
c9effeac326aatagen2026-10-25 10:32 UTC1201
                loop {
c9effeac326aatagen2026-10-25 10:32 UTC1202
                    match subscriber.recv().await {
c9effeac326aatagen2026-10-25 10:32 UTC1203
                        Ok(RoomEventCacheUpdate::UpdateTimelineEvents(update)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1204
                            let events = update.diffs.into_iter().flat_map(added).collect();
c9effeac326aatagen2026-10-25 10:32 UTC1205
                            return Some(((room_id, Some(events)), (subscriber, drop_handles)));
c9effeac326aatagen2026-10-25 10:32 UTC1206
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1207
                        Ok(_) => {}
c9effeac326aatagen2026-10-25 10:32 UTC1208
                        Err(broadcast::error::RecvError::Lagged(_)) => {
c9effeac326aatagen2026-10-25 10:32 UTC1209
                            return Some(((room_id, None), (subscriber, drop_handles)));
c9effeac326aatagen2026-10-25 10:32 UTC1210
                        }
c9effeac326aatagen2026-10-25 10:32 UTC1211
                        Err(broadcast::error::RecvError::Closed) => return None,
c9effeac326aatagen2026-10-25 10:32 UTC1212
                    }
c9effeac326aatagen2026-10-25 10:32 UTC1213
                }
c9effeac326aatagen2026-10-25 10:32 UTC1214
            }
c9effeac326aatagen2026-10-25 10:32 UTC1215
        },
c9effeac326aatagen2026-10-25 10:32 UTC1216
    ))
c9effeac326aatagen2026-10-25 10:32 UTC1217
}
c9effeac326aatagen2026-10-25 10:32 UTC1218
c9effeac326aatagen2026-10-25 10:32 UTC1219
fn added(diff: VectorDiff<SdkTimelineEvent>) -> Vec<SdkTimelineEvent> {
c9effeac326aatagen2026-10-25 10:32 UTC1220
    match diff {
c9effeac326aatagen2026-10-25 10:32 UTC1221
        VectorDiff::Append { values } | VectorDiff::Reset { values } => {
c9effeac326aatagen2026-10-25 10:32 UTC1222
            values.into_iter().collect()
c9effeac326aatagen2026-10-25 10:32 UTC1223
        }
c9effeac326aatagen2026-10-25 10:32 UTC1224
        VectorDiff::PushFront { value }
c9effeac326aatagen2026-10-25 10:32 UTC1225
        | VectorDiff::PushBack { value }
c9effeac326aatagen2026-10-25 10:32 UTC1226
        | VectorDiff::Insert { value, .. }
c9effeac326aatagen2026-10-25 10:32 UTC1227
        | VectorDiff::Set { value, .. } => vec![value],
c9effeac326aatagen2026-10-25 10:32 UTC1228
        VectorDiff::Clear
c9effeac326aatagen2026-10-25 10:32 UTC1229
        | VectorDiff::PopFront
c9effeac326aatagen2026-10-25 10:32 UTC1230
        | VectorDiff::PopBack
c9effeac326aatagen2026-10-25 10:32 UTC1231
        | VectorDiff::Remove { .. }
c9effeac326aatagen2026-10-25 10:32 UTC1232
        | VectorDiff::Truncate { .. } => Vec::new(),
c9effeac326aatagen2026-10-25 10:32 UTC1233
    }
c9effeac326aatagen2026-10-25 10:32 UTC1234
}