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#[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 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 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 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 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 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 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 pub fn emit(&self, value: UiValue) -> Result<(), AsyncRuntimeError> {
662 self.send(Ok(value))
663 }
664
665 pub fn emit_blocking(&self, value: UiValue) -> Result<(), AsyncRuntimeError> {
672 self.send_blocking(Ok(value))
673 }
674
675 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 pub fn close(&self) {
736 self.close_with_reason(SubscriptionCloseReason::ProducerClosed);
737 }
738
739 #[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 #[must_use]
813 pub const fn with_throttle(mut self, throttle: Duration) -> Self {
814 self.throttle = throttle;
815 self
816 }
817
818 #[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 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 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 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 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}