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 entry.output.validate_ui_value(&value) {
496 Ok(()) => AsyncDelivery {
497 callback: entry.callbacks.success,
498 payload: value,
499 scope: entry.scope,
500 },
501 Err(error) => AsyncDelivery {
502 callback: entry.callbacks.error,
503 payload: error_payload(error.to_string()),
504 scope: entry.scope,
505 },
506 },
507 Err(error) => AsyncDelivery {
508 callback: entry.callbacks.error,
509 payload: error_payload(error),
510 scope: entry.scope,
511 },
512 }
513}
514
515const DEFAULT_SUBSCRIPTION_CAPACITY: usize = 64;
516const MAX_SUBSCRIPTION_CAPACITY: usize = 4_096;
517const MAX_SUBSCRIPTION_THROTTLE: Duration = Duration::from_secs(60);
518
519#[derive(Clone, Copy, Debug, Eq, PartialEq)]
520pub enum SubscriptionDeliveryPolicy {
521 All,
522 Latest,
523}
524
525#[derive(Clone, Copy, Debug, Eq, PartialEq)]
526pub struct SubscriptionOptions {
527 delivery: SubscriptionDeliveryPolicy,
528 capacity: usize,
529 throttle: Duration,
530}
531
532impl Default for SubscriptionOptions {
533 fn default() -> Self {
534 Self {
535 delivery: SubscriptionDeliveryPolicy::All,
536 capacity: DEFAULT_SUBSCRIPTION_CAPACITY,
537 throttle: Duration::ZERO,
538 }
539 }
540}
541
542impl SubscriptionOptions {
543 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 entry.output.validate_ui_value(&value) {
1172 Ok(()) => AsyncDelivery {
1173 callback: entry.callbacks.success.clone(),
1174 payload: value,
1175 scope: entry.scope.clone(),
1176 },
1177 Err(error) => AsyncDelivery {
1178 callback: entry.callbacks.error.clone(),
1179 payload: error_payload(error.to_string()),
1180 scope: entry.scope.clone(),
1181 },
1182 },
1183 Err(error) => AsyncDelivery {
1184 callback: entry.callbacks.error.clone(),
1185 payload: error_payload(error),
1186 scope: entry.scope.clone(),
1187 },
1188 }
1189}
1190
1191fn error_payload(message: String) -> UiValue {
1192 UiValue::Map(BTreeMap::from([
1193 ("kind".to_owned(), UiValue::String("async_error".to_owned())),
1194 ("message".to_owned(), UiValue::String(message)),
1195 ]))
1196}
1197
1198#[derive(Debug, Error)]
1199pub enum AsyncRuntimeError {
1200 #[error("failed to spawn async worker: {0}")]
1201 Spawn(std::io::Error),
1202 #[error("background worker pool is unavailable")]
1203 WorkerPoolUnavailable,
1204 #[error("background worker queue is full")]
1205 WorkerQueueFull,
1206 #[error("subscription is closed: {reason}")]
1207 Closed { reason: SubscriptionCloseReason },
1208 #[error("async output is invalid: {0}")]
1209 InvalidOutput(#[from] SchemaValidationError),
1210 #[error("subscription pending queue reached its capacity of {capacity}")]
1211 Backpressure { capacity: usize },
1212 #[error("subscription capacity must be between 1 and {MAX_SUBSCRIPTION_CAPACITY}, got {0}")]
1213 InvalidCapacity(usize),
1214 #[error("subscription throttle must not exceed 60 seconds, got {0:?}")]
1215 InvalidThrottle(Duration),
1216 #[error("subscription pending queue is poisoned")]
1217 Poisoned,
1218 #[error("suspended view delivery queue reached its capacity of {capacity}")]
1219 SuspendedBackpressure { capacity: usize },
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224 use super::*;
1225 use crate::RuntimeEngine;
1226
1227 fn callbacks() -> (ScriptCallback, ScriptCallback, ScriptGeneration) {
1228 let mut runtime = RuntimeEngine::new();
1229 let compiled = runtime
1230 .compile(
1231 r#"
1232 fn view() { text("async") }
1233 fn success(ctx, value) { value }
1234 fn failure(ctx, error) { error }
1235 "#,
1236 )
1237 .unwrap();
1238 runtime.render(&compiled).unwrap();
1239 (
1240 runtime.callback(&compiled, "success").unwrap(),
1241 runtime.callback(&compiled, "failure").unwrap(),
1242 compiled.generation(),
1243 )
1244 }
1245
1246 #[test]
1247 fn task_completion_delivers_on_foreground_drain() {
1248 let (success, error, generation) = callbacks();
1249 let mut tasks = TaskRegistry::new();
1250 tasks
1251 .spawn(
1252 AsyncScope::App,
1253 generation,
1254 success.clone(),
1255 error,
1256 ValueSchema::string(),
1257 || Ok(UiValue::String("done".to_owned())),
1258 )
1259 .unwrap();
1260 let deadline = Instant::now() + Duration::from_secs(1);
1261 loop {
1262 let deliveries = tasks.drain(generation);
1263 if let Some(delivery) = deliveries.into_iter().next() {
1264 assert_eq!(delivery.callback, success);
1265 assert_eq!(delivery.payload, UiValue::String("done".to_owned()));
1266 break;
1267 }
1268 assert!(Instant::now() < deadline);
1269 std::thread::yield_now();
1270 }
1271 }
1272
1273 #[test]
1274 fn task_panic_becomes_an_error_delivery_and_releases_the_entry() {
1275 let (success, error, generation) = callbacks();
1276 let mut tasks = TaskRegistry::new();
1277 tasks
1278 .spawn(
1279 AsyncScope::App,
1280 generation,
1281 success,
1282 error.clone(),
1283 ValueSchema::Null,
1284 || panic!("task failed"),
1285 )
1286 .unwrap();
1287 let deadline = Instant::now() + Duration::from_secs(1);
1288 loop {
1289 let deliveries = tasks.drain(generation);
1290 if let Some(delivery) = deliveries.into_iter().next() {
1291 assert_eq!(delivery.callback, error);
1292 assert_eq!(tasks.active_count(), 0);
1293 break;
1294 }
1295 assert!(Instant::now() < deadline);
1296 std::thread::yield_now();
1297 }
1298 }
1299
1300 #[test]
1301 fn canceled_and_stale_tasks_never_deliver() {
1302 let (success, error, generation) = callbacks();
1303 let mut tasks = TaskRegistry::new();
1304 let handle = tasks
1305 .spawn(
1306 AsyncScope::App,
1307 generation,
1308 success.clone(),
1309 error.clone(),
1310 ValueSchema::Null,
1311 || Ok(UiValue::Null),
1312 )
1313 .unwrap();
1314 assert!(tasks.cancel(handle));
1315 std::thread::sleep(Duration::from_millis(10));
1316 assert!(tasks.drain(generation).is_empty());
1317
1318 tasks
1319 .spawn(
1320 AsyncScope::App,
1321 generation,
1322 success,
1323 error,
1324 ValueSchema::Null,
1325 || Ok(UiValue::Null),
1326 )
1327 .unwrap();
1328 std::thread::sleep(Duration::from_millis(10));
1329 assert!(tasks.drain(generation.next()).is_empty());
1330 }
1331
1332 #[test]
1333 fn task_cancellation_is_reversible_until_transaction_commit() {
1334 let (success, error, generation) = callbacks();
1335 let (release, wait) = std::sync::mpsc::channel();
1336 let mut tasks = TaskRegistry::new();
1337 let handle = tasks
1338 .spawn_cancellable(
1339 AsyncScope::App,
1340 generation,
1341 success,
1342 error,
1343 ValueSchema::integer(),
1344 move |cancellation| {
1345 wait.recv().unwrap();
1346 assert!(!cancellation.is_cancelled());
1347 Ok(UiValue::Integer(9))
1348 },
1349 )
1350 .unwrap();
1351 let snapshot = tasks.snapshot();
1352 tasks.begin_transaction();
1353 assert!(tasks.cancel(handle));
1354 tasks.restore(snapshot);
1355 assert_eq!(tasks.active_count(), 1);
1356 release.send(()).unwrap();
1357 let deadline = Instant::now() + Duration::from_secs(1);
1358 loop {
1359 let deliveries = tasks.drain(generation);
1360 if !deliveries.is_empty() {
1361 assert_eq!(deliveries[0].payload, UiValue::Integer(9));
1362 break;
1363 }
1364 assert!(Instant::now() < deadline);
1365 std::thread::yield_now();
1366 }
1367 }
1368
1369 #[test]
1370 fn window_and_component_scope_cancellation_preserves_app_tasks() {
1371 let (success, error, generation) = callbacks();
1372 let root = ComponentInstancePath::root("App", "settings");
1373 let mut tasks = TaskRegistry::new();
1374 for scope in [
1375 AsyncScope::App,
1376 AsyncScope::Window("settings".to_owned()),
1377 AsyncScope::Component(root.child("Panel", "root")),
1378 AsyncScope::Effect {
1379 component: root.child("Panel", "effect"),
1380 key: "watch".to_owned(),
1381 activation: 1,
1382 },
1383 ] {
1384 tasks
1385 .spawn(
1386 scope,
1387 generation,
1388 success.clone(),
1389 error.clone(),
1390 ValueSchema::Null,
1391 || Ok(UiValue::Null),
1392 )
1393 .unwrap();
1394 }
1395 tasks.cancel_scope(&AsyncScope::Window("settings".to_owned()));
1396 tasks.cancel_component_scope(&root);
1397 assert_eq!(tasks.active_count(), 1);
1398 }
1399
1400 #[test]
1401 fn exact_effect_activation_cancellation_preserves_replacement_work() {
1402 let (success, error, generation) = callbacks();
1403 let component = ComponentInstancePath::root("App", "main").child("Probe", "primary");
1404 let old = AsyncScope::Effect {
1405 component: component.clone(),
1406 key: "watch".to_owned(),
1407 activation: 1,
1408 };
1409 let replacement = AsyncScope::Effect {
1410 component,
1411 key: "watch".to_owned(),
1412 activation: 2,
1413 };
1414 let mut tasks = TaskRegistry::new();
1415 for scope in [old.clone(), replacement.clone()] {
1416 tasks
1417 .spawn(
1418 scope,
1419 generation,
1420 success.clone(),
1421 error.clone(),
1422 ValueSchema::Null,
1423 || Ok(UiValue::Null),
1424 )
1425 .unwrap();
1426 }
1427 tasks.cancel_scope(&old);
1428 assert_eq!(tasks.active_count(), 1);
1429
1430 let mut subscriptions = SubscriptionRegistry::new();
1431 for scope in [old.clone(), replacement] {
1432 let _ = subscriptions.subscribe(SubscriptionRegistration::new(
1433 "app.stream.watch",
1434 scope,
1435 generation,
1436 success.clone(),
1437 error.clone(),
1438 ValueSchema::Null,
1439 ));
1440 }
1441 subscriptions.cancel_scope(&old);
1442 assert_eq!(subscriptions.active_count(), 1);
1443 assert_eq!(subscriptions.take_closures().len(), 1);
1444 }
1445
1446 #[test]
1447 fn subscription_throttles_to_latest_value_and_cancels() {
1448 let (success, error, generation) = callbacks();
1449 let mut subscriptions = SubscriptionRegistry::new();
1450 let registration = SubscriptionRegistration::new(
1451 "app.stream.watch",
1452 AsyncScope::Window("main".to_owned()),
1453 generation,
1454 success,
1455 error,
1456 ValueSchema::integer(),
1457 )
1458 .with_delivery_policy(SubscriptionDeliveryPolicy::Latest)
1459 .with_throttle(Duration::from_millis(50));
1460 let (handle, emitter) = subscriptions.subscribe(registration);
1461 emitter.emit(UiValue::Integer(1)).unwrap();
1462 let first = subscriptions.drain(generation);
1463 assert_eq!(first[0].payload, UiValue::Integer(1));
1464 emitter.emit(UiValue::Integer(2)).unwrap();
1465 emitter.emit(UiValue::Integer(3)).unwrap();
1466 assert!(subscriptions.drain(generation).is_empty());
1467 std::thread::sleep(Duration::from_millis(60));
1468 let latest = subscriptions.drain(generation);
1469 assert_eq!(latest[0].payload, UiValue::Integer(3));
1470 assert!(subscriptions.cancel(handle));
1471 assert!(matches!(
1472 emitter.emit(UiValue::Integer(4)),
1473 Err(AsyncRuntimeError::Closed {
1474 reason: SubscriptionCloseReason::Cancelled
1475 })
1476 ));
1477 assert_eq!(
1478 subscriptions.take_closures(),
1479 vec![SubscriptionClosure {
1480 label: "app.stream.watch".to_owned(),
1481 scope: AsyncScope::Window("main".to_owned()),
1482 reason: SubscriptionCloseReason::Cancelled,
1483 }]
1484 );
1485 }
1486
1487 #[test]
1488 fn subscription_defaults_to_bounded_ordered_delivery() {
1489 let (success, error, generation) = callbacks();
1490 let registration = SubscriptionRegistration::new(
1491 "app.stream.events",
1492 AsyncScope::App,
1493 generation,
1494 success,
1495 error,
1496 ValueSchema::integer(),
1497 )
1498 .with_capacity(2)
1499 .unwrap();
1500 let mut subscriptions = SubscriptionRegistry::new();
1501 let (_, emitter) = subscriptions.subscribe(registration);
1502 emitter.emit(UiValue::Integer(1)).unwrap();
1503 emitter.emit(UiValue::Integer(2)).unwrap();
1504 assert!(matches!(
1505 emitter.emit(UiValue::Integer(3)),
1506 Err(AsyncRuntimeError::Backpressure { capacity: 2 })
1507 ));
1508 assert_eq!(
1509 subscriptions
1510 .drain(generation)
1511 .into_iter()
1512 .map(|delivery| delivery.payload)
1513 .collect::<Vec<_>>(),
1514 vec![UiValue::Integer(1), UiValue::Integer(2)]
1515 );
1516 emitter.emit(UiValue::Integer(3)).unwrap();
1517 assert_eq!(
1518 subscriptions.drain(generation)[0].payload,
1519 UiValue::Integer(3)
1520 );
1521 }
1522
1523 #[test]
1524 fn stale_generation_discards_buffer_and_reclaims_subscription() {
1525 let (success, error, generation) = callbacks();
1526 let mut subscriptions = SubscriptionRegistry::new();
1527 let (_, emitter) = subscriptions.subscribe(
1528 SubscriptionRegistration::new(
1529 "app.stream.stale",
1530 AsyncScope::App,
1531 generation,
1532 success,
1533 error,
1534 ValueSchema::integer(),
1535 )
1536 .with_capacity(1)
1537 .unwrap(),
1538 );
1539 emitter.emit(UiValue::Integer(1)).unwrap();
1540 let pending = Arc::clone(&emitter.pending);
1541 let blocked = emitter.clone();
1542 let (done_tx, done_rx) = std::sync::mpsc::channel();
1543 let producer = std::thread::spawn(move || {
1544 done_tx
1545 .send(blocked.emit_blocking(UiValue::Integer(2)))
1546 .unwrap();
1547 });
1548 let deadline = Instant::now() + Duration::from_secs(1);
1549 while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1550 assert!(
1551 Instant::now() < deadline,
1552 "producer did not reach the capacity wait"
1553 );
1554 std::thread::yield_now();
1555 }
1556
1557 let current = generation.next();
1558 assert!(subscriptions.drain_up_to(current, 0).is_empty());
1559 assert!(matches!(
1560 done_rx.recv_timeout(Duration::from_secs(1)).unwrap(),
1561 Err(AsyncRuntimeError::Closed {
1562 reason: SubscriptionCloseReason::GenerationStale
1563 })
1564 ));
1565 producer.join().unwrap();
1566 assert_eq!(subscriptions.active_count(), 0);
1567 assert_eq!(
1568 subscriptions.take_closures()[0].reason,
1569 SubscriptionCloseReason::GenerationStale
1570 );
1571 assert!(matches!(
1572 emitter.emit(UiValue::Integer(2)),
1573 Err(AsyncRuntimeError::Closed {
1574 reason: SubscriptionCloseReason::GenerationStale
1575 })
1576 ));
1577 }
1578
1579 #[test]
1580 fn cancellation_wakes_a_blocked_lossless_producer() {
1581 let (success, error, generation) = callbacks();
1582 let mut subscriptions = SubscriptionRegistry::new();
1583 let (handle, emitter) = subscriptions.subscribe(
1584 SubscriptionRegistration::new(
1585 "app.stream.blocked",
1586 AsyncScope::App,
1587 generation,
1588 success,
1589 error,
1590 ValueSchema::integer(),
1591 )
1592 .with_capacity(1)
1593 .unwrap(),
1594 );
1595 emitter.emit(UiValue::Integer(1)).unwrap();
1596 let pending = Arc::clone(&emitter.pending);
1597 let (started_tx, started_rx) = std::sync::mpsc::channel();
1598 let (done_tx, done_rx) = std::sync::mpsc::channel();
1599 let producer = std::thread::spawn(move || {
1600 started_tx.send(()).unwrap();
1601 done_tx
1602 .send(emitter.emit_blocking(UiValue::Integer(2)))
1603 .unwrap();
1604 });
1605 started_rx.recv().unwrap();
1606 let deadline = Instant::now() + Duration::from_secs(1);
1607 while pending.blocked_producers.load(Ordering::Acquire) == 0 {
1608 assert!(
1609 Instant::now() < deadline,
1610 "producer did not reach the capacity wait"
1611 );
1612 std::thread::yield_now();
1613 }
1614
1615 assert!(subscriptions.cancel(handle));
1616 let result = done_rx.recv_timeout(Duration::from_secs(1));
1617 if result.is_err() {
1618 pending.space.notify_all();
1620 }
1621 let result = result.expect("cancellation must wake the blocked producer");
1622 assert!(matches!(
1623 result,
1624 Err(AsyncRuntimeError::Closed {
1625 reason: SubscriptionCloseReason::Cancelled
1626 })
1627 ));
1628 producer.join().unwrap();
1629 }
1630
1631 #[test]
1632 fn producer_close_and_registry_drop_report_first_close_reason() {
1633 let (success, error, generation) = callbacks();
1634 let mut subscriptions = SubscriptionRegistry::new();
1635 let registration = SubscriptionRegistration::new(
1636 "app.stream.watch",
1637 AsyncScope::App,
1638 generation,
1639 success.clone(),
1640 error.clone(),
1641 ValueSchema::integer(),
1642 );
1643 let (_, emitter) = subscriptions.subscribe(registration);
1644 emitter.emit(UiValue::Integer(1)).unwrap();
1645 emitter.close();
1646 assert_eq!(
1647 subscriptions.drain_up_to(generation, 1)[0].payload,
1648 UiValue::Integer(1)
1649 );
1650 assert!(matches!(
1651 emitter.emit(UiValue::Integer(1)),
1652 Err(AsyncRuntimeError::Closed {
1653 reason: SubscriptionCloseReason::ProducerClosed
1654 })
1655 ));
1656 assert_eq!(
1657 subscriptions.take_closures()[0].reason,
1658 SubscriptionCloseReason::ProducerClosed
1659 );
1660
1661 let orphan = {
1662 let mut registry = SubscriptionRegistry::new();
1663 let registration = SubscriptionRegistration::new(
1664 "app.stream.orphan",
1665 AsyncScope::App,
1666 generation,
1667 success,
1668 error,
1669 ValueSchema::integer(),
1670 );
1671 let (_, emitter) = registry.subscribe(registration);
1672 emitter
1673 };
1674 assert!(matches!(
1675 orphan.emit(UiValue::Integer(2)),
1676 Err(AsyncRuntimeError::Closed {
1677 reason: SubscriptionCloseReason::RegistryDropped
1678 })
1679 ));
1680 }
1681}