atagen

macltok

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