Skip to main content

gpui_rhai/
async_runtime.rs

1use std::collections::{BTreeMap, VecDeque};
2use std::fmt;
3use std::panic::{AssertUnwindSafe, catch_unwind};
4#[cfg(test)]
5use std::sync::atomic::AtomicUsize;
6use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
7use std::sync::mpsc::{Receiver, Sender, SyncSender, TrySendError, channel, sync_channel};
8use std::sync::{Arc, Condvar, Mutex, OnceLock};
9use std::time::{Duration, Instant};
10
11use event_listener::{Event, EventListener};
12use rhai::{CustomType, TypeBuilder};
13use thiserror::Error;
14
15use crate::{
16    ComponentInstancePath, SchemaValidationError, ScriptCallback, ScriptGeneration, UiValue,
17    ValueSchema,
18};
19
20type BackgroundJob = Box<dyn FnOnce() + Send + 'static>;
21
22struct BackgroundExecutor {
23    sender: SyncSender<BackgroundJob>,
24}
25
26impl BackgroundExecutor {
27    fn start() -> Option<Self> {
28        let (sender, receiver) = sync_channel::<BackgroundJob>(1_024);
29        let receiver = Arc::new(Mutex::new(receiver));
30        let workers = std::thread::available_parallelism()
31            .map_or(2, usize::from)
32            .clamp(1, 4);
33        for index in 0..workers {
34            let receiver = Arc::clone(&receiver);
35            if std::thread::Builder::new()
36                .name(format!("gpui-rhai-worker-{index}"))
37                .spawn(move || {
38                    loop {
39                        let job = receiver
40                            .lock()
41                            .unwrap_or_else(std::sync::PoisonError::into_inner)
42                            .recv();
43                        let Ok(job) = job else {
44                            break;
45                        };
46                        job();
47                    }
48                })
49                .is_err()
50            {
51                return None;
52            }
53        }
54        Some(Self { sender })
55    }
56
57    fn submit(&self, job: BackgroundJob) -> Result<(), AsyncRuntimeError> {
58        self.sender.try_send(job).map_err(|error| match error {
59            TrySendError::Full(_) => AsyncRuntimeError::WorkerQueueFull,
60            TrySendError::Disconnected(_) => AsyncRuntimeError::WorkerPoolUnavailable,
61        })
62    }
63}
64
65fn background_executor() -> Result<&'static BackgroundExecutor, AsyncRuntimeError> {
66    static EXECUTOR: OnceLock<Option<BackgroundExecutor>> = OnceLock::new();
67    EXECUTOR
68        .get_or_init(BackgroundExecutor::start)
69        .as_ref()
70        .ok_or(AsyncRuntimeError::WorkerPoolUnavailable)
71}
72
73/// Thread-safe edge notification for foreground delivery pumps.
74///
75/// The message queues remain the source of truth. A wake may be coalesced or
76/// arrive before a view subscribes; each foreground pump therefore drains once
77/// before awaiting its first notification.
78#[derive(Clone, Debug, Default)]
79pub(crate) struct AsyncWake {
80    event: Arc<Event>,
81}
82
83impl AsyncWake {
84    pub(crate) fn listen(&self) -> EventListener {
85        self.event.listen()
86    }
87
88    pub(crate) fn notify(&self) {
89        self.event.notify(usize::MAX);
90    }
91}
92
93#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
94pub enum AsyncScope {
95    App,
96    Window(String),
97    Component(ComponentInstancePath),
98    Effect {
99        component: ComponentInstancePath,
100        key: String,
101        activation: u64,
102    },
103}
104
105impl AsyncScope {
106    #[must_use]
107    pub const fn component(&self) -> Option<&ComponentInstancePath> {
108        match self {
109            Self::Component(component) | Self::Effect { component, .. } => Some(component),
110            Self::App | Self::Window(_) => None,
111        }
112    }
113
114    #[must_use]
115    pub fn is_within_component(&self, root: &ComponentInstancePath) -> bool {
116        self.component()
117            .is_some_and(|component| component.is_within(root))
118    }
119}
120
121#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
122pub struct TaskHandle(u64);
123
124impl CustomType for TaskHandle {
125    fn build(mut builder: TypeBuilder<Self>) {
126        builder
127            .with_name("TaskHandle")
128            .with_fn("to_string", |handle: &mut Self| {
129                format!("task#{}", handle.0)
130            });
131    }
132}
133
134#[derive(Clone, Debug, Default)]
135pub struct TaskCancellation {
136    cancelled: Arc<AtomicBool>,
137}
138
139impl TaskCancellation {
140    #[must_use]
141    pub fn is_cancelled(&self) -> bool {
142        self.cancelled.load(Ordering::Acquire)
143    }
144
145    fn cancel(&self) {
146        self.cancelled.store(true, Ordering::Release);
147    }
148}
149
150#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
151pub struct SubscriptionHandle(u64);
152
153impl CustomType for SubscriptionHandle {
154    fn build(mut builder: TypeBuilder<Self>) {
155        builder
156            .with_name("SubscriptionHandle")
157            .with_fn("to_string", |handle: &mut Self| {
158                format!("subscription#{}", handle.0)
159            });
160    }
161}
162
163#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
164pub enum SubscriptionCloseReason {
165    #[error("the subscription work returned")]
166    WorkReturned,
167    #[error("the producer closed its emitter")]
168    ProducerClosed,
169    #[error("the subscription was cancelled explicitly")]
170    Cancelled,
171    #[error("the owning scope was disposed")]
172    ScopeDisposed,
173    #[error("the script generation became stale")]
174    GenerationStale,
175    #[error("the creating transaction was rolled back")]
176    TransactionRolledBack,
177    #[error("subscription startup failed")]
178    StartupFailed,
179    #[error("the subscription registry was dropped")]
180    RegistryDropped,
181}
182
183impl SubscriptionCloseReason {
184    const fn code(self) -> u8 {
185        match self {
186            Self::WorkReturned => 1,
187            Self::ProducerClosed => 2,
188            Self::Cancelled => 3,
189            Self::ScopeDisposed => 4,
190            Self::GenerationStale => 5,
191            Self::TransactionRolledBack => 6,
192            Self::StartupFailed => 7,
193            Self::RegistryDropped => 8,
194        }
195    }
196
197    const fn from_code(code: u8) -> Option<Self> {
198        match code {
199            1 => Some(Self::WorkReturned),
200            2 => Some(Self::ProducerClosed),
201            3 => Some(Self::Cancelled),
202            4 => Some(Self::ScopeDisposed),
203            5 => Some(Self::GenerationStale),
204            6 => Some(Self::TransactionRolledBack),
205            7 => Some(Self::StartupFailed),
206            8 => Some(Self::RegistryDropped),
207            _ => None,
208        }
209    }
210}
211
212#[derive(Debug)]
213struct SubscriptionLifetime {
214    close_reason: AtomicU8,
215}
216
217impl SubscriptionLifetime {
218    fn new() -> Self {
219        Self {
220            close_reason: AtomicU8::new(0),
221        }
222    }
223
224    fn close(&self, reason: SubscriptionCloseReason) -> bool {
225        self.close_reason
226            .compare_exchange(0, reason.code(), Ordering::AcqRel, Ordering::Acquire)
227            .is_ok()
228    }
229
230    fn close_reason(&self) -> Option<SubscriptionCloseReason> {
231        SubscriptionCloseReason::from_code(self.close_reason.load(Ordering::Acquire))
232    }
233}
234
235#[derive(Clone, Debug)]
236pub struct AsyncDelivery {
237    pub callback: ScriptCallback,
238    pub payload: UiValue,
239    pub scope: AsyncScope,
240}
241
242#[derive(Clone, Debug)]
243struct CallbackPair {
244    success: ScriptCallback,
245    error: ScriptCallback,
246}
247
248#[derive(Clone, Debug)]
249struct TaskEntry {
250    scope: AsyncScope,
251    generation: ScriptGeneration,
252    callbacks: CallbackPair,
253    output: ValueSchema,
254    cancellation: TaskCancellation,
255}
256
257struct TaskMessage {
258    id: u64,
259    result: Result<UiValue, String>,
260}
261
262pub struct TaskRegistry {
263    next_id: u64,
264    entries: BTreeMap<u64, TaskEntry>,
265    sender: Sender<TaskMessage>,
266    receiver: Receiver<TaskMessage>,
267    wake: AsyncWake,
268    deferred_cancellations: BTreeMap<u64, TaskEntry>,
269    transaction_depth: usize,
270}
271
272#[derive(Clone, Debug)]
273pub(crate) struct TaskRegistrySnapshot {
274    entries: BTreeMap<u64, TaskEntry>,
275    deferred_cancellations: BTreeMap<u64, TaskEntry>,
276    transaction_depth: usize,
277}
278
279impl Default for TaskRegistry {
280    fn default() -> Self {
281        let (sender, receiver) = channel();
282        Self {
283            next_id: 1,
284            entries: BTreeMap::new(),
285            sender,
286            receiver,
287            wake: AsyncWake::default(),
288            deferred_cancellations: BTreeMap::new(),
289            transaction_depth: 0,
290        }
291    }
292}
293
294impl fmt::Debug for TaskRegistry {
295    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
296        formatter
297            .debug_struct("TaskRegistry")
298            .field("active", &self.entries.keys().collect::<Vec<_>>())
299            .finish_non_exhaustive()
300    }
301}
302
303impl TaskRegistry {
304    #[must_use]
305    pub fn new() -> Self {
306        Self::default()
307    }
308
309    pub(crate) fn begin_transaction(&mut self) {
310        self.transaction_depth = self.transaction_depth.saturating_add(1);
311    }
312
313    pub(crate) fn commit_transaction(&mut self) {
314        self.transaction_depth = self.transaction_depth.saturating_sub(1);
315        if self.transaction_depth == 0 {
316            for entry in std::mem::take(&mut self.deferred_cancellations).into_values() {
317                entry.cancellation.cancel();
318            }
319        }
320    }
321
322    /// Spawn Rust work while retaining Rhai callbacks on the foreground side.
323    ///
324    /// # Errors
325    ///
326    /// Returns [`AsyncRuntimeError::Spawn`] when the worker thread cannot start.
327    pub fn spawn(
328        &mut self,
329        scope: AsyncScope,
330        generation: ScriptGeneration,
331        success: ScriptCallback,
332        error: ScriptCallback,
333        output: ValueSchema,
334        work: impl FnOnce() -> Result<UiValue, String> + Send + 'static,
335    ) -> Result<TaskHandle, AsyncRuntimeError> {
336        self.spawn_cancellable(scope, generation, success, error, output, move |_| work())
337    }
338
339    pub(crate) fn spawn_cancellable(
340        &mut self,
341        scope: AsyncScope,
342        generation: ScriptGeneration,
343        success: ScriptCallback,
344        error: ScriptCallback,
345        output: ValueSchema,
346        work: impl FnOnce(TaskCancellation) -> Result<UiValue, String> + Send + 'static,
347    ) -> Result<TaskHandle, AsyncRuntimeError> {
348        let id = self.next_id;
349        self.next_id = self.next_id.saturating_add(1);
350        let cancellation = TaskCancellation::default();
351        self.entries.insert(
352            id,
353            TaskEntry {
354                scope,
355                generation,
356                callbacks: CallbackPair { success, error },
357                output,
358                cancellation: cancellation.clone(),
359            },
360        );
361        let sender = self.sender.clone();
362        let wake = self.wake.clone();
363        let job = Box::new(move || {
364            let result =
365                catch_unwind(AssertUnwindSafe(|| work(cancellation))).unwrap_or_else(|payload| {
366                    let message = payload.downcast_ref::<&str>().map_or_else(
367                        || {
368                            payload.downcast_ref::<String>().map_or_else(
369                                || "background task panicked".to_owned(),
370                                |message| format!("background task panicked: {message}"),
371                            )
372                        },
373                        |message| format!("background task panicked: {message}"),
374                    );
375                    Err(message)
376                });
377            if sender.send(TaskMessage { id, result }).is_ok() {
378                wake.notify();
379            }
380        });
381        if let Err(error) = background_executor().and_then(|executor| executor.submit(job)) {
382            self.entries.remove(&id);
383            return Err(error);
384        }
385        Ok(TaskHandle(id))
386    }
387
388    #[must_use]
389    pub fn cancel(&mut self, handle: TaskHandle) -> bool {
390        let Some(entry) = self.entries.remove(&handle.0) else {
391            return false;
392        };
393        self.defer_or_cancel(handle.0, entry);
394        true
395    }
396
397    pub fn cancel_scope(&mut self, scope: &AsyncScope) {
398        let ids = self
399            .entries
400            .iter()
401            .filter_map(|(id, entry)| (&entry.scope == scope).then_some(*id))
402            .collect::<Vec<_>>();
403        self.cancel_ids(ids);
404    }
405
406    pub fn cancel_component_scope(&mut self, component: &ComponentInstancePath) {
407        let ids = self
408            .entries
409            .iter()
410            .filter_map(|(id, entry)| entry.scope.is_within_component(component).then_some(*id))
411            .collect::<Vec<_>>();
412        self.cancel_ids(ids);
413    }
414
415    #[must_use]
416    pub fn drain(&mut self, current: ScriptGeneration) -> Vec<AsyncDelivery> {
417        self.drain_up_to(current, usize::MAX)
418    }
419
420    #[must_use]
421    pub(crate) fn drain_up_to(
422        &mut self,
423        current: ScriptGeneration,
424        limit: usize,
425    ) -> Vec<AsyncDelivery> {
426        let mut deliveries = Vec::new();
427        while deliveries.len() < limit {
428            let Ok(message) = self.receiver.try_recv() else {
429                break;
430            };
431            let Some(entry) = self.entries.remove(&message.id) else {
432                continue;
433            };
434            if entry.generation != current {
435                continue;
436            }
437            deliveries.push(task_delivery(entry, message.result));
438        }
439        deliveries
440    }
441
442    #[must_use]
443    pub fn active_count(&self) -> usize {
444        self.entries.len()
445    }
446
447    pub(crate) fn snapshot(&self) -> TaskRegistrySnapshot {
448        TaskRegistrySnapshot {
449            entries: self.entries.clone(),
450            deferred_cancellations: self.deferred_cancellations.clone(),
451            transaction_depth: self.transaction_depth,
452        }
453    }
454
455    pub(crate) fn restore(&mut self, snapshot: TaskRegistrySnapshot) {
456        for (id, entry) in self
457            .entries
458            .iter()
459            .chain(self.deferred_cancellations.iter())
460        {
461            if !snapshot.entries.contains_key(id)
462                && !snapshot.deferred_cancellations.contains_key(id)
463            {
464                entry.cancellation.cancel();
465            }
466        }
467        self.entries = snapshot.entries;
468        self.deferred_cancellations = snapshot.deferred_cancellations;
469        self.transaction_depth = snapshot.transaction_depth;
470    }
471
472    fn cancel_ids(&mut self, ids: impl IntoIterator<Item = u64>) {
473        for id in ids {
474            if let Some(entry) = self.entries.remove(&id) {
475                self.defer_or_cancel(id, entry);
476            }
477        }
478    }
479
480    fn defer_or_cancel(&mut self, id: u64, entry: TaskEntry) {
481        if self.transaction_depth > 0 {
482            self.deferred_cancellations.insert(id, entry);
483        } else {
484            entry.cancellation.cancel();
485        }
486    }
487
488    pub(crate) fn wake(&self) -> AsyncWake {
489        self.wake.clone()
490    }
491}
492
493fn task_delivery(entry: TaskEntry, result: Result<UiValue, String>) -> AsyncDelivery {
494    match result {
495        Ok(value) => match entry.output.validate_ui_value(&value) {
496            Ok(()) => AsyncDelivery {
497                callback: entry.callbacks.success,
498                payload: value,
499                scope: entry.scope,
500            },
501            Err(error) => AsyncDelivery {
502                callback: entry.callbacks.error,
503                payload: error_payload(error.to_string()),
504                scope: entry.scope,
505            },
506        },
507        Err(error) => AsyncDelivery {
508            callback: entry.callbacks.error,
509            payload: error_payload(error),
510            scope: entry.scope,
511        },
512    }
513}
514
515const DEFAULT_SUBSCRIPTION_CAPACITY: usize = 64;
516const MAX_SUBSCRIPTION_CAPACITY: usize = 4_096;
517const MAX_SUBSCRIPTION_THROTTLE: Duration = Duration::from_secs(60);
518
519#[derive(Clone, Copy, Debug, Eq, PartialEq)]
520pub enum SubscriptionDeliveryPolicy {
521    All,
522    Latest,
523}
524
525#[derive(Clone, Copy, Debug, Eq, PartialEq)]
526pub struct SubscriptionOptions {
527    delivery: SubscriptionDeliveryPolicy,
528    capacity: usize,
529    throttle: Duration,
530}
531
532impl Default for SubscriptionOptions {
533    fn default() -> Self {
534        Self {
535            delivery: SubscriptionDeliveryPolicy::All,
536            capacity: DEFAULT_SUBSCRIPTION_CAPACITY,
537            throttle: Duration::ZERO,
538        }
539    }
540}
541
542impl SubscriptionOptions {
543    /// Build an explicit bounded subscription delivery contract.
544    ///
545    /// # Errors
546    ///
547    /// Returns for a capacity outside 1..=4096 or a throttle above 60 seconds.
548    pub fn new(
549        delivery: SubscriptionDeliveryPolicy,
550        capacity: usize,
551        throttle: Duration,
552    ) -> Result<Self, AsyncRuntimeError> {
553        if !(1..=MAX_SUBSCRIPTION_CAPACITY).contains(&capacity) {
554            return Err(AsyncRuntimeError::InvalidCapacity(capacity));
555        }
556        if throttle > MAX_SUBSCRIPTION_THROTTLE {
557            return Err(AsyncRuntimeError::InvalidThrottle(throttle));
558        }
559        Ok(Self {
560            delivery,
561            capacity,
562            throttle,
563        })
564    }
565
566    #[must_use]
567    pub const fn delivery(self) -> SubscriptionDeliveryPolicy {
568        self.delivery
569    }
570
571    #[must_use]
572    pub const fn capacity(self) -> usize {
573        self.capacity
574    }
575
576    #[must_use]
577    pub const fn throttle(self) -> Duration {
578        self.throttle
579    }
580}
581
582#[derive(Debug)]
583struct SubscriptionBuffer {
584    values: VecDeque<Result<UiValue, String>>,
585    delivery: SubscriptionDeliveryPolicy,
586    capacity: usize,
587}
588
589#[derive(Debug)]
590struct SubscriptionQueue {
591    buffer: Mutex<SubscriptionBuffer>,
592    space: Condvar,
593    #[cfg(test)]
594    blocked_producers: AtomicUsize,
595}
596
597impl SubscriptionBuffer {
598    fn push(&mut self, result: Result<UiValue, String>) -> Result<(), AsyncRuntimeError> {
599        match self.delivery {
600            SubscriptionDeliveryPolicy::All if self.values.len() >= self.capacity => {
601                Err(AsyncRuntimeError::Backpressure {
602                    capacity: self.capacity,
603                })
604            }
605            SubscriptionDeliveryPolicy::All => {
606                self.values.push_back(result);
607                Ok(())
608            }
609            SubscriptionDeliveryPolicy::Latest => {
610                if let Some(latest) = self.values.back_mut() {
611                    *latest = result;
612                } else {
613                    self.values.push_back(result);
614                }
615                Ok(())
616            }
617        }
618    }
619}
620
621#[derive(Clone)]
622pub struct SubscriptionEmitter {
623    pending: Arc<SubscriptionQueue>,
624    lifetime: Arc<SubscriptionLifetime>,
625    wake: AsyncWake,
626}
627
628impl SubscriptionEmitter {
629    /// Emit one value from any Rust worker thread.
630    ///
631    /// # Errors
632    ///
633    /// Returns [`AsyncRuntimeError::Closed`] after teardown, or
634    /// [`AsyncRuntimeError::Backpressure`] when a lossless stream's bounded
635    /// pending queue is full. Latest-only streams explicitly replace their
636    /// pending value instead.
637    pub fn emit(&self, value: UiValue) -> Result<(), AsyncRuntimeError> {
638        self.send(Ok(value))
639    }
640
641    /// Emit one lossless value, waiting for foreground capacity or cancellation.
642    /// Latest-only streams never block because they explicitly replace a value.
643    ///
644    /// # Errors
645    ///
646    /// Returns when the stream closes or its shared queue is poisoned.
647    pub fn emit_blocking(&self, value: UiValue) -> Result<(), AsyncRuntimeError> {
648        self.send_blocking(Ok(value))
649    }
650
651    /// Emit a structured error to the Rhai error callback.
652    ///
653    /// # Errors
654    ///
655    /// Returns [`AsyncRuntimeError::Closed`] after the producer work returns,
656    /// explicit cancellation, scope teardown, generation replacement, or
657    /// registry drop.
658    pub fn emit_error(&self, message: impl Into<String>) -> Result<(), AsyncRuntimeError> {
659        self.send(Err(message.into()))
660    }
661
662    fn send(&self, result: Result<UiValue, String>) -> Result<(), AsyncRuntimeError> {
663        let mut pending = self
664            .pending
665            .buffer
666            .lock()
667            .map_err(|_| AsyncRuntimeError::Poisoned)?;
668        if let Some(reason) = self.lifetime.close_reason() {
669            return Err(AsyncRuntimeError::Closed { reason });
670        }
671        pending.push(result)?;
672        drop(pending);
673        self.wake.notify();
674        Ok(())
675    }
676
677    fn send_blocking(&self, result: Result<UiValue, String>) -> Result<(), AsyncRuntimeError> {
678        let mut result = Some(result);
679        let mut pending = self
680            .pending
681            .buffer
682            .lock()
683            .map_err(|_| AsyncRuntimeError::Poisoned)?;
684        loop {
685            if let Some(reason) = self.lifetime.close_reason() {
686                return Err(AsyncRuntimeError::Closed { reason });
687            }
688            if pending.delivery == SubscriptionDeliveryPolicy::All
689                && pending.values.len() >= pending.capacity
690            {
691                #[cfg(test)]
692                self.pending
693                    .blocked_producers
694                    .fetch_add(1, Ordering::AcqRel);
695                let waited = self.pending.space.wait(pending);
696                #[cfg(test)]
697                self.pending
698                    .blocked_producers
699                    .fetch_sub(1, Ordering::AcqRel);
700                pending = waited.map_err(|_| AsyncRuntimeError::Poisoned)?;
701                continue;
702            }
703            pending.push(result.take().expect("subscription value is sent once"))?;
704            drop(pending);
705            self.wake.notify();
706            return Ok(());
707        }
708    }
709
710    /// Close the stream explicitly from the producer side.
711    pub fn close(&self) {
712        self.close_with_reason(SubscriptionCloseReason::ProducerClosed);
713    }
714
715    /// Return the first reason that closed this emitter, if any.
716    #[must_use]
717    pub fn close_reason(&self) -> Option<SubscriptionCloseReason> {
718        self.lifetime.close_reason()
719    }
720
721    pub(crate) fn close_with_reason(&self, reason: SubscriptionCloseReason) {
722        if close_subscription_queue(&self.pending, &self.lifetime, reason, false) == reason {
723            self.wake.notify();
724        }
725    }
726}
727
728#[derive(Clone, Debug)]
729struct SubscriptionEntry {
730    label: String,
731    scope: AsyncScope,
732    generation: ScriptGeneration,
733    callbacks: CallbackPair,
734    output: ValueSchema,
735    lifetime: Arc<SubscriptionLifetime>,
736    pending: Arc<SubscriptionQueue>,
737    delivery: SubscriptionDeliveryPolicy,
738    throttle: Duration,
739    last_delivery: Option<Instant>,
740}
741
742#[derive(Clone, Debug, Eq, PartialEq)]
743pub(crate) struct SubscriptionClosure {
744    pub label: String,
745    pub scope: AsyncScope,
746    pub reason: SubscriptionCloseReason,
747}
748
749pub struct SubscriptionRegistration {
750    label: String,
751    scope: AsyncScope,
752    generation: ScriptGeneration,
753    success: ScriptCallback,
754    error: ScriptCallback,
755    output: ValueSchema,
756    delivery: SubscriptionDeliveryPolicy,
757    capacity: usize,
758    throttle: Duration,
759}
760
761impl SubscriptionRegistration {
762    #[must_use]
763    pub fn new(
764        label: impl Into<String>,
765        scope: AsyncScope,
766        generation: ScriptGeneration,
767        success: ScriptCallback,
768        error: ScriptCallback,
769        output: ValueSchema,
770    ) -> Self {
771        Self {
772            label: label.into(),
773            scope,
774            generation,
775            success,
776            error,
777            output,
778            delivery: SubscriptionDeliveryPolicy::All,
779            capacity: DEFAULT_SUBSCRIPTION_CAPACITY,
780            throttle: Duration::ZERO,
781        }
782    }
783
784    /// Set the minimum interval between foreground delivery batches.
785    ///
786    /// Lossless streams retain every value. Latest-only streams coalesce values
787    /// received during the interval.
788    #[must_use]
789    pub const fn with_throttle(mut self, throttle: Duration) -> Self {
790        self.throttle = throttle;
791        self
792    }
793
794    /// Select lossless ordered or explicit latest-only delivery.
795    #[must_use]
796    pub const fn with_delivery_policy(mut self, delivery: SubscriptionDeliveryPolicy) -> Self {
797        self.delivery = delivery;
798        self
799    }
800
801    #[must_use]
802    pub const fn with_options(mut self, options: SubscriptionOptions) -> Self {
803        self.delivery = options.delivery;
804        self.capacity = options.capacity;
805        self.throttle = options.throttle;
806        self
807    }
808
809    /// Bound pending values for lossless delivery.
810    ///
811    /// # Errors
812    ///
813    /// Returns [`AsyncRuntimeError::InvalidCapacity`] outside 1..=4096.
814    pub fn with_capacity(mut self, capacity: usize) -> Result<Self, AsyncRuntimeError> {
815        if !(1..=MAX_SUBSCRIPTION_CAPACITY).contains(&capacity) {
816            return Err(AsyncRuntimeError::InvalidCapacity(capacity));
817        }
818        self.capacity = capacity;
819        Ok(self)
820    }
821}
822
823pub struct SubscriptionRegistry {
824    next_id: u64,
825    entries: BTreeMap<u64, SubscriptionEntry>,
826    closures: Vec<SubscriptionClosure>,
827    deferred_closures: BTreeMap<u64, (SubscriptionEntry, SubscriptionCloseReason)>,
828    transaction_depth: usize,
829    wake: AsyncWake,
830}
831
832impl Default for SubscriptionRegistry {
833    fn default() -> Self {
834        Self {
835            next_id: 1,
836            entries: BTreeMap::new(),
837            closures: Vec::new(),
838            deferred_closures: BTreeMap::new(),
839            transaction_depth: 0,
840            wake: AsyncWake::default(),
841        }
842    }
843}
844
845#[derive(Clone, Debug)]
846pub(crate) struct SubscriptionRegistrySnapshot {
847    entries: BTreeMap<u64, SubscriptionEntry>,
848    deferred_closures: BTreeMap<u64, (SubscriptionEntry, SubscriptionCloseReason)>,
849    transaction_depth: usize,
850}
851
852impl fmt::Debug for SubscriptionRegistry {
853    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
854        formatter
855            .debug_struct("SubscriptionRegistry")
856            .field("active", &self.entries.keys().collect::<Vec<_>>())
857            .finish_non_exhaustive()
858    }
859}
860
861impl SubscriptionRegistry {
862    #[must_use]
863    pub fn new() -> Self {
864        Self::default()
865    }
866
867    pub(crate) fn begin_transaction(&mut self) {
868        self.transaction_depth = self.transaction_depth.saturating_add(1);
869    }
870
871    pub(crate) fn commit_transaction(&mut self) {
872        self.transaction_depth = self.transaction_depth.saturating_sub(1);
873        if self.transaction_depth == 0 {
874            self.finalize_deferred_closures();
875        }
876    }
877
878    pub(crate) fn snapshot(&self) -> SubscriptionRegistrySnapshot {
879        SubscriptionRegistrySnapshot {
880            entries: self.entries.clone(),
881            deferred_closures: self.deferred_closures.clone(),
882            transaction_depth: self.transaction_depth,
883        }
884    }
885
886    pub(crate) fn restore(&mut self, snapshot: SubscriptionRegistrySnapshot) {
887        for (id, entry) in &self.entries {
888            if !snapshot.entries.contains_key(id) && !snapshot.deferred_closures.contains_key(id) {
889                self.closures.push(close_subscription_entry(
890                    entry,
891                    SubscriptionCloseReason::TransactionRolledBack,
892                ));
893            }
894        }
895        for (id, (entry, _)) in &self.deferred_closures {
896            if !snapshot.entries.contains_key(id) && !snapshot.deferred_closures.contains_key(id) {
897                self.closures.push(close_subscription_entry(
898                    entry,
899                    SubscriptionCloseReason::TransactionRolledBack,
900                ));
901            }
902        }
903        self.entries = snapshot.entries;
904        self.deferred_closures = snapshot.deferred_closures;
905        self.transaction_depth = snapshot.transaction_depth;
906    }
907
908    fn defer_or_close(
909        &mut self,
910        id: u64,
911        entry: SubscriptionEntry,
912        reason: SubscriptionCloseReason,
913    ) {
914        if self.transaction_depth > 0 {
915            self.deferred_closures.insert(id, (entry, reason));
916        } else {
917            self.closures.push(close_subscription_entry(&entry, reason));
918        }
919    }
920
921    fn finalize_deferred_closures(&mut self) {
922        let deferred = std::mem::take(&mut self.deferred_closures);
923        self.closures.extend(
924            deferred
925                .into_values()
926                .map(|(entry, reason)| close_subscription_entry(&entry, reason)),
927        );
928    }
929
930    #[must_use]
931    pub fn subscribe(
932        &mut self,
933        registration: SubscriptionRegistration,
934    ) -> (SubscriptionHandle, SubscriptionEmitter) {
935        let id = self.next_id;
936        self.next_id = self.next_id.saturating_add(1);
937        let lifetime = Arc::new(SubscriptionLifetime::new());
938        let pending = Arc::new(SubscriptionQueue {
939            buffer: Mutex::new(SubscriptionBuffer {
940                values: VecDeque::new(),
941                delivery: registration.delivery,
942                capacity: registration.capacity,
943            }),
944            space: Condvar::new(),
945            #[cfg(test)]
946            blocked_producers: AtomicUsize::new(0),
947        });
948        self.entries.insert(
949            id,
950            SubscriptionEntry {
951                label: registration.label,
952                scope: registration.scope,
953                generation: registration.generation,
954                callbacks: CallbackPair {
955                    success: registration.success,
956                    error: registration.error,
957                },
958                output: registration.output,
959                lifetime: Arc::clone(&lifetime),
960                pending: Arc::clone(&pending),
961                delivery: registration.delivery,
962                throttle: registration.throttle,
963                last_delivery: None,
964            },
965        );
966        (
967            SubscriptionHandle(id),
968            SubscriptionEmitter {
969                pending,
970                lifetime,
971                wake: self.wake.clone(),
972            },
973        )
974    }
975
976    #[must_use]
977    pub fn cancel(&mut self, handle: SubscriptionHandle) -> bool {
978        self.cancel_with_reason(handle, SubscriptionCloseReason::Cancelled)
979    }
980
981    pub(crate) fn cancel_with_reason(
982        &mut self,
983        handle: SubscriptionHandle,
984        reason: SubscriptionCloseReason,
985    ) -> bool {
986        let Some(entry) = self.entries.remove(&handle.0) else {
987            return false;
988        };
989        self.defer_or_close(handle.0, entry, reason);
990        true
991    }
992
993    pub fn cancel_scope(&mut self, scope: &AsyncScope) {
994        let mut closures = Vec::new();
995        self.entries.retain(|id, entry| {
996            if &entry.scope == scope {
997                closures.push((*id, entry.clone(), SubscriptionCloseReason::ScopeDisposed));
998                false
999            } else {
1000                true
1001            }
1002        });
1003        for (id, entry, reason) in closures {
1004            self.defer_or_close(id, entry, reason);
1005        }
1006    }
1007
1008    pub fn cancel_component_scope(&mut self, component: &ComponentInstancePath) {
1009        let mut closures = Vec::new();
1010        self.entries.retain(|id, entry| {
1011            let remove = entry.scope.is_within_component(component);
1012            if remove {
1013                closures.push((*id, entry.clone(), SubscriptionCloseReason::ScopeDisposed));
1014            }
1015            !remove
1016        });
1017        for (id, entry, reason) in closures {
1018            self.defer_or_close(id, entry, reason);
1019        }
1020    }
1021
1022    #[must_use]
1023    pub fn drain(&mut self, current: ScriptGeneration) -> Vec<AsyncDelivery> {
1024        self.drain_up_to(current, usize::MAX)
1025    }
1026
1027    #[must_use]
1028    pub(crate) fn drain_up_to(
1029        &mut self,
1030        current: ScriptGeneration,
1031        limit: usize,
1032    ) -> Vec<AsyncDelivery> {
1033        let now = Instant::now();
1034        let mut closed = BTreeMap::new();
1035        let mut deliveries = Vec::new();
1036        for (id, entry) in &mut self.entries {
1037            if entry.generation != current {
1038                let reason = close_subscription_queue(
1039                    &entry.pending,
1040                    &entry.lifetime,
1041                    SubscriptionCloseReason::GenerationStale,
1042                    true,
1043                );
1044                closed.entry(*id).or_insert(reason);
1045                continue;
1046            }
1047            if let Some(reason) = entry.lifetime.close_reason() {
1048                closed.insert(*id, reason);
1049            }
1050            let ready = closed.contains_key(id)
1051                || entry
1052                    .last_delivery
1053                    .is_none_or(|last| now.duration_since(last) >= entry.throttle);
1054            if ready && deliveries.len() < limit {
1055                let mut pending = entry
1056                    .pending
1057                    .buffer
1058                    .lock()
1059                    .unwrap_or_else(std::sync::PoisonError::into_inner);
1060                let requested = if closed.contains_key(id)
1061                    || entry.delivery == SubscriptionDeliveryPolicy::Latest
1062                    || entry.throttle.is_zero()
1063                {
1064                    pending.values.len()
1065                } else {
1066                    usize::from(!pending.values.is_empty())
1067                };
1068                let take = requested.min(limit.saturating_sub(deliveries.len()));
1069                for result in pending.values.drain(..take) {
1070                    deliveries.push(subscription_delivery(entry, result));
1071                }
1072                if take > 0 {
1073                    entry.pending.space.notify_all();
1074                }
1075                if take > 0 {
1076                    entry.last_delivery = Some(now);
1077                }
1078            }
1079        }
1080        for (id, reason) in closed {
1081            let empty = self.entries.get(&id).is_none_or(|entry| {
1082                entry
1083                    .pending
1084                    .buffer
1085                    .lock()
1086                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1087                    .values
1088                    .is_empty()
1089            });
1090            if empty && let Some(entry) = self.entries.remove(&id) {
1091                self.closures.push(close_subscription_entry(&entry, reason));
1092            }
1093        }
1094        deliveries
1095    }
1096
1097    #[must_use]
1098    pub fn active_count(&self) -> usize {
1099        self.entries.len()
1100    }
1101
1102    pub(crate) fn take_closures(&mut self) -> Vec<SubscriptionClosure> {
1103        if self.transaction_depth == 0 {
1104            self.finalize_deferred_closures();
1105        }
1106        std::mem::take(&mut self.closures)
1107    }
1108
1109    pub(crate) fn wake(&self) -> AsyncWake {
1110        self.wake.clone()
1111    }
1112}
1113
1114impl Drop for SubscriptionRegistry {
1115    fn drop(&mut self) {
1116        for entry in self
1117            .entries
1118            .values()
1119            .chain(self.deferred_closures.values().map(|(entry, _)| entry))
1120        {
1121            close_subscription_queue(
1122                &entry.pending,
1123                &entry.lifetime,
1124                SubscriptionCloseReason::RegistryDropped,
1125                true,
1126            );
1127        }
1128    }
1129}
1130
1131fn close_subscription_queue(
1132    pending: &SubscriptionQueue,
1133    lifetime: &SubscriptionLifetime,
1134    reason: SubscriptionCloseReason,
1135    discard: bool,
1136) -> SubscriptionCloseReason {
1137    // The close predicate and Condvar wait are synchronized by the same mutex.
1138    // This prevents a producer from observing an open stream, missing the close
1139    // notification, and then sleeping forever on a full queue.
1140    let mut buffer = pending
1141        .buffer
1142        .lock()
1143        .unwrap_or_else(std::sync::PoisonError::into_inner);
1144    lifetime.close(reason);
1145    if discard {
1146        buffer.values.clear();
1147    }
1148    let reason = lifetime.close_reason().unwrap_or(reason);
1149    drop(buffer);
1150    pending.space.notify_all();
1151    reason
1152}
1153
1154fn close_subscription_entry(
1155    entry: &SubscriptionEntry,
1156    reason: SubscriptionCloseReason,
1157) -> SubscriptionClosure {
1158    let reason = close_subscription_queue(&entry.pending, &entry.lifetime, reason, true);
1159    SubscriptionClosure {
1160        label: entry.label.clone(),
1161        scope: entry.scope.clone(),
1162        reason,
1163    }
1164}
1165
1166fn subscription_delivery(
1167    entry: &SubscriptionEntry,
1168    result: Result<UiValue, String>,
1169) -> AsyncDelivery {
1170    match result {
1171        Ok(value) => match entry.output.validate_ui_value(&value) {
1172            Ok(()) => AsyncDelivery {
1173                callback: entry.callbacks.success.clone(),
1174                payload: value,
1175                scope: entry.scope.clone(),
1176            },
1177            Err(error) => AsyncDelivery {
1178                callback: entry.callbacks.error.clone(),
1179                payload: error_payload(error.to_string()),
1180                scope: entry.scope.clone(),
1181            },
1182        },
1183        Err(error) => AsyncDelivery {
1184            callback: entry.callbacks.error.clone(),
1185            payload: error_payload(error),
1186            scope: entry.scope.clone(),
1187        },
1188    }
1189}
1190
1191fn error_payload(message: String) -> UiValue {
1192    UiValue::Map(BTreeMap::from([
1193        ("kind".to_owned(), UiValue::String("async_error".to_owned())),
1194        ("message".to_owned(), UiValue::String(message)),
1195    ]))
1196}
1197
1198#[derive(Debug, Error)]
1199pub enum AsyncRuntimeError {
1200    #[error("failed to spawn async worker: {0}")]
1201    Spawn(std::io::Error),
1202    #[error("background worker pool is unavailable")]
1203    WorkerPoolUnavailable,
1204    #[error("background worker queue is full")]
1205    WorkerQueueFull,
1206    #[error("subscription is closed: {reason}")]
1207    Closed { reason: SubscriptionCloseReason },
1208    #[error("async output is invalid: {0}")]
1209    InvalidOutput(#[from] SchemaValidationError),
1210    #[error("subscription pending queue reached its capacity of {capacity}")]
1211    Backpressure { capacity: usize },
1212    #[error("subscription capacity must be between 1 and {MAX_SUBSCRIPTION_CAPACITY}, got {0}")]
1213    InvalidCapacity(usize),
1214    #[error("subscription throttle must not exceed 60 seconds, got {0:?}")]
1215    InvalidThrottle(Duration),
1216    #[error("subscription pending queue is poisoned")]
1217    Poisoned,
1218    #[error("suspended view delivery queue reached its capacity of {capacity}")]
1219    SuspendedBackpressure { capacity: usize },
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224    use super::*;
1225    use crate::RuntimeEngine;
1226
1227    fn callbacks() -> (ScriptCallback, ScriptCallback, ScriptGeneration) {
1228        let mut runtime = RuntimeEngine::new();
1229        let compiled = runtime
1230            .compile(
1231                r#"
1232                    fn view() { text("async") }
1233                    fn success(ctx, value) { value }
1234                    fn failure(ctx, error) { error }
1235                "#,
1236            )
1237            .unwrap();
1238        runtime.render(&compiled).unwrap();
1239        (
1240            runtime.callback(&compiled, "success").unwrap(),
1241            runtime.callback(&compiled, "failure").unwrap(),
1242            compiled.generation(),
1243        )
1244    }
1245
1246    #[test]
1247    fn task_completion_delivers_on_foreground_drain() {
1248        let (success, error, generation) = callbacks();
1249        let mut tasks = TaskRegistry::new();
1250        tasks
1251            .spawn(
1252                AsyncScope::App,
1253                generation,
1254                success.clone(),
1255                error,
1256                ValueSchema::string(),
1257                || Ok(UiValue::String("done".to_owned())),
1258            )
1259            .unwrap();
1260        let deadline = Instant::now() + Duration::from_secs(1);
1261        loop {
1262            let deliveries = tasks.drain(generation);
1263            if let Some(delivery) = deliveries.into_iter().next() {
1264                assert_eq!(delivery.callback, success);
1265                assert_eq!(delivery.payload, UiValue::String("done".to_owned()));
1266                break;
1267            }
1268            assert!(Instant::now() < deadline);
1269            std::thread::yield_now();
1270        }
1271    }
1272
1273    #[test]
1274    fn task_panic_becomes_an_error_delivery_and_releases_the_entry() {
1275        let (success, error, generation) = callbacks();
1276        let mut tasks = TaskRegistry::new();
1277        tasks
1278            .spawn(
1279                AsyncScope::App,
1280                generation,
1281                success,
1282                error.clone(),
1283                ValueSchema::Null,
1284                || panic!("task failed"),
1285            )
1286            .unwrap();
1287        let deadline = Instant::now() + Duration::from_secs(1);
1288        loop {
1289            let deliveries = tasks.drain(generation);
1290            if let Some(delivery) = deliveries.into_iter().next() {
1291                assert_eq!(delivery.callback, error);
1292                assert_eq!(tasks.active_count(), 0);
1293                break;
1294            }
1295            assert!(Instant::now() < deadline);
1296            std::thread::yield_now();
1297        }
1298    }
1299
1300    #[test]
1301    fn canceled_and_stale_tasks_never_deliver() {
1302        let (success, error, generation) = callbacks();
1303        let mut tasks = TaskRegistry::new();
1304        let handle = tasks
1305            .spawn(
1306                AsyncScope::App,
1307                generation,
1308                success.clone(),
1309                error.clone(),
1310                ValueSchema::Null,
1311                || Ok(UiValue::Null),
1312            )
1313            .unwrap();
1314        assert!(tasks.cancel(handle));
1315        std::thread::sleep(Duration::from_millis(10));
1316        assert!(tasks.drain(generation).is_empty());
1317
1318        tasks
1319            .spawn(
1320                AsyncScope::App,
1321                generation,
1322                success,
1323                error,
1324                ValueSchema::Null,
1325                || Ok(UiValue::Null),
1326            )
1327            .unwrap();
1328        std::thread::sleep(Duration::from_millis(10));
1329        assert!(tasks.drain(generation.next()).is_empty());
1330    }
1331
1332    #[test]
1333    fn task_cancellation_is_reversible_until_transaction_commit() {
1334        let (success, error, generation) = callbacks();
1335        let (release, wait) = std::sync::mpsc::channel();
1336        let mut tasks = TaskRegistry::new();
1337        let handle = tasks
1338            .spawn_cancellable(
1339                AsyncScope::App,
1340                generation,
1341                success,
1342                error,
1343                ValueSchema::integer(),
1344                move |cancellation| {
1345                    wait.recv().unwrap();
1346                    assert!(!cancellation.is_cancelled());
1347                    Ok(UiValue::Integer(9))
1348                },
1349            )
1350            .unwrap();
1351        let snapshot = tasks.snapshot();
1352        tasks.begin_transaction();
1353        assert!(tasks.cancel(handle));
1354        tasks.restore(snapshot);
1355        assert_eq!(tasks.active_count(), 1);
1356        release.send(()).unwrap();
1357        let deadline = Instant::now() + Duration::from_secs(1);
1358        loop {
1359            let deliveries = tasks.drain(generation);
1360            if !deliveries.is_empty() {
1361                assert_eq!(deliveries[0].payload, UiValue::Integer(9));
1362                break;
1363            }
1364            assert!(Instant::now() < deadline);
1365            std::thread::yield_now();
1366        }
1367    }
1368
1369    #[test]
1370    fn window_and_component_scope_cancellation_preserves_app_tasks() {
1371        let (success, error, generation) = callbacks();
1372        let root = ComponentInstancePath::root("App", "settings");
1373        let mut tasks = TaskRegistry::new();
1374        for scope in [
1375            AsyncScope::App,
1376            AsyncScope::Window("settings".to_owned()),
1377            AsyncScope::Component(root.child("Panel", "root")),
1378            AsyncScope::Effect {
1379                component: root.child("Panel", "effect"),
1380                key: "watch".to_owned(),
1381                activation: 1,
1382            },
1383        ] {
1384            tasks
1385                .spawn(
1386                    scope,
1387                    generation,
1388                    success.clone(),
1389                    error.clone(),
1390                    ValueSchema::Null,
1391                    || Ok(UiValue::Null),
1392                )
1393                .unwrap();
1394        }
1395        tasks.cancel_scope(&AsyncScope::Window("settings".to_owned()));
1396        tasks.cancel_component_scope(&root);
1397        assert_eq!(tasks.active_count(), 1);
1398    }
1399
1400    #[test]
1401    fn exact_effect_activation_cancellation_preserves_replacement_work() {
1402        let (success, error, generation) = callbacks();
1403        let component = ComponentInstancePath::root("App", "main").child("Probe", "primary");
1404        let old = AsyncScope::Effect {
1405            component: component.clone(),
1406            key: "watch".to_owned(),
1407            activation: 1,
1408        };
1409        let replacement = AsyncScope::Effect {
1410            component,
1411            key: "watch".to_owned(),
1412            activation: 2,
1413        };
1414        let mut tasks = TaskRegistry::new();
1415        for scope in [old.clone(), replacement.clone()] {
1416            tasks
1417                .spawn(
1418                    scope,
1419                    generation,
1420                    success.clone(),
1421                    error.clone(),
1422                    ValueSchema::Null,
1423                    || Ok(UiValue::Null),
1424                )
1425                .unwrap();
1426        }
1427        tasks.cancel_scope(&old);
1428        assert_eq!(tasks.active_count(), 1);
1429
1430        let mut subscriptions = SubscriptionRegistry::new();
1431        for scope in [old.clone(), replacement] {
1432            let _ = subscriptions.subscribe(SubscriptionRegistration::new(
1433                "app.stream.watch",
1434                scope,
1435                generation,
1436                success.clone(),
1437                error.clone(),
1438                ValueSchema::Null,
1439            ));
1440        }
1441        subscriptions.cancel_scope(&old);
1442        assert_eq!(subscriptions.active_count(), 1);
1443        assert_eq!(subscriptions.take_closures().len(), 1);
1444    }
1445
1446    #[test]
1447    fn subscription_throttles_to_latest_value_and_cancels() {
1448        let (success, error, generation) = callbacks();
1449        let mut subscriptions = SubscriptionRegistry::new();
1450        let registration = SubscriptionRegistration::new(
1451            "app.stream.watch",
1452            AsyncScope::Window("main".to_owned()),
1453            generation,
1454            success,
1455            error,
1456            ValueSchema::integer(),
1457        )
1458        .with_delivery_policy(SubscriptionDeliveryPolicy::Latest)
1459        .with_throttle(Duration::from_millis(50));
1460        let (handle, emitter) = subscriptions.subscribe(registration);
1461        emitter.emit(UiValue::Integer(1)).unwrap();
1462        let first = subscriptions.drain(generation);
1463        assert_eq!(first[0].payload, UiValue::Integer(1));
1464        emitter.emit(UiValue::Integer(2)).unwrap();
1465        emitter.emit(UiValue::Integer(3)).unwrap();
1466        assert!(subscriptions.drain(generation).is_empty());
1467        std::thread::sleep(Duration::from_millis(60));
1468        let latest = subscriptions.drain(generation);
1469        assert_eq!(latest[0].payload, UiValue::Integer(3));
1470        assert!(subscriptions.cancel(handle));
1471        assert!(matches!(
1472            emitter.emit(UiValue::Integer(4)),
1473            Err(AsyncRuntimeError::Closed {
1474                reason: SubscriptionCloseReason::Cancelled
1475            })
1476        ));
1477        assert_eq!(
1478            subscriptions.take_closures(),
1479            vec![SubscriptionClosure {
1480                label: "app.stream.watch".to_owned(),
1481                scope: AsyncScope::Window("main".to_owned()),
1482                reason: SubscriptionCloseReason::Cancelled,
1483            }]
1484        );
1485    }
1486
1487    #[test]
1488    fn subscription_defaults_to_bounded_ordered_delivery() {
1489        let (success, error, generation) = callbacks();
1490        let registration = SubscriptionRegistration::new(
1491            "app.stream.events",
1492            AsyncScope::App,
1493            generation,
1494            success,
1495            error,
1496            ValueSchema::integer(),
1497        )
1498        .with_capacity(2)
1499        .unwrap();
1500        let mut subscriptions = SubscriptionRegistry::new();
1501        let (_, emitter) = subscriptions.subscribe(registration);
1502        emitter.emit(UiValue::Integer(1)).unwrap();
1503        emitter.emit(UiValue::Integer(2)).unwrap();
1504        assert!(matches!(
1505            emitter.emit(UiValue::Integer(3)),
1506            Err(AsyncRuntimeError::Backpressure { capacity: 2 })
1507        ));
1508        assert_eq!(
1509            subscriptions
1510                .drain(generation)
1511                .into_iter()
1512                .map(|delivery| delivery.payload)
1513                .collect::<Vec<_>>(),
1514            vec![UiValue::Integer(1), UiValue::Integer(2)]
1515        );
1516        emitter.emit(UiValue::Integer(3)).unwrap();
1517        assert_eq!(
1518            subscriptions.drain(generation)[0].payload,
1519            UiValue::Integer(3)
1520        );
1521    }
1522
1523    #[test]
1524    fn stale_generation_discards_buffer_and_reclaims_subscription() {
1525        let (success, error, generation) = callbacks();
1526        let mut subscriptions = SubscriptionRegistry::new();
1527        let (_, emitter) = subscriptions.subscribe(
1528            SubscriptionRegistration::new(
1529                "app.stream.stale",
1530                AsyncScope::App,
1531                generation,
1532                success,
1533                error,
1534                ValueSchema::integer(),
1535            )
1536            .with_capacity(1)
1537            .unwrap(),
1538        );
1539        emitter.emit(UiValue::Integer(1)).unwrap();
1540        let pending = Arc::clone(&emitter.pending);
1541        let blocked = emitter.clone();
1542        let (done_tx, done_rx) = std::sync::mpsc::channel();
1543        let producer = std::thread::spawn(move || {
1544            done_tx
1545                .send(blocked.emit_blocking(UiValue::Integer(2)))
1546                .unwrap();
1547        });
1548        let deadline = Instant::now() + Duration::from_secs(1);
1549        while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1550            assert!(
1551                Instant::now() < deadline,
1552                "producer did not reach the capacity wait"
1553            );
1554            std::thread::yield_now();
1555        }
1556
1557        let current = generation.next();
1558        assert!(subscriptions.drain_up_to(current, 0).is_empty());
1559        assert!(matches!(
1560            done_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1561            Err(AsyncRuntimeError::Closed {
1562                reason: SubscriptionCloseReason::GenerationStale
1563            })
1564        ));
1565        producer.join().unwrap();
1566        assert_eq!(subscriptions.active_count(), 0);
1567        assert_eq!(
1568            subscriptions.take_closures()[0].reason,
1569            SubscriptionCloseReason::GenerationStale
1570        );
1571        assert!(matches!(
1572            emitter.emit(UiValue::Integer(2)),
1573            Err(AsyncRuntimeError::Closed {
1574                reason: SubscriptionCloseReason::GenerationStale
1575            })
1576        ));
1577    }
1578
1579    #[test]
1580    fn cancellation_wakes_a_blocked_lossless_producer() {
1581        let (success, error, generation) = callbacks();
1582        let mut subscriptions = SubscriptionRegistry::new();
1583        let (handle, emitter) = subscriptions.subscribe(
1584            SubscriptionRegistration::new(
1585                "app.stream.blocked",
1586                AsyncScope::App,
1587                generation,
1588                success,
1589                error,
1590                ValueSchema::integer(),
1591            )
1592            .with_capacity(1)
1593            .unwrap(),
1594        );
1595        emitter.emit(UiValue::Integer(1)).unwrap();
1596        let pending = Arc::clone(&emitter.pending);
1597        let (started_tx, started_rx) = std::sync::mpsc::channel();
1598        let (done_tx, done_rx) = std::sync::mpsc::channel();
1599        let producer = std::thread::spawn(move || {
1600            started_tx.send(()).unwrap();
1601            done_tx
1602                .send(emitter.emit_blocking(UiValue::Integer(2)))
1603                .unwrap();
1604        });
1605        started_rx.recv().unwrap();
1606        let deadline = Instant::now() + Duration::from_secs(1);
1607        while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1608            assert!(
1609                Instant::now() < deadline,
1610                "producer did not reach the capacity wait"
1611            );
1612            std::thread::yield_now();
1613        }
1614
1615        assert!(subscriptions.cancel(handle));
1616        let result = done_rx.recv_timeout(Duration::from_secs(1));
1617        if result.is_err() {
1618            // Keep a failing regression from stranding the test process.
1619            pending.space.notify_all();
1620        }
1621        let result = result.expect("cancellation must wake the blocked producer");
1622        assert!(matches!(
1623            result,
1624            Err(AsyncRuntimeError::Closed {
1625                reason: SubscriptionCloseReason::Cancelled
1626            })
1627        ));
1628        producer.join().unwrap();
1629    }
1630
1631    #[test]
1632    fn producer_close_and_registry_drop_report_first_close_reason() {
1633        let (success, error, generation) = callbacks();
1634        let mut subscriptions = SubscriptionRegistry::new();
1635        let registration = SubscriptionRegistration::new(
1636            "app.stream.watch",
1637            AsyncScope::App,
1638            generation,
1639            success.clone(),
1640            error.clone(),
1641            ValueSchema::integer(),
1642        );
1643        let (_, emitter) = subscriptions.subscribe(registration);
1644        emitter.emit(UiValue::Integer(1)).unwrap();
1645        emitter.close();
1646        assert_eq!(
1647            subscriptions.drain_up_to(generation, 1)[0].payload,
1648            UiValue::Integer(1)
1649        );
1650        assert!(matches!(
1651            emitter.emit(UiValue::Integer(1)),
1652            Err(AsyncRuntimeError::Closed {
1653                reason: SubscriptionCloseReason::ProducerClosed
1654            })
1655        ));
1656        assert_eq!(
1657            subscriptions.take_closures()[0].reason,
1658            SubscriptionCloseReason::ProducerClosed
1659        );
1660
1661        let orphan = {
1662            let mut registry = SubscriptionRegistry::new();
1663            let registration = SubscriptionRegistration::new(
1664                "app.stream.orphan",
1665                AsyncScope::App,
1666                generation,
1667                success,
1668                error,
1669                ValueSchema::integer(),
1670            );
1671            let (_, emitter) = registry.subscribe(registration);
1672            emitter
1673        };
1674        assert!(matches!(
1675            orphan.emit(UiValue::Integer(2)),
1676            Err(AsyncRuntimeError::Closed {
1677                reason: SubscriptionCloseReason::RegistryDropped
1678            })
1679        ));
1680    }
1681}