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    /// What the delivery runs in response to.
241    pub origin: crate::InvocationOrigin,
242}
243
244#[derive(Clone, Debug)]
245struct CallbackPair {
246    success: ScriptCallback,
247    error: ScriptCallback,
248}
249
250#[derive(Clone, Debug)]
251struct TaskEntry {
252    scope: AsyncScope,
253    generation: ScriptGeneration,
254    callbacks: CallbackPair,
255    output: ValueSchema,
256    cancellation: TaskCancellation,
257    started_by: Box<crate::InvocationOrigin>,
258}
259
260struct TaskMessage {
261    id: u64,
262    result: Result<UiValue, String>,
263}
264
265pub struct TaskRegistry {
266    next_id: u64,
267    entries: BTreeMap<u64, TaskEntry>,
268    sender: Sender<TaskMessage>,
269    receiver: Receiver<TaskMessage>,
270    wake: AsyncWake,
271    deferred_cancellations: BTreeMap<u64, TaskEntry>,
272    transaction_depth: usize,
273    /// The root origin of the current invocation, recorded on tasks it starts.
274    origin_root: crate::InvocationOrigin,
275}
276
277#[derive(Clone, Debug)]
278pub(crate) struct TaskRegistrySnapshot {
279    entries: BTreeMap<u64, TaskEntry>,
280    deferred_cancellations: BTreeMap<u64, TaskEntry>,
281    transaction_depth: usize,
282}
283
284impl Default for TaskRegistry {
285    fn default() -> Self {
286        let (sender, receiver) = channel();
287        Self {
288            next_id: 1,
289            entries: BTreeMap::new(),
290            sender,
291            receiver,
292            wake: AsyncWake::default(),
293            deferred_cancellations: BTreeMap::new(),
294            transaction_depth: 0,
295            origin_root: crate::InvocationOrigin::default(),
296        }
297    }
298}
299
300impl fmt::Debug for TaskRegistry {
301    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
302        formatter
303            .debug_struct("TaskRegistry")
304            .field("active", &self.entries.keys().collect::<Vec<_>>())
305            .finish_non_exhaustive()
306    }
307}
308
309impl TaskRegistry {
310    /// Record the root origin of the invocation now running; tasks it starts
311    /// deliver as completions started by it.
312    pub(crate) fn set_origin_root(&mut self, origin: &crate::InvocationOrigin) {
313        self.origin_root = origin.root().clone();
314    }
315
316    #[must_use]
317    pub fn new() -> Self {
318        Self::default()
319    }
320
321    pub(crate) fn begin_transaction(&mut self) {
322        self.transaction_depth = self.transaction_depth.saturating_add(1);
323    }
324
325    pub(crate) fn commit_transaction(&mut self) {
326        self.transaction_depth = self.transaction_depth.saturating_sub(1);
327        if self.transaction_depth == 0 {
328            for entry in std::mem::take(&mut self.deferred_cancellations).into_values() {
329                entry.cancellation.cancel();
330            }
331        }
332    }
333
334    /// Spawn Rust work while retaining Rhai callbacks on the foreground side.
335    ///
336    /// # Errors
337    ///
338    /// Returns [`AsyncRuntimeError::Spawn`] when the worker thread cannot start.
339    pub fn spawn(
340        &mut self,
341        scope: AsyncScope,
342        generation: ScriptGeneration,
343        success: ScriptCallback,
344        error: ScriptCallback,
345        output: ValueSchema,
346        work: impl FnOnce() -> Result<UiValue, String> + Send + 'static,
347    ) -> Result<TaskHandle, AsyncRuntimeError> {
348        self.spawn_cancellable(scope, generation, success, error, output, move |_| work())
349    }
350
351    pub(crate) fn spawn_cancellable(
352        &mut self,
353        scope: AsyncScope,
354        generation: ScriptGeneration,
355        success: ScriptCallback,
356        error: ScriptCallback,
357        output: ValueSchema,
358        work: impl FnOnce(TaskCancellation) -> Result<UiValue, String> + Send + 'static,
359    ) -> Result<TaskHandle, AsyncRuntimeError> {
360        let id = self.next_id;
361        self.next_id = self.next_id.saturating_add(1);
362        let cancellation = TaskCancellation::default();
363        self.entries.insert(
364            id,
365            TaskEntry {
366                scope,
367                generation,
368                callbacks: CallbackPair { success, error },
369                output,
370                cancellation: cancellation.clone(),
371                started_by: Box::new(self.origin_root.clone()),
372            },
373        );
374        let sender = self.sender.clone();
375        let wake = self.wake.clone();
376        let job = Box::new(move || {
377            let result =
378                catch_unwind(AssertUnwindSafe(|| work(cancellation))).unwrap_or_else(|payload| {
379                    let message = payload.downcast_ref::<&str>().map_or_else(
380                        || {
381                            payload.downcast_ref::<String>().map_or_else(
382                                || "background task panicked".to_owned(),
383                                |message| format!("background task panicked: {message}"),
384                            )
385                        },
386                        |message| format!("background task panicked: {message}"),
387                    );
388                    Err(message)
389                });
390            if sender.send(TaskMessage { id, result }).is_ok() {
391                wake.notify();
392            }
393        });
394        if let Err(error) = background_executor().and_then(|executor| executor.submit(job)) {
395            self.entries.remove(&id);
396            return Err(error);
397        }
398        Ok(TaskHandle(id))
399    }
400
401    /// The scope that owns a pending task; `None` once it finished.
402    pub(crate) fn scope(&self, handle: TaskHandle) -> Option<&AsyncScope> {
403        self.entries.get(&handle.0).map(|entry| &entry.scope)
404    }
405
406    #[must_use]
407    pub fn cancel(&mut self, handle: TaskHandle) -> bool {
408        let Some(entry) = self.entries.remove(&handle.0) else {
409            return false;
410        };
411        self.defer_or_cancel(handle.0, entry);
412        true
413    }
414
415    pub fn cancel_scope(&mut self, scope: &AsyncScope) {
416        let ids = self
417            .entries
418            .iter()
419            .filter_map(|(id, entry)| (&entry.scope == scope).then_some(*id))
420            .collect::<Vec<_>>();
421        self.cancel_ids(ids);
422    }
423
424    pub fn cancel_component_scope(&mut self, component: &ComponentInstancePath) {
425        let ids = self
426            .entries
427            .iter()
428            .filter_map(|(id, entry)| entry.scope.is_within_component(component).then_some(*id))
429            .collect::<Vec<_>>();
430        self.cancel_ids(ids);
431    }
432
433    #[must_use]
434    pub fn drain(&mut self, current: ScriptGeneration) -> Vec<AsyncDelivery> {
435        self.drain_up_to(current, usize::MAX)
436    }
437
438    #[must_use]
439    pub(crate) fn drain_up_to(
440        &mut self,
441        current: ScriptGeneration,
442        limit: usize,
443    ) -> Vec<AsyncDelivery> {
444        let mut deliveries = Vec::new();
445        while deliveries.len() < limit {
446            let Ok(message) = self.receiver.try_recv() else {
447                break;
448            };
449            let Some(entry) = self.entries.remove(&message.id) else {
450                continue;
451            };
452            if entry.generation != current {
453                continue;
454            }
455            deliveries.push(task_delivery(entry, message.result));
456        }
457        deliveries
458    }
459
460    #[must_use]
461    pub fn active_count(&self) -> usize {
462        self.entries.len()
463    }
464
465    pub(crate) fn snapshot(&self) -> TaskRegistrySnapshot {
466        TaskRegistrySnapshot {
467            entries: self.entries.clone(),
468            deferred_cancellations: self.deferred_cancellations.clone(),
469            transaction_depth: self.transaction_depth,
470        }
471    }
472
473    pub(crate) fn restore(&mut self, snapshot: TaskRegistrySnapshot) {
474        for (id, entry) in self
475            .entries
476            .iter()
477            .chain(self.deferred_cancellations.iter())
478        {
479            if !snapshot.entries.contains_key(id)
480                && !snapshot.deferred_cancellations.contains_key(id)
481            {
482                entry.cancellation.cancel();
483            }
484        }
485        self.entries = snapshot.entries;
486        self.deferred_cancellations = snapshot.deferred_cancellations;
487        self.transaction_depth = snapshot.transaction_depth;
488    }
489
490    fn cancel_ids(&mut self, ids: impl IntoIterator<Item = u64>) {
491        for id in ids {
492            if let Some(entry) = self.entries.remove(&id) {
493                self.defer_or_cancel(id, entry);
494            }
495        }
496    }
497
498    fn defer_or_cancel(&mut self, id: u64, entry: TaskEntry) {
499        if self.transaction_depth > 0 {
500            self.deferred_cancellations.insert(id, entry);
501        } else {
502            entry.cancellation.cancel();
503        }
504    }
505
506    pub(crate) fn wake(&self) -> AsyncWake {
507        self.wake.clone()
508    }
509}
510
511fn task_delivery(entry: TaskEntry, result: Result<UiValue, String>) -> AsyncDelivery {
512    let origin = crate::InvocationOrigin::TaskCompletion {
513        started_by: entry.started_by,
514    };
515    match result {
516        Ok(value) => match validate_async_payload(&entry.output, &value) {
517            Ok(()) => AsyncDelivery {
518                callback: entry.callbacks.success,
519                payload: value,
520                scope: entry.scope,
521                origin,
522            },
523            Err(payload) => AsyncDelivery {
524                callback: entry.callbacks.error,
525                payload,
526                scope: entry.scope,
527                origin,
528            },
529        },
530        Err(error) => AsyncDelivery {
531            callback: entry.callbacks.error,
532            payload: error_payload(&error),
533            scope: entry.scope,
534            origin,
535        },
536    }
537}
538
539const DEFAULT_SUBSCRIPTION_CAPACITY: usize = 64;
540const MAX_SUBSCRIPTION_CAPACITY: usize = 4_096;
541const MAX_SUBSCRIPTION_THROTTLE: Duration = Duration::from_mins(1);
542
543#[derive(Clone, Copy, Debug, Eq, PartialEq)]
544pub enum SubscriptionDeliveryPolicy {
545    All,
546    Latest,
547}
548
549#[derive(Clone, Copy, Debug, Eq, PartialEq)]
550pub struct SubscriptionOptions {
551    delivery: SubscriptionDeliveryPolicy,
552    capacity: usize,
553    throttle: Duration,
554}
555
556impl Default for SubscriptionOptions {
557    fn default() -> Self {
558        Self {
559            delivery: SubscriptionDeliveryPolicy::All,
560            capacity: DEFAULT_SUBSCRIPTION_CAPACITY,
561            throttle: Duration::ZERO,
562        }
563    }
564}
565
566impl SubscriptionOptions {
567    /// Build an explicit bounded subscription delivery contract.
568    ///
569    /// # Errors
570    ///
571    /// Returns for a capacity outside 1..=4096 or a throttle above 60 seconds.
572    pub fn new(
573        delivery: SubscriptionDeliveryPolicy,
574        capacity: usize,
575        throttle: Duration,
576    ) -> Result<Self, AsyncRuntimeError> {
577        if !(1..=MAX_SUBSCRIPTION_CAPACITY).contains(&capacity) {
578            return Err(AsyncRuntimeError::InvalidCapacity(capacity));
579        }
580        if throttle > MAX_SUBSCRIPTION_THROTTLE {
581            return Err(AsyncRuntimeError::InvalidThrottle(throttle));
582        }
583        Ok(Self {
584            delivery,
585            capacity,
586            throttle,
587        })
588    }
589
590    #[must_use]
591    pub const fn delivery(self) -> SubscriptionDeliveryPolicy {
592        self.delivery
593    }
594
595    #[must_use]
596    pub const fn capacity(self) -> usize {
597        self.capacity
598    }
599
600    #[must_use]
601    pub const fn throttle(self) -> Duration {
602        self.throttle
603    }
604}
605
606#[derive(Debug)]
607struct SubscriptionBuffer {
608    values: VecDeque<Result<UiValue, String>>,
609    delivery: SubscriptionDeliveryPolicy,
610    capacity: usize,
611}
612
613#[derive(Debug)]
614struct SubscriptionQueue {
615    buffer: Mutex<SubscriptionBuffer>,
616    space: Condvar,
617    #[cfg(test)]
618    blocked_producers: AtomicUsize,
619}
620
621impl SubscriptionBuffer {
622    fn push(&mut self, result: Result<UiValue, String>) -> Result<(), AsyncRuntimeError> {
623        match self.delivery {
624            SubscriptionDeliveryPolicy::All if self.values.len() >= self.capacity => {
625                Err(AsyncRuntimeError::Backpressure {
626                    capacity: self.capacity,
627                })
628            }
629            SubscriptionDeliveryPolicy::All => {
630                self.values.push_back(result);
631                Ok(())
632            }
633            SubscriptionDeliveryPolicy::Latest => {
634                if let Some(latest) = self.values.back_mut() {
635                    *latest = result;
636                } else {
637                    self.values.push_back(result);
638                }
639                Ok(())
640            }
641        }
642    }
643}
644
645#[derive(Clone)]
646pub struct SubscriptionEmitter {
647    pending: Arc<SubscriptionQueue>,
648    lifetime: Arc<SubscriptionLifetime>,
649    wake: AsyncWake,
650}
651
652impl SubscriptionEmitter {
653    /// Emit one value from any Rust worker thread.
654    ///
655    /// # Errors
656    ///
657    /// Returns [`AsyncRuntimeError::Closed`] after teardown, or
658    /// [`AsyncRuntimeError::Backpressure`] when a lossless stream's bounded
659    /// pending queue is full. Latest-only streams explicitly replace their
660    /// pending value instead.
661    pub fn emit(&self, value: UiValue) -> Result<(), AsyncRuntimeError> {
662        self.send(Ok(value))
663    }
664
665    /// Emit one lossless value, waiting for foreground capacity or cancellation.
666    /// Latest-only streams never block because they explicitly replace a value.
667    ///
668    /// # Errors
669    ///
670    /// Returns when the stream closes or its shared queue is poisoned.
671    pub fn emit_blocking(&self, value: UiValue) -> Result<(), AsyncRuntimeError> {
672        self.send_blocking(Ok(value))
673    }
674
675    /// Emit a structured error to the Rhai error callback.
676    ///
677    /// # Errors
678    ///
679    /// Returns [`AsyncRuntimeError::Closed`] after the producer work returns,
680    /// explicit cancellation, scope teardown, generation replacement, or
681    /// registry drop.
682    pub fn emit_error(&self, message: impl Into<String>) -> Result<(), AsyncRuntimeError> {
683        self.send(Err(message.into()))
684    }
685
686    fn send(&self, result: Result<UiValue, String>) -> Result<(), AsyncRuntimeError> {
687        let mut pending = self
688            .pending
689            .buffer
690            .lock()
691            .map_err(|_| AsyncRuntimeError::Poisoned)?;
692        if let Some(reason) = self.lifetime.close_reason() {
693            return Err(AsyncRuntimeError::Closed { reason });
694        }
695        pending.push(result)?;
696        drop(pending);
697        self.wake.notify();
698        Ok(())
699    }
700
701    fn send_blocking(&self, result: Result<UiValue, String>) -> Result<(), AsyncRuntimeError> {
702        let mut result = Some(result);
703        let mut pending = self
704            .pending
705            .buffer
706            .lock()
707            .map_err(|_| AsyncRuntimeError::Poisoned)?;
708        loop {
709            if let Some(reason) = self.lifetime.close_reason() {
710                return Err(AsyncRuntimeError::Closed { reason });
711            }
712            if pending.delivery == SubscriptionDeliveryPolicy::All
713                && pending.values.len() >= pending.capacity
714            {
715                #[cfg(test)]
716                self.pending
717                    .blocked_producers
718                    .fetch_add(1, Ordering::AcqRel);
719                let waited = self.pending.space.wait(pending);
720                #[cfg(test)]
721                self.pending
722                    .blocked_producers
723                    .fetch_sub(1, Ordering::AcqRel);
724                pending = waited.map_err(|_| AsyncRuntimeError::Poisoned)?;
725                continue;
726            }
727            pending.push(result.take().expect("subscription value is sent once"))?;
728            drop(pending);
729            self.wake.notify();
730            return Ok(());
731        }
732    }
733
734    /// Close the stream explicitly from the producer side.
735    pub fn close(&self) {
736        self.close_with_reason(SubscriptionCloseReason::ProducerClosed);
737    }
738
739    /// Return the first reason that closed this emitter, if any.
740    #[must_use]
741    pub fn close_reason(&self) -> Option<SubscriptionCloseReason> {
742        self.lifetime.close_reason()
743    }
744
745    pub(crate) fn close_with_reason(&self, reason: SubscriptionCloseReason) {
746        if close_subscription_queue(&self.pending, &self.lifetime, reason, false) == reason {
747            self.wake.notify();
748        }
749    }
750}
751
752#[derive(Clone, Debug)]
753struct SubscriptionEntry {
754    label: String,
755    scope: AsyncScope,
756    generation: ScriptGeneration,
757    callbacks: CallbackPair,
758    output: ValueSchema,
759    lifetime: Arc<SubscriptionLifetime>,
760    pending: Arc<SubscriptionQueue>,
761    delivery: SubscriptionDeliveryPolicy,
762    throttle: Duration,
763    last_delivery: Option<Instant>,
764}
765
766#[derive(Clone, Debug, Eq, PartialEq)]
767pub(crate) struct SubscriptionClosure {
768    pub label: String,
769    pub scope: AsyncScope,
770    pub reason: SubscriptionCloseReason,
771}
772
773pub struct SubscriptionRegistration {
774    label: String,
775    scope: AsyncScope,
776    generation: ScriptGeneration,
777    success: ScriptCallback,
778    error: ScriptCallback,
779    output: ValueSchema,
780    delivery: SubscriptionDeliveryPolicy,
781    capacity: usize,
782    throttle: Duration,
783}
784
785impl SubscriptionRegistration {
786    #[must_use]
787    pub fn new(
788        label: impl Into<String>,
789        scope: AsyncScope,
790        generation: ScriptGeneration,
791        success: ScriptCallback,
792        error: ScriptCallback,
793        output: ValueSchema,
794    ) -> Self {
795        Self {
796            label: label.into(),
797            scope,
798            generation,
799            success,
800            error,
801            output,
802            delivery: SubscriptionDeliveryPolicy::All,
803            capacity: DEFAULT_SUBSCRIPTION_CAPACITY,
804            throttle: Duration::ZERO,
805        }
806    }
807
808    /// Set the minimum interval between foreground delivery batches.
809    ///
810    /// Lossless streams retain every value. Latest-only streams coalesce values
811    /// received during the interval.
812    #[must_use]
813    pub const fn with_throttle(mut self, throttle: Duration) -> Self {
814        self.throttle = throttle;
815        self
816    }
817
818    /// Select lossless ordered or explicit latest-only delivery.
819    #[must_use]
820    pub const fn with_delivery_policy(mut self, delivery: SubscriptionDeliveryPolicy) -> Self {
821        self.delivery = delivery;
822        self
823    }
824
825    #[must_use]
826    pub const fn with_options(mut self, options: SubscriptionOptions) -> Self {
827        self.delivery = options.delivery;
828        self.capacity = options.capacity;
829        self.throttle = options.throttle;
830        self
831    }
832
833    /// Bound pending values for lossless delivery.
834    ///
835    /// # Errors
836    ///
837    /// Returns [`AsyncRuntimeError::InvalidCapacity`] outside 1..=4096.
838    pub fn with_capacity(mut self, capacity: usize) -> Result<Self, AsyncRuntimeError> {
839        if !(1..=MAX_SUBSCRIPTION_CAPACITY).contains(&capacity) {
840            return Err(AsyncRuntimeError::InvalidCapacity(capacity));
841        }
842        self.capacity = capacity;
843        Ok(self)
844    }
845}
846
847pub struct SubscriptionRegistry {
848    next_id: u64,
849    entries: BTreeMap<u64, SubscriptionEntry>,
850    closures: Vec<SubscriptionClosure>,
851    deferred_closures: BTreeMap<u64, (SubscriptionEntry, SubscriptionCloseReason)>,
852    transaction_depth: usize,
853    wake: AsyncWake,
854}
855
856impl Default for SubscriptionRegistry {
857    fn default() -> Self {
858        Self {
859            next_id: 1,
860            entries: BTreeMap::new(),
861            closures: Vec::new(),
862            deferred_closures: BTreeMap::new(),
863            transaction_depth: 0,
864            wake: AsyncWake::default(),
865        }
866    }
867}
868
869#[derive(Clone, Debug)]
870pub(crate) struct SubscriptionRegistrySnapshot {
871    entries: BTreeMap<u64, SubscriptionEntry>,
872    deferred_closures: BTreeMap<u64, (SubscriptionEntry, SubscriptionCloseReason)>,
873    transaction_depth: usize,
874}
875
876impl fmt::Debug for SubscriptionRegistry {
877    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
878        formatter
879            .debug_struct("SubscriptionRegistry")
880            .field("active", &self.entries.keys().collect::<Vec<_>>())
881            .finish_non_exhaustive()
882    }
883}
884
885impl SubscriptionRegistry {
886    #[must_use]
887    pub fn new() -> Self {
888        Self::default()
889    }
890
891    pub(crate) fn begin_transaction(&mut self) {
892        self.transaction_depth = self.transaction_depth.saturating_add(1);
893    }
894
895    pub(crate) fn commit_transaction(&mut self) {
896        self.transaction_depth = self.transaction_depth.saturating_sub(1);
897        if self.transaction_depth == 0 {
898            self.finalize_deferred_closures();
899        }
900    }
901
902    pub(crate) fn snapshot(&self) -> SubscriptionRegistrySnapshot {
903        SubscriptionRegistrySnapshot {
904            entries: self.entries.clone(),
905            deferred_closures: self.deferred_closures.clone(),
906            transaction_depth: self.transaction_depth,
907        }
908    }
909
910    pub(crate) fn restore(&mut self, snapshot: SubscriptionRegistrySnapshot) {
911        for (id, entry) in &self.entries {
912            if !snapshot.entries.contains_key(id) && !snapshot.deferred_closures.contains_key(id) {
913                self.closures.push(close_subscription_entry(
914                    entry,
915                    SubscriptionCloseReason::TransactionRolledBack,
916                ));
917            }
918        }
919        for (id, (entry, _)) in &self.deferred_closures {
920            if !snapshot.entries.contains_key(id) && !snapshot.deferred_closures.contains_key(id) {
921                self.closures.push(close_subscription_entry(
922                    entry,
923                    SubscriptionCloseReason::TransactionRolledBack,
924                ));
925            }
926        }
927        self.entries = snapshot.entries;
928        self.deferred_closures = snapshot.deferred_closures;
929        self.transaction_depth = snapshot.transaction_depth;
930    }
931
932    fn defer_or_close(
933        &mut self,
934        id: u64,
935        entry: SubscriptionEntry,
936        reason: SubscriptionCloseReason,
937    ) {
938        if self.transaction_depth > 0 {
939            self.deferred_closures.insert(id, (entry, reason));
940        } else {
941            self.closures.push(close_subscription_entry(&entry, reason));
942        }
943    }
944
945    fn finalize_deferred_closures(&mut self) {
946        let deferred = std::mem::take(&mut self.deferred_closures);
947        self.closures.extend(
948            deferred
949                .into_values()
950                .map(|(entry, reason)| close_subscription_entry(&entry, reason)),
951        );
952    }
953
954    #[must_use]
955    pub fn subscribe(
956        &mut self,
957        registration: SubscriptionRegistration,
958    ) -> (SubscriptionHandle, SubscriptionEmitter) {
959        let id = self.next_id;
960        self.next_id = self.next_id.saturating_add(1);
961        let lifetime = Arc::new(SubscriptionLifetime::new());
962        let pending = Arc::new(SubscriptionQueue {
963            buffer: Mutex::new(SubscriptionBuffer {
964                values: VecDeque::new(),
965                delivery: registration.delivery,
966                capacity: registration.capacity,
967            }),
968            space: Condvar::new(),
969            #[cfg(test)]
970            blocked_producers: AtomicUsize::new(0),
971        });
972        self.entries.insert(
973            id,
974            SubscriptionEntry {
975                label: registration.label,
976                scope: registration.scope,
977                generation: registration.generation,
978                callbacks: CallbackPair {
979                    success: registration.success,
980                    error: registration.error,
981                },
982                output: registration.output,
983                lifetime: Arc::clone(&lifetime),
984                pending: Arc::clone(&pending),
985                delivery: registration.delivery,
986                throttle: registration.throttle,
987                last_delivery: None,
988            },
989        );
990        (
991            SubscriptionHandle(id),
992            SubscriptionEmitter {
993                pending,
994                lifetime,
995                wake: self.wake.clone(),
996            },
997        )
998    }
999
1000    #[must_use]
1001    pub fn cancel(&mut self, handle: SubscriptionHandle) -> bool {
1002        self.cancel_with_reason(handle, SubscriptionCloseReason::Cancelled)
1003    }
1004
1005    /// The scope that owns an open subscription; `None` once it closed.
1006    pub(crate) fn scope(&self, handle: SubscriptionHandle) -> Option<&AsyncScope> {
1007        self.entries.get(&handle.0).map(|entry| &entry.scope)
1008    }
1009
1010    pub(crate) fn cancel_with_reason(
1011        &mut self,
1012        handle: SubscriptionHandle,
1013        reason: SubscriptionCloseReason,
1014    ) -> bool {
1015        let Some(entry) = self.entries.remove(&handle.0) else {
1016            return false;
1017        };
1018        self.defer_or_close(handle.0, entry, reason);
1019        true
1020    }
1021
1022    pub fn cancel_scope(&mut self, scope: &AsyncScope) {
1023        let mut closures = Vec::new();
1024        self.entries.retain(|id, entry| {
1025            if &entry.scope == scope {
1026                closures.push((*id, entry.clone(), SubscriptionCloseReason::ScopeDisposed));
1027                false
1028            } else {
1029                true
1030            }
1031        });
1032        for (id, entry, reason) in closures {
1033            self.defer_or_close(id, entry, reason);
1034        }
1035    }
1036
1037    pub fn cancel_component_scope(&mut self, component: &ComponentInstancePath) {
1038        let mut closures = Vec::new();
1039        self.entries.retain(|id, entry| {
1040            let remove = entry.scope.is_within_component(component);
1041            if remove {
1042                closures.push((*id, entry.clone(), SubscriptionCloseReason::ScopeDisposed));
1043            }
1044            !remove
1045        });
1046        for (id, entry, reason) in closures {
1047            self.defer_or_close(id, entry, reason);
1048        }
1049    }
1050
1051    #[must_use]
1052    pub fn drain(&mut self, current: ScriptGeneration) -> Vec<AsyncDelivery> {
1053        self.drain_up_to(current, usize::MAX)
1054    }
1055
1056    #[must_use]
1057    pub(crate) fn drain_up_to(
1058        &mut self,
1059        current: ScriptGeneration,
1060        limit: usize,
1061    ) -> Vec<AsyncDelivery> {
1062        let now = Instant::now();
1063        let mut closed = BTreeMap::new();
1064        let mut deliveries = Vec::new();
1065        for (id, entry) in &mut self.entries {
1066            if entry.generation != current {
1067                let reason = close_subscription_queue(
1068                    &entry.pending,
1069                    &entry.lifetime,
1070                    SubscriptionCloseReason::GenerationStale,
1071                    true,
1072                );
1073                closed.entry(*id).or_insert(reason);
1074                continue;
1075            }
1076            if let Some(reason) = entry.lifetime.close_reason() {
1077                closed.insert(*id, reason);
1078            }
1079            let ready = closed.contains_key(id)
1080                || entry
1081                    .last_delivery
1082                    .is_none_or(|last| now.duration_since(last) >= entry.throttle);
1083            if ready && deliveries.len() < limit {
1084                let mut pending = entry
1085                    .pending
1086                    .buffer
1087                    .lock()
1088                    .unwrap_or_else(std::sync::PoisonError::into_inner);
1089                let requested = if closed.contains_key(id)
1090                    || entry.delivery == SubscriptionDeliveryPolicy::Latest
1091                    || entry.throttle.is_zero()
1092                {
1093                    pending.values.len()
1094                } else {
1095                    usize::from(!pending.values.is_empty())
1096                };
1097                let take = requested.min(limit.saturating_sub(deliveries.len()));
1098                for result in pending.values.drain(..take) {
1099                    deliveries.push(subscription_delivery(entry, result));
1100                }
1101                if take > 0 {
1102                    entry.pending.space.notify_all();
1103                }
1104                if take > 0 {
1105                    entry.last_delivery = Some(now);
1106                }
1107            }
1108        }
1109        for (id, reason) in closed {
1110            let empty = self.entries.get(&id).is_none_or(|entry| {
1111                entry
1112                    .pending
1113                    .buffer
1114                    .lock()
1115                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1116                    .values
1117                    .is_empty()
1118            });
1119            if empty && let Some(entry) = self.entries.remove(&id) {
1120                self.closures.push(close_subscription_entry(&entry, reason));
1121            }
1122        }
1123        deliveries
1124    }
1125
1126    #[must_use]
1127    pub fn active_count(&self) -> usize {
1128        self.entries.len()
1129    }
1130
1131    pub(crate) fn take_closures(&mut self) -> Vec<SubscriptionClosure> {
1132        if self.transaction_depth == 0 {
1133            self.finalize_deferred_closures();
1134        }
1135        std::mem::take(&mut self.closures)
1136    }
1137
1138    pub(crate) fn wake(&self) -> AsyncWake {
1139        self.wake.clone()
1140    }
1141}
1142
1143impl Drop for SubscriptionRegistry {
1144    fn drop(&mut self) {
1145        for entry in self
1146            .entries
1147            .values()
1148            .chain(self.deferred_closures.values().map(|(entry, _)| entry))
1149        {
1150            close_subscription_queue(
1151                &entry.pending,
1152                &entry.lifetime,
1153                SubscriptionCloseReason::RegistryDropped,
1154                true,
1155            );
1156        }
1157    }
1158}
1159
1160fn close_subscription_queue(
1161    pending: &SubscriptionQueue,
1162    lifetime: &SubscriptionLifetime,
1163    reason: SubscriptionCloseReason,
1164    discard: bool,
1165) -> SubscriptionCloseReason {
1166    // The close predicate and Condvar wait are synchronized by the same mutex.
1167    // This prevents a producer from observing an open stream, missing the close
1168    // notification, and then sleeping forever on a full queue.
1169    let mut buffer = pending
1170        .buffer
1171        .lock()
1172        .unwrap_or_else(std::sync::PoisonError::into_inner);
1173    lifetime.close(reason);
1174    if discard {
1175        buffer.values.clear();
1176    }
1177    let reason = lifetime.close_reason().unwrap_or(reason);
1178    drop(buffer);
1179    pending.space.notify_all();
1180    reason
1181}
1182
1183fn close_subscription_entry(
1184    entry: &SubscriptionEntry,
1185    reason: SubscriptionCloseReason,
1186) -> SubscriptionClosure {
1187    let reason = close_subscription_queue(&entry.pending, &entry.lifetime, reason, true);
1188    SubscriptionClosure {
1189        label: entry.label.clone(),
1190        scope: entry.scope.clone(),
1191        reason,
1192    }
1193}
1194
1195fn subscription_delivery(
1196    entry: &SubscriptionEntry,
1197    result: Result<UiValue, String>,
1198) -> AsyncDelivery {
1199    match result {
1200        Ok(value) => match validate_async_payload(&entry.output, &value) {
1201            Ok(()) => AsyncDelivery {
1202                callback: entry.callbacks.success.clone(),
1203                payload: value,
1204                scope: entry.scope.clone(),
1205                origin: crate::InvocationOrigin::Subscription,
1206            },
1207            Err(payload) => AsyncDelivery {
1208                callback: entry.callbacks.error.clone(),
1209                payload,
1210                scope: entry.scope.clone(),
1211                origin: crate::InvocationOrigin::Subscription,
1212            },
1213        },
1214        Err(error) => AsyncDelivery {
1215            callback: entry.callbacks.error.clone(),
1216            payload: error_payload(&error),
1217            scope: entry.scope.clone(),
1218            origin: crate::InvocationOrigin::Subscription,
1219        },
1220    }
1221}
1222
1223fn error_payload(message: &str) -> UiValue {
1224    let message = bounded_utf8(message, 1_024);
1225    UiValue::Map(BTreeMap::from([
1226        ("kind".to_owned(), UiValue::String("async_error".to_owned())),
1227        ("message".to_owned(), UiValue::String(message)),
1228    ]))
1229}
1230
1231fn validate_async_payload(output: &ValueSchema, value: &UiValue) -> Result<(), UiValue> {
1232    validate_rhai_delivery(value).map_err(delivery_limit_payload)?;
1233    output
1234        .validate_ui_value_first(value)
1235        .map_err(|error| error_payload(&error.to_string()))
1236}
1237
1238fn delivery_limit_payload(error: DeliveryLimit) -> UiValue {
1239    UiValue::Map(BTreeMap::from([
1240        (
1241            "kind".to_owned(),
1242            UiValue::String("async_delivery_limit".to_owned()),
1243        ),
1244        (
1245            "resource".to_owned(),
1246            UiValue::String(error.resource.to_owned()),
1247        ),
1248        (
1249            "actual".to_owned(),
1250            UiValue::Integer(i64::try_from(error.actual).unwrap_or(i64::MAX)),
1251        ),
1252        (
1253            "limit".to_owned(),
1254            UiValue::Integer(i64::try_from(error.limit).unwrap_or(i64::MAX)),
1255        ),
1256        ("message".to_owned(), UiValue::String(error.message())),
1257    ]))
1258}
1259
1260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1261struct DeliveryLimit {
1262    resource: &'static str,
1263    actual: usize,
1264    limit: usize,
1265}
1266
1267impl DeliveryLimit {
1268    fn message(self) -> String {
1269        format!(
1270            "async result exceeds Rhai {} limit: {} > {}",
1271            self.resource, self.actual, self.limit
1272        )
1273    }
1274}
1275
1276#[derive(Default)]
1277struct DeliveryUsage {
1278    strings: usize,
1279    arrays: usize,
1280    maps: usize,
1281}
1282
1283fn validate_rhai_delivery(value: &UiValue) -> Result<(), DeliveryLimit> {
1284    fn visit(
1285        value: &UiValue,
1286        depth: usize,
1287        usage: &mut DeliveryUsage,
1288    ) -> Result<(), DeliveryLimit> {
1289        if depth > crate::engine::RHAI_MAX_DATA_DEPTH {
1290            return Err(DeliveryLimit {
1291                resource: "data_depth",
1292                actual: depth,
1293                limit: crate::engine::RHAI_MAX_DATA_DEPTH,
1294            });
1295        }
1296        match value {
1297            UiValue::String(value) => {
1298                usage.strings = usage.strings.saturating_add(value.len());
1299            }
1300            UiValue::Array(values) => {
1301                usage.arrays = usage.arrays.saturating_add(values.len());
1302                for value in values {
1303                    visit(value, depth.saturating_add(1), usage)?;
1304                }
1305            }
1306            UiValue::Map(values) => {
1307                usage.maps = usage.maps.saturating_add(values.len());
1308                for value in values.values() {
1309                    visit(value, depth.saturating_add(1), usage)?;
1310                }
1311            }
1312            UiValue::Null
1313            | UiValue::Bool(_)
1314            | UiValue::Integer(_)
1315            | UiValue::Float(_)
1316            | UiValue::Handle(_) => {}
1317        }
1318        for (resource, actual, limit) in [
1319            (
1320                "string_bytes",
1321                usage.strings,
1322                crate::engine::RHAI_MAX_STRING_SIZE,
1323            ),
1324            (
1325                "array_items",
1326                usage.arrays,
1327                crate::engine::RHAI_MAX_ARRAY_SIZE,
1328            ),
1329            ("map_entries", usage.maps, crate::engine::RHAI_MAX_MAP_SIZE),
1330        ] {
1331            if actual > limit {
1332                return Err(DeliveryLimit {
1333                    resource,
1334                    actual,
1335                    limit,
1336                });
1337            }
1338        }
1339        Ok(())
1340    }
1341
1342    visit(value, 0, &mut DeliveryUsage::default())
1343}
1344
1345fn bounded_utf8(value: &str, maximum: usize) -> String {
1346    if value.len() <= maximum {
1347        return value.to_owned();
1348    }
1349    let mut end = maximum;
1350    while !value.is_char_boundary(end) {
1351        end = end.saturating_sub(1);
1352    }
1353    format!("{}…", &value[..end])
1354}
1355
1356#[derive(Debug, Error)]
1357pub enum AsyncRuntimeError {
1358    #[error("failed to spawn async worker: {0}")]
1359    Spawn(std::io::Error),
1360    #[error("background worker pool is unavailable")]
1361    WorkerPoolUnavailable,
1362    #[error("background worker queue is full")]
1363    WorkerQueueFull,
1364    #[error("subscription is closed: {reason}")]
1365    Closed { reason: SubscriptionCloseReason },
1366    #[error("async output is invalid: {0}")]
1367    InvalidOutput(#[from] SchemaValidationError),
1368    #[error("subscription pending queue reached its capacity of {capacity}")]
1369    Backpressure { capacity: usize },
1370    #[error("subscription capacity must be between 1 and {MAX_SUBSCRIPTION_CAPACITY}, got {0}")]
1371    InvalidCapacity(usize),
1372    #[error("subscription throttle must not exceed 60 seconds, got {0:?}")]
1373    InvalidThrottle(Duration),
1374    #[error("subscription pending queue is poisoned")]
1375    Poisoned,
1376    #[error("suspended view delivery queue reached its capacity of {capacity}")]
1377    SuspendedBackpressure { capacity: usize },
1378}
1379
1380#[cfg(test)]
1381mod tests {
1382    use super::*;
1383    use crate::RuntimeEngine;
1384
1385    #[test]
1386    fn rhai_delivery_preflight_matches_recursive_engine_limits() {
1387        assert!(
1388            validate_rhai_delivery(&UiValue::String(
1389                "x".repeat(crate::engine::RHAI_MAX_STRING_SIZE)
1390            ))
1391            .is_ok()
1392        );
1393        let oversized = UiValue::Array(vec![
1394            UiValue::String("x".repeat(600_000)),
1395            UiValue::String("y".repeat(600_000)),
1396        ]);
1397        assert_eq!(
1398            validate_rhai_delivery(&oversized).unwrap_err().resource,
1399            "string_bytes"
1400        );
1401        let oversized = UiValue::Array(
1402            (0..=crate::engine::RHAI_MAX_ARRAY_SIZE)
1403                .map(|_| UiValue::Null)
1404                .collect(),
1405        );
1406        assert_eq!(
1407            validate_rhai_delivery(&oversized).unwrap_err().resource,
1408            "array_items"
1409        );
1410    }
1411
1412    #[test]
1413    fn async_error_payload_is_always_bounded_utf8() {
1414        let UiValue::Map(payload) = error_payload(&"界".repeat(100_000)) else {
1415            panic!("error payload must be an object");
1416        };
1417        let Some(UiValue::String(message)) = payload.get("message") else {
1418            panic!("error payload must contain a message");
1419        };
1420        assert!(message.len() <= 1_028);
1421        assert!(validate_rhai_delivery(&UiValue::Map(payload)).is_ok());
1422    }
1423
1424    fn callbacks() -> (ScriptCallback, ScriptCallback, ScriptGeneration) {
1425        let mut runtime = RuntimeEngine::new();
1426        let compiled = runtime
1427            .compile(
1428                r#"
1429                    fn view() { text("async") }
1430                    fn success(ctx, value) { value }
1431                    fn failure(ctx, error) { error }
1432                "#,
1433            )
1434            .unwrap();
1435        runtime.render(&compiled).unwrap();
1436        (
1437            runtime.callback(&compiled, "success").unwrap(),
1438            runtime.callback(&compiled, "failure").unwrap(),
1439            compiled.generation(),
1440        )
1441    }
1442
1443    #[test]
1444    fn task_completion_delivers_on_foreground_drain() {
1445        let (success, error, generation) = callbacks();
1446        let mut tasks = TaskRegistry::new();
1447        tasks
1448            .spawn(
1449                AsyncScope::App,
1450                generation,
1451                success.clone(),
1452                error,
1453                ValueSchema::string(),
1454                || Ok(UiValue::String("done".to_owned())),
1455            )
1456            .unwrap();
1457        let deadline = Instant::now() + Duration::from_secs(1);
1458        loop {
1459            let deliveries = tasks.drain(generation);
1460            if let Some(delivery) = deliveries.into_iter().next() {
1461                assert_eq!(delivery.callback, success);
1462                assert_eq!(delivery.payload, UiValue::String("done".to_owned()));
1463                break;
1464            }
1465            assert!(Instant::now() < deadline);
1466            std::thread::yield_now();
1467        }
1468    }
1469
1470    #[test]
1471    fn task_panic_becomes_an_error_delivery_and_releases_the_entry() {
1472        let (success, error, generation) = callbacks();
1473        let mut tasks = TaskRegistry::new();
1474        tasks
1475            .spawn(
1476                AsyncScope::App,
1477                generation,
1478                success,
1479                error.clone(),
1480                ValueSchema::Null,
1481                || panic!("task failed"),
1482            )
1483            .unwrap();
1484        let deadline = Instant::now() + Duration::from_secs(1);
1485        loop {
1486            let deliveries = tasks.drain(generation);
1487            if let Some(delivery) = deliveries.into_iter().next() {
1488                assert_eq!(delivery.callback, error);
1489                assert_eq!(tasks.active_count(), 0);
1490                break;
1491            }
1492            assert!(Instant::now() < deadline);
1493            std::thread::yield_now();
1494        }
1495    }
1496
1497    #[test]
1498    fn canceled_and_stale_tasks_never_deliver() {
1499        let (success, error, generation) = callbacks();
1500        let mut tasks = TaskRegistry::new();
1501        let handle = tasks
1502            .spawn(
1503                AsyncScope::App,
1504                generation,
1505                success.clone(),
1506                error.clone(),
1507                ValueSchema::Null,
1508                || Ok(UiValue::Null),
1509            )
1510            .unwrap();
1511        assert!(tasks.cancel(handle));
1512        std::thread::sleep(Duration::from_millis(10));
1513        assert!(tasks.drain(generation).is_empty());
1514
1515        tasks
1516            .spawn(
1517                AsyncScope::App,
1518                generation,
1519                success,
1520                error,
1521                ValueSchema::Null,
1522                || Ok(UiValue::Null),
1523            )
1524            .unwrap();
1525        std::thread::sleep(Duration::from_millis(10));
1526        assert!(tasks.drain(generation.next()).is_empty());
1527    }
1528
1529    #[test]
1530    fn task_cancellation_is_reversible_until_transaction_commit() {
1531        let (success, error, generation) = callbacks();
1532        let (release, wait) = std::sync::mpsc::channel();
1533        let mut tasks = TaskRegistry::new();
1534        let handle = tasks
1535            .spawn_cancellable(
1536                AsyncScope::App,
1537                generation,
1538                success,
1539                error,
1540                ValueSchema::integer(),
1541                move |cancellation| {
1542                    wait.recv().unwrap();
1543                    assert!(!cancellation.is_cancelled());
1544                    Ok(UiValue::Integer(9))
1545                },
1546            )
1547            .unwrap();
1548        let snapshot = tasks.snapshot();
1549        tasks.begin_transaction();
1550        assert!(tasks.cancel(handle));
1551        tasks.restore(snapshot);
1552        assert_eq!(tasks.active_count(), 1);
1553        release.send(()).unwrap();
1554        let deadline = Instant::now() + Duration::from_secs(1);
1555        loop {
1556            let deliveries = tasks.drain(generation);
1557            if !deliveries.is_empty() {
1558                assert_eq!(deliveries[0].payload, UiValue::Integer(9));
1559                break;
1560            }
1561            assert!(Instant::now() < deadline);
1562            std::thread::yield_now();
1563        }
1564    }
1565
1566    #[test]
1567    fn window_and_component_scope_cancellation_preserves_app_tasks() {
1568        let (success, error, generation) = callbacks();
1569        let root = ComponentInstancePath::root("App", "settings");
1570        let mut tasks = TaskRegistry::new();
1571        for scope in [
1572            AsyncScope::App,
1573            AsyncScope::Window("settings".to_owned()),
1574            AsyncScope::Component(root.child("Panel", "root")),
1575            AsyncScope::Effect {
1576                component: root.child("Panel", "effect"),
1577                key: "watch".to_owned(),
1578                activation: 1,
1579            },
1580        ] {
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(&AsyncScope::Window("settings".to_owned()));
1593        tasks.cancel_component_scope(&root);
1594        assert_eq!(tasks.active_count(), 1);
1595    }
1596
1597    #[test]
1598    fn exact_effect_activation_cancellation_preserves_replacement_work() {
1599        let (success, error, generation) = callbacks();
1600        let component = ComponentInstancePath::root("App", "main").child("Probe", "primary");
1601        let old = AsyncScope::Effect {
1602            component: component.clone(),
1603            key: "watch".to_owned(),
1604            activation: 1,
1605        };
1606        let replacement = AsyncScope::Effect {
1607            component,
1608            key: "watch".to_owned(),
1609            activation: 2,
1610        };
1611        let mut tasks = TaskRegistry::new();
1612        for scope in [old.clone(), replacement.clone()] {
1613            tasks
1614                .spawn(
1615                    scope,
1616                    generation,
1617                    success.clone(),
1618                    error.clone(),
1619                    ValueSchema::Null,
1620                    || Ok(UiValue::Null),
1621                )
1622                .unwrap();
1623        }
1624        tasks.cancel_scope(&old);
1625        assert_eq!(tasks.active_count(), 1);
1626
1627        let mut subscriptions = SubscriptionRegistry::new();
1628        for scope in [old.clone(), replacement] {
1629            let _ = subscriptions.subscribe(SubscriptionRegistration::new(
1630                "app.stream.watch",
1631                scope,
1632                generation,
1633                success.clone(),
1634                error.clone(),
1635                ValueSchema::Null,
1636            ));
1637        }
1638        subscriptions.cancel_scope(&old);
1639        assert_eq!(subscriptions.active_count(), 1);
1640        assert_eq!(subscriptions.take_closures().len(), 1);
1641    }
1642
1643    #[test]
1644    fn subscription_throttles_to_latest_value_and_cancels() {
1645        let (success, error, generation) = callbacks();
1646        let mut subscriptions = SubscriptionRegistry::new();
1647        let registration = SubscriptionRegistration::new(
1648            "app.stream.watch",
1649            AsyncScope::Window("main".to_owned()),
1650            generation,
1651            success,
1652            error,
1653            ValueSchema::integer(),
1654        )
1655        .with_delivery_policy(SubscriptionDeliveryPolicy::Latest)
1656        .with_throttle(Duration::from_millis(50));
1657        let (handle, emitter) = subscriptions.subscribe(registration);
1658        emitter.emit(UiValue::Integer(1)).unwrap();
1659        let first = subscriptions.drain(generation);
1660        assert_eq!(first[0].payload, UiValue::Integer(1));
1661        emitter.emit(UiValue::Integer(2)).unwrap();
1662        emitter.emit(UiValue::Integer(3)).unwrap();
1663        assert!(subscriptions.drain(generation).is_empty());
1664        std::thread::sleep(Duration::from_millis(60));
1665        let latest = subscriptions.drain(generation);
1666        assert_eq!(latest[0].payload, UiValue::Integer(3));
1667        assert!(subscriptions.cancel(handle));
1668        assert!(matches!(
1669            emitter.emit(UiValue::Integer(4)),
1670            Err(AsyncRuntimeError::Closed {
1671                reason: SubscriptionCloseReason::Cancelled
1672            })
1673        ));
1674        assert_eq!(
1675            subscriptions.take_closures(),
1676            vec![SubscriptionClosure {
1677                label: "app.stream.watch".to_owned(),
1678                scope: AsyncScope::Window("main".to_owned()),
1679                reason: SubscriptionCloseReason::Cancelled,
1680            }]
1681        );
1682    }
1683
1684    #[test]
1685    fn subscription_defaults_to_bounded_ordered_delivery() {
1686        let (success, error, generation) = callbacks();
1687        let registration = SubscriptionRegistration::new(
1688            "app.stream.events",
1689            AsyncScope::App,
1690            generation,
1691            success,
1692            error,
1693            ValueSchema::integer(),
1694        )
1695        .with_capacity(2)
1696        .unwrap();
1697        let mut subscriptions = SubscriptionRegistry::new();
1698        let (_, emitter) = subscriptions.subscribe(registration);
1699        emitter.emit(UiValue::Integer(1)).unwrap();
1700        emitter.emit(UiValue::Integer(2)).unwrap();
1701        assert!(matches!(
1702            emitter.emit(UiValue::Integer(3)),
1703            Err(AsyncRuntimeError::Backpressure { capacity: 2 })
1704        ));
1705        assert_eq!(
1706            subscriptions
1707                .drain(generation)
1708                .into_iter()
1709                .map(|delivery| delivery.payload)
1710                .collect::<Vec<_>>(),
1711            vec![UiValue::Integer(1), UiValue::Integer(2)]
1712        );
1713        emitter.emit(UiValue::Integer(3)).unwrap();
1714        assert_eq!(
1715            subscriptions.drain(generation)[0].payload,
1716            UiValue::Integer(3)
1717        );
1718    }
1719
1720    #[test]
1721    fn stale_generation_discards_buffer_and_reclaims_subscription() {
1722        let (success, error, generation) = callbacks();
1723        let mut subscriptions = SubscriptionRegistry::new();
1724        let (_, emitter) = subscriptions.subscribe(
1725            SubscriptionRegistration::new(
1726                "app.stream.stale",
1727                AsyncScope::App,
1728                generation,
1729                success,
1730                error,
1731                ValueSchema::integer(),
1732            )
1733            .with_capacity(1)
1734            .unwrap(),
1735        );
1736        emitter.emit(UiValue::Integer(1)).unwrap();
1737        let pending = Arc::clone(&emitter.pending);
1738        let blocked = emitter.clone();
1739        let (done_tx, done_rx) = std::sync::mpsc::channel();
1740        let producer = std::thread::spawn(move || {
1741            done_tx
1742                .send(blocked.emit_blocking(UiValue::Integer(2)))
1743                .unwrap();
1744        });
1745        let deadline = Instant::now() + Duration::from_secs(1);
1746        while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1747            assert!(
1748                Instant::now() < deadline,
1749                "producer did not reach the capacity wait"
1750            );
1751            std::thread::yield_now();
1752        }
1753
1754        let current = generation.next();
1755        assert!(subscriptions.drain_up_to(current, 0).is_empty());
1756        assert!(matches!(
1757            done_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1758            Err(AsyncRuntimeError::Closed {
1759                reason: SubscriptionCloseReason::GenerationStale
1760            })
1761        ));
1762        producer.join().unwrap();
1763        assert_eq!(subscriptions.active_count(), 0);
1764        assert_eq!(
1765            subscriptions.take_closures()[0].reason,
1766            SubscriptionCloseReason::GenerationStale
1767        );
1768        assert!(matches!(
1769            emitter.emit(UiValue::Integer(2)),
1770            Err(AsyncRuntimeError::Closed {
1771                reason: SubscriptionCloseReason::GenerationStale
1772            })
1773        ));
1774    }
1775
1776    #[test]
1777    fn cancellation_wakes_a_blocked_lossless_producer() {
1778        let (success, error, generation) = callbacks();
1779        let mut subscriptions = SubscriptionRegistry::new();
1780        let (handle, emitter) = subscriptions.subscribe(
1781            SubscriptionRegistration::new(
1782                "app.stream.blocked",
1783                AsyncScope::App,
1784                generation,
1785                success,
1786                error,
1787                ValueSchema::integer(),
1788            )
1789            .with_capacity(1)
1790            .unwrap(),
1791        );
1792        emitter.emit(UiValue::Integer(1)).unwrap();
1793        let pending = Arc::clone(&emitter.pending);
1794        let (started_tx, started_rx) = std::sync::mpsc::channel();
1795        let (done_tx, done_rx) = std::sync::mpsc::channel();
1796        let producer = std::thread::spawn(move || {
1797            started_tx.send(()).unwrap();
1798            done_tx
1799                .send(emitter.emit_blocking(UiValue::Integer(2)))
1800                .unwrap();
1801        });
1802        started_rx.recv().unwrap();
1803        let deadline = Instant::now() + Duration::from_secs(1);
1804        while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1805            assert!(
1806                Instant::now() < deadline,
1807                "producer did not reach the capacity wait"
1808            );
1809            std::thread::yield_now();
1810        }
1811
1812        assert!(subscriptions.cancel(handle));
1813        let result = done_rx.recv_timeout(Duration::from_secs(1));
1814        if result.is_err() {
1815            // Keep a failing regression from stranding the test process.
1816            pending.space.notify_all();
1817        }
1818        let result = result.expect("cancellation must wake the blocked producer");
1819        assert!(matches!(
1820            result,
1821            Err(AsyncRuntimeError::Closed {
1822                reason: SubscriptionCloseReason::Cancelled
1823            })
1824        ));
1825        producer.join().unwrap();
1826    }
1827
1828    #[test]
1829    fn producer_close_and_registry_drop_report_first_close_reason() {
1830        let (success, error, generation) = callbacks();
1831        let mut subscriptions = SubscriptionRegistry::new();
1832        let registration = SubscriptionRegistration::new(
1833            "app.stream.watch",
1834            AsyncScope::App,
1835            generation,
1836            success.clone(),
1837            error.clone(),
1838            ValueSchema::integer(),
1839        );
1840        let (_, emitter) = subscriptions.subscribe(registration);
1841        emitter.emit(UiValue::Integer(1)).unwrap();
1842        emitter.close();
1843        assert_eq!(
1844            subscriptions.drain_up_to(generation, 1)[0].payload,
1845            UiValue::Integer(1)
1846        );
1847        assert!(matches!(
1848            emitter.emit(UiValue::Integer(1)),
1849            Err(AsyncRuntimeError::Closed {
1850                reason: SubscriptionCloseReason::ProducerClosed
1851            })
1852        ));
1853        assert_eq!(
1854            subscriptions.take_closures()[0].reason,
1855            SubscriptionCloseReason::ProducerClosed
1856        );
1857
1858        let orphan = {
1859            let mut registry = SubscriptionRegistry::new();
1860            let registration = SubscriptionRegistration::new(
1861                "app.stream.orphan",
1862                AsyncScope::App,
1863                generation,
1864                success,
1865                error,
1866                ValueSchema::integer(),
1867            );
1868            let (_, emitter) = registry.subscribe(registration);
1869            emitter
1870        };
1871        assert!(matches!(
1872            orphan.emit(UiValue::Integer(2)),
1873            Err(AsyncRuntimeError::Closed {
1874                reason: SubscriptionCloseReason::RegistryDropped
1875            })
1876        ));
1877    }
1878
1879    #[test]
1880    fn online_schema_rejection_formats_only_the_first_issue() {
1881        let value = UiValue::Map(
1882            (0..100_000)
1883                .map(|index| (format!("field-{index:06}"), UiValue::Null))
1884                .collect(),
1885        );
1886        let error = validate_async_payload(
1887            &ValueSchema::Map {
1888                values: Box::new(ValueSchema::integer()),
1889            },
1890            &value,
1891        )
1892        .unwrap_err();
1893        let UiValue::Map(error) = error else {
1894            panic!("async schema error must be structured");
1895        };
1896        let Some(UiValue::String(message)) = error.get("message") else {
1897            panic!("async schema error must contain a message");
1898        };
1899        assert!(message.contains("$.field-000000"), "{message}");
1900        assert!(!message.contains("field-000001"), "{message}");
1901    }
1902}