atagen

macltok

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