atagen

macltok

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