atagen

macltok

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

Blame: src/engine/watchers.rs

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