atagen

macltok

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