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 <leeeon233@gmail.com>
This commit is contained in:
Zixuan Chen 2024-11-12 21:15:46 +08:00 committed by GitHub
parent 55e0a4596e
commit ee26952fc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 162 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
"loro-crdt": patch
---
Add isDeleted() method to each container

View file

@ -1268,6 +1268,7 @@ impl ContainerState for TreeState {
}
fn apply_local_op(&mut self, raw_op: &RawOp, _op: &Op) -> LoroResult<ApplyLocalOpReturn> {
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(

View file

@ -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);
});

View file

@ -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<JsID> {
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<JsStrPeerID> {
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 {

View file

@ -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);
})
})

View file

@ -77,4 +77,8 @@ impl ContainerTrait for LoroCounter {
fn try_from_container(container: Container) -> Option<Self> {
container.into_counter().ok()
}
fn is_deleted(&self) -> bool {
self.handler.is_deleted()
}
}

View file

@ -892,6 +892,8 @@ pub trait ContainerTrait: SealedTrait {
fn get_attached(&self) -> Option<Self>
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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 {

View file

@ -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());
}