atagen

macltok

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