atagen

macltok

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

Blame: src/mentions/state.rs

cbb715e11963atagen2026-10-24 23:24 UTC1
use std::collections::{BTreeMap, BTreeSet};
cbb715e11963atagen2026-10-24 23:24 UTC2
038cc6faeb14atagen2026-10-26 02:05 UTC3
use ruma::{OwnedRoomId, OwnedUserId};
cbb715e11963atagen2026-10-24 23:24 UTC4
use serde_json::Value;
cbb715e11963atagen2026-10-24 23:24 UTC5
cbb715e11963atagen2026-10-24 23:24 UTC6
use crate::{
7cdaa86b9489atagen2026-10-25 08:43 UTC7
    Error, ErrorKind, EventContext, MentionCaptureState, MentionCoverage, MentionEntry, MentionGap,
7cdaa86b9489atagen2026-10-25 08:43 UTC8
    MentionGapReason, MentionKey, MentionsBoundary, MentionsPage, SavedMentionContext,
7cdaa86b9489atagen2026-10-25 08:43 UTC9
    TimelineItem, normalise::MediaRegistration,
cbb715e11963atagen2026-10-24 23:24 UTC10
};
cbb715e11963atagen2026-10-24 23:24 UTC11
7cdaa86b9489atagen2026-10-25 08:43 UTC12
const CANDIDATE_LIFETIME_MS: u64 = 7 * 24 * 60 * 60 * 1000;
7cdaa86b9489atagen2026-10-25 08:43 UTC13
cbb715e11963atagen2026-10-24 23:24 UTC14
#[derive(Clone)]
cbb715e11963atagen2026-10-24 23:24 UTC15
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC16
pub(super) struct SavedEvent {
cbb715e11963atagen2026-10-24 23:24 UTC17
    pub item: TimelineItem,
cbb715e11963atagen2026-10-24 23:24 UTC18
    pub media: Vec<MediaRegistration>,
cbb715e11963atagen2026-10-24 23:24 UTC19
    pub raw: Option<Value>,
cbb715e11963atagen2026-10-24 23:24 UTC20
}
cbb715e11963atagen2026-10-24 23:24 UTC21
7cdaa86b9489atagen2026-10-25 08:43 UTC22
impl PartialEq for SavedEvent {
7cdaa86b9489atagen2026-10-25 08:43 UTC23
    fn eq(&self, other: &Self) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC24
        self.item == other.item
7cdaa86b9489atagen2026-10-25 08:43 UTC25
            && self.raw == other.raw
7cdaa86b9489atagen2026-10-25 08:43 UTC26
            && self
7cdaa86b9489atagen2026-10-25 08:43 UTC27
                .media
7cdaa86b9489atagen2026-10-25 08:43 UTC28
                .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC29
                .map(|media| &media.id)
7cdaa86b9489atagen2026-10-25 08:43 UTC30
                .eq(other.media.iter().map(|media| &media.id))
7cdaa86b9489atagen2026-10-25 08:43 UTC31
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC32
}
7cdaa86b9489atagen2026-10-25 08:43 UTC33
7cdaa86b9489atagen2026-10-25 08:43 UTC34
#[derive(Clone, PartialEq)]
cbb715e11963atagen2026-10-24 23:24 UTC35
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC36
pub(super) struct Candidate {
cbb715e11963atagen2026-10-24 23:24 UTC37
    pub room_id: ruma::OwnedRoomId,
cbb715e11963atagen2026-10-24 23:24 UTC38
    pub raw: Value,
cbb715e11963atagen2026-10-24 23:24 UTC39
    pub attempts: u32,
cbb715e11963atagen2026-10-24 23:24 UTC40
    pub retry_at_ms: u64,
7cdaa86b9489atagen2026-10-25 08:43 UTC41
    pub first_seen_ms: u64,
cbb715e11963atagen2026-10-24 23:24 UTC42
}
cbb715e11963atagen2026-10-24 23:24 UTC43
7cdaa86b9489atagen2026-10-25 08:43 UTC44
impl Candidate {
7cdaa86b9489atagen2026-10-25 08:43 UTC45
    pub fn new(room_id: ruma::OwnedRoomId, raw: Value, retry_at_ms: u64) -> Self {
7cdaa86b9489atagen2026-10-25 08:43 UTC46
        Self {
7cdaa86b9489atagen2026-10-25 08:43 UTC47
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC48
            raw,
7cdaa86b9489atagen2026-10-25 08:43 UTC49
            attempts: 0,
7cdaa86b9489atagen2026-10-25 08:43 UTC50
            retry_at_ms,
7cdaa86b9489atagen2026-10-25 08:43 UTC51
            first_seen_ms: now(),
7cdaa86b9489atagen2026-10-25 08:43 UTC52
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC53
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC54
7cdaa86b9489atagen2026-10-25 08:43 UTC55
    pub fn encrypted(&self) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC56
        self.raw["type"] == "m.room.encrypted"
7cdaa86b9489atagen2026-10-25 08:43 UTC57
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC58
}
7cdaa86b9489atagen2026-10-25 08:43 UTC59
7cdaa86b9489atagen2026-10-25 08:43 UTC60
#[derive(Clone, PartialEq)]
cbb715e11963atagen2026-10-24 23:24 UTC61
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC62
pub(super) struct Window {
cbb715e11963atagen2026-10-24 23:24 UTC63
    pub before: Vec<String>,
cbb715e11963atagen2026-10-24 23:24 UTC64
    pub target: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC65
    pub after: Vec<String>,
cbb715e11963atagen2026-10-24 23:24 UTC66
    pub start: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC67
    pub end: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC68
}
cbb715e11963atagen2026-10-24 23:24 UTC69
7cdaa86b9489atagen2026-10-25 08:43 UTC70
#[derive(Clone, Default, PartialEq)]
cbb715e11963atagen2026-10-24 23:24 UTC71
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
cbb715e11963atagen2026-10-24 23:24 UTC72
pub(super) struct Data {
cbb715e11963atagen2026-10-24 23:24 UTC73
    pub revision: u64,
cbb715e11963atagen2026-10-24 23:24 UTC74
    pub entries: BTreeMap<String, MentionEntry>,
cbb715e11963atagen2026-10-24 23:24 UTC75
    pub events: BTreeMap<String, SavedEvent>,
cbb715e11963atagen2026-10-24 23:24 UTC76
    pub windows: BTreeMap<String, Window>,
cbb715e11963atagen2026-10-24 23:24 UTC77
    pub capture_attempts: BTreeMap<String, u32>,
cbb715e11963atagen2026-10-24 23:24 UTC78
    pub candidates: BTreeMap<String, Candidate>,
cbb715e11963atagen2026-10-24 23:24 UTC79
    pub redacted: BTreeMap<String, u64>,
cbb715e11963atagen2026-10-24 23:24 UTC80
    pub edits: BTreeMap<String, Value>,
cbb715e11963atagen2026-10-24 23:24 UTC81
    pub coverage: MentionCoverage,
cbb715e11963atagen2026-10-24 23:24 UTC82
    pub history_cursor: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC83
    pub history_exhausted: bool,
cbb715e11963atagen2026-10-24 23:24 UTC84
    pub latest_cursor: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC85
    pub latest_anchor: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC86
    pub latest_head: Option<String>,
7cdaa86b9489atagen2026-10-25 08:43 UTC87
}
7cdaa86b9489atagen2026-10-25 08:43 UTC88
7cdaa86b9489atagen2026-10-25 08:43 UTC89
#[derive(Clone, Default, PartialEq)]
7cdaa86b9489atagen2026-10-25 08:43 UTC90
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7cdaa86b9489atagen2026-10-25 08:43 UTC91
pub(super) struct SyncState {
7cdaa86b9489atagen2026-10-25 08:43 UTC92
    pub inbox_revision: u64,
7cdaa86b9489atagen2026-10-25 08:43 UTC93
    pub checkpoints: BTreeMap<String, Checkpoint>,
82d37c369846atagen2026-10-25 10:55 UTC94
}
82d37c369846atagen2026-10-25 10:55 UTC95
82d37c369846atagen2026-10-25 10:55 UTC96
/// what a set of rooms holds in the inbox, compared before and after a change so an
82d37c369846atagen2026-10-25 10:55 UTC97
/// unchanged inbox costs only the rooms it touched
82d37c369846atagen2026-10-25 10:55 UTC98
#[derive(PartialEq)]
82d37c369846atagen2026-10-25 10:55 UTC99
pub(super) struct Scope {
82d37c369846atagen2026-10-25 10:55 UTC100
    rooms: Vec<RoomScope>,
82d37c369846atagen2026-10-25 10:55 UTC101
    coverage: MentionCoverage,
82d37c369846atagen2026-10-25 10:55 UTC102
    cursors: (
82d37c369846atagen2026-10-25 10:55 UTC103
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC104
        bool,
82d37c369846atagen2026-10-25 10:55 UTC105
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC106
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC107
        Option<String>,
82d37c369846atagen2026-10-25 10:55 UTC108
    ),
82d37c369846atagen2026-10-25 10:55 UTC109
}
82d37c369846atagen2026-10-25 10:55 UTC110
82d37c369846atagen2026-10-25 10:55 UTC111
#[derive(PartialEq)]
82d37c369846atagen2026-10-25 10:55 UTC112
struct RoomScope {
82d37c369846atagen2026-10-25 10:55 UTC113
    entries: Vec<MentionEntry>,
82d37c369846atagen2026-10-25 10:55 UTC114
    events: Vec<SavedEvent>,
82d37c369846atagen2026-10-25 10:55 UTC115
    windows: Vec<Window>,
82d37c369846atagen2026-10-25 10:55 UTC116
    capture_attempts: Vec<u32>,
82d37c369846atagen2026-10-25 10:55 UTC117
    candidates: Vec<Candidate>,
82d37c369846atagen2026-10-25 10:55 UTC118
    redacted: Vec<u64>,
82d37c369846atagen2026-10-25 10:55 UTC119
    edits: Vec<Value>,
82d37c369846atagen2026-10-25 10:55 UTC120
    keys: Vec<String>,
82d37c369846atagen2026-10-25 10:55 UTC121
}
82d37c369846atagen2026-10-25 10:55 UTC122
82d37c369846atagen2026-10-25 10:55 UTC123
fn in_room<V: Clone>(
82d37c369846atagen2026-10-25 10:55 UTC124
    map: &BTreeMap<String, V>,
82d37c369846atagen2026-10-25 10:55 UTC125
    room: &ruma::RoomId,
82d37c369846atagen2026-10-25 10:55 UTC126
    keys: &mut Vec<String>,
82d37c369846atagen2026-10-25 10:55 UTC127
) -> Vec<V> {
82d37c369846atagen2026-10-25 10:55 UTC128
    // no room id holds a newline, so a room's "room\nevent" keys sit below "room\x0b"
82d37c369846atagen2026-10-25 10:55 UTC129
    map.range(format!("{room}\n")..format!("{room}\u{b}"))
82d37c369846atagen2026-10-25 10:55 UTC130
        .map(|(key, value)| {
82d37c369846atagen2026-10-25 10:55 UTC131
            keys.push(key.clone());
82d37c369846atagen2026-10-25 10:55 UTC132
            value.clone()
82d37c369846atagen2026-10-25 10:55 UTC133
        })
82d37c369846atagen2026-10-25 10:55 UTC134
        .collect()
7cdaa86b9489atagen2026-10-25 08:43 UTC135
}
7cdaa86b9489atagen2026-10-25 08:43 UTC136
7cdaa86b9489atagen2026-10-25 08:43 UTC137
#[derive(Clone, Debug, PartialEq)]
7cdaa86b9489atagen2026-10-25 08:43 UTC138
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7cdaa86b9489atagen2026-10-25 08:43 UTC139
pub(super) struct Checkpoint {
7cdaa86b9489atagen2026-10-25 08:43 UTC140
    pub pos: String,
7cdaa86b9489atagen2026-10-25 08:43 UTC141
    pub previous: Option<String>,
cbb715e11963atagen2026-10-24 23:24 UTC142
}
cbb715e11963atagen2026-10-24 23:24 UTC143
cbb715e11963atagen2026-10-24 23:24 UTC144
pub(super) fn id(key: &MentionKey) -> String {
cbb715e11963atagen2026-10-24 23:24 UTC145
    format!("{}\n{}", key.room_id, key.event_id)
cbb715e11963atagen2026-10-24 23:24 UTC146
}
cbb715e11963atagen2026-10-24 23:24 UTC147
cbb715e11963atagen2026-10-24 23:24 UTC148
pub(super) fn now() -> u64 {
cbb715e11963atagen2026-10-24 23:24 UTC149
    matrix_sdk::ruma::MilliSecondsSinceUnixEpoch::now()
cbb715e11963atagen2026-10-24 23:24 UTC150
        .get()
cbb715e11963atagen2026-10-24 23:24 UTC151
        .into()
cbb715e11963atagen2026-10-24 23:24 UTC152
}
cbb715e11963atagen2026-10-24 23:24 UTC153
cbb715e11963atagen2026-10-24 23:24 UTC154
pub(super) fn invalid(message: &'static str) -> Error {
cbb715e11963atagen2026-10-24 23:24 UTC155
    Error::new(ErrorKind::Invalid, message)
cbb715e11963atagen2026-10-24 23:24 UTC156
}
cbb715e11963atagen2026-10-24 23:24 UTC157
7cdaa86b9489atagen2026-10-25 08:43 UTC158
#[cfg(feature = "serde")]
7cdaa86b9489atagen2026-10-25 08:43 UTC159
pub(super) fn json_len(value: &impl serde::Serialize) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC160
    struct Count(usize);
7cdaa86b9489atagen2026-10-25 08:43 UTC161
    impl std::io::Write for Count {
7cdaa86b9489atagen2026-10-25 08:43 UTC162
        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
7cdaa86b9489atagen2026-10-25 08:43 UTC163
            self.0 += bytes.len();
7cdaa86b9489atagen2026-10-25 08:43 UTC164
            Ok(bytes.len())
7cdaa86b9489atagen2026-10-25 08:43 UTC165
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC166
        fn flush(&mut self) -> std::io::Result<()> {
7cdaa86b9489atagen2026-10-25 08:43 UTC167
            Ok(())
7cdaa86b9489atagen2026-10-25 08:43 UTC168
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC169
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC170
    let mut count = Count(0);
7cdaa86b9489atagen2026-10-25 08:43 UTC171
    let _ = serde_json::to_writer(&mut count, value);
7cdaa86b9489atagen2026-10-25 08:43 UTC172
    count.0
7cdaa86b9489atagen2026-10-25 08:43 UTC173
}
7cdaa86b9489atagen2026-10-25 08:43 UTC174
7cdaa86b9489atagen2026-10-25 08:43 UTC175
#[cfg(not(feature = "serde"))]
7cdaa86b9489atagen2026-10-25 08:43 UTC176
pub(super) fn json_len<T>(_: &T) -> usize {
7cdaa86b9489atagen2026-10-25 08:43 UTC177
    0
7cdaa86b9489atagen2026-10-25 08:43 UTC178
}
7cdaa86b9489atagen2026-10-25 08:43 UTC179
7cdaa86b9489atagen2026-10-25 08:43 UTC180
fn timestamp(value: &Value) -> u64 {
7cdaa86b9489atagen2026-10-25 08:43 UTC181
    value["origin_server_ts"].as_u64().unwrap_or(0)
7cdaa86b9489atagen2026-10-25 08:43 UTC182
}
7cdaa86b9489atagen2026-10-25 08:43 UTC183
cbb715e11963atagen2026-10-24 23:24 UTC184
impl Data {
82d37c369846atagen2026-10-25 10:55 UTC185
    pub fn scope<'a>(&self, rooms: impl IntoIterator<Item = &'a ruma::OwnedRoomId>) -> Scope {
82d37c369846atagen2026-10-25 10:55 UTC186
        Scope {
82d37c369846atagen2026-10-25 10:55 UTC187
            rooms: rooms
82d37c369846atagen2026-10-25 10:55 UTC188
                .into_iter()
82d37c369846atagen2026-10-25 10:55 UTC189
                .map(|room| {
82d37c369846atagen2026-10-25 10:55 UTC190
                    let mut keys = Vec::new();
82d37c369846atagen2026-10-25 10:55 UTC191
                    RoomScope {
82d37c369846atagen2026-10-25 10:55 UTC192
                        entries: in_room(&self.entries, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC193
                        events: in_room(&self.events, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC194
                        windows: in_room(&self.windows, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC195
                        capture_attempts: in_room(&self.capture_attempts, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC196
                        candidates: in_room(&self.candidates, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC197
                        redacted: in_room(&self.redacted, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC198
                        edits: in_room(&self.edits, room, &mut keys),
82d37c369846atagen2026-10-25 10:55 UTC199
                        keys,
82d37c369846atagen2026-10-25 10:55 UTC200
                    }
82d37c369846atagen2026-10-25 10:55 UTC201
                })
82d37c369846atagen2026-10-25 10:55 UTC202
                .collect(),
82d37c369846atagen2026-10-25 10:55 UTC203
            coverage: self.coverage.clone(),
82d37c369846atagen2026-10-25 10:55 UTC204
            cursors: (
82d37c369846atagen2026-10-25 10:55 UTC205
                self.history_cursor.clone(),
82d37c369846atagen2026-10-25 10:55 UTC206
                self.history_exhausted,
82d37c369846atagen2026-10-25 10:55 UTC207
                self.latest_cursor.clone(),
82d37c369846atagen2026-10-25 10:55 UTC208
                self.latest_anchor.clone(),
82d37c369846atagen2026-10-25 10:55 UTC209
                self.latest_head.clone(),
82d37c369846atagen2026-10-25 10:55 UTC210
            ),
82d37c369846atagen2026-10-25 10:55 UTC211
        }
82d37c369846atagen2026-10-25 10:55 UTC212
    }
82d37c369846atagen2026-10-25 10:55 UTC213
82d37c369846atagen2026-10-25 10:55 UTC214
    /// folds an inbox built in memory while the store was unreachable into the one the
82d37c369846atagen2026-10-25 10:55 UTC215
    /// store held, keeping the newer side where both have a key
82d37c369846atagen2026-10-25 10:55 UTC216
    pub fn absorb(&mut self, newer: Data) {
82d37c369846atagen2026-10-25 10:55 UTC217
        self.revision = self.revision.max(newer.revision);
82d37c369846atagen2026-10-25 10:55 UTC218
        self.entries.extend(newer.entries);
82d37c369846atagen2026-10-25 10:55 UTC219
        self.events.extend(newer.events);
82d37c369846atagen2026-10-25 10:55 UTC220
        self.windows.extend(newer.windows);
82d37c369846atagen2026-10-25 10:55 UTC221
        self.capture_attempts.extend(newer.capture_attempts);
82d37c369846atagen2026-10-25 10:55 UTC222
        self.candidates.extend(newer.candidates);
82d37c369846atagen2026-10-25 10:55 UTC223
        self.redacted.extend(newer.redacted);
82d37c369846atagen2026-10-25 10:55 UTC224
        self.edits.extend(newer.edits);
82d37c369846atagen2026-10-25 10:55 UTC225
        for gap in newer.coverage.gaps {
82d37c369846atagen2026-10-25 10:55 UTC226
            self.gap(gap.room_id, gap.reason, gap.token);
82d37c369846atagen2026-10-25 10:55 UTC227
        }
82d37c369846atagen2026-10-25 10:55 UTC228
        self.coverage.earliest_timestamp_ms = self
82d37c369846atagen2026-10-25 10:55 UTC229
            .coverage
82d37c369846atagen2026-10-25 10:55 UTC230
            .earliest_timestamp_ms
82d37c369846atagen2026-10-25 10:55 UTC231
            .into_iter()
82d37c369846atagen2026-10-25 10:55 UTC232
            .chain(newer.coverage.earliest_timestamp_ms)
82d37c369846atagen2026-10-25 10:55 UTC233
            .min();
82d37c369846atagen2026-10-25 10:55 UTC234
        self.coverage.truncated |= newer.coverage.truncated;
82d37c369846atagen2026-10-25 10:55 UTC235
        self.history_cursor = newer.history_cursor.or(self.history_cursor.take());
82d37c369846atagen2026-10-25 10:55 UTC236
        self.latest_cursor = newer.latest_cursor.or(self.latest_cursor.take());
82d37c369846atagen2026-10-25 10:55 UTC237
        self.latest_anchor = newer.latest_anchor.or(self.latest_anchor.take());
82d37c369846atagen2026-10-25 10:55 UTC238
        self.latest_head = newer.latest_head.or(self.latest_head.take());
82d37c369846atagen2026-10-25 10:55 UTC239
    }
82d37c369846atagen2026-10-25 10:55 UTC240
7cdaa86b9489atagen2026-10-25 08:43 UTC241
    pub fn gap(
7cdaa86b9489atagen2026-10-25 08:43 UTC242
        &mut self,
7cdaa86b9489atagen2026-10-25 08:43 UTC243
        room_id: Option<ruma::OwnedRoomId>,
7cdaa86b9489atagen2026-10-25 08:43 UTC244
        reason: MentionGapReason,
7cdaa86b9489atagen2026-10-25 08:43 UTC245
        token: Option<String>,
7cdaa86b9489atagen2026-10-25 08:43 UTC246
    ) {
7cdaa86b9489atagen2026-10-25 08:43 UTC247
        if self
7cdaa86b9489atagen2026-10-25 08:43 UTC248
            .coverage
7cdaa86b9489atagen2026-10-25 08:43 UTC249
            .gaps
7cdaa86b9489atagen2026-10-25 08:43 UTC250
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC251
            .any(|gap| gap.room_id == room_id && gap.reason == reason && gap.token == token)
7cdaa86b9489atagen2026-10-25 08:43 UTC252
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC253
            return;
7cdaa86b9489atagen2026-10-25 08:43 UTC254
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC255
        self.coverage.gaps.push(MentionGap {
7cdaa86b9489atagen2026-10-25 08:43 UTC256
            room_id,
7cdaa86b9489atagen2026-10-25 08:43 UTC257
            reason,
7cdaa86b9489atagen2026-10-25 08:43 UTC258
            token,
7cdaa86b9489atagen2026-10-25 08:43 UTC259
        });
7cdaa86b9489atagen2026-10-25 08:43 UTC260
        if self.coverage.gaps.len() > 128 {
7cdaa86b9489atagen2026-10-25 08:43 UTC261
            self.coverage.gaps.remove(0);
7cdaa86b9489atagen2026-10-25 08:43 UTC262
            self.coverage.truncated = true;
7cdaa86b9489atagen2026-10-25 08:43 UTC263
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC264
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC265
7cdaa86b9489atagen2026-10-25 08:43 UTC266
    pub fn expire_candidates(&mut self, now_ms: u64) -> bool {
7cdaa86b9489atagen2026-10-25 08:43 UTC267
        let expired: Vec<_> = self
7cdaa86b9489atagen2026-10-25 08:43 UTC268
            .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC269
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC270
            .filter(|(_, candidate)| {
7cdaa86b9489atagen2026-10-25 08:43 UTC271
                now_ms.saturating_sub(candidate.first_seen_ms) > CANDIDATE_LIFETIME_MS
7cdaa86b9489atagen2026-10-25 08:43 UTC272
            })
7cdaa86b9489atagen2026-10-25 08:43 UTC273
            .map(|(key, candidate)| (key.clone(), candidate.room_id.clone()))
7cdaa86b9489atagen2026-10-25 08:43 UTC274
            .collect();
7cdaa86b9489atagen2026-10-25 08:43 UTC275
        for (key, room_id) in &expired {
7cdaa86b9489atagen2026-10-25 08:43 UTC276
            self.candidates.remove(key);
7cdaa86b9489atagen2026-10-25 08:43 UTC277
            self.gap(
7cdaa86b9489atagen2026-10-25 08:43 UTC278
                Some(room_id.clone()),
7cdaa86b9489atagen2026-10-25 08:43 UTC279
                MentionGapReason::CandidateExpired,
7cdaa86b9489atagen2026-10-25 08:43 UTC280
                None,
7cdaa86b9489atagen2026-10-25 08:43 UTC281
            );
7cdaa86b9489atagen2026-10-25 08:43 UTC282
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC283
        !expired.is_empty()
7cdaa86b9489atagen2026-10-25 08:43 UTC284
    }
7cdaa86b9489atagen2026-10-25 08:43 UTC285
cbb715e11963atagen2026-10-24 23:24 UTC286
    pub fn touch_contexts(&mut self, identity: &str) {
cbb715e11963atagen2026-10-24 23:24 UTC287
        for (key, window) in &self.windows {
cbb715e11963atagen2026-10-24 23:24 UTC288
            if window.target.as_deref() == Some(identity)
cbb715e11963atagen2026-10-24 23:24 UTC289
                || window
cbb715e11963atagen2026-10-24 23:24 UTC290
                    .before
cbb715e11963atagen2026-10-24 23:24 UTC291
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC292
                    .chain(&window.after)
cbb715e11963atagen2026-10-24 23:24 UTC293
                    .any(|key| key == identity)
cbb715e11963atagen2026-10-24 23:24 UTC294
            {
cbb715e11963atagen2026-10-24 23:24 UTC295
                if let Some(entry) = self.entries.get_mut(key) {
cbb715e11963atagen2026-10-24 23:24 UTC296
                    entry.revision = self.revision + 1;
cbb715e11963atagen2026-10-24 23:24 UTC297
                }
cbb715e11963atagen2026-10-24 23:24 UTC298
            }
cbb715e11963atagen2026-10-24 23:24 UTC299
        }
cbb715e11963atagen2026-10-24 23:24 UTC300
    }
cbb715e11963atagen2026-10-24 23:24 UTC301
cbb715e11963atagen2026-10-24 23:24 UTC302
    pub fn page(
cbb715e11963atagen2026-10-24 23:24 UTC303
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC304
        account: &OwnedUserId,
cbb715e11963atagen2026-10-24 23:24 UTC305
        cursor: Option<&str>,
cbb715e11963atagen2026-10-24 23:24 UTC306
        limit: u16,
038cc6faeb14atagen2026-10-26 02:05 UTC307
        hidden: &BTreeSet<OwnedRoomId>,
cbb715e11963atagen2026-10-24 23:24 UTC308
    ) -> Result<MentionsPage, Error> {
cbb715e11963atagen2026-10-24 23:24 UTC309
        let (boundary, last) = if let Some(cursor) = cursor {
cbb715e11963atagen2026-10-24 23:24 UTC310
            let value: Value =
cbb715e11963atagen2026-10-24 23:24 UTC311
                serde_json::from_str(cursor).map_err(|_| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC312
            if value["account"].as_str() != Some(account.as_str()) {
cbb715e11963atagen2026-10-24 23:24 UTC313
                return Err(invalid("mentions cursor belongs to a different account"));
cbb715e11963atagen2026-10-24 23:24 UTC314
            }
cbb715e11963atagen2026-10-24 23:24 UTC315
            let boundary = value["revision"]
cbb715e11963atagen2026-10-24 23:24 UTC316
                .as_u64()
cbb715e11963atagen2026-10-24 23:24 UTC317
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC318
            let timestamp = value["timestamp"]
cbb715e11963atagen2026-10-24 23:24 UTC319
                .as_u64()
cbb715e11963atagen2026-10-24 23:24 UTC320
                .ok_or_else(|| invalid("invalid mentions cursor"))?;
cbb715e11963atagen2026-10-24 23:24 UTC321
            let key = value["key"]
cbb715e11963atagen2026-10-24 23:24 UTC322
                .as_str()
cbb715e11963atagen2026-10-24 23:24 UTC323
                .ok_or_else(|| invalid("invalid mentions cursor"))?
cbb715e11963atagen2026-10-24 23:24 UTC324
                .to_owned();
cbb715e11963atagen2026-10-24 23:24 UTC325
            if boundary > self.revision {
cbb715e11963atagen2026-10-24 23:24 UTC326
                return Err(invalid("mentions cursor is newer than this store"));
cbb715e11963atagen2026-10-24 23:24 UTC327
            }
cbb715e11963atagen2026-10-24 23:24 UTC328
            (boundary, Some((timestamp, key)))
cbb715e11963atagen2026-10-24 23:24 UTC329
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC330
            (self.revision, None)
cbb715e11963atagen2026-10-24 23:24 UTC331
        };
cbb715e11963atagen2026-10-24 23:24 UTC332
        let mut entries: Vec<_> = self
cbb715e11963atagen2026-10-24 23:24 UTC333
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC334
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC335
            .filter(|(key, entry)| {
038cc6faeb14atagen2026-10-26 02:05 UTC336
                !hidden.contains(&entry.key.room_id)
038cc6faeb14atagen2026-10-26 02:05 UTC337
                    && entry.discovery_revision <= boundary
cbb715e11963atagen2026-10-24 23:24 UTC338
                    && last.as_ref().is_none_or(|last| {
cbb715e11963atagen2026-10-24 23:24 UTC339
                        (entry.timestamp_ms, key.as_str()) < (last.0, last.1.as_str())
cbb715e11963atagen2026-10-24 23:24 UTC340
                    })
cbb715e11963atagen2026-10-24 23:24 UTC341
            })
cbb715e11963atagen2026-10-24 23:24 UTC342
            .map(|(_, entry)| entry.clone())
cbb715e11963atagen2026-10-24 23:24 UTC343
            .collect();
cbb715e11963atagen2026-10-24 23:24 UTC344
        entries.sort_by(|a, b| (b.timestamp_ms, id(&b.key)).cmp(&(a.timestamp_ms, id(&a.key))));
cbb715e11963atagen2026-10-24 23:24 UTC345
        let limit = usize::from(limit.clamp(1, 50));
cbb715e11963atagen2026-10-24 23:24 UTC346
        let end_reached = entries.len() <= limit;
cbb715e11963atagen2026-10-24 23:24 UTC347
        entries.truncate(limit);
cbb715e11963atagen2026-10-24 23:24 UTC348
        let next_cursor = if end_reached {
cbb715e11963atagen2026-10-24 23:24 UTC349
            None
cbb715e11963atagen2026-10-24 23:24 UTC350
        } else {
cbb715e11963atagen2026-10-24 23:24 UTC351
            entries.last().map(|entry| serde_json::json!({
cbb715e11963atagen2026-10-24 23:24 UTC352
            "account": account.as_str(), "revision": boundary, "timestamp": entry.timestamp_ms, "key": id(&entry.key)
cbb715e11963atagen2026-10-24 23:24 UTC353
        }).to_string())
cbb715e11963atagen2026-10-24 23:24 UTC354
        };
cbb715e11963atagen2026-10-24 23:24 UTC355
        let mut coverage = self.coverage.clone();
038cc6faeb14atagen2026-10-26 02:05 UTC356
        coverage.gaps.retain(|gap| {
038cc6faeb14atagen2026-10-26 02:05 UTC357
            gap.room_id
038cc6faeb14atagen2026-10-26 02:05 UTC358
                .as_ref()
038cc6faeb14atagen2026-10-26 02:05 UTC359
                .is_none_or(|room_id| !hidden.contains(room_id))
038cc6faeb14atagen2026-10-26 02:05 UTC360
        });
038cc6faeb14atagen2026-10-26 02:05 UTC361
        coverage.unresolved_candidates = self
038cc6faeb14atagen2026-10-26 02:05 UTC362
            .candidates
038cc6faeb14atagen2026-10-26 02:05 UTC363
            .values()
038cc6faeb14atagen2026-10-26 02:05 UTC364
            .filter(|candidate| !hidden.contains(&candidate.room_id))
038cc6faeb14atagen2026-10-26 02:05 UTC365
            .count();
cbb715e11963atagen2026-10-24 23:24 UTC366
        Ok(MentionsPage {
cbb715e11963atagen2026-10-24 23:24 UTC367
            entries,
cbb715e11963atagen2026-10-24 23:24 UTC368
            next_cursor,
cbb715e11963atagen2026-10-24 23:24 UTC369
            end_reached,
cbb715e11963atagen2026-10-24 23:24 UTC370
            coverage,
038cc6faeb14atagen2026-10-26 02:05 UTC371
            unread_count: self
038cc6faeb14atagen2026-10-26 02:05 UTC372
                .entries
038cc6faeb14atagen2026-10-26 02:05 UTC373
                .values()
038cc6faeb14atagen2026-10-26 02:05 UTC374
                .filter(|entry| !entry.read && !hidden.contains(&entry.key.room_id))
038cc6faeb14atagen2026-10-26 02:05 UTC375
                .count(),
cbb715e11963atagen2026-10-24 23:24 UTC376
            revision: self.revision,
cbb715e11963atagen2026-10-24 23:24 UTC377
            boundary: MentionsBoundary {
cbb715e11963atagen2026-10-24 23:24 UTC378
                account: account.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC379
                revision: boundary,
cbb715e11963atagen2026-10-24 23:24 UTC380
            },
cbb715e11963atagen2026-10-24 23:24 UTC381
        })
cbb715e11963atagen2026-10-24 23:24 UTC382
    }
cbb715e11963atagen2026-10-24 23:24 UTC383
cbb715e11963atagen2026-10-24 23:24 UTC384
    pub fn context(
cbb715e11963atagen2026-10-24 23:24 UTC385
        &self,
cbb715e11963atagen2026-10-24 23:24 UTC386
        key: &MentionKey,
cbb715e11963atagen2026-10-24 23:24 UTC387
    ) -> Result<(SavedMentionContext, Vec<MediaRegistration>), Error> {
cbb715e11963atagen2026-10-24 23:24 UTC388
        let entry = self
cbb715e11963atagen2026-10-24 23:24 UTC389
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC390
            .get(&id(key))
cbb715e11963atagen2026-10-24 23:24 UTC391
            .ok_or_else(|| Error::new(ErrorKind::EventNotFound, "mention was not found"))?;
cbb715e11963atagen2026-10-24 23:24 UTC392
        let mut media = Vec::new();
cbb715e11963atagen2026-10-24 23:24 UTC393
        let mut item = |key: &String| {
cbb715e11963atagen2026-10-24 23:24 UTC394
            self.events.get(key).map(|event| {
cbb715e11963atagen2026-10-24 23:24 UTC395
                media.extend(event.media.clone());
cbb715e11963atagen2026-10-24 23:24 UTC396
                event.item.clone()
cbb715e11963atagen2026-10-24 23:24 UTC397
            })
cbb715e11963atagen2026-10-24 23:24 UTC398
        };
258c08ec2168atagen2026-10-25 09:15 UTC399
        let context = self.windows.get(&id(key)).and_then(|window| {
258c08ec2168atagen2026-10-25 09:15 UTC400
            let event = window.target.as_ref().and_then(&mut item)?;
258c08ec2168atagen2026-10-25 09:15 UTC401
            Some(EventContext {
258c08ec2168atagen2026-10-25 09:15 UTC402
                events_before: window.before.iter().filter_map(&mut item).collect(),
258c08ec2168atagen2026-10-25 09:15 UTC403
                event,
258c08ec2168atagen2026-10-25 09:15 UTC404
                events_after: window.after.iter().filter_map(&mut item).collect(),
258c08ec2168atagen2026-10-25 09:15 UTC405
                start_token: window.start.clone(),
258c08ec2168atagen2026-10-25 09:15 UTC406
                end_token: window.end.clone(),
258c08ec2168atagen2026-10-25 09:15 UTC407
            })
cbb715e11963atagen2026-10-24 23:24 UTC408
        });
cbb715e11963atagen2026-10-24 23:24 UTC409
        Ok((
cbb715e11963atagen2026-10-24 23:24 UTC410
            SavedMentionContext {
cbb715e11963atagen2026-10-24 23:24 UTC411
                key: key.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC412
                state: entry.capture.clone(),
cbb715e11963atagen2026-10-24 23:24 UTC413
                context,
cbb715e11963atagen2026-10-24 23:24 UTC414
                revision: self.revision,
cbb715e11963atagen2026-10-24 23:24 UTC415
            },
cbb715e11963atagen2026-10-24 23:24 UTC416
            media,
cbb715e11963atagen2026-10-24 23:24 UTC417
        ))
cbb715e11963atagen2026-10-24 23:24 UTC418
    }
cbb715e11963atagen2026-10-24 23:24 UTC419
7cdaa86b9489atagen2026-10-25 08:43 UTC420
    pub fn collect_events(&mut self) -> usize {
cbb715e11963atagen2026-10-24 23:24 UTC421
        let mut used: BTreeSet<String> = self.entries.keys().cloned().collect();
cbb715e11963atagen2026-10-24 23:24 UTC422
        for window in self.windows.values() {
cbb715e11963atagen2026-10-24 23:24 UTC423
            used.extend(window.before.iter().chain(&window.after).cloned());
cbb715e11963atagen2026-10-24 23:24 UTC424
            used.extend(window.target.iter().cloned());
cbb715e11963atagen2026-10-24 23:24 UTC425
        }
7cdaa86b9489atagen2026-10-25 08:43 UTC426
        let mut freed = 0;
7cdaa86b9489atagen2026-10-25 08:43 UTC427
        self.events.retain(|key, event| {
7cdaa86b9489atagen2026-10-25 08:43 UTC428
            let keep = used.contains(key);
7cdaa86b9489atagen2026-10-25 08:43 UTC429
            if !keep {
7cdaa86b9489atagen2026-10-25 08:43 UTC430
                freed += key.len() + json_len(event);
7cdaa86b9489atagen2026-10-25 08:43 UTC431
            }
7cdaa86b9489atagen2026-10-25 08:43 UTC432
            keep
7cdaa86b9489atagen2026-10-25 08:43 UTC433
        });
cbb715e11963atagen2026-10-24 23:24 UTC434
        self.capture_attempts
cbb715e11963atagen2026-10-24 23:24 UTC435
            .retain(|key, _| self.entries.contains_key(key));
7cdaa86b9489atagen2026-10-25 08:43 UTC436
        freed
cbb715e11963atagen2026-10-24 23:24 UTC437
    }
cbb715e11963atagen2026-10-24 23:24 UTC438
7cdaa86b9489atagen2026-10-25 08:43 UTC439
    /// returns roughly how many plaintext JSON bytes the eviction freed
7cdaa86b9489atagen2026-10-25 08:43 UTC440
    pub fn evict_one(&mut self) -> Option<usize> {
cbb715e11963atagen2026-10-24 23:24 UTC441
        let oldest = |keys: Vec<String>, entries: &BTreeMap<String, MentionEntry>| {
cbb715e11963atagen2026-10-24 23:24 UTC442
            keys.into_iter()
cbb715e11963atagen2026-10-24 23:24 UTC443
                .min_by_key(|key| entries.get(key).map_or(0, |entry| entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC444
        };
7cdaa86b9489atagen2026-10-25 08:43 UTC445
        let freed = if let Some(key) = oldest(
cbb715e11963atagen2026-10-24 23:24 UTC446
            self.windows
cbb715e11963atagen2026-10-24 23:24 UTC447
                .keys()
cbb715e11963atagen2026-10-24 23:24 UTC448
                .filter(|key| self.entries.get(*key).is_some_and(|entry| entry.read))
cbb715e11963atagen2026-10-24 23:24 UTC449
                .cloned()
cbb715e11963atagen2026-10-24 23:24 UTC450
                .collect(),
cbb715e11963atagen2026-10-24 23:24 UTC451
            &self.entries,
cbb715e11963atagen2026-10-24 23:24 UTC452
        )
cbb715e11963atagen2026-10-24 23:24 UTC453
        .or_else(|| oldest(self.windows.keys().cloned().collect(), &self.entries))
cbb715e11963atagen2026-10-24 23:24 UTC454
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC455
            let window = self.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC456
            if let Some(entry) = self.entries.get_mut(&key) {
cbb715e11963atagen2026-10-24 23:24 UTC457
                entry.capture = MentionCaptureState::Unavailable;
cbb715e11963atagen2026-10-24 23:24 UTC458
                entry.revision = self.revision;
cbb715e11963atagen2026-10-24 23:24 UTC459
            }
7cdaa86b9489atagen2026-10-25 08:43 UTC460
            key.len() + json_len(&window)
cbb715e11963atagen2026-10-24 23:24 UTC461
        } else if let Some(key) = self
cbb715e11963atagen2026-10-24 23:24 UTC462
            .entries
cbb715e11963atagen2026-10-24 23:24 UTC463
            .iter()
cbb715e11963atagen2026-10-24 23:24 UTC464
            .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC465
            .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC466
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC467
            key.len() + json_len(&self.entries.remove(&key))
7cdaa86b9489atagen2026-10-25 08:43 UTC468
        } else if let Some(key) = self
7cdaa86b9489atagen2026-10-25 08:43 UTC469
            .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC470
            .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC471
            .min_by_key(|(_, candidate)| timestamp(&candidate.raw))
7cdaa86b9489atagen2026-10-25 08:43 UTC472
            .map(|(key, _)| key.clone())
7cdaa86b9489atagen2026-10-25 08:43 UTC473
        {
7cdaa86b9489atagen2026-10-25 08:43 UTC474
            key.len() + json_len(&self.candidates.remove(&key))
cbb715e11963atagen2026-10-24 23:24 UTC475
        } else if let Some(key) = self.edits.keys().next().cloned() {
7cdaa86b9489atagen2026-10-25 08:43 UTC476
            key.len() + json_len(&self.edits.remove(&key))
cbb715e11963atagen2026-10-24 23:24 UTC477
        } else {
7cdaa86b9489atagen2026-10-25 08:43 UTC478
            let key = self.redacted.keys().next().cloned()?;
7cdaa86b9489atagen2026-10-25 08:43 UTC479
            self.redacted.remove(&key);
7cdaa86b9489atagen2026-10-25 08:43 UTC480
            key.len() + 24
7cdaa86b9489atagen2026-10-25 08:43 UTC481
        };
cbb715e11963atagen2026-10-24 23:24 UTC482
        self.coverage.truncated = true;
7cdaa86b9489atagen2026-10-25 08:43 UTC483
        Some(freed + self.collect_events())
cbb715e11963atagen2026-10-24 23:24 UTC484
    }
cbb715e11963atagen2026-10-24 23:24 UTC485
cbb715e11963atagen2026-10-24 23:24 UTC486
    pub fn bound(&mut self, entries: usize, events: usize, candidates: usize) {
cbb715e11963atagen2026-10-24 23:24 UTC487
        while self.entries.len() > entries {
cbb715e11963atagen2026-10-24 23:24 UTC488
            if let Some(key) = self
cbb715e11963atagen2026-10-24 23:24 UTC489
                .entries
cbb715e11963atagen2026-10-24 23:24 UTC490
                .iter()
cbb715e11963atagen2026-10-24 23:24 UTC491
                .min_by_key(|(_, entry)| (!entry.read, entry.timestamp_ms))
cbb715e11963atagen2026-10-24 23:24 UTC492
                .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC493
            {
cbb715e11963atagen2026-10-24 23:24 UTC494
                self.entries.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC495
                self.windows.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC496
                self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC497
            }
cbb715e11963atagen2026-10-24 23:24 UTC498
        }
cbb715e11963atagen2026-10-24 23:24 UTC499
        self.collect_events();
7cdaa86b9489atagen2026-10-25 08:43 UTC500
        while self.events.len() > events && self.evict_one().is_some() {}
7cdaa86b9489atagen2026-10-25 08:43 UTC501
        if self.candidates.len() > candidates {
7cdaa86b9489atagen2026-10-25 08:43 UTC502
            let mut oldest: Vec<_> = self
7cdaa86b9489atagen2026-10-25 08:43 UTC503
                .candidates
7cdaa86b9489atagen2026-10-25 08:43 UTC504
                .iter()
7cdaa86b9489atagen2026-10-25 08:43 UTC505
                .map(|(key, candidate)| (timestamp(&candidate.raw), key.clone()))
7cdaa86b9489atagen2026-10-25 08:43 UTC506
                .collect();
7cdaa86b9489atagen2026-10-25 08:43 UTC507
            oldest.sort();
7cdaa86b9489atagen2026-10-25 08:43 UTC508
            for (_, key) in oldest.into_iter().take(self.candidates.len() - candidates) {
7cdaa86b9489atagen2026-10-25 08:43 UTC509
                self.candidates.remove(&key);
7cdaa86b9489atagen2026-10-25 08:43 UTC510
            }
cbb715e11963atagen2026-10-24 23:24 UTC511
            self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC512
        }
cbb715e11963atagen2026-10-24 23:24 UTC513
        for map in [&mut self.redacted] {
cbb715e11963atagen2026-10-24 23:24 UTC514
            while map.len() > entries + candidates {
cbb715e11963atagen2026-10-24 23:24 UTC515
                let key = map
cbb715e11963atagen2026-10-24 23:24 UTC516
                    .iter()
cbb715e11963atagen2026-10-24 23:24 UTC517
                    .min_by_key(|(_, timestamp)| *timestamp)
cbb715e11963atagen2026-10-24 23:24 UTC518
                    .map(|(key, _)| key.clone())
cbb715e11963atagen2026-10-24 23:24 UTC519
                    .unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC520
                map.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC521
                self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC522
            }
cbb715e11963atagen2026-10-24 23:24 UTC523
        }
cbb715e11963atagen2026-10-24 23:24 UTC524
        while self.edits.len() > candidates {
cbb715e11963atagen2026-10-24 23:24 UTC525
            let key = self.edits.keys().next().cloned().unwrap();
cbb715e11963atagen2026-10-24 23:24 UTC526
            self.edits.remove(&key);
cbb715e11963atagen2026-10-24 23:24 UTC527
            self.coverage.truncated = true;
cbb715e11963atagen2026-10-24 23:24 UTC528
        }
cbb715e11963atagen2026-10-24 23:24 UTC529
        self.coverage.unresolved_candidates = self.candidates.len();
cbb715e11963atagen2026-10-24 23:24 UTC530
    }
cbb715e11963atagen2026-10-24 23:24 UTC531
}