ailloli_ui_runtime/app/
ui_inbox.rs

1//! Bounded cross-thread inbox for dispatching work onto the UI thread.
2
3use std::fmt;
4use std::num::NonZeroUsize;
5use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
6use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
7use std::sync::{Arc, Mutex};
8
9/// Narrow thread-safe callback used to wake the UI host.
10///
11/// Implementations should return quickly and must be callable concurrently.
12/// A successful wake means only that the host was signaled; mailbox draining
13/// remains the host's responsibility.
14///
15/// # Examples
16///
17/// ```
18/// use ailloli_ui_runtime::app::{UiWake, UiWakeError};
19/// struct Wake;
20/// impl UiWake for Wake { fn wake(&self) -> Result<(), UiWakeError> { Ok(()) } }
21/// assert!(Wake.wake().is_ok());
22/// ```
23pub trait UiWake: Send + Sync + 'static {
24    /// Signals the UI host or reports a closed/transient target.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`UiWakeError::TargetClosed`] when the target cannot accept any
29    /// future wake, or [`UiWakeError::TemporarilyUnavailable`] when retrying may
30    /// succeed.
31    ///
32    /// # Examples
33    ///
34    /// ```
35    /// use ailloli_ui_runtime::app::{UiWake, UiWakeError};
36    /// struct Closed;
37    /// impl UiWake for Closed { fn wake(&self) -> Result<(), UiWakeError> { Err(UiWakeError::TargetClosed) } }
38    /// assert_eq!(Closed.wake(), Err(UiWakeError::TargetClosed));
39    /// ```
40    fn wake(&self) -> Result<(), UiWakeError>;
41}
42
43/// Failure reported by a host wake callback.
44///
45/// # Examples
46///
47/// ```
48/// use ailloli_ui_runtime::app::UiWakeError;
49/// assert_eq!(UiWakeError::TargetClosed.to_string(), "the UI wake target is closed");
50/// ```
51#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
52pub enum UiWakeError {
53    /// The host target has permanently closed.
54    #[error("the UI wake target is closed")]
55    TargetClosed,
56    /// The target may accept a later retry.
57    #[error("the UI wake target is temporarily unavailable")]
58    TemporarilyUnavailable,
59}
60
61/// Failure sending through a bounded UI mailbox.
62///
63/// `EnqueuedButWakeFailed` is not a send rollback: the message remains queued
64/// and must not be blindly resent. Call [`UiSender::request_wake`] to retry only
65/// the notification.
66///
67/// # Examples
68///
69/// ```
70/// use ailloli_ui_runtime::app::{UiSendError, UiWakeError};
71/// assert!(matches!(UiSendError::EnqueuedButWakeFailed(UiWakeError::TargetClosed), UiSendError::EnqueuedButWakeFailed(_)));
72/// ```
73#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
74pub enum UiSendError {
75    /// Bounded queue has no free slot; the message was not enqueued.
76    #[error("the UI inbox is full")]
77    Full,
78    /// Receiver is gone; the message was not enqueued.
79    #[error("the UI inbox is closed")]
80    Closed,
81    /// Message is queued, but the host notification failed.
82    #[error("the message was enqueued, but waking the UI host failed: {0}")]
83    EnqueuedButWakeFailed(UiWakeError),
84}
85
86/// Atomic instrumentation snapshot for a generic UI mailbox.
87///
88/// Counters are cumulative and use atomic wrapping addition at their integer
89/// widths. A snapshot loads fields independently, so concurrent producers can
90/// make it internally non-transactional. `current_depth` includes queued and
91/// prefetched-pending messages; `max_depth` is its observed high-water mark.
92///
93/// # Examples
94///
95/// ```
96/// use ailloli_ui_runtime::app::UiInboxStats;
97/// let stats = UiInboxStats::default();
98/// assert_eq!((stats.enqueued, stats.current_depth, stats.max_depth), (0, 0, 0));
99/// ```
100#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
101pub struct UiInboxStats {
102    /// Successfully queued messages.
103    pub enqueued: u64,
104    /// Messages returned in successful or subsequently failed drain calls.
105    pub drained: u64,
106    /// Nonblocking sends rejected because the queue was full.
107    pub overflow: u64,
108    /// First observed channel disconnection (zero or one).
109    pub disconnected: u64,
110    /// Host wake callback invocations.
111    pub wake_calls: u64,
112    /// Wake callback invocations returning an error.
113    pub wake_failures: u64,
114    /// Queued plus prefetched messages not yet delivered to a drain result.
115    pub current_depth: usize,
116    /// Maximum observed `current_depth`.
117    pub max_depth: usize,
118}
119
120/// Atomic backing counters shared between producers and consumer.
121#[derive(Default)]
122struct AtomicStats {
123    /// Messages accepted by the bounded channel.
124    enqueued: AtomicU64,
125    /// Messages returned to the UI consumer.
126    drained: AtomicU64,
127    /// Sends rejected because the bounded channel was full.
128    overflow: AtomicU64,
129    /// Operations that observed the opposite endpoint closed.
130    disconnected: AtomicU64,
131    /// Host wake callback invocations attempted.
132    wake_calls: AtomicU64,
133    /// Host wake callbacks that returned an error.
134    wake_failures: AtomicU64,
135    /// Messages queued or prefetched but not yet delivered.
136    current_depth: AtomicUsize,
137    /// Maximum observed pending depth since channel creation.
138    max_depth: AtomicUsize,
139}
140
141/// Coalesced host-notification state.
142#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
143enum WakeStatus {
144    #[default]
145    /// No queued work currently requires a host notification.
146    Idle,
147    /// Work is pending and still needs a host notification.
148    NeedsWake,
149    /// Callback invocation `u64` is executing outside the state mutex.
150    InFlight(u64),
151    /// A callback succeeded for the currently pending work.
152    Signaled,
153}
154
155/// Installed callback, notification state, and wrapping attempt sequence.
156struct WakeState {
157    /// Optional thread-safe host notification callback.
158    callback: Option<Arc<dyn UiWake>>,
159    /// Coalesced notification lifecycle for queued work.
160    status: WakeStatus,
161    /// Wrapping identity reserved for the next callback invocation.
162    next_attempt: u64,
163}
164
165/// Implements the Default contract for WakeState.
166impl Default for WakeState {
167    /// Constructs the documented default value.
168    fn default() -> Self {
169        Self {
170            callback: None,
171            status: WakeStatus::Idle,
172            next_attempt: 1,
173        }
174    }
175}
176
177/// Shared synchronization and instrumentation state.
178struct Shared {
179    /// Installed callback and wake lifecycle protected from producers.
180    wake: Mutex<WakeState>,
181    /// Whether any operation has observed channel disconnection.
182    disconnected_observed: AtomicBool,
183    /// Shared wrapping instrumentation counters.
184    stats: AtomicStats,
185}
186
187/// Provides the operations defined for Shared.
188impl Shared {
189    /// Creates disconnected-false, wake-idle shared state with zero counters.
190    fn new() -> Self {
191        Self {
192            wake: Mutex::new(WakeState::default()),
193            disconnected_observed: AtomicBool::new(false),
194            stats: AtomicStats::default(),
195        }
196    }
197
198    /// Atomically reserves one diagnostic depth before queue publication.
199    fn reserve_depth(&self) {
200        let depth = self.stats.current_depth.fetch_add(1, Ordering::AcqRel) + 1;
201        self.stats.max_depth.fetch_max(depth, Ordering::Relaxed);
202    }
203
204    /// Reverses a reservation after a failed queue publication.
205    fn cancel_depth_reservation(&self) {
206        self.stats.current_depth.fetch_sub(1, Ordering::AcqRel);
207    }
208
209    /// Records one successful enqueue using wrapping atomic addition.
210    fn enqueued(&self) {
211        self.stats.enqueued.fetch_add(1, Ordering::Relaxed);
212    }
213
214    /// Records one delivered message and releases its depth reservation.
215    fn drained(&self) {
216        self.stats.drained.fetch_add(1, Ordering::Relaxed);
217        self.stats.current_depth.fetch_sub(1, Ordering::AcqRel);
218    }
219
220    /// Records channel disconnection at most once.
221    fn disconnected(&self) {
222        if !self.disconnected_observed.swap(true, Ordering::AcqRel) {
223            self.stats.disconnected.fetch_add(1, Ordering::Relaxed);
224        }
225    }
226
227    /// Reserves a wrapping attempt ID when a callback is installed.
228    fn start_attempt(state: &mut WakeState) -> Option<(u64, Arc<dyn UiWake>)> {
229        let callback = state.callback.clone()?;
230        let attempt = state.next_attempt;
231        state.next_attempt = state.next_attempt.wrapping_add(1);
232        state.status = WakeStatus::InFlight(attempt);
233        Some((attempt, callback))
234    }
235
236    /// Coalesces or starts a host wake attempt.
237    ///
238    /// # Errors
239    ///
240    /// Returns the installed [`UiWake`] callback's error when this call starts
241    /// an attempt. Coalesced requests and requests without a callback succeed.
242    fn request_wake(&self) -> Result<(), UiWakeError> {
243        let attempt = {
244            let mut state = self.wake.lock().unwrap_or_else(|error| error.into_inner());
245            match state.status {
246                WakeStatus::Signaled | WakeStatus::InFlight(_) => return Ok(()),
247                WakeStatus::Idle | WakeStatus::NeedsWake => {
248                    let attempt = Self::start_attempt(&mut state);
249                    if attempt.is_none() {
250                        state.status = WakeStatus::NeedsWake;
251                    }
252                    attempt
253                }
254            }
255        };
256        match attempt {
257            Some((attempt, callback)) => self.invoke_wake(attempt, callback),
258            None => Ok(()),
259        }
260    }
261
262    /// Invokes a callback outside the mutex and conditionally commits its result.
263    ///
264    /// # Errors
265    ///
266    /// Returns the exact error produced by [`UiWake::wake`]. Failed attempts
267    /// leave the notification pending for a later retry.
268    fn invoke_wake(&self, attempt: u64, callback: Arc<dyn UiWake>) -> Result<(), UiWakeError> {
269        self.stats.wake_calls.fetch_add(1, Ordering::Relaxed);
270        let result = callback.wake();
271        if result.is_err() {
272            self.stats.wake_failures.fetch_add(1, Ordering::Relaxed);
273        }
274        let mut state = self.wake.lock().unwrap_or_else(|error| error.into_inner());
275        if state.status == WakeStatus::InFlight(attempt) {
276            state.status = if result.is_ok() {
277                WakeStatus::Signaled
278            } else {
279                WakeStatus::NeedsWake
280            };
281        }
282        result
283    }
284
285    /// Replaces the callback and immediately retries outstanding notification.
286    ///
287    /// # Errors
288    ///
289    /// Returns the newly installed callback's [`UiWake::wake`] error when an
290    /// outstanding notification triggers an immediate attempt.
291    fn install_wake(&self, callback: Arc<dyn UiWake>) -> Result<(), UiWakeError> {
292        let attempt = {
293            let mut state = self.wake.lock().unwrap_or_else(|error| error.into_inner());
294            state.callback = Some(callback);
295            match state.status {
296                WakeStatus::Idle => None,
297                WakeStatus::NeedsWake | WakeStatus::InFlight(_) | WakeStatus::Signaled => {
298                    Self::start_attempt(&mut state)
299                }
300            }
301        };
302        match attempt {
303            Some((attempt, callback)) => self.invoke_wake(attempt, callback),
304            None => Ok(()),
305        }
306    }
307
308    /// Removes the callback while retaining any outstanding wake requirement.
309    fn detach_wake(&self) {
310        let mut state = self.wake.lock().unwrap_or_else(|error| error.into_inner());
311        state.callback = None;
312        if state.status != WakeStatus::Idle {
313            state.status = WakeStatus::NeedsWake;
314        }
315    }
316
317    /// Reopens the notification slot before a consumer examines the queue.
318    fn begin_drain(&self) {
319        self.wake
320            .lock()
321            .unwrap_or_else(|error| error.into_inner())
322            .status = WakeStatus::Idle;
323    }
324
325    /// Loads a non-transactional point-in-time statistics snapshot.
326    fn snapshot(&self) -> UiInboxStats {
327        UiInboxStats {
328            enqueued: self.stats.enqueued.load(Ordering::Relaxed),
329            drained: self.stats.drained.load(Ordering::Relaxed),
330            overflow: self.stats.overflow.load(Ordering::Relaxed),
331            disconnected: self.stats.disconnected.load(Ordering::Relaxed),
332            wake_calls: self.stats.wake_calls.load(Ordering::Relaxed),
333            wake_failures: self.stats.wake_failures.load(Ordering::Relaxed),
334            current_depth: self.stats.current_depth.load(Ordering::Relaxed),
335            max_depth: self.stats.max_depth.load(Ordering::Relaxed),
336        }
337    }
338}
339
340/// Thread-safe producer for a bounded, wakeable UI mailbox.
341///
342/// Clones share the same channel and counters. `UiSender<T>` is `Send + Sync`
343/// when `T` permits it; ordering follows the standard MPSC channel across all
344/// producers.
345///
346/// # Examples
347///
348/// ```
349/// use std::num::NonZeroUsize;
350/// use ailloli_ui_runtime::app::UiInbox;
351/// let (sender, _inbox) = UiInbox::<u8>::channel(NonZeroUsize::new(2).unwrap());
352/// assert_eq!(sender.stats().enqueued, 0);
353/// ```
354pub struct UiSender<T> {
355    /// Bounded MPSC producer preserving standard channel order.
356    sender: SyncSender<T>,
357    /// Shared wake, disconnect, and instrumentation state.
358    shared: Arc<Shared>,
359}
360
361/// Implements the `Clone` contract for `UiSender<T>`.
362impl<T> Clone for UiSender<T> {
363    /// Produces the clone required by the standard cloning contract.
364    fn clone(&self) -> Self {
365        Self {
366            sender: self.sender.clone(),
367            shared: self.shared.clone(),
368        }
369    }
370}
371
372/// Implements the `fmt::Debug` contract for `UiSender<T>`.
373impl<T> fmt::Debug for UiSender<T> {
374    /// Formats the value for human-readable diagnostics.
375    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
376        formatter
377            .debug_struct("UiSender")
378            .field("stats", &self.stats())
379            .finish_non_exhaustive()
380    }
381}
382
383/// Provides the operations defined for `UiSender<T>`.
384impl<T> UiSender<T> {
385    /// Attempts a nonblocking enqueue, then coalesces a host wake.
386    ///
387    /// On `Full` or `Closed`, ownership of `message` is lost with the error and
388    /// the queue is unchanged. `EnqueuedButWakeFailed` means the message remains
389    /// queued. With no callback installed, the send succeeds and records a wake
390    /// requirement for late installation.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`UiSendError::Full`] or [`UiSendError::Closed`] without
395    /// enqueueing. [`UiSendError::EnqueuedButWakeFailed`] means enqueueing
396    /// succeeded but the installed wake callback failed.
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use std::num::NonZeroUsize;
402    /// use ailloli_ui_runtime::app::{UiInbox, UiSendError};
403    /// let (sender, _inbox) = UiInbox::channel(NonZeroUsize::new(1).unwrap());
404    /// sender.send(1).unwrap();
405    /// assert_eq!(sender.send(2), Err(UiSendError::Full));
406    /// ```
407    pub fn send(&self, message: T) -> Result<(), UiSendError> {
408        // Reserve the diagnostic depth before publishing the message. The
409        // consumer is allowed to run immediately after `try_send`, so updating
410        // the counter afterwards can otherwise underflow during `drain`.
411        self.shared.reserve_depth();
412        match self.sender.try_send(message) {
413            Ok(()) => self.shared.enqueued(),
414            Err(TrySendError::Full(_)) => {
415                self.shared.cancel_depth_reservation();
416                self.shared.stats.overflow.fetch_add(1, Ordering::Relaxed);
417                return Err(UiSendError::Full);
418            }
419            Err(TrySendError::Disconnected(_)) => {
420                self.shared.cancel_depth_reservation();
421                self.shared.disconnected();
422                return Err(UiSendError::Closed);
423            }
424        }
425        self.shared
426            .request_wake()
427            .map_err(UiSendError::EnqueuedButWakeFailed)
428    }
429
430    /// Enqueues with bounded backpressure, then wakes the UI host.
431    ///
432    /// Worker threads may use this when dropping a response would violate
433    /// their protocol. UI threads should prefer [`Self::send`] so they never
434    /// block on a full mailbox.
435    ///
436    /// This can block indefinitely while the receiver remains alive but does
437    /// not drain a full queue. A wake failure still leaves the message queued.
438    ///
439    /// # Errors
440    ///
441    /// Returns [`UiSendError::Closed`] if the receiver disappears before the
442    /// enqueue, or [`UiSendError::EnqueuedButWakeFailed`] after a successful
443    /// enqueue whose host wake failed.
444    ///
445    /// # Examples
446    ///
447    /// ```
448    /// use std::num::NonZeroUsize;
449    /// use ailloli_ui_runtime::app::UiInbox;
450    /// let (sender, mut inbox) = UiInbox::channel(NonZeroUsize::new(1).unwrap());
451    /// sender.send_blocking("ready").unwrap();
452    /// assert_eq!(inbox.drain(NonZeroUsize::new(1).unwrap()).unwrap().messages, ["ready"]);
453    /// ```
454    pub fn send_blocking(&self, message: T) -> Result<(), UiSendError> {
455        self.shared.reserve_depth();
456        if self.sender.send(message).is_err() {
457            self.shared.cancel_depth_reservation();
458            self.shared.disconnected();
459            return Err(UiSendError::Closed);
460        }
461        self.shared.enqueued();
462        self.shared
463            .request_wake()
464            .map_err(UiSendError::EnqueuedButWakeFailed)
465    }
466
467    /// Retries an outstanding wake without adding another message.
468    ///
469    /// A signaled/in-flight wake coalesces to success. With no callback this
470    /// records `NeedsWake` and also returns success.
471    ///
472    /// # Errors
473    ///
474    /// Returns [`UiSendError::EnqueuedButWakeFailed`] when the installed wake
475    /// callback reports a closed or temporarily unavailable target.
476    ///
477    /// # Examples
478    ///
479    /// ```
480    /// use std::num::NonZeroUsize;
481    /// use ailloli_ui_runtime::app::UiInbox;
482    /// let (sender, _inbox) = UiInbox::<()>::channel(NonZeroUsize::new(1).unwrap());
483    /// assert!(sender.request_wake().is_ok());
484    /// assert_eq!(sender.stats().enqueued, 0);
485    /// ```
486    pub fn request_wake(&self) -> Result<(), UiSendError> {
487        self.shared
488            .request_wake()
489            .map_err(UiSendError::EnqueuedButWakeFailed)
490    }
491
492    /// Returns a non-resetting shared statistics snapshot.
493    ///
494    /// # Examples
495    ///
496    /// ```
497    /// use std::num::NonZeroUsize;
498    /// use ailloli_ui_runtime::app::UiInbox;
499    /// let (sender, _inbox) = UiInbox::channel(NonZeroUsize::new(1).unwrap());
500    /// sender.send(1).unwrap();
501    /// assert_eq!(sender.stats().current_depth, 1);
502    /// ```
503    pub fn stats(&self) -> UiInboxStats {
504        self.shared.snapshot()
505    }
506}
507
508/// Result of one bounded generic-mailbox drain.
509///
510/// `remaining` means at least one message was prefetched beyond the requested
511/// limit and another host wake was requested. An empty `messages` vector is a
512/// normal result when the queue is currently empty or disconnected.
513///
514/// # Examples
515///
516/// ```
517/// use ailloli_ui_runtime::app::UiDrain;
518/// let drain = UiDrain { messages: vec![1, 2], remaining: true };
519/// assert_eq!(drain.messages.len(), 2);
520/// ```
521#[derive(Debug)]
522pub struct UiDrain<T> {
523    /// Delivered messages in FIFO order, up to the requested limit.
524    pub messages: Vec<T>,
525    /// Whether a prefetched message remains pending in the inbox.
526    pub remaining: bool,
527}
528
529/// UI-thread side of a bounded generic mailbox.
530///
531/// `Receiver` is single-consumer and this type is intended to stay on the UI
532/// thread. Dropping it closes the channel after buffered messages are dropped.
533///
534/// # Examples
535///
536/// ```
537/// use std::num::NonZeroUsize;
538/// use ailloli_ui_runtime::app::UiInbox;
539/// let (_sender, inbox) = UiInbox::<u8>::channel(NonZeroUsize::new(4).unwrap());
540/// assert_eq!(inbox.stats().current_depth, 0);
541/// ```
542pub struct UiInbox<T> {
543    /// Single consumer for queued messages.
544    receiver: Receiver<T>,
545    /// Shared wake, disconnect, and instrumentation state.
546    shared: Arc<Shared>,
547    /// One prefetched message retained when a bounded drain stops early.
548    pending: Option<T>,
549}
550
551/// Provides the operations defined for `UiInbox<T>`.
552impl<T> UiInbox<T> {
553    /// Creates a bounded channel with exact nonzero message capacity.
554    ///
555    /// No wake callback is installed. Producers may enqueue before late wake
556    /// installation; the outstanding notification is retained.
557    ///
558    /// # Examples
559    ///
560    /// ```
561    /// use std::num::NonZeroUsize;
562    /// use ailloli_ui_runtime::app::UiInbox;
563    /// let (sender, mut inbox) = UiInbox::channel(NonZeroUsize::new(2).unwrap());
564    /// sender.send(5).unwrap();
565    /// assert_eq!(inbox.drain(NonZeroUsize::new(2).unwrap()).unwrap().messages, [5]);
566    /// ```
567    pub fn channel(capacity: NonZeroUsize) -> (UiSender<T>, Self) {
568        let (sender, receiver) = mpsc::sync_channel(capacity.get());
569        let shared = Arc::new(Shared::new());
570        (
571            UiSender {
572                sender,
573                shared: shared.clone(),
574            },
575            Self {
576                receiver,
577                shared,
578                pending: None,
579            },
580        )
581    }
582
583    /// Installs/replaces the thread-safe host wake callback.
584    ///
585    /// If a wake is outstanding, the new callback is invoked synchronously
586    /// before this method returns. Its error is returned; the callback remains
587    /// installed and notification stays retryable.
588    ///
589    /// # Errors
590    ///
591    /// Returns the installed callback's [`UiWakeError`] when delivering an
592    /// already outstanding notification fails.
593    ///
594    /// # Examples
595    ///
596    /// ```
597    /// use std::num::NonZeroUsize;
598    /// use std::sync::Arc;
599    /// use ailloli_ui_runtime::app::{UiInbox, UiWake, UiWakeError};
600    /// struct Wake; impl UiWake for Wake { fn wake(&self)->Result<(),UiWakeError>{Ok(())} }
601    /// let (_sender, inbox) = UiInbox::<()>::channel(NonZeroUsize::new(1).unwrap());
602    /// assert!(inbox.install_wake(Arc::new(Wake)).is_ok());
603    /// ```
604    pub fn install_wake(&self, wake: Arc<dyn UiWake>) -> Result<(), UiWakeError> {
605        self.shared.install_wake(wake)
606    }
607
608    /// Removes the current callback without discarding an outstanding wake.
609    ///
610    /// # Examples
611    ///
612    /// ```
613    /// use std::num::NonZeroUsize;
614    /// use ailloli_ui_runtime::app::UiInbox;
615    /// let (_sender, inbox) = UiInbox::<()>::channel(NonZeroUsize::new(1).unwrap());
616    /// inbox.detach_wake();
617    /// assert_eq!(inbox.stats().wake_calls, 0);
618    /// ```
619    pub fn detach_wake(&self) {
620        self.shared.detach_wake();
621    }
622
623    /// Returns a non-resetting shared statistics snapshot.
624    ///
625    /// # Examples
626    ///
627    /// ```
628    /// use std::num::NonZeroUsize;
629    /// use ailloli_ui_runtime::app::UiInbox;
630    /// let (_sender, inbox) = UiInbox::<()>::channel(NonZeroUsize::new(1).unwrap());
631    /// assert_eq!(inbox.stats().enqueued, 0);
632    /// ```
633    pub fn stats(&self) -> UiInboxStats {
634        self.shared.snapshot()
635    }
636
637    /// Removes up to `limit` FIFO messages and reports whether more remain.
638    ///
639    /// When exactly `limit` messages were removed, the method probes one extra
640    /// message and retains it internally to determine `remaining`. If work
641    /// remains it synchronously requests another wake. A wake error is returned
642    /// after the batch has already been removed and counters updated, so the
643    /// caller does not receive those messages and must treat such a failure as
644    /// host-fatal or arrange higher-level recovery.
645    ///
646    /// # Errors
647    ///
648    /// Returns a wake error only when a pending remainder needs another signal.
649    /// Channel disconnection is recorded in stats and otherwise yields a normal
650    /// drain result.
651    ///
652    /// # Examples
653    ///
654    /// ```
655    /// use std::num::NonZeroUsize;
656    /// use ailloli_ui_runtime::app::UiInbox;
657    /// let (sender, mut inbox) = UiInbox::channel(NonZeroUsize::new(3).unwrap());
658    /// sender.send(1).unwrap(); sender.send(2).unwrap();
659    /// let first = inbox.drain(NonZeroUsize::new(1).unwrap()).unwrap();
660    /// assert_eq!(first.messages, [1]);
661    /// assert!(first.remaining);
662    /// ```
663    pub fn drain(&mut self, limit: NonZeroUsize) -> Result<UiDrain<T>, UiWakeError> {
664        self.shared.begin_drain();
665        let mut messages = Vec::with_capacity(limit.get());
666        while messages.len() < limit.get() {
667            match self.pending.take() {
668                Some(message) => {
669                    self.shared.drained();
670                    messages.push(message);
671                }
672                None => match self.receiver.try_recv() {
673                    Ok(message) => {
674                        self.shared.drained();
675                        messages.push(message);
676                    }
677                    Err(TryRecvError::Empty) => break,
678                    Err(TryRecvError::Disconnected) => {
679                        self.shared.disconnected();
680                        break;
681                    }
682                },
683            }
684        }
685        let remaining = if messages.len() == limit.get() {
686            match self.receiver.try_recv() {
687                Ok(message) => {
688                    self.pending = Some(message);
689                    true
690                }
691                Err(TryRecvError::Empty) => false,
692                Err(TryRecvError::Disconnected) => {
693                    self.shared.disconnected();
694                    false
695                }
696            }
697        } else {
698            false
699        };
700        if remaining {
701            self.shared.request_wake()?;
702        }
703        Ok(UiDrain {
704            messages,
705            remaining,
706        })
707    }
708}
709
710#[cfg(test)]
711/// Tests implementation details.
712mod tests {
713    use super::*;
714    use std::sync::atomic::{AtomicUsize, Ordering};
715
716    #[derive(Default)]
717    /// Test support type for Wake scenarios.
718    struct Wake(AtomicUsize);
719
720    /// Implements the UiWake contract for Wake.
721    impl UiWake for Wake {
722        /// Notifies the test or host wake target.
723        ///
724        /// # Errors
725        ///
726        /// This counting test callback never returns an error.
727        fn wake(&self) -> Result<(), UiWakeError> {
728            self.0.fetch_add(1, Ordering::Relaxed);
729            Ok(())
730        }
731    }
732
733    #[test]
734    /// Verifies that late binding budget and last drain do not lose wakes.
735    fn late_binding_budget_and_last_drain_do_not_lose_wakes() {
736        let (sender, mut inbox) = UiInbox::channel(NonZeroUsize::new(4).unwrap());
737        sender.send(1).unwrap();
738        sender.send(2).unwrap();
739        let wake = Arc::new(Wake::default());
740        inbox.install_wake(wake.clone()).unwrap();
741        assert_eq!(wake.0.load(Ordering::Relaxed), 1);
742        let first = inbox.drain(NonZeroUsize::new(1).unwrap()).unwrap();
743        assert_eq!(first.messages, [1]);
744        assert!(first.remaining);
745        let second = inbox.drain(NonZeroUsize::new(1).unwrap()).unwrap();
746        assert_eq!(second.messages, [2]);
747        assert!(!second.remaining);
748        sender.send(3).unwrap();
749        assert_eq!(wake.0.load(Ordering::Relaxed), 3);
750    }
751
752    #[test]
753    /// Verifies that concurrent blocking sender keeps depth counters balanced.
754    fn concurrent_blocking_sender_keeps_depth_counters_balanced() {
755        /// Number of messages sent by the concurrency regression.
756        const MESSAGE_COUNT: usize = 10_000;
757        let (sender, mut inbox) = UiInbox::channel(NonZeroUsize::new(1).unwrap());
758        let producer = std::thread::spawn(move || {
759            for message in 0..MESSAGE_COUNT {
760                sender.send_blocking(message).unwrap();
761            }
762        });
763        let mut received = 0;
764        while received < MESSAGE_COUNT {
765            let drain = inbox.drain(NonZeroUsize::new(1).unwrap()).unwrap();
766            received += drain.messages.len();
767            if drain.messages.is_empty() {
768                std::thread::yield_now();
769            }
770        }
771        producer.join().unwrap();
772
773        let stats = inbox.stats();
774        assert_eq!(stats.enqueued, MESSAGE_COUNT as u64);
775        assert_eq!(stats.drained, MESSAGE_COUNT as u64);
776        assert_eq!(stats.current_depth, 0);
777        assert!(stats.max_depth >= 1);
778    }
779}