atagen

macltok

public
Star0
Watch0
a33777af1c268b8a169a26f4c8fd759ef1a9c3d0
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/engine/mod.rsRust958 lines34.1 KB
1
mod commands;
2
pub(crate) mod executor;
3
mod media;
4
mod mutations;
5
mod room;
6
mod session;
7
mod timeline;
8
mod watchers;
9
10
use std::{
11
    collections::{HashMap, VecDeque},
12
    future::Future,
13
    panic::AssertUnwindSafe,
14
    sync::Arc,
15
};
16
17
use futures_util::FutureExt;
18
use matrix_sdk::{Client, SendOutsideWasm, SessionChange};
19
use matrix_sdk_ui::sync_service::SyncService;
20
use ruma::{OwnedDeviceId, OwnedRoomId};
21
use thiserror::Error as ThisError;
22
use tokio::sync::{broadcast, mpsc, watch};
23
24
pub(crate) use media::MediaService;
25
pub(crate) use session::Persistence;
26
27
use self::{
28
    commands::Job,
29
    executor::{EngineBoxFuture, TaskSet},
30
    mutations::{Mutation, MutationJob},
31
    room::{ActorNote, ActorRequest, RoomActor},
32
    timeline::{SessionContext, Supervisor},
33
};
34
use crate::{
35
    AccountCommand, ClientHandle, Command, ConnectionState, EncryptionCommand, EngineErrorKind,
36
    EngineFailure, Error, ErrorKind, Event, EventStream, LifecycleCommand, MediaCommand,
37
    NotificationCommand, RoomCommand, Termination, TimelineCommand,
38
    transport::{DriverPort, Request, Responder, transport_pair},
39
};
40
41
#[derive(Clone, Debug, Eq, PartialEq)]
42
#[non_exhaustive]
43
pub struct EngineConfig {
44
    pub command_capacity: usize,
45
    pub event_capacity: usize,
46
    pub initial_history_limit: u16,
47
    pub max_media_bytes: usize,
48
    pub max_media_sources: usize,
49
}
50
51
impl EngineConfig {
52
    #[must_use]
53
    pub const fn with_command_capacity(mut self, capacity: usize) -> Self {
54
        self.command_capacity = capacity;
55
        self
56
    }
57
58
    #[must_use]
59
    pub const fn with_event_capacity(mut self, capacity: usize) -> Self {
60
        self.event_capacity = capacity;
61
        self
62
    }
63
64
    #[must_use]
65
    pub const fn with_initial_history_limit(mut self, limit: u16) -> Self {
66
        self.initial_history_limit = limit;
67
        self
68
    }
69
70
    #[must_use]
71
    pub const fn with_max_media_bytes(mut self, bytes: usize) -> Self {
72
        self.max_media_bytes = bytes;
73
        self
74
    }
75
76
    #[must_use]
77
    pub const fn with_max_media_sources(mut self, sources: usize) -> Self {
78
        self.max_media_sources = sources;
79
        self
80
    }
81
}
82
83
impl Default for EngineConfig {
84
    fn default() -> Self {
85
        Self {
86
            command_capacity: 64,
87
            event_capacity: 128,
88
            initial_history_limit: 30,
89
            max_media_bytes: 20 * 1024 * 1024,
90
            max_media_sources: 2_048,
91
        }
92
    }
93
}
94
95
#[derive(Debug, ThisError)]
96
pub enum EngineError {
97
    #[error("the Matrix client is not authenticated")]
98
    Unauthenticated,
99
    #[error("command, event, and media source capacities must be nonzero")]
100
    ZeroCapacity,
101
    #[error("could not build the sync service")]
102
    SyncService(#[source] matrix_sdk_ui::sync_service::Error),
103
    #[error("the event receiver has closed")]
104
    EventReceiverClosed,
105
    #[error("engine task {task} panicked")]
106
    TaskPanicked { task: String },
107
}
108
109
impl EngineError {
110
    fn failure(&self) -> EngineFailure {
111
        EngineFailure {
112
            kind: match self {
113
                Self::EventReceiverClosed => EngineErrorKind::EventReceiverClosed,
114
                Self::TaskPanicked { .. } => EngineErrorKind::Watcher,
115
                Self::Unauthenticated | Self::ZeroCapacity | Self::SyncService(_) => {
116
                    EngineErrorKind::Other
117
                }
118
            },
119
            message: self.to_string(),
120
        }
121
    }
122
}
123
124
#[derive(Clone)]
125
enum TaskContext {
126
    /// a panic here costs the caller an `Interrupted` answer and nothing else
127
    Command(&'static str),
128
    /// a panic here takes the engine with it
129
    Watcher(&'static str),
130
    RoomActor {
131
        room_id: OwnedRoomId,
132
        generation: u64,
133
    },
134
}
135
136
impl TaskContext {
137
    fn label(&self) -> String {
138
        match self {
139
            Self::Command(name) | Self::Watcher(name) => (*name).to_owned(),
140
            Self::RoomActor {
141
                room_id,
142
                generation,
143
            } => format!("room actor {room_id} generation {generation}"),
144
        }
145
    }
146
}
147
148
/// tasks catch their own panics so each comes back with its context, since joining
149
/// through the sdk's executor gives no task id to look one up by. wasm has nothing to catch
150
fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
151
    payload
152
        .downcast_ref::<&str>()
153
        .map(ToString::to_string)
154
        .or_else(|| payload.downcast_ref::<String>().cloned())
155
        .unwrap_or_else(|| "task panicked with a non-string payload".to_owned())
156
}
157
158
struct TaskRegistry {
159
    tasks: TaskSet<(TaskContext, Option<String>)>,
160
}
161
162
impl TaskRegistry {
163
    fn spawn(
164
        &mut self,
165
        context: TaskContext,
166
        task: impl Future<Output = ()> + SendOutsideWasm + 'static,
167
    ) {
168
        self.tasks.spawn(async move {
169
            let panic = AssertUnwindSafe(task)
170
                .catch_unwind()
171
                .await
172
                .err()
173
                .map(panic_message);
174
            (context, panic)
175
        });
176
    }
177
178
    fn is_empty(&self) -> bool {
179
        self.tasks.is_empty()
180
    }
181
182
    async fn join_next(&mut self) -> Option<(TaskContext, Option<String>)> {
183
        loop {
184
            if let Ok(done) = self.tasks.join_next().await? {
185
                return Some(done);
186
            }
187
        }
188
    }
189
190
    async fn shutdown(&mut self) {
191
        self.tasks.shutdown().await;
192
    }
193
}
194
195
enum RoomSlot {
196
    Active {
197
        generation: u64,
198
        sender: mpsc::Sender<ActorRequest>,
199
    },
200
    /// opens and closes that arrive while the actor winds down, replayed in order once it's gone
201
    Closing {
202
        generation: u64,
203
        pending: VecDeque<Deferred>,
204
    },
205
}
206
207
impl RoomSlot {
208
    const fn generation(&self) -> u64 {
209
        match self {
210
            Self::Active { generation, .. } | Self::Closing { generation, .. } => *generation,
211
        }
212
    }
213
}
214
215
enum Deferred {
216
    Open(Responder),
217
    Close(Responder),
218
}
219
220
/// logouts in flight, and a token rejection held back because one of them caused it
221
#[derive(Default)]
222
pub(super) struct Logouts {
223
    pub(super) in_flight: usize,
224
    pub(super) rejected: Option<Option<OwnedDeviceId>>,
225
}
226
227
struct Queues {
228
    mutations: mpsc::Receiver<MutationJob>,
229
    notifications: mpsc::Receiver<Job<NotificationCommand>>,
230
    encryption: mpsc::Receiver<Job<EncryptionCommand>>,
231
    session_changes: broadcast::Receiver<SessionChange>,
232
    persistence: Option<Persistence>,
233
    upload_reports: mpsc::Receiver<watchers::RoomUpload>,
234
}
235
236
/// requests other devices send us, handed from the sdk's event handler to the encryption worker
237
type IncomingVerifications =
238
    mpsc::UnboundedSender<matrix_sdk::encryption::verification::VerificationRequest>;
239
240
pub struct Engine {
241
    client: Client,
242
    sync: Arc<SyncService>,
243
    port: DriverPort,
244
    config: EngineConfig,
245
    media: Arc<MediaService>,
246
    tasks: TaskRegistry,
247
    rooms: HashMap<OwnedRoomId, RoomSlot>,
248
    next_generation: u64,
249
    notes: mpsc::UnboundedReceiver<ActorNote>,
250
    note_sender: mpsc::UnboundedSender<ActorNote>,
251
    /// the rooms sliding sync should follow, applied off the run loop since it touches the store
252
    subscriptions: watch::Sender<Vec<OwnedRoomId>>,
253
    mutations: mpsc::Sender<MutationJob>,
254
    notifications: mpsc::Sender<Job<NotificationCommand>>,
255
    encryption: mpsc::Sender<Job<EncryptionCommand>>,
256
    queues: Option<Queues>,
257
    /// one progress forwarder per room that has uploaded, and whether it's subscribed yet
258
    uploads: HashMap<OwnedRoomId, watch::Receiver<bool>>,
259
    upload_reports: mpsc::Sender<watchers::RoomUpload>,
260
    /// the client can outlive us, so the handler has to come off it when we stop
261
    verification_handler: Option<matrix_sdk::event_handler::EventHandlerHandle>,
262
    stop: mpsc::UnboundedSender<Termination>,
263
    stops: mpsc::UnboundedReceiver<Termination>,
264
    logouts: Arc<watch::Sender<Logouts>>,
265
}
266
267
impl Engine {
268
    /// the escape hatch for a client you built yourself. `Session` is the usual way in
269
    pub async fn from_client(
270
        client: Client,
271
        config: EngineConfig,
272
    ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
273
        let session_changes = client.subscribe_to_session_changes();
274
        Self::with_persistence(client, config, session_changes, None).await
275
    }
276
277
    /// `session_changes` is taken when the client is built, since the sdk can refresh
278
    /// before an engine exists
279
    pub(crate) async fn with_persistence(
280
        client: Client,
281
        config: EngineConfig,
282
        session_changes: broadcast::Receiver<SessionChange>,
283
        persistence: Option<Persistence>,
284
    ) -> Result<(Self, ClientHandle, EventStream), EngineError> {
285
        if client.user_id().is_none() {
286
            return Err(EngineError::Unauthenticated);
287
        }
288
        if config.command_capacity == 0
289
            || config.event_capacity == 0
290
            || config.max_media_sources == 0
291
        {
292
            return Err(EngineError::ZeroCapacity);
293
        }
294
        let sync = Arc::new(
295
            SyncService::builder(client.clone())
296
                .with_offline_mode()
297
                .build()
298
                .await
299
                .map_err(EngineError::SyncService)?,
300
        );
301
        let media = Arc::new(MediaService::new(
302
            client.clone(),
303
            config.max_media_bytes,
304
            config.max_media_sources,
305
        ));
306
        let (handle, port, events) = transport_pair(
307
            client.clone(),
308
            sync.clone(),
309
            media.clone(),
310
            config.command_capacity,
311
            config.event_capacity,
312
        );
313
        client.send_queue().enable_upload_progress(true);
314
        let (note_sender, notes) = mpsc::unbounded_channel();
315
        let (stop, stops) = mpsc::unbounded_channel();
316
        let (upload_reports, upload_report_queue) = mpsc::channel(config.event_capacity);
317
        let (mutations, mutation_queue) = mpsc::channel(config.command_capacity);
318
        let (notifications, notification_queue) = mpsc::channel(config.command_capacity);
319
        let (encryption, encryption_queue) = mpsc::channel(config.command_capacity);
320
        let engine = Self {
321
            client,
322
            sync,
323
            port,
324
            config,
325
            media,
326
            tasks: TaskRegistry {
327
                tasks: TaskSet::new(),
328
            },
329
            rooms: HashMap::new(),
330
            next_generation: 0,
331
            notes,
332
            note_sender,
333
            subscriptions: watch::Sender::new(Vec::new()),
334
            mutations,
335
            notifications,
336
            encryption,
337
            queues: Some(Queues {
338
                mutations: mutation_queue,
339
                notifications: notification_queue,
340
                encryption: encryption_queue,
341
                session_changes,
342
                persistence,
343
                upload_reports: upload_report_queue,
344
            }),
345
            upload_reports,
346
            uploads: HashMap::new(),
347
            verification_handler: None,
348
            stop,
349
            stops,
350
            logouts: Arc::new(watch::Sender::new(Logouts::default())),
351
        };
352
        Ok((engine, handle, events))
353
    }
354
355
    /// boxed because the engine's future is deep enough to blow the compiler's query depth
356
    /// in every crate that spawns it, which isn't something a client should have to fix
357
    pub fn run(self) -> EngineBoxFuture<'static, Result<Termination, EngineError>> {
358
        Box::pin(self.run_inner())
359
    }
360
361
    async fn run_inner(mut self) -> Result<Termination, EngineError> {
362
        let events = self.port.event_sender();
363
        let user_id = self
364
            .client
365
            .user_id()
366
            .expect("authentication was checked during construction")
367
            .to_owned();
368
        self.port
369
            .connection_sender()
370
            .send_replace(ConnectionState::Connecting);
371
        for event in [
372
            Event::Authenticated { user_id },
373
            Event::ConnectionChanged(ConnectionState::Connecting),
374
        ] {
375
            if events.send(event).await.is_err() {
376
                self.shutdown().await;
377
                return Err(EngineError::EventReceiverClosed);
378
            }
379
        }
380
        self.start_tasks();
381
        self.resume_upload_forwarders().await;
382
        self.sync.start().await;
383
384
        let mut shutdown_reply = None;
385
        let mut handles_dropped = false;
386
        let mut logouts = self.logouts.subscribe();
387
        let ending = loop {
388
            tokio::select! {
389
                biased;
390
                Some(termination) = self.stops.recv() => break Ok(termination),
391
                () = events.closed() => break Err(EngineError::EventReceiverClosed),
392
                () = settled(&mut logouts), if handles_dropped => {
393
                    break Ok(Termination::HandlesDropped);
394
                }
395
                Some((context, panic)) = self.tasks.join_next(), if !self.tasks.is_empty() => {
396
                    let Some(message) = panic else { continue };
397
                    if let TaskContext::Command(label) = context {
398
                        tracing::error!(label, panic = %message, "command task panicked");
399
                        continue;
400
                    }
401
                    break Err(EngineError::TaskPanicked { task: context.label() });
402
                }
403
                Some(note) = self.notes.recv() => self.actor_note(note).await,
404
                request = self.port.recv(), if !handles_dropped => {
405
                    // nobody's left to ask, but a logout they asked for still deserves to land
406
                    let Some(request) = request else {
407
                        handles_dropped = true;
408
                        continue;
409
                    };
410
                    if let Some(reply) = self.dispatch(request).await {
411
                        shutdown_reply = Some(reply);
412
                        break Ok(Termination::ShutDown);
413
                    }
414
                }
415
            }
416
        };
417
418
        if let Err(error) = &ending {
419
            let _ = events.send(Event::Failed(error.failure())).await;
420
        }
421
        self.shutdown().await;
422
        let _ = events.send(Event::Stopped).await;
423
        if let Some(reply) = shutdown_reply {
424
            reply.done(Ok(()));
425
        }
426
        // everything else still queued answers `Interrupted` as it drops, but a shutdown did happen
427
        while let Some(Request { command, reply }) = self.port.try_recv() {
428
            if matches!(command, Command::Lifecycle(LifecycleCommand::Shutdown)) {
429
                reply.done(Ok(()));
430
            }
431
        }
432
        ending
433
    }
434
435
    fn start_tasks(&mut self) {
436
        let queues = self.queues.take().expect("engine tasks start once");
437
        let (incoming, incoming_verifications) = mpsc::unbounded_channel();
438
        self.verification_handler = Some(self.verification_handler(incoming));
439
        let client = &self.client;
440
        let events = || self.port.event_sender();
441
        let (notices, notice_queue) = mpsc::unbounded_channel();
442
        let watchers: [(&'static str, EngineBoxFuture<'static, ()>); 12] = [
443
            (
444
                "upload tracker",
445
                Box::pin(watchers::uploads(
446
                    client.clone(),
447
                    client.send_queue().subscribe(),
448
                    queues.upload_reports,
449
                    events(),
450
                )),
451
            ),
452
            (
453
                "room subscription watcher",
454
                Box::pin(watchers::room_subscriptions(
455
                    self.sync.clone(),
456
                    self.subscriptions.subscribe(),
457
                )),
458
            ),
459
            (
460
                "session change watcher",
461
                Box::pin(session::session(
462
                    client.clone(),
463
                    queues.session_changes,
464
                    queues.persistence,
465
                    notices,
466
                    client
467
                        .device_id()
468
                        .expect("authentication was checked during construction")
469
                        .to_owned(),
470
                    self.logouts.clone(),
471
                )),
472
            ),
473
            (
474
                "session notice watcher",
475
                Box::pin(session::session_notices(
476
                    notice_queue,
477
                    events(),
478
                    self.stop.clone(),
479
                )),
480
            ),
481
            (
482
                "connection state watcher",
483
                Box::pin(watchers::connection(
484
                    self.sync.state(),
485
                    self.sync.clone(),
486
                    client.clone(),
487
                    events(),
488
                    self.port.connection_sender(),
489
                )),
490
            ),
491
            (
492
                "send queue watcher",
493
                Box::pin(watchers::send_queue_errors(
494
                    client.clone(),
495
                    client.send_queue().subscribe_errors(),
496
                    self.port.connection_sender().subscribe(),
497
                )),
498
            ),
499
            (
500
                "encryption state watcher",
501
                Box::pin(watchers::encryption(client.clone(), events())),
502
            ),
503
            (
504
                "room list watcher",
505
                Box::pin(watchers::rooms(client.clone(), events())),
506
            ),
507
            (
508
                "notification settings watcher",
509
                Box::pin(watchers::notification_settings(client.clone(), events())),
510
            ),
511
            (
512
                "room command worker",
513
                Box::pin(mutations::run_scheduler(
514
                    client.clone(),
515
                    queues.mutations,
516
                    self.config.command_capacity,
517
                )),
518
            ),
519
            (
520
                "notification command worker",
521
                Box::pin(commands::run_notifications(
522
                    client.clone(),
523
                    queues.notifications,
524
                )),
525
            ),
526
            (
527
                "encryption command worker",
528
                Box::pin(commands::run_encryption(
529
                    client.clone(),
530
                    events(),
531
                    queues.encryption,
532
                    incoming_verifications,
533
                )),
534
            ),
535
        ];
536
        for (label, task) in watchers {
537
            self.tasks.spawn(TaskContext::Watcher(label), task);
538
        }
539
    }
540
541
    fn verification_handler(
542
        &self,
543
        incoming: IncomingVerifications,
544
    ) -> matrix_sdk::event_handler::EventHandlerHandle {
545
        use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
546
        self.client.add_event_handler(
547
            move |event: ToDeviceKeyVerificationRequestEvent, client: Client| {
548
                let incoming = incoming.clone();
549
                async move {
550
                    let request = client
551
                        .encryption()
552
                        .get_verification_request(
553
                            &event.sender,
554
                            event.content.transaction_id.as_str(),
555
                        )
556
                        .await;
557
                    if let Some(request) = request {
558
                        let _ = incoming.send(request);
559
                    }
560
                }
561
            },
562
        )
563
    }
564
565
    /// hands back the shutdown request's responder, which is answered once we've stopped
566
    async fn dispatch(&mut self, Request { command, reply }: Request) -> Option<Responder> {
567
        let client = self.client.clone();
568
        match command {
569
            Command::Lifecycle(LifecycleCommand::Shutdown) => return Some(reply),
570
            Command::Account(AccountCommand::Logout) => {
571
                let events = self.port.event_sender();
572
                let stop = self.stop.clone();
573
                let logouts = self.logouts.clone();
574
                logouts.send_modify(|logouts| logouts.in_flight += 1);
575
                self.spawn_command("logout", reply, async move {
576
                    let result = client.logout().await;
577
                    if result.is_ok() {
578
                        let _ = events.send(Event::LoggedOut).await;
579
                        let _ = stop.send(Termination::LoggedOut);
580
                    }
581
                    let mut rejected = None;
582
                    logouts.send_if_modified(|logouts| {
583
                        if result.is_err() && logouts.in_flight == 1 {
584
                            rejected = logouts.rejected.take();
585
                        }
586
                        false
587
                    });
588
                    if let Some(soft_logout) = rejected {
589
                        session::reject_session(&events, &stop, soft_logout).await;
590
                    }
591
                    logouts.send_modify(|logouts| logouts.in_flight -= 1);
592
                    result.map_err(|source| Error::from_sdk("logout failed", source))
593
                });
594
            }
595
            Command::Account(AccountCommand::SetAccountData {
596
                event_type,
597
                content,
598
            }) => enqueue(
599
                &self.mutations,
600
                Job {
601
                    command: Mutation::GlobalAccountData {
602
                        event_type,
603
                        content,
604
                    },
605
                    reply,
606
                },
607
            ),
608
            Command::Account(AccountCommand::SetDisplayName(name)) => {
609
                self.spawn_command("set display name", reply, async move {
610
                    client
611
                        .account()
612
                        .set_display_name(name.as_deref())
613
                        .await
614
                        .map_err(|source| Error::from_sdk("setting display name failed", source))
615
                });
616
            }
617
            Command::Account(AccountCommand::SetAvatar(avatar)) => {
618
                self.spawn_command("set avatar", reply, async move {
619
                    client
620
                        .account()
621
                        .set_avatar_url(avatar.as_deref())
622
                        .await
623
                        .map_err(|source| Error::from_sdk("setting avatar failed", source))
624
                });
625
            }
626
            Command::Account(AccountCommand::UploadAvatar { mimetype, bytes }) => {
627
                let max_bytes = self.config.max_media_bytes;
628
                self.spawn_command("upload avatar", reply, async move {
629
                    commands::upload_avatar(&client, &mimetype, bytes, max_bytes).await
630
                });
631
            }
632
            Command::Account(AccountCommand::RenameDevice {
633
                device_id,
634
                display_name,
635
            }) => self.spawn_command("rename device", reply, async move {
636
                commands::rename_device(&client, &device_id, &display_name).await
637
            }),
638
            Command::Account(AccountCommand::DeleteDevices { device_ids, auth }) => {
639
                self.spawn_command("delete devices", reply, async move {
640
                    commands::delete_devices(&client, &device_ids, auth).await
641
                });
642
            }
643
            Command::Room(RoomCommand::Open(room_id)) => self.open_room(room_id, reply).await,
644
            Command::Room(RoomCommand::Close(room_id)) => self.close_room(room_id, reply).await,
645
            Command::Room(RoomCommand::SetTyping { room_id, typing }) => {
646
                self.spawn_command("typing", reply, async move {
647
                    commands::set_typing(&client, &room_id, typing).await
648
                });
649
            }
650
            Command::Room(RoomCommand::CancelSend {
651
                room_id,
652
                transaction_id,
653
            }) => self.spawn_command("cancel send", reply, async move {
654
                commands::cancel_send(&client, &room_id, &transaction_id).await
655
            }),
656
            Command::Room(RoomCommand::RetrySend {
657
                room_id,
658
                transaction_id,
659
            }) => self.spawn_command("retry send", reply, async move {
660
                commands::retry_send(&client, &room_id, &transaction_id).await
661
            }),
662
            Command::Room(command) => enqueue(
663
                &self.mutations,
664
                Job {
665
                    command: Mutation::Room(command),
666
                    reply,
667
                },
668
            ),
669
            Command::Timeline(command) => self.route_timeline(command, reply),
670
            Command::Media(MediaCommand::Upload(upload)) => self.upload(upload, reply),
671
            Command::Encryption(command) => enqueue(&self.encryption, Job { command, reply }),
672
            Command::Notification(command) => {
673
                enqueue(&self.notifications, Job { command, reply });
674
            }
675
        }
676
        None
677
    }
678
679
    fn spawn_command(
680
        &mut self,
681
        label: &'static str,
682
        reply: Responder,
683
        work: impl Future<Output = Result<(), Error>> + SendOutsideWasm + 'static,
684
    ) {
685
        self.tasks.spawn(TaskContext::Command(label), async move {
686
            reply.done(work.await);
687
        });
688
    }
689
690
    fn upload(&mut self, upload: crate::AttachmentUpload, reply: Responder) {
691
        let Some(room) = self.client.get_room(upload.timeline_id.room_id()) else {
692
            return reply.done(Err(Error::room_unknown()));
693
        };
694
        let mut ready = self.upload_forwarder(&room);
695
        let max_bytes = self.config.max_media_bytes;
696
        // queueing before the forwarder listens would lose the first progress ticks
697
        self.spawn_command("upload", reply, async move {
698
            ready
699
                .wait_for(|subscribed| *subscribed)
700
                .await
701
                .map_err(|_| Error::new(ErrorKind::Store, "reading the media send queue failed"))?;
702
            media::queue_upload(room, upload, max_bytes).await
703
        });
704
    }
705
706
    fn upload_forwarder(&mut self, room: &matrix_sdk::Room) -> watch::Receiver<bool> {
707
        self.uploads.retain(|_, ready| ready.has_changed().is_ok());
708
        if let Some(ready) = self.uploads.get(room.room_id()) {
709
            return ready.clone();
710
        }
711
        let (subscribed, ready) = watch::channel(false);
712
        self.tasks.spawn(
713
            TaskContext::Command("upload watcher"),
714
            watchers::room_uploads(room.clone(), self.upload_reports.clone(), subscribed),
715
        );
716
        self.uploads
717
            .insert(room.room_id().to_owned(), ready.clone());
718
        ready
719
    }
720
721
    /// the sdk resumes uploads left over from last time, and they deserve progress too
722
    async fn resume_upload_forwarders(&mut self) {
723
        let Ok(echoes) = self.client.send_queue().local_echoes().await else {
724
            return;
725
        };
726
        for room_id in echoes.keys() {
727
            if let Some(room) = self.client.get_room(room_id) {
728
                self.upload_forwarder(&room);
729
            }
730
        }
731
    }
732
733
    fn route_timeline(&mut self, command: TimelineCommand, reply: Responder) {
734
        let sender = match self.rooms.get(command.room_id()) {
735
            Some(RoomSlot::Active { sender, .. }) => Some(sender.clone()),
736
            _ => None,
737
        };
738
        match (sender, command) {
739
            (Some(sender), command) => send_actor(&sender, ActorRequest::Execute(command, reply)),
740
            (
741
                None,
742
                TimelineCommand::MarkRead {
743
                    timeline_id,
744
                    event_id,
745
                },
746
            ) => {
747
                let client = self.client.clone();
748
                self.spawn_command("mark read", reply, async move {
749
                    commands::mark_read(&client, &timeline_id, event_id).await
750
                });
751
            }
752
            (None, _) => reply.done(Err(Error::new(ErrorKind::RoomNotOpen, "room is not open"))),
753
        }
754
    }
755
756
    async fn open_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
757
        match self.rooms.get_mut(&room_id) {
758
            Some(RoomSlot::Active { sender, .. }) => send_actor(sender, ActorRequest::Open(reply)),
759
            Some(RoomSlot::Closing { pending, .. }) => {
760
                defer(pending, Deferred::Open(reply), self.config.command_capacity);
761
            }
762
            None => self.start_actor(room_id, reply).await,
763
        }
764
    }
765
766
    async fn close_room(&mut self, room_id: OwnedRoomId, reply: Responder) {
767
        let (generation, sender) = match self.rooms.get_mut(&room_id) {
768
            None => return reply.done(Ok(())),
769
            Some(RoomSlot::Closing { pending, .. }) => {
770
                return defer(
771
                    pending,
772
                    Deferred::Close(reply),
773
                    self.config.command_capacity,
774
                );
775
            }
776
            Some(RoomSlot::Active { generation, sender }) => (*generation, sender.clone()),
777
        };
778
        match sender.try_send(ActorRequest::Close(reply)) {
779
            Ok(()) => {}
780
            // the actor already left on its own, which is closed enough
781
            Err(mpsc::error::TrySendError::Closed(request)) => request.into_reply().done(Ok(())),
782
            Err(mpsc::error::TrySendError::Full(request)) => {
783
                return request.into_reply().done(Err(Error::new(
784
                    ErrorKind::RoomBusy,
785
                    "room command queue is full",
786
                )));
787
            }
788
        }
789
        self.rooms.insert(
790
            room_id,
791
            RoomSlot::Closing {
792
                generation,
793
                pending: VecDeque::new(),
794
            },
795
        );
796
        self.update_room_subscriptions();
797
    }
798
799
    async fn start_actor(&mut self, room_id: OwnedRoomId, reply: Responder) {
800
        let Some(room) = self.client.get_room(&room_id) else {
801
            return reply.done(Err(Error::room_unknown()));
802
        };
803
        self.next_generation = self.next_generation.wrapping_add(1);
804
        let generation = self.next_generation;
805
        let (sender, requests) = mpsc::channel(self.config.command_capacity);
806
        let (supervisor, panics) = Supervisor::new();
807
        let actor = RoomActor {
808
            room_id: room_id.clone(),
809
            generation,
810
            room,
811
            requests,
812
            notes: self.note_sender.clone(),
813
            context: SessionContext {
814
                events: self.port.event_sender(),
815
                media: self.media.clone(),
816
                current_user_id: self.client.user_id().map(ToOwned::to_owned),
817
                initial_history_limit: self.config.initial_history_limit,
818
                supervisor,
819
            },
820
            panics,
821
            opening: vec![reply],
822
        };
823
        self.rooms
824
            .insert(room_id.clone(), RoomSlot::Active { generation, sender });
825
        self.tasks.spawn(
826
            TaskContext::RoomActor {
827
                room_id,
828
                generation,
829
            },
830
            actor.run(),
831
        );
832
        self.update_room_subscriptions();
833
    }
834
835
    async fn actor_note(&mut self, note: ActorNote) {
836
        let (room_id, generation) = match &note {
837
            ActorNote::Leaving {
838
                room_id,
839
                generation,
840
            }
841
            | ActorNote::Exited {
842
                room_id,
843
                generation,
844
                ..
845
            } => (room_id.clone(), *generation),
846
        };
847
        if self
848
            .rooms
849
            .get(&room_id)
850
            .is_none_or(|slot| slot.generation() != generation)
851
        {
852
            return;
853
        }
854
        let ActorNote::Exited { leftovers, .. } = note else {
855
            if let Some(slot @ RoomSlot::Active { .. }) = self.rooms.get_mut(&room_id) {
856
                *slot = RoomSlot::Closing {
857
                    generation,
858
                    pending: VecDeque::new(),
859
                };
860
                self.update_room_subscriptions();
861
            }
862
            return;
863
        };
864
        let pending = match self.rooms.remove(&room_id) {
865
            Some(RoomSlot::Closing { pending, .. }) => pending,
866
            _ => VecDeque::new(),
867
        };
868
        // leftovers were sent before anything deferred, so they go first
869
        for request in leftovers {
870
            match request {
871
                ActorRequest::Open(reply) => self.open_room(room_id.clone(), reply).await,
872
                ActorRequest::Close(reply) => reply.done(Ok(())),
873
                ActorRequest::Execute(command, reply) => self.route_timeline(command, reply),
874
            }
875
        }
876
        for deferred in pending {
877
            match deferred {
878
                Deferred::Open(reply) => self.open_room(room_id.clone(), reply).await,
879
                Deferred::Close(reply) => self.close_room(room_id.clone(), reply).await,
880
            }
881
        }
882
        self.update_room_subscriptions();
883
    }
884
885
    fn update_room_subscriptions(&self) {
886
        let room_ids = self
887
            .rooms
888
            .iter()
889
            .filter(|(_, slot)| matches!(slot, RoomSlot::Active { .. }))
890
            .map(|(room_id, _)| room_id.clone())
891
            .collect();
892
        self.subscriptions.send_replace(room_ids);
893
    }
894
895
    async fn shutdown(&mut self) {
896
        if let Some(handler) = self.verification_handler.take() {
897
            self.client.remove_event_handler(handler);
898
        }
899
        self.port.close();
900
        self.tasks.shutdown().await;
901
        self.rooms.clear();
902
        self.uploads.clear();
903
        self.media.clear().await;
904
        self.sync.stop().await;
905
        self.port
906
            .connection_sender()
907
            .send_replace(ConnectionState::Stopped);
908
        let _ = self
909
            .port
910
            .event_sender()
911
            .send(Event::ConnectionChanged(ConnectionState::Stopped))
912
            .await;
913
    }
914
}
915
916
fn send_actor(sender: &mpsc::Sender<ActorRequest>, request: ActorRequest) {
917
    if let Err(error) = sender.try_send(request) {
918
        let (request, error) = match error {
919
            mpsc::error::TrySendError::Full(request) => (
920
                request,
921
                Error::new(ErrorKind::RoomBusy, "room command queue is full"),
922
            ),
923
            mpsc::error::TrySendError::Closed(request) => (
924
                request,
925
                Error::new(ErrorKind::RoomNotOpen, "room is not open"),
926
            ),
927
        };
928
        request.into_reply().done(Err(error));
929
    }
930
}
931
932
fn defer(pending: &mut VecDeque<Deferred>, deferred: Deferred, capacity: usize) {
933
    if pending.len() < capacity {
934
        return pending.push_back(deferred);
935
    }
936
    let (Deferred::Open(reply) | Deferred::Close(reply)) = deferred;
937
    reply.done(Err(Error::new(
938
        ErrorKind::RoomBusy,
939
        "room lifecycle queue is full",
940
    )));
941
}
942
943
async fn settled(logouts: &mut watch::Receiver<Logouts>) {
944
    let _ = logouts.wait_for(|logouts| logouts.in_flight == 0).await;
945
}
946
947
fn enqueue<T>(sender: &mpsc::Sender<Job<T>>, job: Job<T>) {
948
    if let Err(error) = sender.try_send(job) {
949
        let (job, error) = match error {
950
            mpsc::error::TrySendError::Full(job) => (
951
                job,
952
                Error::new(ErrorKind::QueueFull, "command queue is full"),
953
            ),
954
            mpsc::error::TrySendError::Closed(job) => (job, Error::interrupted()),
955
        };
956
        job.reply.done(Err(error));
957
    }
958
}