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 validate_async_payload(&entry.output, &value) {
496            Ok(()) => AsyncDelivery {
497                callback: entry.callbacks.success,
498                payload: value,
499                scope: entry.scope,
500            },
501            Err(payload) => AsyncDelivery {
502                callback: entry.callbacks.error,
503                payload,
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_mins(1);
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 validate_async_payload(&entry.output, &value) {
1172            Ok(()) => AsyncDelivery {
1173                callback: entry.callbacks.success.clone(),
1174                payload: value,
1175                scope: entry.scope.clone(),
1176            },
1177            Err(payload) => AsyncDelivery {
1178                callback: entry.callbacks.error.clone(),
1179                payload,
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: &str) -> UiValue {
1192    let message = bounded_utf8(message, 1_024);
1193    UiValue::Map(BTreeMap::from([
1194        ("kind".to_owned(), UiValue::String("async_error".to_owned())),
1195        ("message".to_owned(), UiValue::String(message)),
1196    ]))
1197}
1198
1199fn validate_async_payload(output: &ValueSchema, value: &UiValue) -> Result<(), UiValue> {
1200    validate_rhai_delivery(value).map_err(delivery_limit_payload)?;
1201    output
1202        .validate_ui_value_first(value)
1203        .map_err(|error| error_payload(&error.to_string()))
1204}
1205
1206fn delivery_limit_payload(error: DeliveryLimit) -> UiValue {
1207    UiValue::Map(BTreeMap::from([
1208        (
1209            "kind".to_owned(),
1210            UiValue::String("async_delivery_limit".to_owned()),
1211        ),
1212        (
1213            "resource".to_owned(),
1214            UiValue::String(error.resource.to_owned()),
1215        ),
1216        (
1217            "actual".to_owned(),
1218            UiValue::Integer(i64::try_from(error.actual).unwrap_or(i64::MAX)),
1219        ),
1220        (
1221            "limit".to_owned(),
1222            UiValue::Integer(i64::try_from(error.limit).unwrap_or(i64::MAX)),
1223        ),
1224        ("message".to_owned(), UiValue::String(error.message())),
1225    ]))
1226}
1227
1228#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1229struct DeliveryLimit {
1230    resource: &'static str,
1231    actual: usize,
1232    limit: usize,
1233}
1234
1235impl DeliveryLimit {
1236    fn message(self) -> String {
1237        format!(
1238            "async result exceeds Rhai {} limit: {} > {}",
1239            self.resource, self.actual, self.limit
1240        )
1241    }
1242}
1243
1244#[derive(Default)]
1245struct DeliveryUsage {
1246    strings: usize,
1247    arrays: usize,
1248    maps: usize,
1249}
1250
1251fn validate_rhai_delivery(value: &UiValue) -> Result<(), DeliveryLimit> {
1252    fn visit(
1253        value: &UiValue,
1254        depth: usize,
1255        usage: &mut DeliveryUsage,
1256    ) -> Result<(), DeliveryLimit> {
1257        if depth > crate::engine::RHAI_MAX_DATA_DEPTH {
1258            return Err(DeliveryLimit {
1259                resource: "data_depth",
1260                actual: depth,
1261                limit: crate::engine::RHAI_MAX_DATA_DEPTH,
1262            });
1263        }
1264        match value {
1265            UiValue::String(value) => {
1266                usage.strings = usage.strings.saturating_add(value.len());
1267            }
1268            UiValue::Array(values) => {
1269                usage.arrays = usage.arrays.saturating_add(values.len());
1270                for value in values {
1271                    visit(value, depth.saturating_add(1), usage)?;
1272                }
1273            }
1274            UiValue::Map(values) => {
1275                usage.maps = usage.maps.saturating_add(values.len());
1276                for value in values.values() {
1277                    visit(value, depth.saturating_add(1), usage)?;
1278                }
1279            }
1280            UiValue::Null
1281            | UiValue::Bool(_)
1282            | UiValue::Integer(_)
1283            | UiValue::Float(_)
1284            | UiValue::Handle(_) => {}
1285        }
1286        for (resource, actual, limit) in [
1287            (
1288                "string_bytes",
1289                usage.strings,
1290                crate::engine::RHAI_MAX_STRING_SIZE,
1291            ),
1292            (
1293                "array_items",
1294                usage.arrays,
1295                crate::engine::RHAI_MAX_ARRAY_SIZE,
1296            ),
1297            ("map_entries", usage.maps, crate::engine::RHAI_MAX_MAP_SIZE),
1298        ] {
1299            if actual > limit {
1300                return Err(DeliveryLimit {
1301                    resource,
1302                    actual,
1303                    limit,
1304                });
1305            }
1306        }
1307        Ok(())
1308    }
1309
1310    visit(value, 0, &mut DeliveryUsage::default())
1311}
1312
1313fn bounded_utf8(value: &str, maximum: usize) -> String {
1314    if value.len() <= maximum {
1315        return value.to_owned();
1316    }
1317    let mut end = maximum;
1318    while !value.is_char_boundary(end) {
1319        end = end.saturating_sub(1);
1320    }
1321    format!("{}…", &value[..end])
1322}
1323
1324#[derive(Debug, Error)]
1325pub enum AsyncRuntimeError {
1326    #[error("failed to spawn async worker: {0}")]
1327    Spawn(std::io::Error),
1328    #[error("background worker pool is unavailable")]
1329    WorkerPoolUnavailable,
1330    #[error("background worker queue is full")]
1331    WorkerQueueFull,
1332    #[error("subscription is closed: {reason}")]
1333    Closed { reason: SubscriptionCloseReason },
1334    #[error("async output is invalid: {0}")]
1335    InvalidOutput(#[from] SchemaValidationError),
1336    #[error("subscription pending queue reached its capacity of {capacity}")]
1337    Backpressure { capacity: usize },
1338    #[error("subscription capacity must be between 1 and {MAX_SUBSCRIPTION_CAPACITY}, got {0}")]
1339    InvalidCapacity(usize),
1340    #[error("subscription throttle must not exceed 60 seconds, got {0:?}")]
1341    InvalidThrottle(Duration),
1342    #[error("subscription pending queue is poisoned")]
1343    Poisoned,
1344    #[error("suspended view delivery queue reached its capacity of {capacity}")]
1345    SuspendedBackpressure { capacity: usize },
1346}
1347
1348#[cfg(test)]
1349mod tests {
1350    use super::*;
1351    use crate::RuntimeEngine;
1352
1353    #[test]
1354    fn rhai_delivery_preflight_matches_recursive_engine_limits() {
1355        assert!(
1356            validate_rhai_delivery(&UiValue::String(
1357                "x".repeat(crate::engine::RHAI_MAX_STRING_SIZE)
1358            ))
1359            .is_ok()
1360        );
1361        let oversized = UiValue::Array(vec![
1362            UiValue::String("x".repeat(600_000)),
1363            UiValue::String("y".repeat(600_000)),
1364        ]);
1365        assert_eq!(
1366            validate_rhai_delivery(&oversized).unwrap_err().resource,
1367            "string_bytes"
1368        );
1369        let oversized = UiValue::Array(
1370            (0..=crate::engine::RHAI_MAX_ARRAY_SIZE)
1371                .map(|_| UiValue::Null)
1372                .collect(),
1373        );
1374        assert_eq!(
1375            validate_rhai_delivery(&oversized).unwrap_err().resource,
1376            "array_items"
1377        );
1378    }
1379
1380    #[test]
1381    fn async_error_payload_is_always_bounded_utf8() {
1382        let UiValue::Map(payload) = error_payload(&"界".repeat(100_000)) else {
1383            panic!("error payload must be an object");
1384        };
1385        let Some(UiValue::String(message)) = payload.get("message") else {
1386            panic!("error payload must contain a message");
1387        };
1388        assert!(message.len() <= 1_028);
1389        assert!(validate_rhai_delivery(&UiValue::Map(payload)).is_ok());
1390    }
1391
1392    fn callbacks() -> (ScriptCallback, ScriptCallback, ScriptGeneration) {
1393        let mut runtime = RuntimeEngine::new();
1394        let compiled = runtime
1395            .compile(
1396                r#"
1397                    fn view() { text("async") }
1398                    fn success(ctx, value) { value }
1399                    fn failure(ctx, error) { error }
1400                "#,
1401            )
1402            .unwrap();
1403        runtime.render(&compiled).unwrap();
1404        (
1405            runtime.callback(&compiled, "success").unwrap(),
1406            runtime.callback(&compiled, "failure").unwrap(),
1407            compiled.generation(),
1408        )
1409    }
1410
1411    #[test]
1412    fn task_completion_delivers_on_foreground_drain() {
1413        let (success, error, generation) = callbacks();
1414        let mut tasks = TaskRegistry::new();
1415        tasks
1416            .spawn(
1417                AsyncScope::App,
1418                generation,
1419                success.clone(),
1420                error,
1421                ValueSchema::string(),
1422                || Ok(UiValue::String("done".to_owned())),
1423            )
1424            .unwrap();
1425        let deadline = Instant::now() + Duration::from_secs(1);
1426        loop {
1427            let deliveries = tasks.drain(generation);
1428            if let Some(delivery) = deliveries.into_iter().next() {
1429                assert_eq!(delivery.callback, success);
1430                assert_eq!(delivery.payload, UiValue::String("done".to_owned()));
1431                break;
1432            }
1433            assert!(Instant::now() < deadline);
1434            std::thread::yield_now();
1435        }
1436    }
1437
1438    #[test]
1439    fn task_panic_becomes_an_error_delivery_and_releases_the_entry() {
1440        let (success, error, generation) = callbacks();
1441        let mut tasks = TaskRegistry::new();
1442        tasks
1443            .spawn(
1444                AsyncScope::App,
1445                generation,
1446                success,
1447                error.clone(),
1448                ValueSchema::Null,
1449                || panic!("task failed"),
1450            )
1451            .unwrap();
1452        let deadline = Instant::now() + Duration::from_secs(1);
1453        loop {
1454            let deliveries = tasks.drain(generation);
1455            if let Some(delivery) = deliveries.into_iter().next() {
1456                assert_eq!(delivery.callback, error);
1457                assert_eq!(tasks.active_count(), 0);
1458                break;
1459            }
1460            assert!(Instant::now() < deadline);
1461            std::thread::yield_now();
1462        }
1463    }
1464
1465    #[test]
1466    fn canceled_and_stale_tasks_never_deliver() {
1467        let (success, error, generation) = callbacks();
1468        let mut tasks = TaskRegistry::new();
1469        let handle = tasks
1470            .spawn(
1471                AsyncScope::App,
1472                generation,
1473                success.clone(),
1474                error.clone(),
1475                ValueSchema::Null,
1476                || Ok(UiValue::Null),
1477            )
1478            .unwrap();
1479        assert!(tasks.cancel(handle));
1480        std::thread::sleep(Duration::from_millis(10));
1481        assert!(tasks.drain(generation).is_empty());
1482
1483        tasks
1484            .spawn(
1485                AsyncScope::App,
1486                generation,
1487                success,
1488                error,
1489                ValueSchema::Null,
1490                || Ok(UiValue::Null),
1491            )
1492            .unwrap();
1493        std::thread::sleep(Duration::from_millis(10));
1494        assert!(tasks.drain(generation.next()).is_empty());
1495    }
1496
1497    #[test]
1498    fn task_cancellation_is_reversible_until_transaction_commit() {
1499        let (success, error, generation) = callbacks();
1500        let (release, wait) = std::sync::mpsc::channel();
1501        let mut tasks = TaskRegistry::new();
1502        let handle = tasks
1503            .spawn_cancellable(
1504                AsyncScope::App,
1505                generation,
1506                success,
1507                error,
1508                ValueSchema::integer(),
1509                move |cancellation| {
1510                    wait.recv().unwrap();
1511                    assert!(!cancellation.is_cancelled());
1512                    Ok(UiValue::Integer(9))
1513                },
1514            )
1515            .unwrap();
1516        let snapshot = tasks.snapshot();
1517        tasks.begin_transaction();
1518        assert!(tasks.cancel(handle));
1519        tasks.restore(snapshot);
1520        assert_eq!(tasks.active_count(), 1);
1521        release.send(()).unwrap();
1522        let deadline = Instant::now() + Duration::from_secs(1);
1523        loop {
1524            let deliveries = tasks.drain(generation);
1525            if !deliveries.is_empty() {
1526                assert_eq!(deliveries[0].payload, UiValue::Integer(9));
1527                break;
1528            }
1529            assert!(Instant::now() < deadline);
1530            std::thread::yield_now();
1531        }
1532    }
1533
1534    #[test]
1535    fn window_and_component_scope_cancellation_preserves_app_tasks() {
1536        let (success, error, generation) = callbacks();
1537        let root = ComponentInstancePath::root("App", "settings");
1538        let mut tasks = TaskRegistry::new();
1539        for scope in [
1540            AsyncScope::App,
1541            AsyncScope::Window("settings".to_owned()),
1542            AsyncScope::Component(root.child("Panel", "root")),
1543            AsyncScope::Effect {
1544                component: root.child("Panel", "effect"),
1545                key: "watch".to_owned(),
1546                activation: 1,
1547            },
1548        ] {
1549            tasks
1550                .spawn(
1551                    scope,
1552                    generation,
1553                    success.clone(),
1554                    error.clone(),
1555                    ValueSchema::Null,
1556                    || Ok(UiValue::Null),
1557                )
1558                .unwrap();
1559        }
1560        tasks.cancel_scope(&AsyncScope::Window("settings".to_owned()));
1561        tasks.cancel_component_scope(&root);
1562        assert_eq!(tasks.active_count(), 1);
1563    }
1564
1565    #[test]
1566    fn exact_effect_activation_cancellation_preserves_replacement_work() {
1567        let (success, error, generation) = callbacks();
1568        let component = ComponentInstancePath::root("App", "main").child("Probe", "primary");
1569        let old = AsyncScope::Effect {
1570            component: component.clone(),
1571            key: "watch".to_owned(),
1572            activation: 1,
1573        };
1574        let replacement = AsyncScope::Effect {
1575            component,
1576            key: "watch".to_owned(),
1577            activation: 2,
1578        };
1579        let mut tasks = TaskRegistry::new();
1580        for scope in [old.clone(), replacement.clone()] {
1581            tasks
1582                .spawn(
1583                    scope,
1584                    generation,
1585                    success.clone(),
1586                    error.clone(),
1587                    ValueSchema::Null,
1588                    || Ok(UiValue::Null),
1589                )
1590                .unwrap();
1591        }
1592        tasks.cancel_scope(&old);
1593        assert_eq!(tasks.active_count(), 1);
1594
1595        let mut subscriptions = SubscriptionRegistry::new();
1596        for scope in [old.clone(), replacement] {
1597            let _ = subscriptions.subscribe(SubscriptionRegistration::new(
1598                "app.stream.watch",
1599                scope,
1600                generation,
1601                success.clone(),
1602                error.clone(),
1603                ValueSchema::Null,
1604            ));
1605        }
1606        subscriptions.cancel_scope(&old);
1607        assert_eq!(subscriptions.active_count(), 1);
1608        assert_eq!(subscriptions.take_closures().len(), 1);
1609    }
1610
1611    #[test]
1612    fn subscription_throttles_to_latest_value_and_cancels() {
1613        let (success, error, generation) = callbacks();
1614        let mut subscriptions = SubscriptionRegistry::new();
1615        let registration = SubscriptionRegistration::new(
1616            "app.stream.watch",
1617            AsyncScope::Window("main".to_owned()),
1618            generation,
1619            success,
1620            error,
1621            ValueSchema::integer(),
1622        )
1623        .with_delivery_policy(SubscriptionDeliveryPolicy::Latest)
1624        .with_throttle(Duration::from_millis(50));
1625        let (handle, emitter) = subscriptions.subscribe(registration);
1626        emitter.emit(UiValue::Integer(1)).unwrap();
1627        let first = subscriptions.drain(generation);
1628        assert_eq!(first[0].payload, UiValue::Integer(1));
1629        emitter.emit(UiValue::Integer(2)).unwrap();
1630        emitter.emit(UiValue::Integer(3)).unwrap();
1631        assert!(subscriptions.drain(generation).is_empty());
1632        std::thread::sleep(Duration::from_millis(60));
1633        let latest = subscriptions.drain(generation);
1634        assert_eq!(latest[0].payload, UiValue::Integer(3));
1635        assert!(subscriptions.cancel(handle));
1636        assert!(matches!(
1637            emitter.emit(UiValue::Integer(4)),
1638            Err(AsyncRuntimeError::Closed {
1639                reason: SubscriptionCloseReason::Cancelled
1640            })
1641        ));
1642        assert_eq!(
1643            subscriptions.take_closures(),
1644            vec![SubscriptionClosure {
1645                label: "app.stream.watch".to_owned(),
1646                scope: AsyncScope::Window("main".to_owned()),
1647                reason: SubscriptionCloseReason::Cancelled,
1648            }]
1649        );
1650    }
1651
1652    #[test]
1653    fn subscription_defaults_to_bounded_ordered_delivery() {
1654        let (success, error, generation) = callbacks();
1655        let registration = SubscriptionRegistration::new(
1656            "app.stream.events",
1657            AsyncScope::App,
1658            generation,
1659            success,
1660            error,
1661            ValueSchema::integer(),
1662        )
1663        .with_capacity(2)
1664        .unwrap();
1665        let mut subscriptions = SubscriptionRegistry::new();
1666        let (_, emitter) = subscriptions.subscribe(registration);
1667        emitter.emit(UiValue::Integer(1)).unwrap();
1668        emitter.emit(UiValue::Integer(2)).unwrap();
1669        assert!(matches!(
1670            emitter.emit(UiValue::Integer(3)),
1671            Err(AsyncRuntimeError::Backpressure { capacity: 2 })
1672        ));
1673        assert_eq!(
1674            subscriptions
1675                .drain(generation)
1676                .into_iter()
1677                .map(|delivery| delivery.payload)
1678                .collect::<Vec<_>>(),
1679            vec![UiValue::Integer(1), UiValue::Integer(2)]
1680        );
1681        emitter.emit(UiValue::Integer(3)).unwrap();
1682        assert_eq!(
1683            subscriptions.drain(generation)[0].payload,
1684            UiValue::Integer(3)
1685        );
1686    }
1687
1688    #[test]
1689    fn stale_generation_discards_buffer_and_reclaims_subscription() {
1690        let (success, error, generation) = callbacks();
1691        let mut subscriptions = SubscriptionRegistry::new();
1692        let (_, emitter) = subscriptions.subscribe(
1693            SubscriptionRegistration::new(
1694                "app.stream.stale",
1695                AsyncScope::App,
1696                generation,
1697                success,
1698                error,
1699                ValueSchema::integer(),
1700            )
1701            .with_capacity(1)
1702            .unwrap(),
1703        );
1704        emitter.emit(UiValue::Integer(1)).unwrap();
1705        let pending = Arc::clone(&emitter.pending);
1706        let blocked = emitter.clone();
1707        let (done_tx, done_rx) = std::sync::mpsc::channel();
1708        let producer = std::thread::spawn(move || {
1709            done_tx
1710                .send(blocked.emit_blocking(UiValue::Integer(2)))
1711                .unwrap();
1712        });
1713        let deadline = Instant::now() + Duration::from_secs(1);
1714        while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1715            assert!(
1716                Instant::now() < deadline,
1717                "producer did not reach the capacity wait"
1718            );
1719            std::thread::yield_now();
1720        }
1721
1722        let current = generation.next();
1723        assert!(subscriptions.drain_up_to(current, 0).is_empty());
1724        assert!(matches!(
1725            done_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1726            Err(AsyncRuntimeError::Closed {
1727                reason: SubscriptionCloseReason::GenerationStale
1728            })
1729        ));
1730        producer.join().unwrap();
1731        assert_eq!(subscriptions.active_count(), 0);
1732        assert_eq!(
1733            subscriptions.take_closures()[0].reason,
1734            SubscriptionCloseReason::GenerationStale
1735        );
1736        assert!(matches!(
1737            emitter.emit(UiValue::Integer(2)),
1738            Err(AsyncRuntimeError::Closed {
1739                reason: SubscriptionCloseReason::GenerationStale
1740            })
1741        ));
1742    }
1743
1744    #[test]
1745    fn cancellation_wakes_a_blocked_lossless_producer() {
1746        let (success, error, generation) = callbacks();
1747        let mut subscriptions = SubscriptionRegistry::new();
1748        let (handle, emitter) = subscriptions.subscribe(
1749            SubscriptionRegistration::new(
1750                "app.stream.blocked",
1751                AsyncScope::App,
1752                generation,
1753                success,
1754                error,
1755                ValueSchema::integer(),
1756            )
1757            .with_capacity(1)
1758            .unwrap(),
1759        );
1760        emitter.emit(UiValue::Integer(1)).unwrap();
1761        let pending = Arc::clone(&emitter.pending);
1762        let (started_tx, started_rx) = std::sync::mpsc::channel();
1763        let (done_tx, done_rx) = std::sync::mpsc::channel();
1764        let producer = std::thread::spawn(move || {
1765            started_tx.send(()).unwrap();
1766            done_tx
1767                .send(emitter.emit_blocking(UiValue::Integer(2)))
1768                .unwrap();
1769        });
1770        started_rx.recv().unwrap();
1771        let deadline = Instant::now() + Duration::from_secs(1);
1772        while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1773            assert!(
1774                Instant::now() < deadline,
1775                "producer did not reach the capacity wait"
1776            );
1777            std::thread::yield_now();
1778        }
1779
1780        assert!(subscriptions.cancel(handle));
1781        let result = done_rx.recv_timeout(Duration::from_secs(1));
1782        if result.is_err() {
1783            // Keep a failing regression from stranding the test process.
1784            pending.space.notify_all();
1785        }
1786        let result = result.expect("cancellation must wake the blocked producer");
1787        assert!(matches!(
1788            result,
1789            Err(AsyncRuntimeError::Closed {
1790                reason: SubscriptionCloseReason::Cancelled
1791            })
1792        ));
1793        producer.join().unwrap();
1794    }
1795
1796    #[test]
1797    fn producer_close_and_registry_drop_report_first_close_reason() {
1798        let (success, error, generation) = callbacks();
1799        let mut subscriptions = SubscriptionRegistry::new();
1800        let registration = SubscriptionRegistration::new(
1801            "app.stream.watch",
1802            AsyncScope::App,
1803            generation,
1804            success.clone(),
1805            error.clone(),
1806            ValueSchema::integer(),
1807        );
1808        let (_, emitter) = subscriptions.subscribe(registration);
1809        emitter.emit(UiValue::Integer(1)).unwrap();
1810        emitter.close();
1811        assert_eq!(
1812            subscriptions.drain_up_to(generation, 1)[0].payload,
1813            UiValue::Integer(1)
1814        );
1815        assert!(matches!(
1816            emitter.emit(UiValue::Integer(1)),
1817            Err(AsyncRuntimeError::Closed {
1818                reason: SubscriptionCloseReason::ProducerClosed
1819            })
1820        ));
1821        assert_eq!(
1822            subscriptions.take_closures()[0].reason,
1823            SubscriptionCloseReason::ProducerClosed
1824        );
1825
1826        let orphan = {
1827            let mut registry = SubscriptionRegistry::new();
1828            let registration = SubscriptionRegistration::new(
1829                "app.stream.orphan",
1830                AsyncScope::App,
1831                generation,
1832                success,
1833                error,
1834                ValueSchema::integer(),
1835            );
1836            let (_, emitter) = registry.subscribe(registration);
1837            emitter
1838        };
1839        assert!(matches!(
1840            orphan.emit(UiValue::Integer(2)),
1841            Err(AsyncRuntimeError::Closed {
1842                reason: SubscriptionCloseReason::RegistryDropped
1843            })
1844        ));
1845    }
1846
1847    #[test]
1848    fn online_schema_rejection_formats_only_the_first_issue() {
1849        let value = UiValue::Map(
1850            (0..100_000)
1851                .map(|index| (format!("field-{index:06}"), UiValue::Null))
1852                .collect(),
1853        );
1854        let error = validate_async_payload(
1855            &ValueSchema::Map {
1856                values: Box::new(ValueSchema::integer()),
1857            },
1858            &value,
1859        )
1860        .unwrap_err();
1861        let UiValue::Map(error) = error else {
1862            panic!("async schema error must be structured");
1863        };
1864        let Some(UiValue::String(message)) = error.get("message") else {
1865            panic!("async schema error must contain a message");
1866        };
1867        assert!(message.contains("$.field-000000"), "{message}");
1868        assert!(!message.contains("field-000001"), "{message}");
1869    }
1870}