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