atagen

macltok

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