Skip to main content

fusor/dom/
commit.rs

1//! Fallible browser setup runs after the complete prepared subtree is valid,
2//! before activating resources. Child commits join their prepared ancestor.
3use super::{JsValue, Scope};
4use crate::{ContextKey, OwnerHandle};
5use std::cell::RefCell;
6
7struct Action {
8    owner: OwnerHandle,
9    setup: Box<dyn FnOnce() -> Result<(), JsValue>>,
10}
11
12#[derive(Default)]
13pub(super) struct CommitQueue(RefCell<Vec<Action>>);
14
15impl CommitQueue {
16    /// No scope sharing this queue has setup left to finish.
17    pub(super) fn is_idle(&self) -> bool {
18        self.0.borrow().is_empty()
19    }
20}
21
22struct MountContext;
23impl ContextKey for MountContext {
24    type Value = CommitQueue;
25}
26
27impl Scope {
28    pub(super) fn prepare_queue(&mut self, parent: Option<&OwnerHandle>) {
29        self.mount_parent = parent.cloned();
30        if self.owner.is_disposed() {
31            self.mount_queue = None;
32            return;
33        }
34        let queue = parent
35            .and_then(|parent| parent.context::<MountContext>())
36            .unwrap_or_else(|| {
37                let owner = self.owner();
38                owner
39                    .provide::<MountContext>(CommitQueue::default())
40                    .expect("fresh component owner");
41                owner.context::<MountContext>().expect("just provided")
42            });
43        self.mount_queue = Some(queue);
44    }
45
46    /// Internal integration boundary for fallible browser setup. The callback
47    /// must arrange rollback on owner disposal if later setup fails. Captures
48    /// should not keep a disposed component or integration alive.
49    #[doc(hidden)]
50    pub fn before_commit(
51        &mut self,
52        setup: impl FnOnce() -> Result<(), JsValue> + 'static,
53    ) -> Result<(), JsValue> {
54        if self.owner.is_disposed() {
55            return Err(JsValue::from_str("cannot initialize a disposed component"));
56        }
57        if self.owner.is_active() {
58            return setup();
59        }
60        let queue = self.mount_queue.as_ref().expect("prepared component queue");
61        queue.0.borrow_mut().push(Action {
62            owner: self.owner(),
63            setup: Box::new(setup),
64        });
65        Ok(())
66    }
67
68    /// Finish fallible setup without activating the component's resources.
69    /// Used before replacing a previous view.
70    #[doc(hidden)]
71    pub fn finish_prepare(&self) -> Result<(), JsValue> {
72        if self.owner.is_disposed() {
73            return Err(JsValue::from_str("cannot commit a disposed component"));
74        }
75        self.owner.mark_mount_ready();
76        if self
77            .mount_parent
78            .as_ref()
79            .is_some_and(|parent| !parent.is_active())
80        {
81            return Ok(());
82        }
83        self.finish_prepare_subtree()
84    }
85
86    /// Finish this prepared subtree inside an externally owned transaction.
87    /// Unlike finish_prepare, an inactive parent does not defer fallible setup.
88    /// The caller must keep work inactive until all participants can commit.
89    pub fn finish_prepare_subtree(&self) -> Result<(), JsValue> {
90        if self.owner.is_disposed() {
91            return Err(JsValue::from_str("cannot prepare a disposed subtree"));
92        }
93        self.owner.mark_mount_ready();
94        if let Some(queue) = &self.mount_queue {
95            let pending = queue.0.take();
96            let owner = self.owner();
97            let (ready, waiting): (Vec<_>, Vec<_>) = pending
98                .into_iter()
99                .filter(|action| !action.owner.is_disposed())
100                .partition(|action| {
101                    action.owner.is_within(&owner) && action.owner.is_mount_ready()
102                });
103            queue.0.borrow_mut().extend(waiting);
104            for action in ready {
105                if !action.owner.is_disposed() {
106                    (action.setup)()?;
107                }
108            }
109        }
110        if self.owner.is_disposed() {
111            return Err(JsValue::from_str(
112                "component disposed during initialization",
113            ));
114        }
115        Ok(())
116    }
117
118    /// Finish browser setup and activate owned work. Prepared ancestors still
119    /// gate activation. Failure is returned to the mounting caller.
120    pub fn try_commit(&self) -> Result<(), JsValue> {
121        // Reconciliation revisits retained scopes. An active scope has already
122        // prepared and committed, but new descendants can share its setup queue.
123        // Only skip preparation when there is no pending setup anywhere in it.
124        if self.owner.is_active()
125            && self
126                .mount_queue
127                .as_ref()
128                .is_none_or(|queue| queue.0.borrow().is_empty())
129        {
130            return Ok(());
131        }
132        self.finish_prepare()?;
133        self.owner.commit();
134        if self.owner.is_disposed() {
135            return Err(JsValue::from_str("component disposed during activation"));
136        }
137        Ok(())
138    }
139}