atagen

macltok

public
Star0
Watch0
21b9227729a49cccc12924f2ca63c0f1890c6b89
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/mirror.rsRust851 lines29.3 KB
1
use std::{
2
    collections::HashMap,
3
    fmt,
4
    pin::Pin,
5
    sync::{Arc, Mutex, MutexGuard, PoisonError},
6
    task::{Context, Poll},
7
    time::Duration,
8
};
9
10
use futures_util::{Stream, StreamExt};
11
use imbl::Vector;
12
use matrix_sdk::{
13
    SendOutsideWasm,
14
    executor::{AbortHandle, spawn},
15
};
16
use ruma::{OwnedDeviceId, OwnedRoomId, OwnedTransactionId, OwnedUserId};
17
#[cfg(feature = "serde")]
18
use serde::{Deserialize, Serialize};
19
use tokio::sync::broadcast;
20
21
use crate::{
22
    ClientHandle, Command, ConnectionState, EncryptionState, EngineFailure, ErrorKind, Event,
23
    EventContextCloseReason, HistoryRepairReport, HistoryState, ImagePacks, RecentHistoryState,
24
    RoomSummary, ThreadListClosure, ThreadListEntry, TimelineCommand, TimelineDiff, TimelineId,
25
    TimelineItem, TypingState,
26
    UploadOutcome, VerificationState, VerificationTarget, engine::executor::EngineBoxFuture,
27
    transport::PublicationRegistration,
28
};
29
30
#[cfg(not(target_family = "wasm"))]
31
type EngineBoxStream<T> = futures_util::stream::BoxStream<'static, T>;
32
#[cfg(target_family = "wasm")]
33
type EngineBoxStream<T> = futures_util::stream::LocalBoxStream<'static, T>;
34
35
const DEFAULT_CAPACITY: usize = 256;
36
const RESYNC_BACKOFF: Duration = Duration::from_millis(100);
37
const RESYNC_BACKOFF_CAP: Duration = Duration::from_secs(10);
38
39
/// everything the engine has said so far, folded together
40
#[derive(Clone, Debug, PartialEq)]
41
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42
#[non_exhaustive]
43
pub struct State {
44
    pub user_id: Option<OwnedUserId>,
45
    pub session: SessionStatus,
46
    pub connection: ConnectionState,
47
    pub encryption: Option<EncryptionState>,
48
    pub verification: Option<VerificationState>,
49
    pub verification_target: Option<VerificationTarget>,
50
    pub mentions: Option<crate::MentionsPage>,
51
    pub rooms: Vector<RoomSummary>,
52
    pub image_packs: ImagePacks,
53
    pub timelines: imbl::HashMap<TimelineId, TimelineState>,
54
    pub thread_lists: imbl::HashMap<OwnedRoomId, ThreadListState>,
55
    /// a room without an entry is idle. closing a room drops its entries here and in `repairs`
56
    pub recent_history: imbl::HashMap<OwnedRoomId, RecentHistoryState>,
57
    pub repairs: imbl::HashMap<OwnedRoomId, HistoryRepairReport>,
58
    pub typing: imbl::HashMap<OwnedRoomId, Vec<OwnedUserId>>,
59
    pub uploads: imbl::HashMap<OwnedTransactionId, UploadState>,
60
    /// what stopped the engine, if something went wrong
61
    pub engine_failure: Option<EngineFailure>,
62
    /// the last save of the session record that failed, until one succeeds
63
    pub session_save_failure: Option<String>,
64
}
65
66
#[derive(Clone, Debug, PartialEq)]
67
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68
#[non_exhaustive]
69
pub struct TimelineState {
70
    pub items: Vector<TimelineItem>,
71
    pub history: HistoryState,
72
    /// a batch didn't fit, so `items` is the last good copy until the next reset
73
    pub diverged: bool,
74
}
75
76
#[derive(Clone, Debug, PartialEq)]
77
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
78
#[non_exhaustive]
79
pub struct ThreadListState {
80
    pub entries: Vector<ThreadListEntry>,
81
    pub history: HistoryState,
82
}
83
84
#[derive(Clone, Debug, Eq, PartialEq)]
85
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
86
#[non_exhaustive]
87
pub struct UploadState {
88
    pub room_id: OwnedRoomId,
89
    pub uploaded_bytes: u64,
90
    pub total_bytes: u64,
91
    pub complete: bool,
92
    pub failure: Option<UploadFailure>,
93
}
94
95
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
97
#[non_exhaustive]
98
pub struct UploadFailure {
99
    pub kind: ErrorKind,
100
    pub will_retry: bool,
101
}
102
103
/// the `Termination` it'll end in, as far as the events tell
104
#[derive(Clone, Debug, Eq, PartialEq)]
105
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
106
pub enum SessionStatus {
107
    Active,
108
    SoftLoggedOut {
109
        device_id: OwnedDeviceId,
110
    },
111
    AccessRevoked,
112
    LoggedOut,
113
    /// stopped with the session still good
114
    Stopped,
115
}
116
117
#[derive(Clone, Debug)]
118
#[non_exhaustive]
119
pub struct Update {
120
    /// one more than the update before it, and a `State` carries the one it's current as of
121
    pub sequence: u64,
122
    pub change: StateChange,
123
}
124
125
#[derive(Clone, Debug)]
126
pub enum StateChange {
127
    /// start over from this. a subscription opens with one and gets another after lagging
128
    State(Arc<State>),
129
    Authenticated(OwnedUserId),
130
    Session(SessionStatus),
131
    /// the record on disk may now hold a spent refresh token
132
    SessionSaveFailed(String),
133
    /// saved again after `SessionSaveFailed`
134
    SessionSaved,
135
    Connection(ConnectionState),
136
    Encryption(EncryptionState),
137
    Verification(VerificationState),
138
    VerificationTarget(VerificationTarget),
139
    Mentions(crate::MentionsUpdate),
140
    /// room modes arrive with the `Rooms` that follows
141
    NotificationSettings,
142
    Rooms(Vector<RoomSummary>),
143
    ImagePacks(ImagePacks),
144
    Timeline {
145
        timeline_id: TimelineId,
146
        change: TimelineChange,
147
    },
148
    History {
149
        timeline_id: TimelineId,
150
        state: HistoryState,
151
    },
152
    ThreadList {
153
        room_id: OwnedRoomId,
154
        change: ThreadListChange,
155
    },
156
    RecentHistory {
157
        room_id: OwnedRoomId,
158
        state: RecentHistoryState,
159
    },
160
    HistoryRepair(HistoryRepairReport),
161
    Typing(TypingState),
162
    EventContextClosed {
163
        context_id: String,
164
        reason: EventContextCloseReason,
165
    },
166
    Upload {
167
        transaction_id: OwnedTransactionId,
168
        upload: UploadState,
169
    },
170
    UploadFinished {
171
        room_id: OwnedRoomId,
172
        transaction_id: OwnedTransactionId,
173
        outcome: UploadOutcome,
174
    },
175
    Failed(EngineFailure),
176
}
177
178
#[derive(Clone, Debug)]
179
pub enum TimelineChange {
180
    Reset {
181
        items: Vector<TimelineItem>,
182
    },
183
    /// `items` is what applying `diffs` left
184
    Changed {
185
        diffs: Arc<[TimelineDiff]>,
186
        items: Vector<TimelineItem>,
187
    },
188
    /// a batch didn't fit. a resync is on its way and nothing changes until its reset
189
    Diverged,
190
    Closed,
191
}
192
193
#[derive(Clone, Debug)]
194
pub enum ThreadListChange {
195
    Reset {
196
        entries: Vector<ThreadListEntry>,
197
    },
198
    /// `entries` is what applying `upserts` left
199
    Changed {
200
        upserts: Arc<[ThreadListEntry]>,
201
        entries: Vector<ThreadListEntry>,
202
    },
203
    History(HistoryState),
204
    Closed(ThreadListClosure),
205
}
206
207
/// drains the event stream into a `State` that any number of `MirrorHandle`s watch
208
pub struct Mirror {
209
    handle: ClientHandle,
210
    events: EngineBoxStream<Event>,
211
    shared: Arc<Shared>,
212
    _publication: PublicationRegistration,
213
    resyncs: HashMap<TimelineId, AbortHandle>,
214
    rooms_fetch: Option<AbortHandle>,
215
    packs_fetch: Option<AbortHandle>,
216
}
217
218
impl fmt::Debug for Mirror {
219
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220
        formatter.debug_struct("Mirror").finish_non_exhaustive()
221
    }
222
}
223
224
impl Mirror {
225
    /// takes any stream of the engine's events, so a host can still look at them on the way past
226
    pub fn new(
227
        handle: ClientHandle,
228
        events: impl Stream<Item = Event> + SendOutsideWasm + 'static,
229
    ) -> (Self, MirrorHandle) {
230
        Self::with_capacity(handle, events, DEFAULT_CAPACITY)
231
    }
232
233
    /// `capacity` updates can queue for a subscriber before it lags and starts over. a zero
234
    /// capacity counts as one
235
    pub fn with_capacity(
236
        handle: ClientHandle,
237
        events: impl Stream<Item = Event> + SendOutsideWasm + 'static,
238
        capacity: usize,
239
    ) -> (Self, MirrorHandle) {
240
        let state = State {
241
            user_id: None,
242
            session: SessionStatus::Active,
243
            connection: *handle.connection_state().borrow(),
244
            encryption: handle.encryption_state(),
245
            verification: None,
246
            verification_target: None,
247
            mentions: None,
248
            rooms: Vector::new(),
249
            image_packs: ImagePacks::default(),
250
            timelines: imbl::HashMap::new(),
251
            thread_lists: imbl::HashMap::new(),
252
            recent_history: imbl::HashMap::new(),
253
            repairs: imbl::HashMap::new(),
254
            typing: imbl::HashMap::new(),
255
            uploads: imbl::HashMap::new(),
256
            engine_failure: None,
257
            session_save_failure: None,
258
        };
259
        let shared = Arc::new(Shared {
260
            published: Mutex::new(Published {
261
                state: Arc::new(state),
262
                sequence: 0,
263
                updates: Some(broadcast::channel(capacity.max(1)).0),
264
                rooms_fetch: Fetch::Idle,
265
                packs_fetch: Fetch::Idle,
266
            }),
267
        });
268
        let publication = handle.register_publication_mirror();
269
        #[cfg(not(target_family = "wasm"))]
270
        let events = events.boxed();
271
        #[cfg(target_family = "wasm")]
272
        let events = events.boxed_local();
273
        let mirror = Self {
274
            handle,
275
            events,
276
            shared: shared.clone(),
277
            _publication: publication,
278
            resyncs: HashMap::new(),
279
            rooms_fetch: None,
280
            packs_fetch: None,
281
        };
282
        (mirror, MirrorHandle { shared })
283
    }
284
285
    /// resolves once the event stream ends, which is just after `Stopped`
286
    pub fn run(mut self) -> EngineBoxFuture<'static, ()> {
287
        Box::pin(async move {
288
            self.refetch_rooms();
289
            self.refetch_image_packs();
290
            while let Some(event) = self.events.next().await {
291
                self.apply(event);
292
            }
293
        })
294
    }
295
296
    fn apply(&mut self, event: Event) {
297
        match event {
298
            Event::ConnectionChanged(connection) => self.publish(|state| {
299
                state.connection = connection;
300
                Some(StateChange::Connection(connection))
301
            }),
302
            Event::Authenticated { user_id } => self.publish(|state| {
303
                state.user_id = Some(user_id.clone());
304
                Some(StateChange::Authenticated(user_id))
305
            }),
306
            Event::SessionSaveFailed { message } => self.publish(|state| {
307
                state.session_save_failure = Some(message.clone());
308
                Some(StateChange::SessionSaveFailed(message))
309
            }),
310
            Event::SessionTokensChanged => self.publish(|state| {
311
                state.session_save_failure.take()?;
312
                Some(StateChange::SessionSaved)
313
            }),
314
            Event::SessionAccessRejected { soft_logout } => self.publish(|state| {
315
                state.mentions = None;
316
                state.session = match soft_logout {
317
                    Some(device_id) => SessionStatus::SoftLoggedOut { device_id },
318
                    None => SessionStatus::AccessRevoked,
319
                };
320
                Some(StateChange::Session(state.session.clone()))
321
            }),
322
            Event::LoggedOut => self.publish(|state| {
323
                state.mentions = None;
324
                state.session = SessionStatus::LoggedOut;
325
                Some(StateChange::Session(SessionStatus::LoggedOut))
326
            }),
327
            Event::Stopped => {
328
                self.cancel_resyncs();
329
                self.publish(|state| {
330
                    if state.session != SessionStatus::Active {
331
                        return None;
332
                    }
333
                    state.session = SessionStatus::Stopped;
334
                    Some(StateChange::Session(SessionStatus::Stopped))
335
                });
336
            }
337
            Event::Failed(failure) => self.publish(|state| {
338
                state.engine_failure = Some(failure.clone());
339
                Some(StateChange::Failed(failure))
340
            }),
341
            Event::NotificationSettingsChanged => {
342
                self.publish(|_| Some(StateChange::NotificationSettings));
343
            }
344
            Event::RoomsChanged => self.refetch_rooms(),
345
            Event::MentionsChanged(update) => self.publish(|state| {
346
                if state.session != SessionStatus::Active
347
                    || state
348
                        .user_id
349
                        .as_ref()
350
                        .is_some_and(|account| account != &update.snapshot.boundary.account)
351
                {
352
                    return None;
353
                }
354
                if state
355
                    .mentions
356
                    .as_ref()
357
                    .map_or(false, |current| update.snapshot.revision < current.revision)
358
                {
359
                    return None;
360
                }
361
                state.mentions = Some(update.snapshot.clone());
362
                Some(StateChange::Mentions(update))
363
            }),
364
            Event::ImagePacksChanged => self.refetch_image_packs(),
365
            Event::EncryptionChanged(encryption) => self.publish(|state| {
366
                state.encryption = Some(encryption.clone());
367
                Some(StateChange::Encryption(encryption))
368
            }),
369
            Event::VerificationChanged(verification) => self.publish(|state| {
370
                state.verification = Some(verification.clone());
371
                Some(StateChange::Verification(verification))
372
            }),
373
            Event::VerificationTargetChanged(target) => self.publish(|state| {
374
                state.verification_target = Some(target.clone());
375
                Some(StateChange::VerificationTarget(target))
376
            }),
377
            Event::TimelineReset { timeline_id, items } => {
378
                self.cancel_resync(&timeline_id);
379
                self.publish(|state| {
380
                    let history = state
381
                        .timelines
382
                        .get(&timeline_id)
383
                        .map_or(HistoryState::Idle, |timeline| timeline.history.clone());
384
                    let timeline = TimelineState {
385
                        items: items.clone(),
386
                        history,
387
                        diverged: false,
388
                    };
389
                    state.timelines.insert(timeline_id.clone(), timeline);
390
                    Some(StateChange::Timeline {
391
                        timeline_id,
392
                        change: TimelineChange::Reset { items },
393
                    })
394
                });
395
            }
396
            Event::TimelineChanged { timeline_id, diffs } => self.apply_diffs(timeline_id, diffs),
397
            Event::HistoryChanged {
398
                timeline_id,
399
                state: history,
400
            } => self.publish(|state| {
401
                state.timelines.get_mut(&timeline_id)?.history = history.clone();
402
                Some(StateChange::History {
403
                    timeline_id,
404
                    state: history,
405
                })
406
            }),
407
            Event::RecentHistoryChanged {
408
                room_id,
409
                state: recent,
410
            } => self.publish(|state| {
411
                if recent == RecentHistoryState::Idle {
412
                    state.recent_history.remove(&room_id);
413
                } else {
414
                    state.recent_history.insert(room_id.clone(), recent.clone());
415
                }
416
                Some(StateChange::RecentHistory {
417
                    room_id,
418
                    state: recent,
419
                })
420
            }),
421
            Event::HistoryRepairChanged(report) => self.publish(|state| {
422
                state.repairs.insert(report.room_id.clone(), report.clone());
423
                Some(StateChange::HistoryRepair(report))
424
            }),
425
            Event::PublicationBarrier { acknowledgement } => {
426
                if let Some(acknowledgement) = acknowledgement {
427
                    acknowledgement.complete(true);
428
                }
429
            }
430
            Event::TimelineClosed { timeline_id } => {
431
                self.cancel_resync(&timeline_id);
432
                self.publish(|state| {
433
                    state.timelines.remove(&timeline_id)?;
434
                    if let TimelineId::Room(room_id) = &timeline_id {
435
                        state.repairs.remove(room_id);
436
                    }
437
                    Some(StateChange::Timeline {
438
                        timeline_id,
439
                        change: TimelineChange::Closed,
440
                    })
441
                });
442
            }
443
            Event::EventContextClosed { context_id, reason } => {
444
                self.publish(|_| Some(StateChange::EventContextClosed { context_id, reason }));
445
            }
446
            Event::ThreadListReset { room_id, entries } => self.publish(|state| {
447
                let history = state
448
                    .thread_lists
449
                    .get(&room_id)
450
                    .map_or(HistoryState::Idle, |list| list.history.clone());
451
                state.thread_lists.insert(
452
                    room_id.clone(),
453
                    ThreadListState {
454
                        entries: entries.clone(),
455
                        history,
456
                    },
457
                );
458
                Some(StateChange::ThreadList {
459
                    room_id,
460
                    change: ThreadListChange::Reset { entries },
461
                })
462
            }),
463
            Event::ThreadListChanged { room_id, upserts } => self.publish(|state| {
464
                let list = state.thread_lists.get_mut(&room_id)?;
465
                for entry in &upserts {
466
                    ThreadListEntry::upsert(&mut list.entries, entry.clone());
467
                }
468
                Some(StateChange::ThreadList {
469
                    room_id,
470
                    change: ThreadListChange::Changed {
471
                        upserts: upserts.into(),
472
                        entries: list.entries.clone(),
473
                    },
474
                })
475
            }),
476
            Event::ThreadListHistoryChanged {
477
                room_id,
478
                state: history,
479
            } => self.publish(|state| {
480
                state.thread_lists.get_mut(&room_id)?.history = history.clone();
481
                Some(StateChange::ThreadList {
482
                    room_id,
483
                    change: ThreadListChange::History(history),
484
                })
485
            }),
486
            Event::ThreadListClosed { room_id, reason } => self.publish(|state| {
487
                state.thread_lists.remove(&room_id)?;
488
                Some(StateChange::ThreadList {
489
                    room_id,
490
                    change: ThreadListChange::Closed(reason),
491
                })
492
            }),
493
            Event::TypingChanged(typing) => self.publish(|state| {
494
                if typing.users.is_empty() {
495
                    state.typing.remove(&typing.room_id);
496
                } else {
497
                    state
498
                        .typing
499
                        .insert(typing.room_id.clone(), typing.users.clone());
500
                }
501
                Some(StateChange::Typing(typing))
502
            }),
503
            Event::MediaUploadProgress {
504
                room_id,
505
                transaction_id,
506
                uploaded_bytes,
507
                total_bytes,
508
                complete,
509
            } => self.publish(|state| {
510
                let upload = UploadState {
511
                    room_id,
512
                    uploaded_bytes,
513
                    total_bytes,
514
                    complete,
515
                    failure: None,
516
                };
517
                state.uploads.insert(transaction_id.clone(), upload.clone());
518
                Some(StateChange::Upload {
519
                    transaction_id,
520
                    upload,
521
                })
522
            }),
523
            Event::MediaUploadFailed {
524
                room_id,
525
                transaction_id,
526
                kind,
527
                will_retry,
528
            } => self.publish(|state| {
529
                let upload = state
530
                    .uploads
531
                    .entry(transaction_id.clone())
532
                    .or_insert_with(|| UploadState {
533
                        room_id,
534
                        uploaded_bytes: 0,
535
                        total_bytes: 0,
536
                        complete: false,
537
                        failure: None,
538
                    });
539
                upload.failure = Some(UploadFailure { kind, will_retry });
540
                Some(StateChange::Upload {
541
                    transaction_id,
542
                    upload: upload.clone(),
543
                })
544
            }),
545
            Event::MediaUploadFinished {
546
                room_id,
547
                transaction_id,
548
                outcome,
549
            } => self.publish(|state| {
550
                state.uploads.remove(&transaction_id);
551
                Some(StateChange::UploadFinished {
552
                    room_id,
553
                    transaction_id,
554
                    outcome,
555
                })
556
            }),
557
        }
558
    }
559
560
    fn apply_diffs(&mut self, timeline_id: TimelineId, diffs: Vec<TimelineDiff>) {
561
        let mut diverged = false;
562
        self.publish(|state| {
563
            let Some(timeline) = state.timelines.get_mut(&timeline_id) else {
564
                diverged = true;
565
                state.timelines.insert(
566
                    timeline_id.clone(),
567
                    TimelineState {
568
                        items: Vector::new(),
569
                        history: HistoryState::Idle,
570
                        diverged: true,
571
                    },
572
                );
573
                return Some(StateChange::Timeline {
574
                    timeline_id: timeline_id.clone(),
575
                    change: TimelineChange::Diverged,
576
                });
577
            };
578
            if timeline.diverged {
579
                return None;
580
            }
581
            let diffs: Arc<[TimelineDiff]> = diffs.into();
582
            let change = match TimelineDiff::try_apply_all(diffs.to_vec(), &mut timeline.items) {
583
                Ok(()) => TimelineChange::Changed {
584
                    diffs,
585
                    items: timeline.items.clone(),
586
                },
587
                Err(error) => {
588
                    tracing::debug!(?timeline_id, length = error.length, "timeline diverged");
589
                    timeline.diverged = true;
590
                    diverged = true;
591
                    TimelineChange::Diverged
592
                }
593
            };
594
            Some(StateChange::Timeline {
595
                timeline_id: timeline_id.clone(),
596
                change,
597
            })
598
        });
599
        if diverged {
600
            let task = spawn(resync(self.handle.clone(), timeline_id.clone()));
601
            self.resyncs.insert(timeline_id, task.abort_handle());
602
        }
603
    }
604
605
    fn refetch_rooms(&mut self) {
606
        let fetch = fetch_rooms(self.handle.clone(), self.shared.clone());
607
        if let Some(task) = self
608
            .shared
609
            .start(|published| &mut published.rooms_fetch, fetch)
610
        {
611
            self.rooms_fetch = Some(task);
612
        }
613
    }
614
615
    fn refetch_image_packs(&mut self) {
616
        let fetch = fetch_image_packs(self.handle.clone(), self.shared.clone());
617
        if let Some(task) = self
618
            .shared
619
            .start(|published| &mut published.packs_fetch, fetch)
620
        {
621
            self.packs_fetch = Some(task);
622
        }
623
    }
624
625
    fn cancel_resync(&mut self, timeline_id: &TimelineId) {
626
        if let Some(task) = self.resyncs.remove(timeline_id) {
627
            task.abort();
628
        }
629
    }
630
631
    fn cancel_resyncs(&mut self) {
632
        for (_, task) in self.resyncs.drain() {
633
            task.abort();
634
        }
635
    }
636
637
    fn publish(&self, change: impl FnOnce(&mut State) -> Option<StateChange>) {
638
        self.shared.publish(change);
639
    }
640
}
641
642
impl Drop for Mirror {
643
    fn drop(&mut self) {
644
        self.cancel_resyncs();
645
        for task in [self.rooms_fetch.take(), self.packs_fetch.take()]
646
            .into_iter()
647
            .flatten()
648
        {
649
            task.abort();
650
        }
651
        self.shared.lock().updates = None;
652
    }
653
}
654
655
/// asks until the engine answers, since a busy room or a full queue is only ever for now.
656
/// a reset or a close for the timeline aborts it, and the engine sends a reset before it answers
657
async fn resync(handle: ClientHandle, timeline_id: TimelineId) {
658
    let mut backoff = RESYNC_BACKOFF;
659
    loop {
660
        let command = Command::Timeline(TimelineCommand::Resync {
661
            timeline_id: timeline_id.clone(),
662
        });
663
        match handle.execute(command).await {
664
            Ok(_) => return,
665
            Err(error) if error.kind == ErrorKind::Interrupted => return,
666
            Err(error) => {
667
                tracing::debug!(%error, ?timeline_id, "resync refused, trying again");
668
                matrix_sdk::sleep::sleep(backoff).await;
669
                backoff = (backoff * 2).min(RESYNC_BACKOFF_CAP);
670
            }
671
        }
672
    }
673
}
674
675
async fn fetch_rooms(handle: ClientHandle, shared: Arc<Shared>) {
676
    loop {
677
        let rooms: Vector<RoomSummary> = handle.rooms().await.into();
678
        shared.publish(|state| {
679
            state.rooms = rooms.clone();
680
            Some(StateChange::Rooms(rooms))
681
        });
682
        if !shared.fetched(|published| &mut published.rooms_fetch) {
683
            return;
684
        }
685
    }
686
}
687
688
/// a failed read keeps the packs from before, the next change asks again
689
async fn fetch_image_packs(handle: ClientHandle, shared: Arc<Shared>) {
690
    loop {
691
        match handle.image_packs().await {
692
            Ok(packs) => shared.publish(|state| {
693
                if state.image_packs == packs {
694
                    return None;
695
                }
696
                state.image_packs = packs.clone();
697
                Some(StateChange::ImagePacks(packs))
698
            }),
699
            Err(error) => tracing::warn!(%error, "reading image packs failed"),
700
        }
701
        if !shared.fetched(|published| &mut published.packs_fetch) {
702
            return;
703
        }
704
    }
705
}
706
707
/// cheap to clone, one per frontend
708
#[derive(Clone, Debug)]
709
pub struct MirrorHandle {
710
    shared: Arc<Shared>,
711
}
712
713
impl MirrorHandle {
714
    #[must_use]
715
    pub fn snapshot(&self) -> Arc<State> {
716
        self.shared.lock().state.clone()
717
    }
718
719
    /// opens with the current state, then every update after it. ends once the mirror stops
720
    #[must_use]
721
    pub fn subscribe(&self) -> Subscription {
722
        let (first, receiving) = self.shared.clone().subscribe();
723
        let rest = futures_util::stream::unfold(receiving, |(shared, receiver)| async move {
724
            let mut receiver = receiver?;
725
            match receiver.recv().await {
726
                Ok(update) => Some((update, (shared, Some(receiver)))),
727
                Err(broadcast::error::RecvError::Lagged(missed)) => {
728
                    tracing::debug!(missed, "mirror subscriber lagged, starting it over");
729
                    Some(shared.subscribe())
730
                }
731
                Err(broadcast::error::RecvError::Closed) => None,
732
            }
733
        });
734
        let updates = futures_util::stream::once(std::future::ready(first)).chain(rest);
735
        #[cfg(not(target_family = "wasm"))]
736
        let updates = updates.boxed();
737
        #[cfg(target_family = "wasm")]
738
        let updates = updates.boxed_local();
739
        Subscription(updates)
740
    }
741
}
742
743
pub struct Subscription(EngineBoxStream<Update>);
744
745
impl fmt::Debug for Subscription {
746
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
747
        formatter
748
            .debug_struct("Subscription")
749
            .finish_non_exhaustive()
750
    }
751
}
752
753
impl Stream for Subscription {
754
    type Item = Update;
755
756
    fn poll_next(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<Update>> {
757
        self.0.poll_next_unpin(context)
758
    }
759
}
760
761
#[derive(Debug)]
762
struct Shared {
763
    published: Mutex<Published>,
764
}
765
766
#[derive(Debug)]
767
struct Published {
768
    state: Arc<State>,
769
    sequence: u64,
770
    /// gone once the mirror stops, which ends every subscription
771
    updates: Option<broadcast::Sender<Update>>,
772
    rooms_fetch: Fetch,
773
    packs_fetch: Fetch,
774
}
775
776
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
777
enum Fetch {
778
    Idle,
779
    Running,
780
    Again,
781
}
782
783
impl Shared {
784
    fn lock(&self) -> MutexGuard<'_, Published> {
785
        self.published
786
            .lock()
787
            .unwrap_or_else(PoisonError::into_inner)
788
    }
789
790
    /// one fetch at a time, with at most one more owed
791
    fn start(
792
        &self,
793
        fetch: impl FnOnce(&mut Published) -> &mut Fetch,
794
        task: impl Future<Output = ()> + SendOutsideWasm + 'static,
795
    ) -> Option<AbortHandle> {
796
        let mut published = self.lock();
797
        let fetch = fetch(&mut published);
798
        match *fetch {
799
            Fetch::Idle => {
800
                *fetch = Fetch::Running;
801
                drop(published);
802
                Some(spawn(task).abort_handle())
803
            }
804
            Fetch::Running => {
805
                *fetch = Fetch::Again;
806
                None
807
            }
808
            Fetch::Again => None,
809
        }
810
    }
811
812
    /// whether another fetch was asked for while this one ran
813
    fn fetched(&self, fetch: impl FnOnce(&mut Published) -> &mut Fetch) -> bool {
814
        let mut published = self.lock();
815
        let fetch = fetch(&mut published);
816
        if *fetch == Fetch::Again {
817
            *fetch = Fetch::Running;
818
            return true;
819
        }
820
        *fetch = Fetch::Idle;
821
        false
822
    }
823
824
    /// state, sequence and send move together under the one lock, which is what lets
825
    /// `subscribe` take a snapshot and a receiver with nothing between them
826
    fn publish(&self, change: impl FnOnce(&mut State) -> Option<StateChange>) {
827
        let mut published = self.lock();
828
        let Some(change) = change(Arc::make_mut(&mut published.state)) else {
829
            return;
830
        };
831
        published.sequence += 1;
832
        let update = Update {
833
            sequence: published.sequence,
834
            change,
835
        };
836
        if let Some(updates) = &published.updates {
837
            let _ = updates.send(update);
838
        }
839
    }
840
841
    fn subscribe(self: Arc<Self>) -> (Update, (Arc<Self>, Option<broadcast::Receiver<Update>>)) {
842
        let published = self.lock();
843
        let update = Update {
844
            sequence: published.sequence,
845
            change: StateChange::State(published.state.clone()),
846
        };
847
        let receiver = published.updates.as_ref().map(broadcast::Sender::subscribe);
848
        drop(published);
849
        (update, (self, receiver))
850
    }
851
}