diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0e19ecaec..81b1b64cb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,7 +24,8 @@ jobs: run: cd docs && cd guide && mdbook build -d ../nightly/guide && cd .. && cd reference && mdbook build -d ../nightly/reference && cd .. && - cd router && mdbook build -d ../nightly/router && cd .. + cd router && mdbook build -d ../nightly/router && cd .. && + cd cli && mdbook build -d ../nightly/cli && cd .. - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@v4.2.3 diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 4e9a11c34..5682772be 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -813,6 +813,15 @@ impl<'b> DiffState<'b> { return; } + // remove any old children that are not shared + // todo: make this an iterator + for child in old { + let key = child.key().unwrap(); + if !shared_keys.contains(&key) { + self.remove_nodes([child], true); + } + } + // 4. Compute the LIS of this list let mut lis_sequence = Vec::default(); lis_sequence.reserve(new_index_to_old_index.len()); diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 8c35657d7..7e415801b 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -7,7 +7,7 @@ use std::{ any::{Any, TypeId}, borrow::Borrow, cell::{Cell, RefCell}, - collections::HashMap, + collections::{HashMap, HashSet}, future::Future, pin::Pin, rc::Rc, @@ -68,7 +68,12 @@ impl ScopeArena { heuristics: RefCell::new(FxHashMap::default()), free_scopes: RefCell::new(Vec::new()), nodes: RefCell::new(nodes), - tasks: TaskQueue::new(sender), + tasks: Rc::new(TaskQueue { + tasks: RefCell::new(FxHashMap::default()), + task_map: RefCell::new(FxHashMap::default()), + gen: Cell::new(0), + sender, + }), } } @@ -179,6 +184,15 @@ impl ScopeArena { log::trace!("removing scope {:?}", id); self.ensure_drop_safety(id); + // Dispose of any ongoing tasks + let mut tasks = self.tasks.tasks.borrow_mut(); + let mut task_map = self.tasks.task_map.borrow_mut(); + if let Some(cur_tasks) = task_map.remove(&id) { + for task in cur_tasks { + tasks.remove(&task); + } + } + // Safety: // - ensure_drop_safety ensures that no references to this scope are in use // - this raw pointer is removed from the map @@ -435,7 +449,13 @@ pub struct ScopeId(pub usize); /// once a Task has been completed. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct TaskId(pub usize); +pub struct TaskId { + /// The global ID of the task + pub id: usize, + + /// The original scope that this task was scheduled in + pub scope: ScopeId, +} /// Every component in Dioxus is represented by a `ScopeState`. /// @@ -745,7 +765,7 @@ impl ScopeState { .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) .unwrap(); - self.tasks.push_fut(fut) + self.tasks.spawn(self.our_arena_idx, fut) } /// Spawns the future but does not return the TaskId @@ -753,10 +773,24 @@ impl ScopeState { self.push_future(fut); } + /// Spawn a future that Dioxus will never clean up + /// + /// This is good for tasks that need to be run after the component has been dropped. + pub fn spawn_forever(&self, fut: impl Future + 'static) -> TaskId { + // wake up the scheduler if it is sleeping + self.tasks + .sender + .unbounded_send(SchedulerMsg::NewTask(self.our_arena_idx)) + .unwrap(); + + // The root scope will never be unmounted so we can just add the task at the top of the app + self.tasks.spawn(ScopeId(0), fut) + } + /// Informs the scheduler that this task is no longer needed and should be removed /// on next poll. pub fn remove_future(&self, id: TaskId) { - self.tasks.remove_fut(id); + self.tasks.remove(id); } /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. @@ -940,36 +974,43 @@ impl BumpFrame { pub(crate) struct TaskQueue { pub(crate) tasks: RefCell>, + pub(crate) task_map: RefCell>>, gen: Cell, sender: UnboundedSender, } + pub(crate) type InnerTask = Pin>>; impl TaskQueue { - fn new(sender: UnboundedSender) -> Rc { - Rc::new(Self { - tasks: RefCell::new(FxHashMap::default()), - gen: Cell::new(0), - sender, - }) - } - fn push_fut(&self, task: impl Future + 'static) -> TaskId { + fn spawn(&self, scope: ScopeId, task: impl Future + 'static) -> TaskId { let pinned = Box::pin(task); let id = self.gen.get(); self.gen.set(id + 1); - let tid = TaskId(id); + let tid = TaskId { id, scope }; self.tasks.borrow_mut().insert(tid, pinned); + + // also add to the task map + // when the component is unmounted we know to remove it from the map + self.task_map + .borrow_mut() + .entry(scope) + .or_default() + .insert(tid); + tid } - fn remove_fut(&self, id: TaskId) { + + fn remove(&self, id: TaskId) { if let Ok(mut tasks) = self.tasks.try_borrow_mut() { let _ = tasks.remove(&id); - } else { - // todo: it should be okay to remote a fut while the queue is being polled - // However, it's not currently possible to do that. - log::trace!("Unable to remove task from task queue. This is probably a bug."); + } + + // the task map is still around, but it'll be removed when the scope is unmounted + if let Some(task_map) = self.task_map.borrow_mut().get_mut(&id.scope) { + task_map.remove(&id); } } + pub(crate) fn has_tasks(&self) -> bool { !self.tasks.borrow().is_empty() } diff --git a/packages/core/tests/diffing.rs b/packages/core/tests/diffing.rs index 6bc9b5596..9184d4e4a 100644 --- a/packages/core/tests/diffing.rs +++ b/packages/core/tests/diffing.rs @@ -623,16 +623,17 @@ fn controlled_keyed_diffing_out_of_order() { assert_eq!( changes.edits, [ + Remove { root: 4 }, // move 4 to after 6 PushRoot { root: 1 }, InsertAfter { n: 1, root: 3 }, // remove 7 // create 9 and insert before 6 - CreateElement { root: 5, tag: "div" }, + CreateElement { root: 4, tag: "div" }, InsertBefore { n: 1, root: 3 }, // create 0 and insert before 5 - CreateElement { root: 6, tag: "div" }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 2 }, ] ); @@ -659,7 +660,8 @@ fn controlled_keyed_diffing_out_of_order_max_test() { assert_eq!( changes.edits, [ - CreateElement { root: 6, tag: "div" }, + Remove { root: 5 }, + CreateElement { root: 5, tag: "div" }, InsertBefore { n: 1, root: 3 }, PushRoot { root: 4 }, InsertBefore { n: 1, root: 1 },