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}