atagen

macltok

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