From ee26952fc02adaf0b3a95209aee6755df86d1c13 Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Tue, 12 Nov 2024 21:15:46 +0800 Subject: [PATCH] feat: Add isDeleted() method to containers (#555) * feat: Add isDeleted() method to containers - Add isDeleted() method to all container types (Text, Map, List, Tree, etc.) - Fix deletion tracking for containers in tree operations - Add tests to verify deletion state across different scenarios * chore: fix redundant field names --------- Co-authored-by: Leon Zhao --- .changeset/soft-goats-peel.md | 5 ++ crates/loro-internal/src/state/tree_state.rs | 7 ++- crates/loro-wasm/deno_tests/basic.test.ts | 15 +++++- crates/loro-wasm/src/lib.rs | 25 ++++++++++ crates/loro-wasm/tests/basic.test.ts | 51 ++++++++++++++++++++ crates/loro/src/counter.rs | 4 ++ crates/loro/src/lib.rs | 39 +++++++++++++++ crates/loro/tests/loro_rust_test.rs | 20 +++++++- 8 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 .changeset/soft-goats-peel.md diff --git a/.changeset/soft-goats-peel.md b/.changeset/soft-goats-peel.md new file mode 100644 index 00000000..2b1f6c72 --- /dev/null +++ b/.changeset/soft-goats-peel.md @@ -0,0 +1,5 @@ +--- +"loro-crdt": patch +--- + +Add isDeleted() method to each container diff --git a/crates/loro-internal/src/state/tree_state.rs b/crates/loro-internal/src/state/tree_state.rs index 734dfc40..4a7d0cf5 100644 --- a/crates/loro-internal/src/state/tree_state.rs +++ b/crates/loro-internal/src/state/tree_state.rs @@ -1268,6 +1268,7 @@ impl ContainerState for TreeState { } fn apply_local_op(&mut self, raw_op: &RawOp, _op: &Op) -> LoroResult { + let mut deleted_containers = vec![]; match &raw_op.content { crate::op::RawOpContent::Tree(tree) => match &**tree { TreeOp::Create { @@ -1291,13 +1292,17 @@ impl ContainerState for TreeState { } TreeOp::Delete { target } => { let parent = TreeParentId::Deleted; + deleted_containers.push(ContainerID::new_normal( + target.id(), + loro_common::ContainerType::Map, + )); self.mov(*target, parent, raw_op.id_full(), None, true)?; } }, _ => unreachable!(), } // self.check_tree_integrity(); - Ok(Default::default()) + Ok(ApplyLocalOpReturn { deleted_containers }) } fn to_diff( diff --git a/crates/loro-wasm/deno_tests/basic.test.ts b/crates/loro-wasm/deno_tests/basic.test.ts index c3e1e7a0..4e595564 100644 --- a/crates/loro-wasm/deno_tests/basic.test.ts +++ b/crates/loro-wasm/deno_tests/basic.test.ts @@ -1,4 +1,4 @@ -import init, { initSync, LoroDoc } from "../web/loro_wasm.js"; +import init, { initSync, LoroDoc, LoroMap } from "../web/loro_wasm.js"; import { expect } from "npm:expect"; await init(); @@ -28,3 +28,16 @@ Deno.test("fork when detached", () => { doc.checkoutToLatest(); console.log(doc.getText("text").toString()); // "Hello, world! Alice!" }); + +Deno.test("isDeleted", () => { + const doc = new LoroDoc(); + const list = doc.getList("list"); + expect(list.isDeleted()).toBe(false); + const tree = doc.getTree("root"); + const node = tree.createNode(undefined, undefined); + const containerBefore = node.data.setContainer("container", new LoroMap()); + containerBefore.set("A", "B"); + tree.delete(node.id); + const containerAfter = doc.getContainerById(containerBefore.id) as LoroMap; + expect(containerAfter.isDeleted()).toBe(true); +}); diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 92cd53c6..adc7f7f1 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -2273,6 +2273,11 @@ impl LoroText { .get_cursor(pos, Side::Middle) .map(|x| peer_id_to_js(x.id.unwrap().peer)) } + + /// Check if the container is deleted + pub fn isDeleted(&self) -> bool { + self.handler.is_deleted() + } } impl Default for LoroText { @@ -2614,6 +2619,11 @@ impl LoroMap { .get_last_editor(key) .map(|x| JsValue::from_str(&x.to_string()).into()) } + + /// Check if the container is deleted + pub fn isDeleted(&self) -> bool { + self.handler.is_deleted() + } } impl Default for LoroMap { @@ -2938,6 +2948,11 @@ impl LoroList { pub fn getIdAt(&self, pos: usize) -> Option { self.handler.get_id_at(pos).map(|x| id_to_js(&x).into()) } + + /// Check if the container is deleted + pub fn isDeleted(&self) -> bool { + self.handler.is_deleted() + } } impl Default for LoroList { @@ -3315,6 +3330,11 @@ impl LoroMovableList { pub fn getLastEditorAt(&self, pos: usize) -> Option { self.handler.get_last_editor_at(pos).map(peer_id_to_js) } + + /// Check if the container is deleted + pub fn isDeleted(&self) -> bool { + self.handler.is_deleted() + } } /// The handler of a tree(forest) container. @@ -3991,6 +4011,11 @@ impl LoroTree { pub fn is_fractional_index_enabled(&self) -> bool { self.handler.is_fractional_index_enabled() } + + /// Check if the container is deleted + pub fn isDeleted(&self) -> bool { + self.handler.is_deleted() + } } impl Default for LoroTree { diff --git a/crates/loro-wasm/tests/basic.test.ts b/crates/loro-wasm/tests/basic.test.ts index c12530f1..62cca92d 100644 --- a/crates/loro-wasm/tests/basic.test.ts +++ b/crates/loro-wasm/tests/basic.test.ts @@ -700,3 +700,54 @@ it("update VV", () => { const map = vv.toJSON(); expect(map).toStrictEqual(new Map([["1", 1], ["2", 2]])) }) + +describe("isDeleted", () => { + it("test text container deletion", () => { + const doc = new LoroDoc(); + const list = doc.getList("list"); + expect(list.isDeleted()).toBe(false); + const tree = doc.getTree("root"); + const node = tree.createNode(); + const containerBefore = node.data.setContainer("container", new LoroMap()); + containerBefore.set("A", "B"); + tree.delete(node.id); + const containerAfter = node.data; + expect(containerAfter.isDeleted()).toBe(true); + }) + + it("movable list setContainer", () => { + const doc = new LoroDoc(); + const list = doc.getMovableList("list1"); + const map = list.insertContainer(0, new LoroMap()); + expect(map.isDeleted()).toBe(false); + list.set(0, 1); + expect(map.isDeleted()).toBe(true); + }) + + it("map set", () => { + const doc = new LoroDoc(); + const map = doc.getMap("map"); + const sub = map.setContainer("sub", new LoroMap()); + expect(sub.isDeleted()).toBe(false); + map.set("sub", "value"); + expect(sub.isDeleted()).toBe(true); + }) + + it("remote map set", () => { + const doc = new LoroDoc(); + const map = doc.getMap("map"); + const sub = map.setContainer("sub", new LoroMap()); + + const docB = new LoroDoc(); + docB.import(doc.export({ mode: "snapshot" })); + const subB = docB.getByPath("map/sub") as LoroMap; + expect(sub.isDeleted()).toBe(false); + expect(subB.isDeleted()).toBe(false); + + map.set("sub", "value"); + docB.import(doc.export({ mode: "snapshot" })); + + expect(sub.isDeleted()).toBe(true); + expect(subB.isDeleted()).toBe(true); + }) +}) diff --git a/crates/loro/src/counter.rs b/crates/loro/src/counter.rs index 62666142..a600d462 100644 --- a/crates/loro/src/counter.rs +++ b/crates/loro/src/counter.rs @@ -77,4 +77,8 @@ impl ContainerTrait for LoroCounter { fn try_from_container(container: Container) -> Option { container.into_counter().ok() } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 40e494c8..5794148d 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -892,6 +892,8 @@ pub trait ContainerTrait: SealedTrait { fn get_attached(&self) -> Option where Self: Sized; + /// Whether the container is deleted. + fn is_deleted(&self) -> bool; } /// LoroList container. It's used to model array. @@ -943,6 +945,10 @@ impl ContainerTrait for LoroList { fn try_from_container(container: Container) -> Option { container.into_list().ok() } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } impl LoroList { @@ -1213,6 +1219,10 @@ impl ContainerTrait for LoroMap { fn try_from_container(container: Container) -> Option { container.into_map().ok() } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } impl LoroMap { @@ -1380,6 +1390,10 @@ impl ContainerTrait for LoroText { fn try_from_container(container: Container) -> Option { container.into_text().ok() } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } impl LoroText { @@ -1686,6 +1700,10 @@ impl ContainerTrait for LoroTree { fn try_from_container(container: Container) -> Option { container.into_tree().ok() } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } /// A tree node in the [LoroTree]. @@ -2087,6 +2105,10 @@ impl ContainerTrait for LoroMovableList { { self.handler.get_attached().map(Self::from_handler) } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } impl LoroMovableList { @@ -2369,6 +2391,10 @@ impl ContainerTrait for LoroUnknown { { self.handler.get_attached().map(Self::from_handler) } + + fn is_deleted(&self) -> bool { + self.handler.is_deleted() + } } use enum_as_inner::EnumAsInner; @@ -2459,6 +2485,19 @@ impl ContainerTrait for Container { { Some(container) } + + fn is_deleted(&self) -> bool { + match self { + Container::List(x) => x.is_deleted(), + Container::Map(x) => x.is_deleted(), + Container::Text(x) => x.is_deleted(), + Container::Tree(x) => x.is_deleted(), + Container::MovableList(x) => x.is_deleted(), + #[cfg(feature = "counter")] + Container::Counter(x) => x.is_deleted(), + Container::Unknown(x) => x.is_deleted(), + } + } } impl Container { diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index cdd9f6aa..7979f022 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -9,8 +9,9 @@ use std::{ }; use loro::{ - awareness::Awareness, loro_value, CommitOptions, ContainerID, ContainerType, ExportMode, - Frontiers, FrontiersNotIncluded, LoroDoc, LoroError, LoroList, LoroMap, LoroText, ToJson, + awareness::Awareness, loro_value, CommitOptions, ContainerID, ContainerTrait, ContainerType, + ExportMode, Frontiers, FrontiersNotIncluded, LoroDoc, LoroError, LoroList, LoroMap, LoroText, + ToJson, }; use loro_internal::{encoding::EncodedBlobMode, handler::TextDelta, id::ID, vv, LoroResult}; use rand::{Rng, SeedableRng}; @@ -2222,3 +2223,18 @@ fn get_changed_containers_in() { }) ) } + +#[test] +fn is_deleted() { + let doc = LoroDoc::new(); + let list = doc.get_list("list"); + assert!(!list.is_deleted()); + let tree = doc.get_tree("root"); + let node = tree.create(None).unwrap(); + let map = tree.get_meta(node).unwrap(); + let container_before = map.insert_container("container", LoroMap::new()).unwrap(); + container_before.insert("A", "B").unwrap(); + tree.delete(node).unwrap(); + let container_after = doc.get_map(&container_before.id()); + assert!(container_after.is_deleted()); +}