atagen

macltok

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