atagen

macltok

public
Star0
Watch0
aced11e80b47de7a887296b10eedff310df5360f
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/search/mod.rsRust1559 lines53.1 KB
1
mod feed;
2
#[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
3
mod index;
4
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
5
pub(crate) mod store;
6
mod text;
7
8
use std::{
9
    collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
10
    sync::Arc,
11
    time::Duration,
12
};
13
14
use futures_util::{
15
    StreamExt,
16
    future::Abortable,
17
    stream::{AbortHandle, FuturesUnordered, SelectAll, abortable},
18
};
19
use matrix_sdk::{
20
    Client, Room, RoomState, SendOutsideWasm,
21
    deserialized_responses::TimelineEvent as SdkTimelineEvent,
22
    event_cache::{RoomEventCacheSubscriber, RoomEventCacheUpdate},
23
    room::{Messages, MessagesOptions},
24
};
25
use matrix_sdk_base::RoomInfoNotableUpdateReasons;
26
use matrix_sdk_ui::eyeball_im::VectorDiff;
27
use ruma::{
28
    OwnedRoomId, OwnedUserId, UInt,
29
    api::client::filter::RoomEventFilter,
30
    events::{
31
        ignored_user_list::IgnoredUserListEventContent, relation::RelationType,
32
        room::encrypted::OriginalSyncRoomEncryptedEvent,
33
    },
34
    serde::Raw,
35
};
36
use serde_json::Value;
37
use sha2::{Digest, Sha256};
38
use tokio::sync::{Mutex, broadcast, mpsc, oneshot};
39
40
use self::{
41
    feed::{Effect, Observed, Redactions},
42
    index::{Changes, Doc, Hit, KeywordIndex},
43
};
44
use crate::{
45
    CommandOutput, EngineConfig, Error, ErrorKind, Event, RoomSearchCoverage, SearchBackfill,
46
    SearchCloseReason, SearchCommand, SearchCoverage, SearchHit, SearchInMemory, SearchMatch,
47
    SearchPage, SearchScope, SearchSession,
48
    engine::{MediaService, SearchStore, context, executor::EngineBoxFuture},
49
    normalise::{MediaRegistration, MediaScope},
50
    transport::EventSender,
51
};
52
53
const MAX_LIMIT: u16 = 50;
54
/// a consumer that never closes its searches still holds at most this many
55
const MAX_SEARCHES: usize = 8;
56
/// hits read back at once while a page is put together
57
const PREVIEWS_AT_ONCE: usize = 8;
58
const DECRYPTS_AT_ONCE: usize = 8;
59
const COMMIT_AFTER_MS: u64 = 5_000;
60
const COMMIT_BUFFERED: usize = 2_000;
61
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
62
const COMMIT_RETRY_MS: u64 = 60_000;
63
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
64
const COMMIT_ATTEMPTS: u32 = 5;
65
const IDLE_SUBSCRIPTION_MS: u64 = 5 * 60_000;
66
const BACKFILL_RETRY_MS: u64 = 60_000;
67
const BACKFILL_RETRY_MAX_MS: u64 = 15 * 60_000;
68
#[cfg(any(feature = "sqlite", feature = "indexeddb"))]
69
const SCHEMA: u64 = 2;
70
/// waiting undecryptable events are saved in this many blobs, so one arriving rewrites a sixteenth
71
const UTD_SHARDS: u8 = 16;
72
const BACKFILL_TYPES: [&str; 6] = [
73
    "m.room.message",
74
    "m.room.encrypted",
75
    "m.room.redaction",
76
    "m.sticker",
77
    "m.poll.start",
78
    "org.matrix.msc3381.poll.start",
79
];
80
81
#[cfg(not(target_family = "wasm"))]
82
type BoxStream<T> = futures_util::stream::BoxStream<'static, T>;
83
#[cfg(target_family = "wasm")]
84
type BoxStream<T> = futures_util::stream::LocalBoxStream<'static, T>;
85
86
/// a local index of message text. keyword search over tantivy is the only one today, an
87
/// embedding index can stand beside it later without the public api noticing
88
#[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
89
pub(crate) trait MessageIndex: SendOutsideWasm {
90
    fn get(&self, key: &str) -> Option<Doc>;
91
    fn by_content(&self, content: &str) -> Option<Doc>;
92
    /// replaces whatever `doc.key` held
93
    fn put(&mut self, doc: Doc);
94
    fn remove(&mut self, key: &str) -> bool;
95
    /// every provisional doc waiting on `target`, whoever sent its edit
96
    fn remove_provisional(&mut self, target: &str) -> bool;
97
    fn buffered(&self) -> usize;
98
    /// newest first, strictly older than `before`. also hands back the analysed query terms
99
    fn search(
100
        &mut self,
101
        query: &str,
102
        rooms: Option<&[String]>,
103
        before: Option<(u64, u64)>,
104
        limit: usize,
105
    ) -> Result<(Vec<Hit>, Vec<String>), Error>;
106
    fn maintain(&mut self) -> Result<(), Error>;
107
    fn changes(&mut self) -> Result<Changes, Error>;
108
    fn committed(&mut self, changes: &Changes);
109
    /// stops tracking what a commit would need, once nothing will be committed again
110
    fn detach(&mut self);
111
}
112
113
#[derive(Clone, Debug, Default)]
114
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
115
struct RoomProgress {
116
    oldest: Option<u64>,
117
    newest: Option<u64>,
118
    /// where the next backfill page starts, `None` before the first one
119
    tail: Option<String>,
120
    started: bool,
121
    start_reached: bool,
122
}
123
124
#[derive(Clone, Debug)]
125
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126
struct Pending {
127
    room: String,
128
    session_id: String,
129
    ts: u64,
130
    raw: String,
131
}
132
133
#[derive(Clone, Copy)]
134
struct Budget {
135
    pages_per_room: u16,
136
    page_size: u16,
137
    interval_ms: u64,
138
    max_pending: usize,
139
}
140
141
impl Budget {
142
    fn from_config(config: &EngineConfig) -> Self {
143
        Self {
144
            pages_per_room: config.search_backfill_pages_per_room,
145
            page_size: config.search_backfill_page_size.max(1),
146
            interval_ms: config.search_backfill_interval_ms,
147
            max_pending: config.max_search_undecrypted,
148
        }
149
    }
150
}
151
152
struct RawPage {
153
    hits: Vec<Hit>,
154
    terms: Vec<String>,
155
    more: bool,
156
    coverage: SearchCoverage,
157
}
158
159
enum Request {
160
    Search {
161
        query: String,
162
        rooms: Option<Vec<String>>,
163
        before: Option<(u64, u64)>,
164
        limit: usize,
165
        coverage: Vec<OwnedRoomId>,
166
        reply: oneshot::Sender<Result<RawPage, Error>>,
167
    },
168
    Coverage(Vec<OwnedRoomId>, oneshot::Sender<SearchCoverage>),
169
    /// a hit that turned out redacted when its preview was read
170
    Forget(String),
171
    Stop(oneshot::Sender<()>),
172
}
173
174
#[derive(Clone)]
175
pub(crate) struct Handle {
176
    requests: mpsc::Sender<Request>,
177
    client: Client,
178
    media: Arc<MediaService>,
179
    sessions: Arc<Sessions>,
180
}
181
182
enum Work {
183
    Backfill {
184
        room: OwnedRoomId,
185
        head: bool,
186
        from: Option<String>,
187
        result: Result<Messages, Error>,
188
    },
189
    /// the room was left while its page was in flight
190
    BackfillCancelled,
191
    Decrypted {
192
        key: String,
193
        room: OwnedRoomId,
194
        value: Option<Value>,
195
    },
196
    /// the original first, then whatever edits of it the cache or server still has
197
    Refetched {
198
        room: OwnedRoomId,
199
        values: Vec<Value>,
200
    },
201
}
202
203
enum RoomUpdate {
204
    Events(Vec<SdkTimelineEvent>),
205
    Lagged,
206
    Closed,
207
}
208
209
struct Failure {
210
    message: String,
211
    retry_at_ms: u64,
212
    delay_ms: u64,
213
}
214
215
enum Persistence {
216
    Store(SearchStore),
217
    Memory(SearchInMemory),
218
}
219
220
pub(crate) struct Service {
221
    client: Client,
222
    index: Box<dyn MessageIndex>,
223
    persistence: Persistence,
224
    #[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
225
    commit_failures: u32,
226
    /// storage keys a load found nothing pointing at, dropped with the next commit
227
    #[cfg_attr(not(any(feature = "sqlite", feature = "indexeddb")), allow(dead_code))]
228
    orphans: Vec<String>,
229
    rooms: BTreeMap<String, RoomProgress>,
230
    /// each room's newest indexed message when this engine started, what a head pass looks for
231
    baselines: HashMap<String, u64>,
232
    pending: BTreeMap<String, Pending>,
233
    dirty_shards: BTreeSet<u8>,
234
    decrypt_queue: VecDeque<String>,
235
    decrypting: usize,
236
    redactions: Redactions,
237
    dirty_since: Option<u64>,
238
    commit_after: u64,
239
    budget: Budget,
240
    requests: mpsc::Receiver<Request>,
241
    subscriptions: HashMap<OwnedRoomId, (AbortHandle, u64)>,
242
    updates: SelectAll<BoxStream<(OwnedRoomId, RoomUpdate)>>,
243
    work: FuturesUnordered<EngineBoxFuture<'static, Work>>,
244
    pages: HashMap<OwnedRoomId, u16>,
245
    heads: VecDeque<OwnedRoomId>,
246
    headed: HashSet<OwnedRoomId>,
247
    head_tokens: HashMap<OwnedRoomId, String>,
248
    backfilling: Option<(OwnedRoomId, AbortHandle)>,
249
    next_backfill: u64,
250
    failures: HashMap<OwnedRoomId, Failure>,
251
    turn: usize,
252
}
253
254
fn now() -> u64 {
255
    ruma::MilliSecondsSinceUnixEpoch::now().get().into()
256
}
257
258
fn value_of(event: &SdkTimelineEvent) -> Option<Value> {
259
    serde_json::from_str(event.raw().json().get()).ok()
260
}
261
262
fn encode_cursor((ts, tie): (u64, u64)) -> String {
263
    format!("{ts:x}.{tie:x}")
264
}
265
266
fn decode_cursor(cursor: &str) -> Option<(u64, u64)> {
267
    let (ts, tie) = cursor.split_once('.')?;
268
    Some((
269
        u64::from_str_radix(ts, 16).ok()?,
270
        u64::from_str_radix(tie, 16).ok()?,
271
    ))
272
}
273
274
fn shard(key: &str) -> u8 {
275
    Sha256::digest(key.as_bytes())[0] % UTD_SHARDS
276
}
277
278
/// a 400 to a page with a saved token means the server no longer takes the token
279
fn token_rejected(error: &Error) -> bool {
280
    error
281
        .server
282
        .as_ref()
283
        .is_some_and(|server| server.status == 400 || server.errcode == "M_INVALID_PARAM")
284
}
285
286
impl Handle {
287
    pub(crate) async fn execute(&self, command: SearchCommand) -> Result<CommandOutput, Error> {
288
        match command {
289
            SearchCommand::Search {
290
                query,
291
                scope,
292
                limit,
293
            } => {
294
                let media_scope = self.media.context_scope();
295
                let prefix = media_prefix(&media_scope);
296
                let (page, media) = self.search(&query, &scope, None, limit, &prefix).await?;
297
                let search_id = self
298
                    .sessions
299
                    .open(&self.media, query, scope, media_scope, media)
300
                    .await;
301
                Ok(CommandOutput::SearchSession(SearchSession {
302
                    search_id,
303
                    page,
304
                }))
305
            }
306
            SearchCommand::More {
307
                search_id,
308
                cursor,
309
                limit,
310
            } => {
311
                let (query, scope, prefix) = self.sessions.get(&search_id).await?;
312
                let (page, media) = self
313
                    .search(&query, &scope, Some(cursor.as_str()), limit, &prefix)
314
                    .await?;
315
                self.sessions
316
                    .register(&self.media, &search_id, media)
317
                    .await?;
318
                Ok(CommandOutput::SearchPage(page))
319
            }
320
            SearchCommand::Close { search_id } => {
321
                self.sessions.close(&self.media, &search_id).await?;
322
                Ok(CommandOutput::Done)
323
            }
324
            SearchCommand::Coverage(scope) => {
325
                let (reply, response) = oneshot::channel();
326
                self.requests
327
                    .send(Request::Coverage(self.scope_rooms(&scope), reply))
328
                    .await
329
                    .map_err(|_| Error::interrupted())?;
330
                Ok(CommandOutput::SearchCoverage(
331
                    response.await.map_err(|_| Error::interrupted())?,
332
                ))
333
            }
334
        }
335
    }
336
337
    /// the media service is cleared beside this, so nothing is released one by one
338
    pub(crate) async fn stop(&self) {
339
        let (reply, response) = oneshot::channel();
340
        if self.requests.send(Request::Stop(reply)).await.is_ok() {
341
            let _ = response.await;
342
        }
343
        self.sessions.open.lock().await.clear();
344
    }
345
346
    fn scope_rooms(&self, scope: &SearchScope) -> Vec<OwnedRoomId> {
347
        match scope {
348
            SearchScope::Room(room_id) => vec![room_id.clone()],
349
            SearchScope::Rooms(room_ids) => room_ids.clone(),
350
            SearchScope::All => self
351
                .client
352
                .joined_rooms()
353
                .into_iter()
354
                .map(|room| room.room_id().to_owned())
355
                .collect(),
356
        }
357
    }
358
359
    async fn search(
360
        &self,
361
        query: &str,
362
        scope: &SearchScope,
363
        cursor: Option<&str>,
364
        limit: u16,
365
        media_prefix: &str,
366
    ) -> Result<(SearchPage, Vec<MediaRegistration>), Error> {
367
        if limit == 0 || limit > MAX_LIMIT {
368
            return Err(Error::new(
369
                ErrorKind::Invalid,
370
                "search limit must be between 1 and 50",
371
            ));
372
        }
373
        let before = match cursor {
374
            Some(cursor) => Some(
375
                decode_cursor(cursor)
376
                    .ok_or_else(|| Error::new(ErrorKind::Invalid, "search cursor is invalid"))?,
377
            ),
378
            None => None,
379
        };
380
        let rooms = self.scope_rooms(scope);
381
        let filter = Some(rooms.iter().map(ToString::to_string).collect());
382
        let (reply, response) = oneshot::channel();
383
        self.requests
384
            .send(Request::Search {
385
                query: query.to_owned(),
386
                rooms: filter,
387
                before,
388
                limit: usize::from(limit),
389
                coverage: rooms,
390
                reply,
391
            })
392
            .await
393
            .map_err(|_| Error::interrupted())?;
394
        let raw = response.await.map_err(|_| Error::interrupted())??;
395
        let next_cursor = raw
396
            .more
397
            .then(|| raw.hits.last().map(|hit| encode_cursor((hit.ts, hit.tie))))
398
            .flatten();
399
        let ignored = self.ignored_users().await;
400
        let current_user_id = self.client.user_id().map(ToOwned::to_owned);
401
        let terms = &raw.terms;
402
        let previews: Vec<_> = futures_util::stream::iter(
403
            raw.hits
404
                .into_iter()
405
                .filter(|hit| !ignored.iter().any(|user| user.as_str() == hit.sender)),
406
        )
407
        .map(|hit| self.preview(hit, current_user_id.clone(), terms, media_prefix))
408
        .buffered(PREVIEWS_AT_ONCE)
409
        .collect()
410
        .await;
411
        let mut hits = Vec::new();
412
        let mut media = Vec::new();
413
        for preview in previews {
414
            match preview {
415
                Preview::Shown(hit, registrations) => {
416
                    hits.push(hit);
417
                    media.extend(registrations);
418
                }
419
                Preview::Redacted(key) => {
420
                    let _ = self.requests.send(Request::Forget(key)).await;
421
                }
422
                Preview::Unavailable => {}
423
            }
424
        }
425
        let page = SearchPage {
426
            hits,
427
            next_cursor,
428
            coverage: raw.coverage,
429
        };
430
        Ok((page, media))
431
    }
432
433
    async fn ignored_users(&self) -> Vec<OwnedUserId> {
434
        self.client
435
            .account()
436
            .account_data::<IgnoredUserListEventContent>()
437
            .await
438
            .ok()
439
            .flatten()
440
            .and_then(|raw| raw.deserialize().ok())
441
            .map(|content| content.ignored_users.into_keys().collect())
442
            .unwrap_or_default()
443
    }
444
445
    async fn preview(
446
        &self,
447
        hit: Hit,
448
        current_user_id: Option<OwnedUserId>,
449
        terms: &[String],
450
        media_prefix: &str,
451
    ) -> Preview {
452
        let (Ok(room_id), Ok(event_id)) = (
453
            OwnedRoomId::try_from(hit.room.as_str()),
454
            ruma::OwnedEventId::try_from(hit.key.as_str()),
455
        ) else {
456
            return Preview::Unavailable;
457
        };
458
        let Some(room) = self.client.get_room(&room_id) else {
459
            return Preview::Unavailable;
460
        };
461
        let Some((mut original, highlight)) = load(&room, &hit.key).await else {
462
            return Preview::Unavailable;
463
        };
464
        if original.pointer("/unsigned/redacted_because").is_some() {
465
            return Preview::Redacted(hit.key);
466
        }
467
        // the index already settled which edit is newest, and a bundled one can be older
468
        if let Some(relations) = original
469
            .pointer_mut("/unsigned/m.relations")
470
            .and_then(Value::as_object_mut)
471
        {
472
            relations.remove("m.replace");
473
        }
474
        let mut values = vec![original];
475
        if hit.content != hit.key
476
            && let Some((edit, _)) = load(&room, &hit.content).await
477
        {
478
            values.push(edit);
479
        }
480
        let Some((event, registrations)) = context::preview(
481
            &room,
482
            current_user_id.as_deref(),
483
            &values,
484
            highlight,
485
            media_prefix,
486
        )
487
        .await
488
        else {
489
            return Preview::Unavailable;
490
        };
491
        let text = match &event.content {
492
            crate::TimelineContent::Message(message) => Some(message.body.as_str()),
493
            crate::TimelineContent::Sticker { body, .. } => Some(body.as_str()),
494
            crate::TimelineContent::Poll { question } => Some(question.as_str()),
495
            _ => None,
496
        };
497
        let matches = text
498
            .map(|text| text::match_ranges(text, terms))
499
            .unwrap_or_default()
500
            .into_iter()
501
            .filter_map(|(start, end)| {
502
                Some(SearchMatch {
503
                    start: u32::try_from(start).ok()?,
504
                    end: u32::try_from(end).ok()?,
505
                })
506
            })
507
            .collect();
508
        Preview::Shown(
509
            SearchHit {
510
                room_id,
511
                event_id,
512
                timestamp_ms: event.timestamp_ms,
513
                event,
514
                matches,
515
            },
516
            registrations,
517
        )
518
    }
519
}
520
521
enum Preview {
522
    Shown(SearchHit, Vec<MediaRegistration>),
523
    Redacted(String),
524
    Unavailable,
525
}
526
527
/// open searches, oldest first, each owning the media of the hits it handed out
528
struct Sessions {
529
    open: Mutex<VecDeque<Session>>,
530
    events: EventSender,
531
}
532
533
/// media ids carry the search, so two owners of one event never share an entry
534
fn media_prefix(scope: &MediaScope) -> String {
535
    format!("{}#", scope.nonce())
536
}
537
538
struct Session {
539
    id: String,
540
    query: String,
541
    scope: SearchScope,
542
    media: MediaScope,
543
}
544
545
impl Sessions {
546
    async fn open(
547
        &self,
548
        media: &MediaService,
549
        query: String,
550
        scope: SearchScope,
551
        media_scope: MediaScope,
552
        registrations: Vec<MediaRegistration>,
553
    ) -> String {
554
        let id = format!("search-{}", media_scope.nonce());
555
        let mut evicted = Vec::new();
556
        {
557
            let mut open = self.open.lock().await;
558
            media.register(&media_scope, registrations).await;
559
            open.push_back(Session {
560
                id: id.clone(),
561
                query,
562
                scope,
563
                media: media_scope,
564
            });
565
            while open.len() > MAX_SEARCHES {
566
                if let Some(oldest) = open.pop_front() {
567
                    media.release(&oldest.media).await;
568
                    evicted.push(oldest.id);
569
                }
570
            }
571
        }
572
        for search_id in evicted {
573
            let event = Event::SearchClosed {
574
                search_id,
575
                reason: SearchCloseReason::Evicted,
576
            };
577
            if self.events.send(event).await.is_err() {
578
                break;
579
            }
580
        }
581
        id
582
    }
583
584
    /// the query, scope and media prefix the search's pages use
585
    async fn get(&self, id: &str) -> Result<(String, SearchScope, String), Error> {
586
        let open = self.open.lock().await;
587
        let session = open
588
            .iter()
589
            .find(|session| session.id == id)
590
            .ok_or_else(not_open)?;
591
        Ok((
592
            session.query.clone(),
593
            session.scope.clone(),
594
            media_prefix(&session.media),
595
        ))
596
    }
597
598
    /// under the lock, so a page that lands after its close registers nothing
599
    async fn register(
600
        &self,
601
        media: &MediaService,
602
        id: &str,
603
        registrations: Vec<MediaRegistration>,
604
    ) -> Result<(), Error> {
605
        let open = self.open.lock().await;
606
        let session = open
607
            .iter()
608
            .find(|session| session.id == id)
609
            .ok_or_else(not_open)?;
610
        media.register(&session.media, registrations).await;
611
        Ok(())
612
    }
613
614
    async fn close(&self, media: &MediaService, id: &str) -> Result<(), Error> {
615
        let mut open = self.open.lock().await;
616
        let index = open
617
            .iter()
618
            .position(|session| session.id == id)
619
            .ok_or_else(not_open)?;
620
        if let Some(session) = open.remove(index) {
621
            media.release(&session.media).await;
622
        }
623
        Ok(())
624
    }
625
}
626
627
fn not_open() -> Error {
628
    Error::new(
629
        ErrorKind::SearchNotOpen,
630
        "search is closed or was never opened",
631
    )
632
}
633
634
async fn load(room: &Room, event_id: &str) -> Option<(Value, bool)> {
635
    let event_id = ruma::EventId::parse(event_id).ok()?;
636
    let event = room
637
        .load_or_fetch_event(&event_id, None)
638
        .await
639
        .inspect_err(|error| tracing::debug!(%event_id, %error, "loading a search hit failed"))
640
        .ok()?;
641
    let highlight = event
642
        .push_actions()
643
        .is_some_and(|actions| actions.iter().any(ruma::push::Action::is_highlight));
644
    Some((value_of(&event)?, highlight))
645
}
646
647
/// the original and every edit of it still around, so a redacted latest edit falls back to
648
/// the one before it rather than to the original
649
async fn load_with_edits(room: &Room, event_id: &str) -> Vec<Value> {
650
    let Ok(event_id) = ruma::EventId::parse(event_id) else {
651
        return Vec::new();
652
    };
653
    match room
654
        .load_or_fetch_event_with_relations(&event_id, Some(vec![RelationType::Replacement]), None)
655
        .await
656
    {
657
        Ok((original, edits)) => std::iter::once(original)
658
            .chain(edits)
659
            .filter_map(|event| value_of(&event))
660
            .collect(),
661
        Err(error) => {
662
            tracing::debug!(%event_id, %error, "reading an original again failed");
663
            Vec::new()
664
        }
665
    }
666
}
667
668
impl Service {
669
    pub(crate) fn new(
670
        client: Client,
671
        media: Arc<MediaService>,
672
        store: Option<Result<SearchStore, Error>>,
673
        events: EventSender,
674
        config: &EngineConfig,
675
    ) -> (Handle, Self) {
676
        let (sender, requests) = mpsc::channel(config.command_capacity.max(1));
677
        let persistence = match store {
678
            Some(Ok(store)) => Persistence::Store(store),
679
            Some(Err(error)) => {
680
                tracing::error!(%error, "search store unavailable, the index lives in memory");
681
                Persistence::Memory(SearchInMemory::OpenFailed)
682
            }
683
            None => Persistence::Memory(SearchInMemory::NoStore),
684
        };
685
        let service = Self {
686
            client: client.clone(),
687
            index: Box::new(KeywordIndex::new(matches!(
688
                persistence,
689
                Persistence::Store(_)
690
            ))),
691
            persistence,
692
            commit_failures: 0,
693
            orphans: Vec::new(),
694
            rooms: BTreeMap::new(),
695
            baselines: HashMap::new(),
696
            pending: BTreeMap::new(),
697
            dirty_shards: BTreeSet::new(),
698
            decrypt_queue: VecDeque::new(),
699
            decrypting: 0,
700
            redactions: Redactions::default(),
701
            dirty_since: None,
702
            commit_after: 0,
703
            budget: Budget::from_config(config),
704
            requests,
705
            subscriptions: HashMap::new(),
706
            updates: SelectAll::new(),
707
            work: FuturesUnordered::new(),
708
            pages: HashMap::new(),
709
            heads: VecDeque::new(),
710
            headed: HashSet::new(),
711
            head_tokens: HashMap::new(),
712
            backfilling: None,
713
            next_backfill: 0,
714
            failures: HashMap::new(),
715
            turn: 0,
716
        };
717
        (
718
            Handle {
719
                requests: sender,
720
                client,
721
                media,
722
                sessions: Arc::new(Sessions {
723
                    open: Mutex::default(),
724
                    events,
725
                }),
726
            },
727
            service,
728
        )
729
    }
730
731
    fn in_memory(&self) -> Option<SearchInMemory> {
732
        match self.persistence {
733
            Persistence::Store(_) => None,
734
            Persistence::Memory(reason) => Some(reason),
735
        }
736
    }
737
738
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
739
    fn fall_back(&mut self, reason: SearchInMemory) {
740
        self.persistence = Persistence::Memory(reason);
741
        self.index.detach();
742
        self.orphans.clear();
743
    }
744
745
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
746
    async fn load(&mut self) {
747
        let Persistence::Store(store) = &self.persistence else {
748
            return;
749
        };
750
        let mut blobs = match store.load().await {
751
            Ok(blobs) => blobs,
752
            Err(error) => {
753
                tracing::error!(%error, "reading the search store failed, the index lives in memory");
754
                self.fall_back(SearchInMemory::ReadFailed);
755
                return;
756
            }
757
        };
758
        if blobs.is_empty() {
759
            return;
760
        }
761
        let restored = (|| {
762
            let meta: Value = serde_json::from_slice(&store.take(&mut blobs, "meta")?).ok()?;
763
            if meta["schema"].as_u64() != Some(SCHEMA) {
764
                return None;
765
            }
766
            let rooms: BTreeMap<String, RoomProgress> =
767
                serde_json::from_value(meta["rooms"].clone()).ok()?;
768
            let mut pending = BTreeMap::new();
769
            for shard in 0..UTD_SHARDS {
770
                if let Some(bytes) = store.take(&mut blobs, &format!("utd/{shard}")) {
771
                    let part: BTreeMap<String, Pending> = serde_json::from_slice(&bytes).ok()?;
772
                    pending.extend(part);
773
                }
774
            }
775
            let index = KeywordIndex::load(&meta["index"], |name| store.take(&mut blobs, name))
776
                .inspect_err(|error| tracing::warn!(%error, "search segments are unreadable"))
777
                .ok()?;
778
            Some((index, rooms, pending))
779
        })();
780
        match restored {
781
            Some((index, rooms, pending)) => {
782
                self.index = Box::new(index);
783
                self.baselines = rooms
784
                    .iter()
785
                    .filter_map(|(room, progress)| Some((room.clone(), progress.newest?)))
786
                    .collect();
787
                self.rooms = rooms;
788
                self.pending = pending;
789
                self.orphans = blobs.leftover();
790
                if !self.orphans.is_empty() {
791
                    self.touch();
792
                }
793
            }
794
            None => {
795
                tracing::warn!("resetting an unreadable search index");
796
                if let Err(error) = store.reset().await {
797
                    tracing::error!(%error, "search store unavailable, the index lives in memory");
798
                    self.fall_back(SearchInMemory::ReadFailed);
799
                }
800
            }
801
        }
802
    }
803
804
    #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
805
    async fn load(&mut self) {}
806
807
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
808
    async fn commit(&mut self) {
809
        if !matches!(self.persistence, Persistence::Store(_)) {
810
            if let Err(error) = self.index.maintain() {
811
                tracing::warn!(%error, "maintaining the search index failed");
812
            }
813
            self.dirty_since = None;
814
            self.dirty_shards.clear();
815
            return;
816
        }
817
        let mut changes = match self.index.changes() {
818
            Ok(changes) => changes,
819
            Err(error) => {
820
                tracing::warn!(%error, "preparing a search commit failed");
821
                self.commit_failed();
822
                return;
823
            }
824
        };
825
        let mut puts = std::mem::take(&mut changes.puts);
826
        let mut deletes = changes.deletes.clone();
827
        let meta = serde_json::json!({
828
            "schema": SCHEMA,
829
            "index": changes.manifest,
830
            "rooms": self.rooms,
831
        });
832
        puts.push(("meta".to_owned(), meta.to_string().into_bytes()));
833
        for shard in &self.dirty_shards {
834
            let part: BTreeMap<&String, &Pending> = self
835
                .pending
836
                .iter()
837
                .filter(|(key, _)| self::shard(key) == *shard)
838
                .collect();
839
            let name = format!("utd/{shard}");
840
            if part.is_empty() {
841
                deletes.push(name);
842
            } else {
843
                puts.push((name, serde_json::to_vec(&part).unwrap_or_default()));
844
            }
845
        }
846
        let Persistence::Store(store) = &self.persistence else {
847
            return;
848
        };
849
        match store.commit(puts, deletes, self.orphans.clone()).await {
850
            Ok(()) => {
851
                self.index.committed(&changes);
852
                self.dirty_shards.clear();
853
                self.orphans.clear();
854
                self.dirty_since = None;
855
                self.commit_failures = 0;
856
            }
857
            Err(error) => {
858
                tracing::warn!(%error, "saving the search index failed");
859
                self.commit_failed();
860
            }
861
        }
862
    }
863
864
    #[cfg(any(feature = "sqlite", feature = "indexeddb"))]
865
    fn commit_failed(&mut self) {
866
        self.commit_failures += 1;
867
        if self.commit_failures >= COMMIT_ATTEMPTS {
868
            tracing::error!("search commits keep failing, the index lives in memory from here");
869
            self.fall_back(SearchInMemory::CommitFailed);
870
            self.dirty_since = None;
871
            self.dirty_shards.clear();
872
        } else {
873
            self.commit_after = now() + COMMIT_RETRY_MS;
874
        }
875
    }
876
877
    #[cfg(not(any(feature = "sqlite", feature = "indexeddb")))]
878
    async fn commit(&mut self) {
879
        if let Err(error) = self.index.maintain() {
880
            tracing::warn!(%error, "maintaining the search index failed");
881
        }
882
        self.dirty_since = None;
883
        self.dirty_shards.clear();
884
    }
885
886
    fn touch(&mut self) {
887
        self.dirty_since.get_or_insert_with(now);
888
    }
889
890
    fn unpend(&mut self, key: &str) {
891
        if self.pending.remove(key).is_some() {
892
            self.dirty_shards.insert(shard(key));
893
            self.touch();
894
        }
895
    }
896
897
    fn observe(&mut self, room: &OwnedRoomId, value: &Value) {
898
        let observed = feed::observe(value);
899
        let key = match &observed {
900
            Observed::Message { key, .. }
901
            | Observed::Redacted { key }
902
            | Observed::Encrypted { key, .. } => Some(key.clone()),
903
            Observed::Edit(edit) => Some(edit.id.clone()),
904
            Observed::Redaction { .. } | Observed::Other => None,
905
        };
906
        if let Observed::Encrypted {
907
            key,
908
            session_id,
909
            ts,
910
        } = &observed
911
        {
912
            if self.index.get(key).is_none() && !self.pending.contains_key(key) {
913
                self.pending.insert(
914
                    key.clone(),
915
                    Pending {
916
                        room: room.to_string(),
917
                        session_id: session_id.clone(),
918
                        ts: *ts,
919
                        raw: value.to_string(),
920
                    },
921
                );
922
                self.dirty_shards.insert(shard(key));
923
                while self.pending.len() > self.budget.max_pending {
924
                    let Some(oldest) = self
925
                        .pending
926
                        .iter()
927
                        .min_by_key(|(_, pending)| pending.ts)
928
                        .map(|(key, _)| key.clone())
929
                    else {
930
                        break;
931
                    };
932
                    self.unpend(&oldest);
933
                }
934
                self.touch();
935
            }
936
            return;
937
        }
938
        if let Some(key) = key {
939
            self.unpend(&key);
940
        }
941
        match feed::apply(
942
            self.index.as_mut(),
943
            &mut self.redactions,
944
            room.as_str(),
945
            observed,
946
        ) {
947
            Effect::Indexed(ts) => {
948
                let progress = self.rooms.entry(room.to_string()).or_default();
949
                progress.oldest = Some(progress.oldest.map_or(ts, |oldest| oldest.min(ts)));
950
                progress.newest = Some(progress.newest.map_or(ts, |newest| newest.max(ts)));
951
                self.touch();
952
            }
953
            Effect::Removed | Effect::Held => self.touch(),
954
            Effect::Refetch(key) => {
955
                self.touch();
956
                let room_id = room.clone();
957
                let client = self.client.clone();
958
                self.work.push(Box::pin(async move {
959
                    let values = match client.get_room(&room_id) {
960
                        Some(room) => load_with_edits(&room, &key).await,
961
                        None => Vec::new(),
962
                    };
963
                    Work::Refetched {
964
                        room: room_id,
965
                        values,
966
                    }
967
                }));
968
            }
969
            Effect::Nothing => {}
970
        }
971
    }
972
973
    fn observe_all(&mut self, room: &OwnedRoomId, events: &[SdkTimelineEvent]) {
974
        for event in events {
975
            if let Some(value) = value_of(event) {
976
                self.observe(room, &value);
977
            }
978
        }
979
    }
980
981
    async fn subscribe(&mut self, room_id: OwnedRoomId) {
982
        if let Some((_, seen)) = self.subscriptions.get_mut(&room_id) {
983
            *seen = now();
984
            return;
985
        }
986
        let Some(room) = self.client.get_room(&room_id) else {
987
            return;
988
        };
989
        if room.state() != RoomState::Joined {
990
            return;
991
        }
992
        let Ok((cache, drop_handles)) = room.event_cache().await else {
993
            return;
994
        };
995
        let Ok((initial, subscriber)) = cache.subscribe().await else {
996
            return;
997
        };
998
        self.observe_all(&room_id, &initial);
999
        let stream = updates(room_id.clone(), subscriber, drop_handles);
1000
        let (stream, abort) = abortable(stream);
1001
        self.updates.push(Box::pin(stream));
1002
        self.subscriptions.insert(room_id.clone(), (abort, now()));
1003
        let started = self
1004
            .rooms
1005
            .get(room_id.as_str())
1006
            .is_some_and(|progress| progress.started);
1007
        if started
1008
            && self.baselines.contains_key(room_id.as_str())
1009
            && self.headed.insert(room_id.clone())
1010
        {
1011
            self.heads.push_back(room_id);
1012
        }
1013
    }
1014
1015
    /// the generic channel holds 128 updates, and a missed one could be a room we never
1016
    /// follow, so a lag follows every joined room
1017
    async fn generic(&mut self, update: Result<OwnedRoomId, broadcast::error::RecvError>) -> bool {
1018
        match update {
1019
            Ok(room_id) => self.subscribe(room_id).await,
1020
            Err(broadcast::error::RecvError::Lagged(_)) => {
1021
                for room in self.client.joined_rooms() {
1022
                    self.subscribe(room.room_id().to_owned()).await;
1023
                }
1024
            }
1025
            Err(broadcast::error::RecvError::Closed) => return false,
1026
        }
1027
        true
1028
    }
1029
1030
    async fn room_update(&mut self, room_id: OwnedRoomId, update: RoomUpdate) {
1031
        match update {
1032
            RoomUpdate::Events(events) => {
1033
                if let Some((_, seen)) = self.subscriptions.get_mut(&room_id) {
1034
                    *seen = now();
1035
                }
1036
                self.observe_all(&room_id, &events);
1037
            }
1038
            RoomUpdate::Lagged => {
1039
                if let Some((abort, _)) = self.subscriptions.remove(&room_id) {
1040
                    abort.abort();
1041
                }
1042
                self.subscribe(room_id).await;
1043
            }
1044
            RoomUpdate::Closed => {
1045
                self.subscriptions.remove(&room_id);
1046
            }
1047
        }
1048
    }
1049
1050
    fn left(&mut self, room_id: &OwnedRoomId) {
1051
        if let Some((abort, _)) = self.subscriptions.remove(room_id) {
1052
            abort.abort();
1053
        }
1054
        self.heads.retain(|head| head != room_id);
1055
        self.head_tokens.remove(room_id);
1056
        if let Some((backfilling, abort)) = &self.backfilling
1057
            && backfilling == room_id
1058
        {
1059
            abort.abort();
1060
        }
1061
    }
1062
1063
    fn drop_idle(&mut self) {
1064
        let cutoff = now().saturating_sub(IDLE_SUBSCRIPTION_MS);
1065
        self.subscriptions.retain(|_, (abort, seen)| {
1066
            let keep = *seen >= cutoff;
1067
            if !keep {
1068
                abort.abort();
1069
            }
1070
            keep
1071
        });
1072
    }
1073
1074
    fn coverage(&self, rooms: &[OwnedRoomId]) -> SearchCoverage {
1075
        let scoped: HashSet<&str> = rooms.iter().map(|room| room.as_str()).collect();
1076
        let undecrypted = self
1077
            .pending
1078
            .values()
1079
            .filter(|pending| scoped.contains(pending.room.as_str()))
1080
            .count();
1081
        let rooms = rooms
1082
            .iter()
1083
            .map(|room_id| {
1084
                let progress = self
1085
                    .rooms
1086
                    .get(room_id.as_str())
1087
                    .cloned()
1088
                    .unwrap_or_default();
1089
                let backfill = if progress.start_reached {
1090
                    SearchBackfill::Complete
1091
                } else if self.budget.pages_per_room == 0 {
1092
                    SearchBackfill::Disabled
1093
                } else if self
1094
                    .backfilling
1095
                    .as_ref()
1096
                    .is_some_and(|(backfilling, _)| backfilling == room_id)
1097
                {
1098
                    SearchBackfill::Running
1099
                } else if let Some(failure) = self.failures.get(room_id) {
1100
                    SearchBackfill::Failed {
1101
                        message: failure.message.clone(),
1102
                        retry_at_ms: failure.retry_at_ms,
1103
                    }
1104
                } else if self.pages.get(room_id).copied().unwrap_or(0)
1105
                    >= self.budget.pages_per_room
1106
                {
1107
                    SearchBackfill::Paused
1108
                } else {
1109
                    SearchBackfill::Waiting
1110
                };
1111
                RoomSearchCoverage {
1112
                    room_id: room_id.clone(),
1113
                    oldest_indexed_ms: progress.oldest,
1114
                    newest_indexed_ms: progress.newest,
1115
                    start_reached: progress.start_reached,
1116
                    backfill,
1117
                }
1118
            })
1119
            .collect();
1120
        let in_memory = self.in_memory();
1121
        SearchCoverage {
1122
            rooms,
1123
            persistent: in_memory.is_none(),
1124
            in_memory,
1125
            undecrypted,
1126
        }
1127
    }
1128
1129
    fn spent(&self, room_id: &OwnedRoomId) -> bool {
1130
        self.pages.get(room_id).copied().unwrap_or(0) >= self.budget.pages_per_room
1131
    }
1132
1133
    fn blocked(&self, room_id: &OwnedRoomId, at: u64) -> bool {
1134
        self.failures
1135
            .get(room_id)
1136
            .is_some_and(|failure| failure.retry_at_ms > at)
1137
    }
1138
1139
    /// head passes first, so what arrived while we were away is found before anything older
1140
    fn schedule_backfill(&mut self) {
1141
        let at = now();
1142
        if self.backfilling.is_some() || self.budget.pages_per_room == 0 || at < self.next_backfill
1143
        {
1144
            return;
1145
        }
1146
        let mut chosen = None;
1147
        for _ in 0..self.heads.len() {
1148
            let Some(room_id) = self.heads.pop_front() else {
1149
                break;
1150
            };
1151
            if self.spent(&room_id) {
1152
                self.head_tokens.remove(&room_id);
1153
                continue;
1154
            }
1155
            if self.blocked(&room_id, at) {
1156
                self.heads.push_back(room_id);
1157
                continue;
1158
            }
1159
            chosen = Some((room_id, true));
1160
            break;
1161
        }
1162
        if chosen.is_none() {
1163
            let mut joined: Vec<_> = self
1164
                .client
1165
                .joined_rooms()
1166
                .into_iter()
1167
                .map(|room| room.room_id().to_owned())
1168
                .collect();
1169
            joined.sort();
1170
            let count = joined.len();
1171
            for offset in 0..count {
1172
                let room_id = &joined[(self.turn + offset) % count];
1173
                let done = self
1174
                    .rooms
1175
                    .get(room_id.as_str())
1176
                    .is_some_and(|progress| progress.start_reached);
1177
                if !done && !self.spent(room_id) && !self.blocked(room_id, at) {
1178
                    self.turn = (self.turn + offset + 1) % count;
1179
                    chosen = Some((room_id.clone(), false));
1180
                    break;
1181
                }
1182
            }
1183
        }
1184
        let Some((room_id, head)) = chosen else {
1185
            return;
1186
        };
1187
        let Some(room) = self.client.get_room(&room_id) else {
1188
            return;
1189
        };
1190
        let from = if head {
1191
            self.head_tokens.get(&room_id).cloned()
1192
        } else {
1193
            self.rooms
1194
                .get(room_id.as_str())
1195
                .and_then(|progress| progress.tail.clone())
1196
        };
1197
        *self.pages.entry(room_id.clone()).or_default() += 1;
1198
        let mut options = MessagesOptions::backward();
1199
        options.from = from.clone();
1200
        options.limit = UInt::from(self.budget.page_size);
1201
        let mut filter = RoomEventFilter::default();
1202
        filter.types = Some(
1203
            BACKFILL_TYPES
1204
                .iter()
1205
                .map(|kind| (*kind).to_owned())
1206
                .collect(),
1207
        );
1208
        options.filter = filter;
1209
        let (abort, registration) = AbortHandle::new_pair();
1210
        self.backfilling = Some((room_id.clone(), abort));
1211
        let page = Abortable::new(
1212
            async move {
1213
                room.messages(options)
1214
                    .await
1215
                    .map_err(|error| Error::from_sdk("backfilling search history failed", error))
1216
            },
1217
            registration,
1218
        );
1219
        self.work.push(Box::pin(async move {
1220
            match page.await {
1221
                Ok(result) => Work::Backfill {
1222
                    room: room_id,
1223
                    head,
1224
                    from,
1225
                    result,
1226
                },
1227
                Err(_) => Work::BackfillCancelled,
1228
            }
1229
        }));
1230
    }
1231
1232
    fn fail_backfill(&mut self, room_id: OwnedRoomId, message: String, delay_ms: u64) {
1233
        self.failures.insert(
1234
            room_id,
1235
            Failure {
1236
                message,
1237
                retry_at_ms: now() + delay_ms,
1238
                delay_ms,
1239
            },
1240
        );
1241
    }
1242
1243
    fn backfilled(
1244
        &mut self,
1245
        room_id: OwnedRoomId,
1246
        head: bool,
1247
        from: Option<String>,
1248
        result: Result<Messages, Error>,
1249
    ) {
1250
        self.backfilling = None;
1251
        self.next_backfill = now() + self.budget.interval_ms;
1252
        let messages = match result {
1253
            Ok(messages) => messages,
1254
            Err(error) if from.is_some() && token_rejected(&error) => {
1255
                if head {
1256
                    self.head_tokens.remove(&room_id);
1257
                } else {
1258
                    let progress = self.rooms.entry(room_id.to_string()).or_default();
1259
                    progress.tail = None;
1260
                    progress.started = false;
1261
                    self.touch();
1262
                }
1263
                let message = format!("{}, starting again from the newest", error.message);
1264
                self.fail_backfill(room_id, message, self.budget.interval_ms);
1265
                return;
1266
            }
1267
            Err(error) => {
1268
                let delay_ms = self
1269
                    .failures
1270
                    .get(&room_id)
1271
                    .map_or(BACKFILL_RETRY_MS, |failure| {
1272
                        (failure.delay_ms * 2).min(BACKFILL_RETRY_MAX_MS)
1273
                    });
1274
                self.fail_backfill(room_id, error.message.clone(), delay_ms);
1275
                return;
1276
            }
1277
        };
1278
        self.failures.remove(&room_id);
1279
        let baseline = self.baselines.get(room_id.as_str()).copied();
1280
        let mut overlapped = false;
1281
        for event in &messages.chunk {
1282
            let Some(value) = value_of(event) else {
1283
                continue;
1284
            };
1285
            if head && head_overlap(self.index.as_ref(), baseline, &value) {
1286
                overlapped = true;
1287
            }
1288
            self.observe(&room_id, &value);
1289
        }
1290
        let ended = messages.end.is_none() || (messages.chunk.is_empty() && messages.end == from);
1291
        if head {
1292
            match messages.end {
1293
                Some(end) if !overlapped && !ended => {
1294
                    self.head_tokens.insert(room_id.clone(), end);
1295
                    self.heads.push_front(room_id);
1296
                }
1297
                _ => {
1298
                    self.head_tokens.remove(&room_id);
1299
                }
1300
            }
1301
            return;
1302
        }
1303
        let progress = self.rooms.entry(room_id.to_string()).or_default();
1304
        progress.started = true;
1305
        progress.tail = messages.end;
1306
        if ended {
1307
            progress.start_reached = true;
1308
            progress.tail = None;
1309
        }
1310
        self.touch();
1311
    }
1312
1313
    fn retry_pending(&mut self, room_id: Option<&OwnedRoomId>, session_id: Option<&str>) {
1314
        let queued: HashSet<String> = self.decrypt_queue.iter().cloned().collect();
1315
        let due: Vec<_> = self
1316
            .pending
1317
            .iter()
1318
            .filter(|(key, pending)| {
1319
                !queued.contains(*key)
1320
                    && room_id.is_none_or(|room_id| pending.room == room_id.as_str())
1321
                    && session_id.is_none_or(|session_id| pending.session_id == session_id)
1322
            })
1323
            .map(|(key, _)| key.clone())
1324
            .collect();
1325
        self.decrypt_queue.extend(due);
1326
        self.pump_decrypts();
1327
    }
1328
1329
    fn pump_decrypts(&mut self) {
1330
        while self.decrypting < DECRYPTS_AT_ONCE {
1331
            let Some(key) = self.decrypt_queue.pop_front() else {
1332
                return;
1333
            };
1334
            let Some(pending) = self.pending.get(&key).cloned() else {
1335
                continue;
1336
            };
1337
            let Ok(room_id) = OwnedRoomId::try_from(pending.room.as_str()) else {
1338
                continue;
1339
            };
1340
            let Some(room) = self.client.get_room(&room_id) else {
1341
                continue;
1342
            };
1343
            self.decrypting += 1;
1344
            self.work.push(Box::pin(async move {
1345
                let value = async {
1346
                    let raw: Raw<OriginalSyncRoomEncryptedEvent> =
1347
                        Raw::from_json_string(pending.raw).ok()?;
1348
                    let event = room.decrypt_event(&raw, None).await.ok()?;
1349
                    value_of(&event).filter(|value| value["type"] != "m.room.encrypted")
1350
                }
1351
                .await;
1352
                Work::Decrypted {
1353
                    key,
1354
                    room: room_id,
1355
                    value,
1356
                }
1357
            }));
1358
        }
1359
    }
1360
1361
    fn complete(&mut self, work: Work) {
1362
        match work {
1363
            Work::Backfill {
1364
                room,
1365
                head,
1366
                from,
1367
                result,
1368
            } => self.backfilled(room, head, from, result),
1369
            Work::BackfillCancelled => {
1370
                self.backfilling = None;
1371
                self.next_backfill = now() + self.budget.interval_ms;
1372
            }
1373
            Work::Decrypted { key, room, value } => {
1374
                self.decrypting -= 1;
1375
                if let Some(value) = value {
1376
                    self.unpend(&key);
1377
                    self.observe(&room, &value);
1378
                }
1379
                self.pump_decrypts();
1380
            }
1381
            Work::Refetched { room, values } => {
1382
                for value in values {
1383
                    self.observe(&room, &value);
1384
                }
1385
            }
1386
        }
1387
    }
1388
1389
    fn search(
1390
        &mut self,
1391
        query: &str,
1392
        rooms: Option<&[String]>,
1393
        before: Option<(u64, u64)>,
1394
        limit: usize,
1395
        coverage: &[OwnedRoomId],
1396
    ) -> Result<RawPage, Error> {
1397
        let (mut hits, terms) = self.index.search(query, rooms, before, limit + 1)?;
1398
        let more = hits.len() > limit;
1399
        hits.truncate(limit);
1400
        Ok(RawPage {
1401
            hits,
1402
            terms,
1403
            more,
1404
            coverage: self.coverage(coverage),
1405
        })
1406
    }
1407
1408
    pub(crate) async fn run(mut self) {
1409
        let client = self.client.clone();
1410
        let mut generic = client.event_cache().subscribe_to_room_generic_updates();
1411
        let mut room_info = client.room_info_notable_update_receiver();
1412
        self.load().await;
1413
        let mut keys = client
1414
            .encryption()
1415
            .room_keys_received_stream()
1416
            .await
1417
            .map(Box::pin);
1418
        self.retry_pending(None, None);
1419
        let mut tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
1420
        loop {
1421
            self.schedule_backfill();
1422
            tokio::select! {
1423
                biased;
1424
                request = self.requests.recv() => match request {
1425
                    Some(Request::Stop(reply)) => {
1426
                        if self.dirty_since.is_some() || self.index.buffered() > 0 {
1427
                            self.commit().await;
1428
                        }
1429
                        for (abort, _) in self.subscriptions.values() {
1430
                            abort.abort();
1431
                        }
1432
                        let _ = reply.send(());
1433
                        return;
1434
                    }
1435
                    Some(Request::Search { query, rooms, before, limit, coverage, reply }) => {
1436
                        let _ = reply.send(self.search(&query, rooms.as_deref(), before, limit, &coverage));
1437
                    }
1438
                    Some(Request::Coverage(rooms, reply)) => {
1439
                        let _ = reply.send(self.coverage(&rooms));
1440
                    }
1441
                    Some(Request::Forget(key)) => {
1442
                        let observed = Observed::Redacted { key };
1443
                        if feed::apply(self.index.as_mut(), &mut self.redactions, "", observed) == Effect::Removed {
1444
                            self.touch();
1445
                        }
1446
                    }
1447
                    None => return,
1448
                },
1449
                Some(work) = self.work.next(), if !self.work.is_empty() => self.complete(work),
1450
                Some((room_id, update)) = self.updates.next(), if !self.updates.is_empty() => {
1451
                    self.room_update(room_id, update).await;
1452
                }
1453
                update = generic.recv() => {
1454
                    if !self.generic(update.map(|update| update.room_id)).await {
1455
                        return;
1456
                    }
1457
                }
1458
                Some(update) = async { keys.as_mut()?.next().await }, if keys.is_some() => match update {
1459
                    Ok(infos) => {
1460
                        for info in infos {
1461
                            self.retry_pending(Some(&info.room_id), Some(&info.session_id));
1462
                        }
1463
                    }
1464
                    Err(_) => self.retry_pending(None, None),
1465
                },
1466
                update = room_info.recv() => match update {
1467
                    Ok(update) if update.reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP) => {
1468
                        let left = self.client.get_room(&update.room_id).is_none_or(|room| room.state() != RoomState::Joined);
1469
                        if left {
1470
                            self.left(&update.room_id);
1471
                        }
1472
                    }
1473
                    Ok(_) | Err(broadcast::error::RecvError::Lagged(_)) => {}
1474
                    Err(broadcast::error::RecvError::Closed) => return,
1475
                },
1476
                () = &mut tick => {
1477
                    tick = Box::pin(matrix_sdk::sleep::sleep(Duration::from_secs(1)));
1478
                    self.drop_idle();
1479
                    let due = self.dirty_since.is_some_and(|since| now() >= since + COMMIT_AFTER_MS)
1480
                        || self.index.buffered() >= COMMIT_BUFFERED;
1481
                    if due && now() >= self.commit_after {
1482
                        self.commit().await;
1483
                    }
1484
                }
1485
            }
1486
        }
1487
    }
1488
}
1489
1490
/// a head pass stops at what this engine found already indexed, and live events indexed
1491
/// since it started don't count, or the first page would always stop it
1492
fn head_overlap(index: &dyn MessageIndex, baseline: Option<u64>, value: &Value) -> bool {
1493
    let (Some(baseline), Some(key), Some(ts)) = (
1494
        baseline,
1495
        value.get("event_id").and_then(Value::as_str),
1496
        value.get("origin_server_ts").and_then(Value::as_u64),
1497
    ) else {
1498
        return false;
1499
    };
1500
    ts <= baseline && index.get(key).is_some_and(|doc| !doc.provisional)
1501
}
1502
1503
fn updates(
1504
    room_id: OwnedRoomId,
1505
    subscriber: RoomEventCacheSubscriber,
1506
    drop_handles: Arc<matrix_sdk::event_cache::EventCacheDropHandles>,
1507
) -> BoxStream<(OwnedRoomId, RoomUpdate)> {
1508
    Box::pin(futures_util::stream::unfold(
1509
        Some((subscriber, drop_handles)),
1510
        move |state| {
1511
            let room_id = room_id.clone();
1512
            async move {
1513
                let (mut subscriber, drop_handles) = state?;
1514
                loop {
1515
                    match subscriber.recv().await {
1516
                        Ok(RoomEventCacheUpdate::UpdateTimelineEvents(update)) => {
1517
                            let events = update.diffs.into_iter().flat_map(added).collect();
1518
                            return Some((
1519
                                (room_id, RoomUpdate::Events(events)),
1520
                                Some((subscriber, drop_handles)),
1521
                            ));
1522
                        }
1523
                        Ok(_) => {}
1524
                        Err(broadcast::error::RecvError::Lagged(_)) => {
1525
                            return Some((
1526
                                (room_id, RoomUpdate::Lagged),
1527
                                Some((subscriber, drop_handles)),
1528
                            ));
1529
                        }
1530
                        Err(broadcast::error::RecvError::Closed) => {
1531
                            return Some(((room_id, RoomUpdate::Closed), None));
1532
                        }
1533
                    }
1534
                }
1535
            }
1536
        },
1537
    ))
1538
}
1539
1540
fn added(diff: VectorDiff<SdkTimelineEvent>) -> Vec<SdkTimelineEvent> {
1541
    match diff {
1542
        VectorDiff::Append { values } | VectorDiff::Reset { values } => {
1543
            values.into_iter().collect()
1544
        }
1545
        VectorDiff::PushFront { value }
1546
        | VectorDiff::PushBack { value }
1547
        | VectorDiff::Insert { value, .. }
1548
        | VectorDiff::Set { value, .. } => vec![value],
1549
        VectorDiff::Clear
1550
        | VectorDiff::PopFront
1551
        | VectorDiff::PopBack
1552
        | VectorDiff::Remove { .. }
1553
        | VectorDiff::Truncate { .. } => Vec::new(),
1554
    }
1555
}
1556
1557
#[cfg(all(test, feature = "sqlite"))]
1558
#[path = "service_tests.rs"]
1559
mod tests;