atagen

macltok

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