atagen

macltok

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

Blame: src/engine/watchers.rs

411bc1c78b65atagen2026-10-22 07:39 UTC1
use std::{
411bc1c78b65atagen2026-10-22 07:39 UTC2
    collections::{HashSet, VecDeque},
411bc1c78b65atagen2026-10-22 07:39 UTC3
    sync::Arc,
411bc1c78b65atagen2026-10-22 07:39 UTC4
    time::Duration,
411bc1c78b65atagen2026-10-22 07:39 UTC5
};
31ce2453c82fatagen2026-09-24 13:07 UTC6
31ce2453c82fatagen2026-09-24 13:07 UTC7
use futures_util::StreamExt;
31ce2453c82fatagen2026-09-24 13:07 UTC8
use matrix_sdk::{
411bc1c78b65atagen2026-10-22 07:39 UTC9
    Client,
31ce2453c82fatagen2026-09-24 13:07 UTC10
    ruma::events::{AnyMessageLikeEventContent, room::message::MessageType},
0bfa3f46f0beatagen2026-09-26 09:12 UTC11
    send_queue::{LocalEchoContent, RoomSendQueueUpdate, SendQueueUpdate},
31ce2453c82fatagen2026-09-24 13:07 UTC12
};
17e042fbc331atagen2026-10-08 12:39 UTC13
use matrix_sdk_base::RoomInfoNotableUpdateReasons;
31ce2453c82fatagen2026-09-24 13:07 UTC14
use matrix_sdk_ui::sync_service::{State as SyncState, SyncService};
17e042fbc331atagen2026-10-08 12:39 UTC15
use ruma::{
17e042fbc331atagen2026-10-08 12:39 UTC16
    OwnedRoomId, OwnedTransactionId,
17e042fbc331atagen2026-10-08 12:39 UTC17
    events::{AnyGlobalAccountDataEvent, AnySyncStateEvent},
17e042fbc331atagen2026-10-08 12:39 UTC18
    serde::Raw,
17e042fbc331atagen2026-10-08 12:39 UTC19
    time::Instant,
17e042fbc331atagen2026-10-08 12:39 UTC20
};
411bc1c78b65atagen2026-10-22 07:39 UTC21
use tokio::sync::{Notify, broadcast::error::RecvError, watch};
31ce2453c82fatagen2026-09-24 13:07 UTC22
f12377bce9b9atagen2026-09-26 12:32 UTC23
use super::executor::TaskSet;
31ce2453c82fatagen2026-09-24 13:07 UTC24
use crate::{
17e042fbc331atagen2026-10-08 12:39 UTC25
    ConnectionState, Event, UploadOutcome,
17e042fbc331atagen2026-10-08 12:39 UTC26
    encryption::encryption_state,
17e042fbc331atagen2026-10-08 12:39 UTC27
    error::classify_sdk_error,
17e042fbc331atagen2026-10-08 12:39 UTC28
    normalise::{ENABLED_ROOMS, ROOM_PACKS, USER_PACK},
f12377bce9b9atagen2026-09-26 12:32 UTC29
    transport::EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC30
};
31ce2453c82fatagen2026-09-24 13:07 UTC31
31ce2453c82fatagen2026-09-24 13:07 UTC32
const SYNC_RESTART_MIN: Duration = Duration::from_secs(1);
31ce2453c82fatagen2026-09-24 13:07 UTC33
const SYNC_RESTART_MAX: Duration = Duration::from_secs(60);
31ce2453c82fatagen2026-09-24 13:07 UTC34
const SEND_QUEUE_RETRY: Duration = Duration::from_secs(5);
5a52012c9b1datagen2026-09-24 23:45 UTC35
const SYNC_STABLE: Duration = Duration::from_secs(30);
5a52012c9b1datagen2026-09-24 23:45 UTC36
5a52012c9b1datagen2026-09-24 23:45 UTC37
pub(super) async fn room_subscriptions(
5a52012c9b1datagen2026-09-24 23:45 UTC38
    sync: Arc<SyncService>,
5a52012c9b1datagen2026-09-24 23:45 UTC39
    mut rooms: watch::Receiver<Vec<ruma::OwnedRoomId>>,
5a52012c9b1datagen2026-09-24 23:45 UTC40
) {
5a52012c9b1datagen2026-09-24 23:45 UTC41
    while rooms.changed().await.is_ok() {
5a52012c9b1datagen2026-09-24 23:45 UTC42
        let room_ids = rooms.borrow_and_update().clone();
5a52012c9b1datagen2026-09-24 23:45 UTC43
        let room_ids = room_ids.iter().map(AsRef::as_ref).collect::<Vec<_>>();
5a52012c9b1datagen2026-09-24 23:45 UTC44
        sync.room_list_service().subscribe_to_rooms(&room_ids).await;
5a52012c9b1datagen2026-09-24 23:45 UTC45
    }
5a52012c9b1datagen2026-09-24 23:45 UTC46
}
31ce2453c82fatagen2026-09-24 13:07 UTC47
31ce2453c82fatagen2026-09-24 13:07 UTC48
/// `states` must be taken before sync starts, a late subscriber never hears the transition it missed
31ce2453c82fatagen2026-09-24 13:07 UTC49
pub(super) async fn connection(
31ce2453c82fatagen2026-09-24 13:07 UTC50
    mut states: impl futures_util::Stream<Item = SyncState> + Unpin,
31ce2453c82fatagen2026-09-24 13:07 UTC51
    sync: Arc<SyncService>,
31ce2453c82fatagen2026-09-24 13:07 UTC52
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC53
    events: EventSender,
31ce2453c82fatagen2026-09-24 13:07 UTC54
    connection: watch::Sender<ConnectionState>,
31ce2453c82fatagen2026-09-24 13:07 UTC55
) {
31ce2453c82fatagen2026-09-24 13:07 UTC56
    let mut backoff = SYNC_RESTART_MIN;
5a52012c9b1datagen2026-09-24 23:45 UTC57
    let mut online_since: Option<Instant> = None;
31ce2453c82fatagen2026-09-24 13:07 UTC58
    while let Some(state) = states.next().await {
5a52012c9b1datagen2026-09-24 23:45 UTC59
        // only a run that lasted counts as recovered, or a server that answers /versions
5a52012c9b1datagen2026-09-24 23:45 UTC60
        // but refuses sync would have us bounce between offline and online forever
5a52012c9b1datagen2026-09-24 23:45 UTC61
        let stable = online_since
5a52012c9b1datagen2026-09-24 23:45 UTC62
            .take()
5a52012c9b1datagen2026-09-24 23:45 UTC63
            .map(|since| since.elapsed() >= SYNC_STABLE);
5a52012c9b1datagen2026-09-24 23:45 UTC64
        if stable == Some(true) {
5a52012c9b1datagen2026-09-24 23:45 UTC65
            backoff = SYNC_RESTART_MIN;
5a52012c9b1datagen2026-09-24 23:45 UTC66
        }
31ce2453c82fatagen2026-09-24 13:07 UTC67
        let (state, restart) = match state {
5a52012c9b1datagen2026-09-24 23:45 UTC68
            // only ever us stopping it for a restart, nothing a client needs to hear
5a52012c9b1datagen2026-09-24 23:45 UTC69
            SyncState::Idle => continue,
31ce2453c82fatagen2026-09-24 13:07 UTC70
            SyncState::Running => {
5a52012c9b1datagen2026-09-24 23:45 UTC71
                online_since = Some(Instant::now());
31ce2453c82fatagen2026-09-24 13:07 UTC72
                // the sdk parks a room queue on any send error and never unparks it
31ce2453c82fatagen2026-09-24 13:07 UTC73
                client.send_queue().set_enabled(true).await;
31ce2453c82fatagen2026-09-24 13:07 UTC74
                (ConnectionState::Online, false)
31ce2453c82fatagen2026-09-24 13:07 UTC75
            }
5a52012c9b1datagen2026-09-24 23:45 UTC76
            SyncState::Offline => (ConnectionState::Offline, stable == Some(false)),
31ce2453c82fatagen2026-09-24 13:07 UTC77
            SyncState::Terminated => (ConnectionState::Offline, true),
31ce2453c82fatagen2026-09-24 13:07 UTC78
            SyncState::Error(error) => {
31ce2453c82fatagen2026-09-24 13:07 UTC79
                tracing::warn!(%error, "sync service reported an error");
31ce2453c82fatagen2026-09-24 13:07 UTC80
                (ConnectionState::Offline, true)
31ce2453c82fatagen2026-09-24 13:07 UTC81
            }
31ce2453c82fatagen2026-09-24 13:07 UTC82
        };
31ce2453c82fatagen2026-09-24 13:07 UTC83
        if *connection.borrow() != state {
31ce2453c82fatagen2026-09-24 13:07 UTC84
            connection.send_replace(state);
31ce2453c82fatagen2026-09-24 13:07 UTC85
            if events.send(Event::ConnectionChanged(state)).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC86
                return;
31ce2453c82fatagen2026-09-24 13:07 UTC87
            }
31ce2453c82fatagen2026-09-24 13:07 UTC88
        }
31ce2453c82fatagen2026-09-24 13:07 UTC89
        if restart {
5a52012c9b1datagen2026-09-24 23:45 UTC90
            sync.stop().await;
1fe480c16a3catagen2026-09-26 08:44 UTC91
            matrix_sdk::sleep::sleep(backoff).await;
31ce2453c82fatagen2026-09-24 13:07 UTC92
            backoff = (backoff * 2).min(SYNC_RESTART_MAX);
31ce2453c82fatagen2026-09-24 13:07 UTC93
            sync.start().await;
31ce2453c82fatagen2026-09-24 13:07 UTC94
        }
31ce2453c82fatagen2026-09-24 13:07 UTC95
    }
31ce2453c82fatagen2026-09-24 13:07 UTC96
}
31ce2453c82fatagen2026-09-24 13:07 UTC97
31ce2453c82fatagen2026-09-24 13:07 UTC98
/// a recoverable failure while we're online is a hiccup, so give the room another go
31ce2453c82fatagen2026-09-24 13:07 UTC99
pub(super) async fn send_queue_errors(
31ce2453c82fatagen2026-09-24 13:07 UTC100
    client: Client,
31ce2453c82fatagen2026-09-24 13:07 UTC101
    mut errors: tokio::sync::broadcast::Receiver<matrix_sdk::send_queue::SendQueueRoomError>,
31ce2453c82fatagen2026-09-24 13:07 UTC102
    online: watch::Receiver<ConnectionState>,
31ce2453c82fatagen2026-09-24 13:07 UTC103
) {
1fe480c16a3catagen2026-09-26 08:44 UTC104
    let mut retries = TaskSet::new();
31ce2453c82fatagen2026-09-24 13:07 UTC105
    loop {
31ce2453c82fatagen2026-09-24 13:07 UTC106
        let error = tokio::select! {
31ce2453c82fatagen2026-09-24 13:07 UTC107
            error = errors.recv() => error,
31ce2453c82fatagen2026-09-24 13:07 UTC108
            Some(_) = retries.join_next(), if !retries.is_empty() => continue,
31ce2453c82fatagen2026-09-24 13:07 UTC109
        };
31ce2453c82fatagen2026-09-24 13:07 UTC110
        match error {
31ce2453c82fatagen2026-09-24 13:07 UTC111
            Ok(error) if error.is_recoverable => {
31ce2453c82fatagen2026-09-24 13:07 UTC112
                let client = client.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC113
                let online = online.clone();
31ce2453c82fatagen2026-09-24 13:07 UTC114
                retries.spawn(async move {
1fe480c16a3catagen2026-09-26 08:44 UTC115
                    matrix_sdk::sleep::sleep(SEND_QUEUE_RETRY).await;
31ce2453c82fatagen2026-09-24 13:07 UTC116
                    if *online.borrow() == ConnectionState::Online
31ce2453c82fatagen2026-09-24 13:07 UTC117
                        && let Some(room) = client.get_room(&error.room_id)
31ce2453c82fatagen2026-09-24 13:07 UTC118
                    {
31ce2453c82fatagen2026-09-24 13:07 UTC119
                        room.send_queue().set_enabled(true);
31ce2453c82fatagen2026-09-24 13:07 UTC120
                    }
31ce2453c82fatagen2026-09-24 13:07 UTC121
                });
31ce2453c82fatagen2026-09-24 13:07 UTC122
            }
31ce2453c82fatagen2026-09-24 13:07 UTC123
            Ok(_) | Err(RecvError::Lagged(_)) => {}
31ce2453c82fatagen2026-09-24 13:07 UTC124
            Err(RecvError::Closed) => return,
31ce2453c82fatagen2026-09-24 13:07 UTC125
        }
31ce2453c82fatagen2026-09-24 13:07 UTC126
    }
31ce2453c82fatagen2026-09-24 13:07 UTC127
}
31ce2453c82fatagen2026-09-24 13:07 UTC128
7c26fad34822atagen2026-09-26 09:41 UTC129
/// the one place upload events leave from, so an upload is never reported after it
411bc1c78b65atagen2026-10-22 07:39 UTC130
/// ended. the client-wide channel exists before any upload does. it holds 32 updates and
411bc1c78b65atagen2026-10-22 07:39 UTC131
/// progress ticks share it, so this keeps draining while the event stream is full
0bfa3f46f0beatagen2026-09-26 09:12 UTC132
pub(super) async fn uploads(
5a52012c9b1datagen2026-09-24 23:45 UTC133
    client: Client,
0bfa3f46f0beatagen2026-09-26 09:12 UTC134
    mut updates: tokio::sync::broadcast::Receiver<SendQueueUpdate>,
5a52012c9b1datagen2026-09-24 23:45 UTC135
    events: EventSender,
5a52012c9b1datagen2026-09-24 23:45 UTC136
) {
0bfa3f46f0beatagen2026-09-26 09:12 UTC137
    let (_, mut rooms) = client.rooms_stream();
411bc1c78b65atagen2026-10-22 07:39 UTC138
    let mut tracker = UploadTracker::new(attachment_echoes(&client).await);
411bc1c78b65atagen2026-10-22 07:39 UTC139
    let mut pending = VecDeque::new();
5a52012c9b1datagen2026-09-24 23:45 UTC140
    loop {
411bc1c78b65atagen2026-10-22 07:39 UTC141
        let reports: Vec<Event> = tokio::select! {
411bc1c78b65atagen2026-10-22 07:39 UTC142
            biased;
0bfa3f46f0beatagen2026-09-26 09:12 UTC143
            update = updates.recv() => match update {
7c26fad34822atagen2026-09-26 09:41 UTC144
                Ok(update) => tracker.update(update).into_iter().collect(),
0bfa3f46f0beatagen2026-09-26 09:12 UTC145
                Err(RecvError::Lagged(skipped)) => {
7cf348521204atagen2026-09-26 12:28 UTC146
                    tracing::warn!(skipped, "upload tracker lagged");
7cf348521204atagen2026-09-26 12:28 UTC147
                    tracker.resnapshot(attachment_echoes(&client).await)
0bfa3f46f0beatagen2026-09-26 09:12 UTC148
                }
0bfa3f46f0beatagen2026-09-26 09:12 UTC149
                Err(RecvError::Closed) => return,
0bfa3f46f0beatagen2026-09-26 09:12 UTC150
            },
0bfa3f46f0beatagen2026-09-26 09:12 UTC151
            update = rooms.next() => {
0bfa3f46f0beatagen2026-09-26 09:12 UTC152
                if update.is_none() {
0bfa3f46f0beatagen2026-09-26 09:12 UTC153
                    return;
0bfa3f46f0beatagen2026-09-26 09:12 UTC154
                }
7c26fad34822atagen2026-09-26 09:41 UTC155
                tracker.finish_where(
7c26fad34822atagen2026-09-26 09:41 UTC156
                    |(room_id, _)| client.get_room(room_id).is_none(),
7c26fad34822atagen2026-09-26 09:41 UTC157
                    &UploadOutcome::RoomForgotten,
7c26fad34822atagen2026-09-26 09:41 UTC158
                )
31ce2453c82fatagen2026-09-24 13:07 UTC159
            }
411bc1c78b65atagen2026-10-22 07:39 UTC160
            permit = events.reserve(), if !pending.is_empty() => {
411bc1c78b65atagen2026-10-22 07:39 UTC161
                let Ok(permit) = permit else { return };
411bc1c78b65atagen2026-10-22 07:39 UTC162
                permit.send(pending.pop_front().expect("pending was checked"));
411bc1c78b65atagen2026-10-22 07:39 UTC163
                continue;
411bc1c78b65atagen2026-10-22 07:39 UTC164
            }
31ce2453c82fatagen2026-09-24 13:07 UTC165
        };
7c26fad34822atagen2026-09-26 09:41 UTC166
        for event in reports {
411bc1c78b65atagen2026-10-22 07:39 UTC167
            queue_upload_event(&mut pending, event);
411bc1c78b65atagen2026-10-22 07:39 UTC168
        }
411bc1c78b65atagen2026-10-22 07:39 UTC169
    }
411bc1c78b65atagen2026-10-22 07:39 UTC170
}
411bc1c78b65atagen2026-10-22 07:39 UTC171
411bc1c78b65atagen2026-10-22 07:39 UTC172
/// a progress tick replaces a waiting one for the same upload, unless something else about
411bc1c78b65atagen2026-10-22 07:39 UTC173
/// that upload is queued after it
411bc1c78b65atagen2026-10-22 07:39 UTC174
fn queue_upload_event(pending: &mut VecDeque<Event>, event: Event) {
411bc1c78b65atagen2026-10-22 07:39 UTC175
    if matches!(event, Event::MediaUploadProgress { .. })
411bc1c78b65atagen2026-10-22 07:39 UTC176
        && let Some(waiting) = pending
411bc1c78b65atagen2026-10-22 07:39 UTC177
            .iter_mut()
411bc1c78b65atagen2026-10-22 07:39 UTC178
            .rev()
411bc1c78b65atagen2026-10-22 07:39 UTC179
            .find(|waiting| upload_of(waiting) == upload_of(&event))
411bc1c78b65atagen2026-10-22 07:39 UTC180
        && matches!(waiting, Event::MediaUploadProgress { .. })
411bc1c78b65atagen2026-10-22 07:39 UTC181
    {
411bc1c78b65atagen2026-10-22 07:39 UTC182
        *waiting = event;
411bc1c78b65atagen2026-10-22 07:39 UTC183
        return;
411bc1c78b65atagen2026-10-22 07:39 UTC184
    }
411bc1c78b65atagen2026-10-22 07:39 UTC185
    pending.push_back(event);
411bc1c78b65atagen2026-10-22 07:39 UTC186
}
411bc1c78b65atagen2026-10-22 07:39 UTC187
411bc1c78b65atagen2026-10-22 07:39 UTC188
fn upload_of(event: &Event) -> Option<(&OwnedRoomId, &OwnedTransactionId)> {
411bc1c78b65atagen2026-10-22 07:39 UTC189
    match event {
411bc1c78b65atagen2026-10-22 07:39 UTC190
        Event::MediaUploadProgress {
411bc1c78b65atagen2026-10-22 07:39 UTC191
            room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC192
            transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC193
            ..
411bc1c78b65atagen2026-10-22 07:39 UTC194
        }
411bc1c78b65atagen2026-10-22 07:39 UTC195
        | Event::MediaUploadFailed {
411bc1c78b65atagen2026-10-22 07:39 UTC196
            room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC197
            transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC198
            ..
0bfa3f46f0beatagen2026-09-26 09:12 UTC199
        }
411bc1c78b65atagen2026-10-22 07:39 UTC200
        | Event::MediaUploadFinished {
411bc1c78b65atagen2026-10-22 07:39 UTC201
            room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC202
            transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC203
            ..
411bc1c78b65atagen2026-10-22 07:39 UTC204
        } => Some((room_id, transaction_id)),
411bc1c78b65atagen2026-10-22 07:39 UTC205
        _ => None,
0bfa3f46f0beatagen2026-09-26 09:12 UTC206
    }
0bfa3f46f0beatagen2026-09-26 09:12 UTC207
}
0bfa3f46f0beatagen2026-09-26 09:12 UTC208
0bfa3f46f0beatagen2026-09-26 09:12 UTC209
type Upload = (OwnedRoomId, OwnedTransactionId);
0bfa3f46f0beatagen2026-09-26 09:12 UTC210
411bc1c78b65atagen2026-10-22 07:39 UTC211
/// the sdk sends progress ticks from a detached task, so a tick can land after its
411bc1c78b65atagen2026-10-22 07:39 UTC212
/// upload's end. it trails by one task hop, so a short memory of ends covers it
411bc1c78b65atagen2026-10-22 07:39 UTC213
const REMEMBERED_ENDS: usize = 256;
411bc1c78b65atagen2026-10-22 07:39 UTC214
411bc1c78b65atagen2026-10-22 07:39 UTC215
/// everything reported gets exactly one `MediaUploadFinished` and nothing after it
7c26fad34822atagen2026-09-26 09:41 UTC216
struct UploadTracker {
7c26fad34822atagen2026-09-26 09:41 UTC217
    live: HashSet<Upload>,
7c26fad34822atagen2026-09-26 09:41 UTC218
    ended: HashSet<Upload>,
411bc1c78b65atagen2026-10-22 07:39 UTC219
    ended_order: VecDeque<Upload>,
0bfa3f46f0beatagen2026-09-26 09:12 UTC220
}
0bfa3f46f0beatagen2026-09-26 09:12 UTC221
7c26fad34822atagen2026-09-26 09:41 UTC222
impl UploadTracker {
411bc1c78b65atagen2026-10-22 07:39 UTC223
    fn new(live: HashSet<Upload>) -> Self {
411bc1c78b65atagen2026-10-22 07:39 UTC224
        Self {
411bc1c78b65atagen2026-10-22 07:39 UTC225
            live,
411bc1c78b65atagen2026-10-22 07:39 UTC226
            ended: HashSet::new(),
411bc1c78b65atagen2026-10-22 07:39 UTC227
            ended_order: VecDeque::new(),
411bc1c78b65atagen2026-10-22 07:39 UTC228
        }
411bc1c78b65atagen2026-10-22 07:39 UTC229
    }
411bc1c78b65atagen2026-10-22 07:39 UTC230
7c26fad34822atagen2026-09-26 09:41 UTC231
    fn update(&mut self, update: SendQueueUpdate) -> Option<Event> {
7c26fad34822atagen2026-09-26 09:41 UTC232
        let room_id = update.room_id;
7c26fad34822atagen2026-09-26 09:41 UTC233
        let (transaction_id, outcome) = match update.update {
7c26fad34822atagen2026-09-26 09:41 UTC234
            RoomSendQueueUpdate::NewLocalEvent(echo) => {
7c26fad34822atagen2026-09-26 09:41 UTC235
                let upload = (room_id, echo.transaction_id);
7c26fad34822atagen2026-09-26 09:41 UTC236
                if is_attachment_echo(&echo.content) && !self.ended.contains(&upload) {
7c26fad34822atagen2026-09-26 09:41 UTC237
                    self.live.insert(upload);
7c26fad34822atagen2026-09-26 09:41 UTC238
                }
0bfa3f46f0beatagen2026-09-26 09:12 UTC239
                return None;
0bfa3f46f0beatagen2026-09-26 09:12 UTC240
            }
411bc1c78b65atagen2026-10-22 07:39 UTC241
            RoomSendQueueUpdate::MediaUpload {
411bc1c78b65atagen2026-10-22 07:39 UTC242
                related_to,
411bc1c78b65atagen2026-10-22 07:39 UTC243
                file,
411bc1c78b65atagen2026-10-22 07:39 UTC244
                progress,
411bc1c78b65atagen2026-10-22 07:39 UTC245
                ..
411bc1c78b65atagen2026-10-22 07:39 UTC246
            } => {
411bc1c78b65atagen2026-10-22 07:39 UTC247
                let upload = (room_id, related_to);
411bc1c78b65atagen2026-10-22 07:39 UTC248
                if self.ended.contains(&upload) {
411bc1c78b65atagen2026-10-22 07:39 UTC249
                    return None;
411bc1c78b65atagen2026-10-22 07:39 UTC250
                }
411bc1c78b65atagen2026-10-22 07:39 UTC251
                self.live.insert(upload.clone());
411bc1c78b65atagen2026-10-22 07:39 UTC252
                let (room_id, transaction_id) = upload;
411bc1c78b65atagen2026-10-22 07:39 UTC253
                return Some(Event::MediaUploadProgress {
411bc1c78b65atagen2026-10-22 07:39 UTC254
                    room_id,
411bc1c78b65atagen2026-10-22 07:39 UTC255
                    transaction_id,
411bc1c78b65atagen2026-10-22 07:39 UTC256
                    uploaded_bytes: progress.current as u64,
411bc1c78b65atagen2026-10-22 07:39 UTC257
                    total_bytes: progress.total as u64,
411bc1c78b65atagen2026-10-22 07:39 UTC258
                    complete: file.is_some() && progress.current == progress.total,
411bc1c78b65atagen2026-10-22 07:39 UTC259
                });
411bc1c78b65atagen2026-10-22 07:39 UTC260
            }
7c26fad34822atagen2026-09-26 09:41 UTC261
            RoomSendQueueUpdate::SendError {
0bfa3f46f0beatagen2026-09-26 09:12 UTC262
                transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC263
                error,
7c26fad34822atagen2026-09-26 09:41 UTC264
                is_recoverable,
7c26fad34822atagen2026-09-26 09:41 UTC265
            } => {
7c26fad34822atagen2026-09-26 09:41 UTC266
                let upload = (room_id, transaction_id);
7c26fad34822atagen2026-09-26 09:41 UTC267
                if !self.live.contains(&upload) {
7c26fad34822atagen2026-09-26 09:41 UTC268
                    return None;
7c26fad34822atagen2026-09-26 09:41 UTC269
                }
7c26fad34822atagen2026-09-26 09:41 UTC270
                let (room_id, transaction_id) = upload;
7c26fad34822atagen2026-09-26 09:41 UTC271
                return Some(Event::MediaUploadFailed {
7c26fad34822atagen2026-09-26 09:41 UTC272
                    room_id,
7c26fad34822atagen2026-09-26 09:41 UTC273
                    transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC274
                    kind: classify_sdk_error(&error),
7c26fad34822atagen2026-09-26 09:41 UTC275
                    will_retry: is_recoverable,
7c26fad34822atagen2026-09-26 09:41 UTC276
                });
7c26fad34822atagen2026-09-26 09:41 UTC277
            }
7c26fad34822atagen2026-09-26 09:41 UTC278
            RoomSendQueueUpdate::SentEvent {
7c26fad34822atagen2026-09-26 09:41 UTC279
                transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC280
                event_id,
7c26fad34822atagen2026-09-26 09:41 UTC281
            } => (transaction_id, UploadOutcome::Sent { event_id }),
7c26fad34822atagen2026-09-26 09:41 UTC282
            RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => {
7c26fad34822atagen2026-09-26 09:41 UTC283
                (transaction_id, UploadOutcome::Cancelled)
7c26fad34822atagen2026-09-26 09:41 UTC284
            }
7c26fad34822atagen2026-09-26 09:41 UTC285
            _ => return None,
7c26fad34822atagen2026-09-26 09:41 UTC286
        };
411bc1c78b65atagen2026-10-22 07:39 UTC287
        self.finish((room_id, transaction_id), outcome)
7c26fad34822atagen2026-09-26 09:41 UTC288
    }
7c26fad34822atagen2026-09-26 09:41 UTC289
7c26fad34822atagen2026-09-26 09:41 UTC290
    fn resnapshot(&mut self, current: HashSet<Upload>) -> Vec<Event> {
7c26fad34822atagen2026-09-26 09:41 UTC291
        let reports = self.finish_where(|upload| !current.contains(upload), &UploadOutcome::Missed);
7c26fad34822atagen2026-09-26 09:41 UTC292
        let ended = &self.ended;
7c26fad34822atagen2026-09-26 09:41 UTC293
        self.live
7c26fad34822atagen2026-09-26 09:41 UTC294
            .extend(current.into_iter().filter(|upload| !ended.contains(upload)));
7c26fad34822atagen2026-09-26 09:41 UTC295
        reports
7c26fad34822atagen2026-09-26 09:41 UTC296
    }
7c26fad34822atagen2026-09-26 09:41 UTC297
7c26fad34822atagen2026-09-26 09:41 UTC298
    fn finish_where(
7c26fad34822atagen2026-09-26 09:41 UTC299
        &mut self,
7c26fad34822atagen2026-09-26 09:41 UTC300
        ends: impl Fn(&Upload) -> bool,
7c26fad34822atagen2026-09-26 09:41 UTC301
        outcome: &UploadOutcome,
7c26fad34822atagen2026-09-26 09:41 UTC302
    ) -> Vec<Event> {
7c26fad34822atagen2026-09-26 09:41 UTC303
        let ending = self
7c26fad34822atagen2026-09-26 09:41 UTC304
            .live
7c26fad34822atagen2026-09-26 09:41 UTC305
            .iter()
7c26fad34822atagen2026-09-26 09:41 UTC306
            .filter(|upload| ends(upload))
7c26fad34822atagen2026-09-26 09:41 UTC307
            .cloned()
7c26fad34822atagen2026-09-26 09:41 UTC308
            .collect::<Vec<_>>();
7c26fad34822atagen2026-09-26 09:41 UTC309
        ending
7c26fad34822atagen2026-09-26 09:41 UTC310
            .into_iter()
411bc1c78b65atagen2026-10-22 07:39 UTC311
            .filter_map(|upload| self.finish(upload, outcome.clone()))
7c26fad34822atagen2026-09-26 09:41 UTC312
            .collect()
7c26fad34822atagen2026-09-26 09:41 UTC313
    }
7c26fad34822atagen2026-09-26 09:41 UTC314
411bc1c78b65atagen2026-10-22 07:39 UTC315
    /// an end for an upload we never saw is remembered too, its ticks may still be coming
411bc1c78b65atagen2026-10-22 07:39 UTC316
    fn finish(&mut self, upload: Upload, outcome: UploadOutcome) -> Option<Event> {
411bc1c78b65atagen2026-10-22 07:39 UTC317
        if self.ended.insert(upload.clone()) {
411bc1c78b65atagen2026-10-22 07:39 UTC318
            self.ended_order.push_back(upload.clone());
411bc1c78b65atagen2026-10-22 07:39 UTC319
            if self.ended_order.len() > REMEMBERED_ENDS
411bc1c78b65atagen2026-10-22 07:39 UTC320
                && let Some(oldest) = self.ended_order.pop_front()
411bc1c78b65atagen2026-10-22 07:39 UTC321
            {
411bc1c78b65atagen2026-10-22 07:39 UTC322
                self.ended.remove(&oldest);
411bc1c78b65atagen2026-10-22 07:39 UTC323
            }
411bc1c78b65atagen2026-10-22 07:39 UTC324
        }
7c26fad34822atagen2026-09-26 09:41 UTC325
        if !self.live.remove(&upload) {
7c26fad34822atagen2026-09-26 09:41 UTC326
            return None;
31ce2453c82fatagen2026-09-24 13:07 UTC327
        }
7c26fad34822atagen2026-09-26 09:41 UTC328
        let (room_id, transaction_id) = upload;
7c26fad34822atagen2026-09-26 09:41 UTC329
        Some(Event::MediaUploadFinished {
7c26fad34822atagen2026-09-26 09:41 UTC330
            room_id,
7c26fad34822atagen2026-09-26 09:41 UTC331
            transaction_id,
7c26fad34822atagen2026-09-26 09:41 UTC332
            outcome,
7c26fad34822atagen2026-09-26 09:41 UTC333
        })
31ce2453c82fatagen2026-09-24 13:07 UTC334
    }
0bfa3f46f0beatagen2026-09-26 09:12 UTC335
}
0bfa3f46f0beatagen2026-09-26 09:12 UTC336
7c26fad34822atagen2026-09-26 09:41 UTC337
async fn attachment_echoes(client: &Client) -> HashSet<Upload> {
7c26fad34822atagen2026-09-26 09:41 UTC338
    let Ok(echoes) = client.send_queue().local_echoes().await else {
7c26fad34822atagen2026-09-26 09:41 UTC339
        return HashSet::new();
7c26fad34822atagen2026-09-26 09:41 UTC340
    };
7c26fad34822atagen2026-09-26 09:41 UTC341
    echoes
0bfa3f46f0beatagen2026-09-26 09:12 UTC342
        .into_iter()
7c26fad34822atagen2026-09-26 09:41 UTC343
        .flat_map(|(room_id, echoes)| {
7c26fad34822atagen2026-09-26 09:41 UTC344
            echoes
7c26fad34822atagen2026-09-26 09:41 UTC345
                .into_iter()
7c26fad34822atagen2026-09-26 09:41 UTC346
                .filter(|echo| is_attachment_echo(&echo.content))
7c26fad34822atagen2026-09-26 09:41 UTC347
                .map(move |echo| (room_id.clone(), echo.transaction_id))
0bfa3f46f0beatagen2026-09-26 09:12 UTC348
        })
0bfa3f46f0beatagen2026-09-26 09:12 UTC349
        .collect()
31ce2453c82fatagen2026-09-24 13:07 UTC350
}
31ce2453c82fatagen2026-09-24 13:07 UTC351
31ce2453c82fatagen2026-09-24 13:07 UTC352
fn is_attachment_echo(content: &LocalEchoContent) -> bool {
31ce2453c82fatagen2026-09-24 13:07 UTC353
    let LocalEchoContent::Event {
31ce2453c82fatagen2026-09-24 13:07 UTC354
        serialized_event, ..
31ce2453c82fatagen2026-09-24 13:07 UTC355
    } = content
31ce2453c82fatagen2026-09-24 13:07 UTC356
    else {
31ce2453c82fatagen2026-09-24 13:07 UTC357
        return false;
31ce2453c82fatagen2026-09-24 13:07 UTC358
    };
31ce2453c82fatagen2026-09-24 13:07 UTC359
    matches!(
31ce2453c82fatagen2026-09-24 13:07 UTC360
        serialized_event.deserialize(),
31ce2453c82fatagen2026-09-24 13:07 UTC361
        Ok(AnyMessageLikeEventContent::RoomMessage(content))
31ce2453c82fatagen2026-09-24 13:07 UTC362
            if matches!(
31ce2453c82fatagen2026-09-24 13:07 UTC363
                content.msgtype,
31ce2453c82fatagen2026-09-24 13:07 UTC364
                MessageType::Audio(_) | MessageType::File(_) | MessageType::Image(_) | MessageType::Video(_)
31ce2453c82fatagen2026-09-24 13:07 UTC365
            )
31ce2453c82fatagen2026-09-24 13:07 UTC366
    )
31ce2453c82fatagen2026-09-24 13:07 UTC367
}
31ce2453c82fatagen2026-09-24 13:07 UTC368
31ce2453c82fatagen2026-09-24 13:07 UTC369
pub(super) async fn encryption(client: Client, events: EventSender) {
31ce2453c82fatagen2026-09-24 13:07 UTC370
    let Some(device_id) = client.device_id().map(ToOwned::to_owned) else {
31ce2453c82fatagen2026-09-24 13:07 UTC371
        tracing::warn!("encryption watcher stopped because the client has no device id");
31ce2453c82fatagen2026-09-24 13:07 UTC372
        return;
31ce2453c82fatagen2026-09-24 13:07 UTC373
    };
31ce2453c82fatagen2026-09-24 13:07 UTC374
    let recovery = client.encryption().recovery();
31ce2453c82fatagen2026-09-24 13:07 UTC375
    let backups = client.encryption().backups();
31ce2453c82fatagen2026-09-24 13:07 UTC376
    let mut recovery_stream = recovery.state_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC377
    let mut backup_stream = backups.state_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC378
    let mut verification_stream = client.encryption().verification_state();
31ce2453c82fatagen2026-09-24 13:07 UTC379
    let mut recovery_state = recovery.state();
31ce2453c82fatagen2026-09-24 13:07 UTC380
    let mut backup_state = backups.state();
31ce2453c82fatagen2026-09-24 13:07 UTC381
    let mut verification_state = verification_stream.get();
31ce2453c82fatagen2026-09-24 13:07 UTC382
    loop {
31ce2453c82fatagen2026-09-24 13:07 UTC383
        let state = encryption_state(
31ce2453c82fatagen2026-09-24 13:07 UTC384
            device_id.clone(),
31ce2453c82fatagen2026-09-24 13:07 UTC385
            recovery_state,
31ce2453c82fatagen2026-09-24 13:07 UTC386
            verification_state,
31ce2453c82fatagen2026-09-24 13:07 UTC387
            backup_state,
31ce2453c82fatagen2026-09-24 13:07 UTC388
        );
31ce2453c82fatagen2026-09-24 13:07 UTC389
        if events.send(Event::EncryptionChanged(state)).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC390
            return;
31ce2453c82fatagen2026-09-24 13:07 UTC391
        }
31ce2453c82fatagen2026-09-24 13:07 UTC392
        tokio::select! {
31ce2453c82fatagen2026-09-24 13:07 UTC393
            Some(update) = recovery_stream.next() => recovery_state = update,
31ce2453c82fatagen2026-09-24 13:07 UTC394
            Some(update) = verification_stream.next() => verification_state = update,
31ce2453c82fatagen2026-09-24 13:07 UTC395
            Some(update) = backup_stream.next() => {
31ce2453c82fatagen2026-09-24 13:07 UTC396
                backup_state = update.unwrap_or_else(|_| backups.state());
31ce2453c82fatagen2026-09-24 13:07 UTC397
            }
31ce2453c82fatagen2026-09-24 13:07 UTC398
            else => return,
31ce2453c82fatagen2026-09-24 13:07 UTC399
        }
31ce2453c82fatagen2026-09-24 13:07 UTC400
    }
31ce2453c82fatagen2026-09-24 13:07 UTC401
}
31ce2453c82fatagen2026-09-24 13:07 UTC402
31ce2453c82fatagen2026-09-24 13:07 UTC403
pub(super) async fn rooms(client: Client, events: EventSender) {
31ce2453c82fatagen2026-09-24 13:07 UTC404
    let (_, mut rooms) = client.rooms_stream();
31ce2453c82fatagen2026-09-24 13:07 UTC405
    let mut info = client.room_info_notable_update_receiver();
31ce2453c82fatagen2026-09-24 13:07 UTC406
    loop {
31ce2453c82fatagen2026-09-24 13:07 UTC407
        if events.send(Event::RoomsChanged).await.is_err() {
31ce2453c82fatagen2026-09-24 13:07 UTC408
            return;
31ce2453c82fatagen2026-09-24 13:07 UTC409
        }
31ce2453c82fatagen2026-09-24 13:07 UTC410
        tokio::select! {
31ce2453c82fatagen2026-09-24 13:07 UTC411
            update = rooms.next() => if update.is_none() { return },
31ce2453c82fatagen2026-09-24 13:07 UTC412
            update = info.recv() => if matches!(update, Err(RecvError::Closed)) { return },
31ce2453c82fatagen2026-09-24 13:07 UTC413
        }
31ce2453c82fatagen2026-09-24 13:07 UTC414
        // a sync touching fifty rooms is one invalidation, not fifty
31ce2453c82fatagen2026-09-24 13:07 UTC415
        while let Ok(_) | Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_)) =
31ce2453c82fatagen2026-09-24 13:07 UTC416
            info.try_recv()
31ce2453c82fatagen2026-09-24 13:07 UTC417
        {}
31ce2453c82fatagen2026-09-24 13:07 UTC418
    }
31ce2453c82fatagen2026-09-24 13:07 UTC419
}
31ce2453c82fatagen2026-09-24 13:07 UTC420
17e042fbc331atagen2026-10-08 12:39 UTC421
/// packs live in room state and account data, and leaving a room takes its packs with it
17e042fbc331atagen2026-10-08 12:39 UTC422
pub(super) async fn image_packs(client: Client, events: EventSender) {
17e042fbc331atagen2026-10-08 12:39 UTC423
    let changed = Arc::new(Notify::new());
17e042fbc331atagen2026-10-08 12:39 UTC424
    let handlers = [
17e042fbc331atagen2026-10-08 12:39 UTC425
        client.add_event_handler({
17e042fbc331atagen2026-10-08 12:39 UTC426
            let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC427
            move |event: Raw<AnySyncStateEvent>| {
17e042fbc331atagen2026-10-08 12:39 UTC428
                let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC429
                async move {
17e042fbc331atagen2026-10-08 12:39 UTC430
                    if event_type(&event).as_deref() == Some(ROOM_PACKS) {
17e042fbc331atagen2026-10-08 12:39 UTC431
                        changed.notify_one();
17e042fbc331atagen2026-10-08 12:39 UTC432
                    }
17e042fbc331atagen2026-10-08 12:39 UTC433
                }
17e042fbc331atagen2026-10-08 12:39 UTC434
            }
17e042fbc331atagen2026-10-08 12:39 UTC435
        }),
17e042fbc331atagen2026-10-08 12:39 UTC436
        client.add_event_handler({
17e042fbc331atagen2026-10-08 12:39 UTC437
            let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC438
            move |event: Raw<AnyGlobalAccountDataEvent>| {
17e042fbc331atagen2026-10-08 12:39 UTC439
                let changed = changed.clone();
17e042fbc331atagen2026-10-08 12:39 UTC440
                async move {
17e042fbc331atagen2026-10-08 12:39 UTC441
                    if matches!(
17e042fbc331atagen2026-10-08 12:39 UTC442
                        event_type(&event).as_deref(),
17e042fbc331atagen2026-10-08 12:39 UTC443
                        Some(USER_PACK | ENABLED_ROOMS)
17e042fbc331atagen2026-10-08 12:39 UTC444
                    ) {
17e042fbc331atagen2026-10-08 12:39 UTC445
                        changed.notify_one();
17e042fbc331atagen2026-10-08 12:39 UTC446
                    }
17e042fbc331atagen2026-10-08 12:39 UTC447
                }
17e042fbc331atagen2026-10-08 12:39 UTC448
            }
17e042fbc331atagen2026-10-08 12:39 UTC449
        }),
17e042fbc331atagen2026-10-08 12:39 UTC450
    ];
17e042fbc331atagen2026-10-08 12:39 UTC451
    let mut info = client.room_info_notable_update_receiver();
17e042fbc331atagen2026-10-08 12:39 UTC452
    loop {
17e042fbc331atagen2026-10-08 12:39 UTC453
        tokio::select! {
17e042fbc331atagen2026-10-08 12:39 UTC454
            () = changed.notified() => {}
17e042fbc331atagen2026-10-08 12:39 UTC455
            update = info.recv() => match update {
17e042fbc331atagen2026-10-08 12:39 UTC456
                Ok(update) if !update.reasons.contains(RoomInfoNotableUpdateReasons::MEMBERSHIP) => {
17e042fbc331atagen2026-10-08 12:39 UTC457
                    continue;
17e042fbc331atagen2026-10-08 12:39 UTC458
                }
17e042fbc331atagen2026-10-08 12:39 UTC459
                Ok(_) | Err(RecvError::Lagged(_)) => {}
17e042fbc331atagen2026-10-08 12:39 UTC460
                Err(RecvError::Closed) => break,
17e042fbc331atagen2026-10-08 12:39 UTC461
            },
17e042fbc331atagen2026-10-08 12:39 UTC462
        }
17e042fbc331atagen2026-10-08 12:39 UTC463
        if events.send(Event::ImagePacksChanged).await.is_err() {
17e042fbc331atagen2026-10-08 12:39 UTC464
            break;
17e042fbc331atagen2026-10-08 12:39 UTC465
        }
17e042fbc331atagen2026-10-08 12:39 UTC466
    }
17e042fbc331atagen2026-10-08 12:39 UTC467
    for handler in handlers {
17e042fbc331atagen2026-10-08 12:39 UTC468
        client.remove_event_handler(handler);
17e042fbc331atagen2026-10-08 12:39 UTC469
    }
17e042fbc331atagen2026-10-08 12:39 UTC470
}
17e042fbc331atagen2026-10-08 12:39 UTC471
17e042fbc331atagen2026-10-08 12:39 UTC472
fn event_type<T>(event: &Raw<T>) -> Option<String> {
17e042fbc331atagen2026-10-08 12:39 UTC473
    event.get_field("type").ok().flatten()
17e042fbc331atagen2026-10-08 12:39 UTC474
}
17e042fbc331atagen2026-10-08 12:39 UTC475
31ce2453c82fatagen2026-09-24 13:07 UTC476
pub(super) async fn notification_settings(client: Client, events: EventSender) {
31ce2453c82fatagen2026-09-24 13:07 UTC477
    let settings = client.notification_settings().await;
31ce2453c82fatagen2026-09-24 13:07 UTC478
    let mut changes = settings.subscribe_to_changes();
31ce2453c82fatagen2026-09-24 13:07 UTC479
    while let Ok(()) | Err(RecvError::Lagged(_)) = changes.recv().await {
0bfa3f46f0beatagen2026-09-26 09:12 UTC480
        // room summaries carry the effective mode, and push rule changes never touch room info
0bfa3f46f0beatagen2026-09-26 09:12 UTC481
        for event in [Event::NotificationSettingsChanged, Event::RoomsChanged] {
0bfa3f46f0beatagen2026-09-26 09:12 UTC482
            if events.send(event).await.is_err() {
0bfa3f46f0beatagen2026-09-26 09:12 UTC483
                return;
0bfa3f46f0beatagen2026-09-26 09:12 UTC484
            }
31ce2453c82fatagen2026-09-24 13:07 UTC485
        }
31ce2453c82fatagen2026-09-24 13:07 UTC486
    }
31ce2453c82fatagen2026-09-24 13:07 UTC487
}
7c26fad34822atagen2026-09-26 09:41 UTC488
7c26fad34822atagen2026-09-26 09:41 UTC489
#[cfg(test)]
7c26fad34822atagen2026-09-26 09:41 UTC490
mod tests {
411bc1c78b65atagen2026-10-22 07:39 UTC491
    use matrix_sdk::send_queue::{AbstractProgress, RoomSendQueueUpdate, SendQueueUpdate};
7c26fad34822atagen2026-09-26 09:41 UTC492
    use ruma::{OwnedTransactionId, event_id, owned_room_id};
7c26fad34822atagen2026-09-26 09:41 UTC493
411bc1c78b65atagen2026-10-22 07:39 UTC494
    use super::{HashSet, Upload, UploadTracker};
7c26fad34822atagen2026-09-26 09:41 UTC495
    use crate::{Event, UploadOutcome};
7c26fad34822atagen2026-09-26 09:41 UTC496
7cf348521204atagen2026-09-26 12:28 UTC497
    fn upload() -> Upload {
7cf348521204atagen2026-09-26 12:28 UTC498
        (
7c26fad34822atagen2026-09-26 09:41 UTC499
            owned_room_id!("!room:localhost"),
7c26fad34822atagen2026-09-26 09:41 UTC500
            OwnedTransactionId::from("upload"),
7cf348521204atagen2026-09-26 12:28 UTC501
        )
7cf348521204atagen2026-09-26 12:28 UTC502
    }
7cf348521204atagen2026-09-26 12:28 UTC503
411bc1c78b65atagen2026-10-22 07:39 UTC504
    fn update(update: RoomSendQueueUpdate) -> SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC505
        SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC506
            room_id: upload().0,
411bc1c78b65atagen2026-10-22 07:39 UTC507
            update,
7cf348521204atagen2026-09-26 12:28 UTC508
        }
7cf348521204atagen2026-09-26 12:28 UTC509
    }
7cf348521204atagen2026-09-26 12:28 UTC510
411bc1c78b65atagen2026-10-22 07:39 UTC511
    fn tick() -> SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC512
        update(RoomSendQueueUpdate::MediaUpload {
411bc1c78b65atagen2026-10-22 07:39 UTC513
            related_to: upload().1,
411bc1c78b65atagen2026-10-22 07:39 UTC514
            file: None,
411bc1c78b65atagen2026-10-22 07:39 UTC515
            index: 0,
411bc1c78b65atagen2026-10-22 07:39 UTC516
            progress: AbstractProgress {
411bc1c78b65atagen2026-10-22 07:39 UTC517
                current: 1,
411bc1c78b65atagen2026-10-22 07:39 UTC518
                total: 2,
411bc1c78b65atagen2026-10-22 07:39 UTC519
            },
411bc1c78b65atagen2026-10-22 07:39 UTC520
        })
7cf348521204atagen2026-09-26 12:28 UTC521
    }
7cf348521204atagen2026-09-26 12:28 UTC522
411bc1c78b65atagen2026-10-22 07:39 UTC523
    fn sent() -> SendQueueUpdate {
411bc1c78b65atagen2026-10-22 07:39 UTC524
        update(RoomSendQueueUpdate::SentEvent {
411bc1c78b65atagen2026-10-22 07:39 UTC525
            transaction_id: upload().1,
411bc1c78b65atagen2026-10-22 07:39 UTC526
            event_id: event_id!("$sent:localhost").to_owned(),
411bc1c78b65atagen2026-10-22 07:39 UTC527
        })
7cf348521204atagen2026-09-26 12:28 UTC528
    }
7cf348521204atagen2026-09-26 12:28 UTC529
7cf348521204atagen2026-09-26 12:28 UTC530
    fn finished(event: Option<Event>) -> Option<UploadOutcome> {
7cf348521204atagen2026-09-26 12:28 UTC531
        match event? {
7cf348521204atagen2026-09-26 12:28 UTC532
            Event::MediaUploadFinished { outcome, .. } => Some(outcome),
7cf348521204atagen2026-09-26 12:28 UTC533
            _ => None,
7cf348521204atagen2026-09-26 12:28 UTC534
        }
7cf348521204atagen2026-09-26 12:28 UTC535
    }
7c26fad34822atagen2026-09-26 09:41 UTC536
7cf348521204atagen2026-09-26 12:28 UTC537
    #[test]
411bc1c78b65atagen2026-10-22 07:39 UTC538
    fn a_tick_trailing_its_end_says_nothing() {
411bc1c78b65atagen2026-10-22 07:39 UTC539
        let mut tracker = UploadTracker::new(HashSet::new());
411bc1c78b65atagen2026-10-22 07:39 UTC540
        assert!(tracker.update(tick()).is_some());
411bc1c78b65atagen2026-10-22 07:39 UTC541
        assert!(matches!(
411bc1c78b65atagen2026-10-22 07:39 UTC542
            finished(tracker.update(sent())),
411bc1c78b65atagen2026-10-22 07:39 UTC543
            Some(UploadOutcome::Sent { .. })
411bc1c78b65atagen2026-10-22 07:39 UTC544
        ));
411bc1c78b65atagen2026-10-22 07:39 UTC545
        assert!(tracker.update(tick()).is_none());
411bc1c78b65atagen2026-10-22 07:39 UTC546
        assert!(tracker.live.is_empty());
411bc1c78b65atagen2026-10-22 07:39 UTC547
    }
411bc1c78b65atagen2026-10-22 07:39 UTC548
411bc1c78b65atagen2026-10-22 07:39 UTC549
    #[test]
7cf348521204atagen2026-09-26 12:28 UTC550
    fn a_stale_tick_after_a_missed_end_changes_nothing() {
411bc1c78b65atagen2026-10-22 07:39 UTC551
        let mut tracker = UploadTracker::new(HashSet::new());
411bc1c78b65atagen2026-10-22 07:39 UTC552
        assert!(tracker.update(tick()).is_some());
7c26fad34822atagen2026-09-26 09:41 UTC553
        let missed = tracker.resnapshot(HashSet::new());
7c26fad34822atagen2026-09-26 09:41 UTC554
        assert!(matches!(
7c26fad34822atagen2026-09-26 09:41 UTC555
            missed.as_slice(),
7c26fad34822atagen2026-09-26 09:41 UTC556
            [Event::MediaUploadFinished {
7c26fad34822atagen2026-09-26 09:41 UTC557
                outcome: UploadOutcome::Missed,
7c26fad34822atagen2026-09-26 09:41 UTC558
                ..
7c26fad34822atagen2026-09-26 09:41 UTC559
            }]
7c26fad34822atagen2026-09-26 09:41 UTC560
        ));
411bc1c78b65atagen2026-10-22 07:39 UTC561
        assert!(tracker.update(tick()).is_none());
411bc1c78b65atagen2026-10-22 07:39 UTC562
        assert!(tracker.update(sent()).is_none());
411bc1c78b65atagen2026-10-22 07:39 UTC563
        assert!(tracker.live.is_empty());
7c26fad34822atagen2026-09-26 09:41 UTC564
    }
7c26fad34822atagen2026-09-26 09:41 UTC565
}