1use std::{
16 cell::{Cell, RefCell},
17 collections::HashMap,
18 future::Future,
19 pin::Pin,
20 rc::{Rc, Weak},
21};
22
23use incular_widgets::internal::ElementId;
24
25use super::{
26 BuildScope, Dependency, ReactiveNode, ReactiveNodeId, ReactiveQueue, ReactiveRootId, Signal,
27 TaskFailure, TaskHandle, TrackedDependency,
28};
29
30#[derive(Clone)]
31struct ReactiveBinding {
32 owner_element: Option<ElementId>,
33 root: ReactiveRootId,
34 queue: Weak<RefCell<ReactiveQueue>>,
35 spawner: Option<super::tasks::RuntimeSpawner>,
36 owner_scope: Option<super::tasks::TaskScope>,
37}
38
39struct TrackingScope {
40 binding: ReactiveBinding,
41 node: Rc<dyn ReactiveNode>,
42}
43
44thread_local! {
45 static TRACKING_STACK: RefCell<Vec<TrackingScope>> = const { RefCell::new(Vec::new()) };
46}
47
48fn binding_from_build_scope(scope: &BuildScope) -> ReactiveBinding {
49 ReactiveBinding {
50 owner_element: scope.element,
51 root: scope.root,
52 queue: scope.queue.clone(),
53 spawner: Some(scope.spawner.clone()),
54 owner_scope: Some(scope.owner_scope.clone()),
55 }
56}
57
58fn current_binding() -> Option<ReactiveBinding> {
59 TRACKING_STACK
60 .with(|stack| stack.borrow().last().map(|scope| scope.binding.clone()))
61 .or_else(|| {
62 super::BUILD_SCOPE.with(|scope| scope.borrow().as_ref().map(binding_from_build_scope))
63 })
64}
65
66fn current_element() -> Option<ElementId> {
67 super::BUILD_SCOPE.with(|scope| scope.borrow().as_ref().and_then(|scope| scope.element))
68}
69
70fn current_node() -> Option<ReactiveNodeId> {
71 TRACKING_STACK.with(|stack| stack.borrow().last().map(|scope| scope.node.id()))
72}
73
74pub(super) fn track_dependency(dependency: Rc<dyn Dependency>) {
78 let tracking = TRACKING_STACK.with(|stack| {
79 stack.borrow().last().map(|scope| {
80 (
81 scope.binding.root,
82 scope.binding.queue.clone(),
83 scope.node.clone(),
84 )
85 })
86 });
87 if let Some((root, queue, node)) = tracking {
88 let id = node.id();
89 let tracked = subscribe_node(dependency, root, id, Rc::downgrade(&node), queue);
90 node.record_dependency(tracked);
91 return;
92 }
93
94 super::BUILD_SCOPE.with(|scope| {
95 let current = scope.borrow();
96 let Some(scope) = current.as_ref() else {
97 dependency.source().track();
98 return;
99 };
100 if let Some(element) = scope.element {
101 let tracked = subscribe_element(dependency, scope.root, element, scope.queue.clone());
102 if let Some(queue) = scope.queue.upgrade() {
103 queue.borrow_mut().record(element, tracked);
104 }
105 } else if let Some(initial_dependencies) = &scope.initial_dependencies {
106 initial_dependencies.borrow_mut().record_signal(dependency);
107 }
108 });
109}
110
111struct TrackingGuard;
112
113impl Drop for TrackingGuard {
114 fn drop(&mut self) {
115 TRACKING_STACK.with(|stack| {
116 stack.borrow_mut().pop();
117 });
118 }
119}
120
121fn with_tracking<R>(
122 binding: ReactiveBinding,
123 node: Rc<dyn ReactiveNode>,
124 compute: impl FnOnce() -> R,
125) -> R {
126 TRACKING_STACK.with(|stack| {
127 stack.borrow_mut().push(TrackingScope { binding, node });
128 });
129 let guard = TrackingGuard;
130 let _sources = incular_core::reactivity::TrackingGuard::enter(track_source);
131 let result = compute();
132 drop(guard);
133 result
134}
135
136fn detach_sources(sources: &RefCell<Vec<(ReactiveRootId, TrackedDependency)>>) {
137 sources.borrow_mut().clear();
138}
139fn detach_sources_for(
140 root: ReactiveRootId,
141 sources: &RefCell<HashMap<ReactiveRootId, Vec<TrackedDependency>>>,
142) {
143 sources.borrow_mut().remove(&root);
144}
145
146struct SourceDependency(incular_core::reactivity::DependencySource);
147impl Dependency for SourceDependency {
148 fn source(&self) -> &incular_core::reactivity::DependencySource {
149 &self.0
150 }
151}
152pub(super) fn track_source(source: incular_core::reactivity::DependencySource) {
153 track_dependency(Rc::new(SourceDependency(source)));
154}
155
156pub(super) fn subscribe_element(
157 dependency: Rc<dyn Dependency>,
158 root: ReactiveRootId,
159 element: ElementId,
160 queue: Weak<RefCell<ReactiveQueue>>,
161) -> TrackedDependency {
162 if let Some(queue) = queue.upgrade() {
163 dependency.prepare_subscription(&queue);
164 }
165 #[cfg(feature = "devtools")]
166 let weak = Rc::downgrade(&dependency);
167 let source = dependency.source().clone();
168 #[cfg(feature = "devtools")]
169 let callback_source = weak.clone();
170 let is_signal = dependency.is_signal();
171 let skips_current = dependency.skips_current_consumer();
172 let subscription = source.subscribe((root, element), move || {
173 if skips_current
174 && current_element() == Some(element)
175 && current_binding().is_some_and(|binding| binding.root == root)
176 {
177 return;
178 }
179 if let Some(queue) = queue.upgrade() {
180 let mut queue = queue.borrow_mut();
181 let newly_queued = queue.enqueue(element);
182 if is_signal && newly_queued {
183 super::scheduler_counters::DEPENDENTS_ENQUEUED
184 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
185 }
186 #[cfg(feature = "devtools")]
187 {
188 if super::environment::DEV_TASK_COMPLETION.with(Cell::get) {
189 queue.note_cause(
190 element,
191 incular_widgets::internal::InvalidationCause::TaskCompletion,
192 );
193 }
194 if let Some(cause) = callback_source
195 .upgrade()
196 .and_then(|dependency| dependency.cause())
197 {
198 queue.note_cause(element, cause);
199 }
200 }
201 }
202 });
203 TrackedDependency {
204 source_id: dependency.source().id(),
205 source: dependency.source().clone(),
206 dependency: Rc::downgrade(&dependency),
207 _subscription: subscription,
208 }
209}
210
211fn subscribe_node(
212 dependency: Rc<dyn Dependency>,
213 root: ReactiveRootId,
214 id: ReactiveNodeId,
215 node: Weak<dyn ReactiveNode>,
216 queue: Weak<RefCell<ReactiveQueue>>,
217) -> TrackedDependency {
218 if let Some(queue) = queue.upgrade() {
219 dependency.prepare_subscription(&queue);
220 queue.borrow_mut().register_node(id, node.clone());
221 }
222 let skips_current = dependency.skips_current_consumer();
223 let subscription = dependency.source().subscribe((root, id), move || {
224 if skips_current && current_node() == Some(id) {
225 return;
226 }
227 if let (Some(queue), Some(live)) = (queue.upgrade(), node.upgrade()) {
228 live.mark_dirty();
229 queue.borrow_mut().enqueue_node(id, node.clone());
230 }
231 });
232 TrackedDependency {
233 source_id: dependency.source().id(),
234 source: dependency.source().clone(),
235 dependency: Rc::downgrade(&dependency),
236 _subscription: subscription,
237 }
238}
239
240struct BoolGuard<'a>(&'a Cell<bool>);
241
242impl Drop for BoolGuard<'_> {
243 fn drop(&mut self) {
244 self.0.set(false);
245 }
246}
247
248struct MemoInner<T> {
249 id: ReactiveNodeId,
250 value: RefCell<Option<T>>,
251 compute: RefCell<Box<dyn FnMut() -> T>>,
252 dirty: Cell<bool>,
253 evaluating: Cell<bool>,
254 runtime: RefCell<HashMap<ReactiveRootId, ReactiveBinding>>,
255 sources: RefCell<HashMap<ReactiveRootId, Vec<TrackedDependency>>>,
256 self_ref: RefCell<Weak<MemoInner<T>>>,
257 source: incular_core::reactivity::DependencySource,
258}
259
260impl<T> MemoInner<T>
261where
262 T: PartialEq + 'static,
263{
264 fn new(compute: impl FnMut() -> T + 'static) -> Rc<Self> {
265 let inner = Rc::new(Self {
266 id: super::NEXT_REACTIVE_NODE
267 .fetch_update(
268 std::sync::atomic::Ordering::Relaxed,
269 std::sync::atomic::Ordering::Relaxed,
270 |id| id.checked_add(1),
271 )
272 .expect("reactive node identity space exhausted"),
273 value: RefCell::new(None),
274 compute: RefCell::new(Box::new(compute)),
275 dirty: Cell::new(true),
276 evaluating: Cell::new(false),
277 runtime: RefCell::new(HashMap::new()),
278 sources: RefCell::new(HashMap::new()),
279 self_ref: RefCell::new(Weak::new()),
280 source: incular_core::reactivity::DependencySource::default(),
281 });
282 *inner.self_ref.borrow_mut() = Rc::downgrade(&inner);
283 inner
284 }
285
286 fn ensure_fresh(self: &Rc<Self>) {
287 let current = current_binding();
288 let bound = self.runtime.borrow();
289 let needs_tracking = current
290 .as_ref()
291 .is_some_and(|binding| !bound.contains_key(&binding.root));
292 drop(bound);
293 let unbound = self.runtime.borrow().is_empty() && current.is_none();
294 if (unbound || self.value.borrow().is_none() || self.dirty.get() || needs_tracking)
295 && self.recompute(current)
296 {
297 self.notify_dependents();
298 }
299 }
300
301 fn recompute(self: &Rc<Self>, current: Option<ReactiveBinding>) -> bool {
302 assert!(
303 !self.evaluating.replace(true),
304 "reactive memo dependency cycle"
305 );
306 let _evaluating = BoolGuard(&self.evaluating);
307 let _read_only = incular_core::reactivity::ReadOnlyGuard::enter();
308 let binding = current.or_else(|| self.runtime.borrow().values().next().cloned());
309 if let Some(binding) = &binding {
310 detach_sources_for(binding.root, &self.sources);
311 }
312 let value = if let Some(binding) = binding.clone() {
313 let node: Rc<dyn ReactiveNode> = self.clone();
314 with_tracking(binding, node, || (self.compute.borrow_mut())())
315 } else {
316 (self.compute.borrow_mut())()
317 };
318 let changed = {
319 let mut previous = self.value.borrow_mut();
320 let changed = match previous.as_ref() {
321 Some(previous) => previous != &value,
322 None => true,
323 };
324 *previous = Some(value);
325 changed
326 };
327 self.dirty.set(false);
328 if let Some(binding) = binding {
329 self.runtime
330 .borrow_mut()
331 .insert(binding.root, binding.clone());
332 let dependencies = self
335 .sources
336 .borrow()
337 .get(&binding.root)
338 .map(|sources| {
339 sources
340 .iter()
341 .map(|tracked| {
342 tracked.dependency.upgrade().unwrap_or_else(|| {
343 Rc::new(SourceDependency(tracked.source.clone()))
344 })
345 })
346 .collect::<Vec<_>>()
347 })
348 .unwrap_or_default();
349 let others = self
350 .runtime
351 .borrow()
352 .values()
353 .filter(|other| other.root != binding.root)
354 .cloned()
355 .collect::<Vec<_>>();
356 for other in others {
357 let node: Rc<dyn ReactiveNode> = self.clone();
358 let subscriptions = dependencies
359 .iter()
360 .map(|dependency| {
361 subscribe_node(
362 dependency.clone(),
363 other.root,
364 self.id,
365 Rc::downgrade(&node),
366 other.queue.clone(),
367 )
368 })
369 .collect();
370 self.sources.borrow_mut().insert(other.root, subscriptions);
371 }
372 }
373 changed
374 }
375
376 fn notify_dependents(&self) {
377 self.source.notify();
378 }
379
380 fn register_node(&self, queue: &Rc<RefCell<ReactiveQueue>>) {
381 let Some(this) = self.self_ref.borrow().upgrade() else {
382 return;
383 };
384 let node: Rc<dyn ReactiveNode> = this;
385 queue
386 .borrow_mut()
387 .register_node(self.id, Rc::downgrade(&node));
388 }
389
390 fn dispose_root(&self, root: ReactiveRootId) {
391 detach_sources_for(root, &self.sources);
392 self.runtime.borrow_mut().remove(&root);
393 if self.runtime.borrow().is_empty() {
394 self.dirty.set(true);
395 }
396 }
397
398 fn record_source(&self, dependency: TrackedDependency) {
399 let root = current_binding().map(|binding| binding.root);
400 let Some(root) = root.or_else(|| self.runtime.borrow().keys().next().copied()) else {
401 return;
402 };
403 let mut sources = self.sources.borrow_mut();
404 let entries = sources.entry(root).or_default();
405 if !entries
406 .iter()
407 .any(|source| source.source_id == dependency.source_id)
408 {
409 entries.push(dependency);
410 }
411 }
412}
413
414impl<T> ReactiveNode for MemoInner<T>
415where
416 T: PartialEq + 'static,
417{
418 fn id(&self) -> ReactiveNodeId {
419 self.id
420 }
421
422 fn mark_dirty(&self) {
423 self.dirty.set(true);
424 }
425
426 fn run(&self, root: ReactiveRootId) {
427 if !self.dirty.get() {
428 return;
429 }
430 let Some(this) = self.self_ref.borrow().upgrade() else {
431 return;
432 };
433 let binding = self.runtime.borrow().get(&root).cloned();
434 if this.recompute(binding) {
435 this.notify_dependents();
436 }
437 }
438
439 fn record_dependency(&self, dependency: TrackedDependency) {
440 self.record_source(dependency);
441 }
442
443 fn dispose(&self, root: ReactiveRootId) {
444 self.dispose_root(root);
445 }
446}
447
448impl<T: PartialEq + 'static> Dependency for MemoInner<T> {
449 fn skips_current_consumer(&self) -> bool {
450 true
451 }
452 fn source(&self) -> &incular_core::reactivity::DependencySource {
453 &self.source
454 }
455 fn prepare_subscription(&self, queue: &Rc<RefCell<ReactiveQueue>>) {
456 self.register_node(queue);
457 }
458}
459
460#[derive(Clone)]
463pub struct Memo<T> {
464 inner: Rc<MemoInner<T>>,
465}
466
467impl<T> Memo<T>
468where
469 T: PartialEq + 'static,
470{
471 #[must_use]
475 pub fn new(compute: impl Fn() -> T + 'static) -> Self {
476 Self {
477 inner: MemoInner::new(compute),
478 }
479 }
480}
481
482impl<T> Memo<T>
483where
484 T: Clone + PartialEq + 'static,
485{
486 pub fn get(&self) -> T {
489 track_dependency(self.inner.clone());
490 self.inner.ensure_fresh();
491 self.inner
492 .value
493 .borrow()
494 .as_ref()
495 .expect("memo value initialized after evaluation")
496 .clone()
497 }
498
499 pub fn get_untracked(&self) -> T {
502 self.inner.ensure_fresh();
503 self.inner
504 .value
505 .borrow()
506 .as_ref()
507 .expect("memo value initialized after evaluation")
508 .clone()
509 }
510
511 pub fn with<R>(&self, read: impl FnOnce(&T) -> R) -> R {
513 track_dependency(self.inner.clone());
514 self.inner.ensure_fresh();
515 let value = self.inner.value.borrow();
516 read(
517 value
518 .as_ref()
519 .expect("memo value initialized after evaluation"),
520 )
521 }
522
523 pub fn with_untracked<R>(&self, read: impl FnOnce(&T) -> R) -> R {
525 self.inner.ensure_fresh();
526 let value = self.inner.value.borrow();
527 read(
528 value
529 .as_ref()
530 .expect("memo value initialized after evaluation"),
531 )
532 }
533
534 #[must_use]
535 pub fn dependent_count(&self) -> usize {
536 self.inner.source.subscriber_count()
537 }
538}
539
540struct EffectInner {
541 id: ReactiveNodeId,
542 callback: RefCell<Box<dyn FnMut()>>,
543 dirty: Cell<bool>,
544 running: Cell<bool>,
545 mounted: Cell<bool>,
546 runtime: RefCell<Option<ReactiveBinding>>,
547 sources: RefCell<Vec<(ReactiveRootId, TrackedDependency)>>,
548 self_ref: RefCell<Weak<EffectInner>>,
549}
550
551impl EffectInner {
552 fn new(callback: impl FnMut() + 'static) -> Rc<Self> {
553 let inner = Rc::new(Self {
554 id: super::NEXT_REACTIVE_NODE
555 .fetch_update(
556 std::sync::atomic::Ordering::Relaxed,
557 std::sync::atomic::Ordering::Relaxed,
558 |id| id.checked_add(1),
559 )
560 .expect("reactive node identity space exhausted"),
561 callback: RefCell::new(Box::new(callback)),
562 dirty: Cell::new(false),
563 running: Cell::new(false),
564 mounted: Cell::new(false),
565 runtime: RefCell::new(None),
566 sources: RefCell::new(Vec::new()),
567 self_ref: RefCell::new(Weak::new()),
568 });
569 *inner.self_ref.borrow_mut() = Rc::downgrade(&inner);
570 inner
571 }
572
573 fn mount(&self, binding: ReactiveBinding) -> bool {
574 let same_root = self
575 .runtime
576 .borrow()
577 .as_ref()
578 .is_some_and(|current| current.root == binding.root);
579 if self.mounted.get() && same_root {
580 return true;
581 }
582 if self.mounted.get() {
583 detach_sources(&self.sources);
584 }
585 self.mounted.set(true);
586 self.dirty.set(true);
587 *self.runtime.borrow_mut() = Some(binding.clone());
588 let Some(queue) = binding.queue.upgrade() else {
589 self.mounted.set(false);
590 self.dirty.set(false);
591 *self.runtime.borrow_mut() = None;
592 return false;
593 };
594 let node: Rc<dyn ReactiveNode> = self
595 .self_ref
596 .borrow()
597 .upgrade()
598 .expect("mounted effect still alive");
599 queue
600 .borrow_mut()
601 .enqueue_node(self.id, Rc::downgrade(&node));
602 true
603 }
604
605 fn run(&self) {
606 if !self.mounted.get() || !self.dirty.get() || self.running.replace(true) {
607 return;
608 }
609 let _running = BoolGuard(&self.running);
610 self.dirty.set(false);
611 let Some(binding) = self.runtime.borrow().clone() else {
612 return;
613 };
614 detach_sources(&self.sources);
615 let node: Rc<dyn ReactiveNode> = self
616 .self_ref
617 .borrow()
618 .upgrade()
619 .expect("running effect still alive");
620 with_tracking(binding, node, || (self.callback.borrow_mut())());
621 }
622
623 fn record_source(&self, dependency: TrackedDependency) {
624 let Some(root) = self.runtime.borrow().as_ref().map(|binding| binding.root) else {
625 return;
626 };
627 let mut sources = self.sources.borrow_mut();
628 if !sources.iter().any(|(source_root, source)| {
629 *source_root == root && source.source_id == dependency.source_id
630 }) {
631 sources.push((root, dependency));
632 }
633 }
634}
635
636impl ReactiveNode for EffectInner {
637 fn owns_element(&self, root: ReactiveRootId, element: ElementId) -> bool {
638 self.runtime
639 .borrow()
640 .as_ref()
641 .is_some_and(|binding| binding.root == root && binding.owner_element == Some(element))
642 }
643 fn id(&self) -> ReactiveNodeId {
644 self.id
645 }
646
647 fn mark_dirty(&self) {
648 self.dirty.set(true);
649 }
650
651 fn run(&self, _root: ReactiveRootId) {
652 Self::run(self);
653 }
654
655 fn record_dependency(&self, dependency: TrackedDependency) {
656 self.record_source(dependency);
657 }
658
659 fn dispose(&self, root: ReactiveRootId) {
660 let owns_root = self
661 .runtime
662 .borrow()
663 .as_ref()
664 .is_some_and(|binding| binding.root == root);
665 if owns_root {
666 detach_sources(&self.sources);
667 self.mounted.set(false);
668 self.dirty.set(false);
669 *self.runtime.borrow_mut() = None;
670 }
671 }
672}
673
674#[derive(Clone)]
677pub struct Effect {
678 inner: Rc<EffectInner>,
679}
680
681impl Effect {
682 #[must_use]
683 pub fn new(callback: impl FnMut() + 'static) -> Self {
684 Self {
685 inner: EffectInner::new(callback),
686 }
687 }
688
689 pub fn mount(&self) -> bool {
693 let Some(binding) = current_binding() else {
694 return false;
695 };
696 self.inner.mount(binding)
697 }
698
699 #[must_use]
700 pub fn is_mounted(&self) -> bool {
701 self.inner.mounted.get()
702 }
703
704 pub fn dispose(&self) {
705 let root = self
706 .inner
707 .runtime
708 .borrow()
709 .as_ref()
710 .map(|binding| binding.root);
711 if let Some(root) = root {
712 self.inner.dispose(root);
713 }
714 }
715}
716
717#[derive(Clone, Copy, Debug, PartialEq, Eq)]
720pub enum ActionDispatchError {
721 NotMounted,
722}
723
724#[derive(Clone, Debug, PartialEq, Eq)]
726pub enum ActionError<E> {
727 Operation(E),
728 Runtime(TaskFailure),
729}
730
731#[derive(Clone, Debug, PartialEq, Eq)]
733pub enum ActionState<T, E> {
734 Idle,
735 Loading,
736 Ready(T),
737 Error(ActionError<E>),
738}
739
740impl<T, E> ActionState<T, E> {
741 #[must_use]
742 pub const fn is_idle(&self) -> bool {
743 matches!(self, Self::Idle)
744 }
745
746 #[must_use]
747 pub const fn is_loading(&self) -> bool {
748 matches!(self, Self::Loading)
749 }
750
751 #[must_use]
752 pub const fn is_ready(&self) -> bool {
753 matches!(self, Self::Ready(_))
754 }
755
756 #[must_use]
757 pub const fn is_error(&self) -> bool {
758 matches!(self, Self::Error(_))
759 }
760}
761
762type ActionFuture<O, E> = Pin<Box<dyn Future<Output = Result<O, E>> + Send + 'static>>;
763
764#[derive(Clone)]
765struct ActionBinding {
766 root: ReactiveRootId,
767 spawner: super::tasks::RuntimeSpawner,
768 owner_scope: super::tasks::TaskScope,
769}
770
771struct ActionInner<I, O, E> {
772 operation: Rc<dyn Fn(I) -> ActionFuture<O, E>>,
773 state: Signal<ActionState<O, E>>,
774 binding: RefCell<Option<ActionBinding>>,
775 active: RefCell<Option<TaskHandle>>,
776 generation: Cell<u64>,
777}
778
779impl<I, O, E> Drop for ActionInner<I, O, E> {
780 fn drop(&mut self) {
781 if let Some(active) = self.active.get_mut().take() {
782 active.cancel();
783 }
784 }
785}
786
787#[derive(Clone)]
790pub struct Action<I, O, E> {
791 inner: Rc<ActionInner<I, O, E>>,
792}
793
794impl<I, O, E> Action<I, O, E>
795where
796 I: 'static,
797 O: Clone + Send + 'static,
798 E: Clone + Send + 'static,
799{
800 #[must_use]
802 pub fn new<F, Fut>(operation: F) -> Self
803 where
804 F: Fn(I) -> Fut + 'static,
805 Fut: Future<Output = Result<O, E>> + Send + 'static,
806 {
807 let operation: Rc<dyn Fn(I) -> ActionFuture<O, E>> =
808 Rc::new(move |input| Box::pin(operation(input)));
809 Self {
810 inner: Rc::new(ActionInner {
811 operation,
812 state: Signal::new(ActionState::Idle),
813 binding: RefCell::new(None),
814 active: RefCell::new(None),
815 generation: Cell::new(0),
816 }),
817 }
818 }
819
820 fn bind_current(&self) {
821 let Some(binding) = current_binding() else {
822 return;
823 };
824 let (Some(spawner), Some(owner_scope)) = (binding.spawner, binding.owner_scope) else {
825 return;
826 };
827 let mut current = self.inner.binding.borrow_mut();
828 let same_root = current.as_ref().is_some_and(|current| {
829 current.root == binding.root && !current.owner_scope.is_cancelled()
830 });
831 if !same_root {
832 *current = Some(ActionBinding {
833 root: binding.root,
834 spawner,
835 owner_scope,
836 });
837 }
838 }
839
840 pub fn mount(&self) -> bool {
843 self.bind_current();
844 self.inner
845 .binding
846 .borrow()
847 .as_ref()
848 .is_some_and(|binding| !binding.owner_scope.is_cancelled())
849 }
850
851 pub fn state(&self) -> ActionState<O, E> {
853 self.bind_current();
854 self.inner.state.get()
855 }
856
857 pub fn state_signal(&self) -> Signal<ActionState<O, E>> {
859 self.bind_current();
860 self.inner.state.clone()
861 }
862
863 pub fn dispatch(&self, input: I) -> Result<(), ActionDispatchError> {
867 let binding = {
868 let mut current = self.inner.binding.borrow_mut();
869 let Some(binding) = current.clone() else {
870 return Err(ActionDispatchError::NotMounted);
871 };
872 if binding.owner_scope.is_cancelled() {
873 *current = None;
874 return Err(ActionDispatchError::NotMounted);
875 }
876 binding
877 };
878 if let Some(active) = self.inner.active.borrow_mut().take() {
879 active.cancel();
880 }
881 let generation = self
882 .inner
883 .generation
884 .get()
885 .checked_add(1)
886 .expect("action generation exhausted");
887 self.inner.generation.set(generation);
888 self.inner
889 .state
890 .update(|state| *state = ActionState::Loading);
891
892 let operation = self.inner.operation.clone();
893 let weak = Rc::downgrade(&self.inner);
894 let handle = binding.spawner.spawn_into_in(
895 &binding.owner_scope,
896 operation(input),
897 move |result, _runtime| {
898 let Some(inner) = weak.upgrade() else {
899 return;
900 };
901 if inner.generation.get() != generation {
902 return;
903 }
904 inner.active.borrow_mut().take();
905 inner.state.update(|state| {
906 *state = match result {
907 Ok(Ok(value)) => ActionState::Ready(value),
908 Ok(Err(error)) => ActionState::Error(ActionError::Operation(error)),
909 Err(error) => ActionState::Error(ActionError::Runtime(error)),
910 };
911 });
912 },
913 );
914 *self.inner.active.borrow_mut() = Some(handle);
915 Ok(())
916 }
917
918 pub fn cancel(&self) {
920 let generation = self
921 .inner
922 .generation
923 .get()
924 .checked_add(1)
925 .expect("action generation exhausted");
926 self.inner.generation.set(generation);
927 if let Some(active) = self.inner.active.borrow_mut().take() {
928 active.cancel();
929 self.inner.state.update(|state| {
930 *state = ActionState::Error(ActionError::Runtime(TaskFailure::Cancelled));
931 });
932 }
933 }
934}