From 778ca5452d4c688c9f83551ac6d26deab5c60321 Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Sat, 9 Nov 2024 21:00:07 +0800 Subject: [PATCH] feat: allow users to query the changed containers in the target id range (#549) * feat: allow users to query the changed containers in the target id range * chore: add changeset note * chore: update cargo toml * test: add related tests and add a commit before get_changed_container_in --- .changeset/hip-squids-train.md | 5 ++++ Cargo.lock | 1 + crates/loro-internal/src/loro.rs | 12 ++++++++ crates/loro-internal/src/op.rs | 4 +++ crates/loro-wasm/src/lib.rs | 44 +++++++++++++++++++++++----- crates/loro-wasm/tests/basic.test.ts | 14 +++++++++ crates/loro/Cargo.toml | 1 + crates/loro/src/lib.rs | 16 ++++++++++ crates/loro/tests/loro_rust_test.rs | 32 ++++++++++++++++++++ 9 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 .changeset/hip-squids-train.md diff --git a/.changeset/hip-squids-train.md b/.changeset/hip-squids-train.md new file mode 100644 index 00000000..59496915 --- /dev/null +++ b/.changeset/hip-squids-train.md @@ -0,0 +1,5 @@ +--- +"loro-crdt": minor +--- + +Feat: allow users to query the changed containers in the target id range diff --git a/Cargo.lock b/Cargo.lock index 6e46b599..45b4c4f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1069,6 +1069,7 @@ dependencies = [ "ctor 0.2.6", "dev-utils", "enum-as-inner 0.6.0", + "fxhash", "generic-btree", "loro-common 1.0.0-beta.5", "loro-delta 1.0.0-beta.5", diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index d65f0b93..6b0bf741 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -1653,6 +1653,18 @@ impl LoroDoc { Ok(()) } + + pub fn get_changed_containers_in(&self, id: ID, len: usize) -> FxHashSet { + self.commit_then_renew(); + let mut set = FxHashSet::default(); + let oplog = &self.oplog().try_lock().unwrap(); + for op in oplog.iter_ops(id.to_span(len)) { + let id = oplog.arena.get_container_id(op.container()).unwrap(); + set.insert(id); + } + + set + } } // FIXME: PERF: This method is quite slow because it iterates all the changes diff --git a/crates/loro-internal/src/op.rs b/crates/loro-internal/src/op.rs index 4ed9f98d..246b82fc 100644 --- a/crates/loro-internal/src/op.rs +++ b/crates/loro-internal/src/op.rs @@ -328,6 +328,10 @@ impl<'a> RichOp<'a> { self.peer } + pub fn container(&self) -> ContainerIdx { + self.op.container + } + pub fn timestamp(&self) -> i64 { self.timestamp } diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 7c902bcd..00520dbc 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -190,6 +190,8 @@ extern "C" { pub type JsCommitOption; #[wasm_bindgen(typescript_type = "ImportStatus")] pub type JsImportStatus; + #[wasm_bindgen(typescript_type = "(change: ChangeMeta) => boolean")] + pub type JsTravelChangeFunction; } mod observer { @@ -606,7 +608,15 @@ impl LoroDoc { /// @param ids - the changes to visit /// @param f - the callback function, return `true` to continue visiting, return `false` to stop #[wasm_bindgen(js_name = "travelChangeAncestors")] - pub fn travel_change_ancestors(&self, ids: Vec, f: js_sys::Function) -> JsResult<()> { + pub fn travel_change_ancestors( + &self, + ids: Vec, + f: JsTravelChangeFunction, + ) -> JsResult<()> { + let f: js_sys::Function = match f.dyn_into::() { + Ok(f) => f, + Err(_) => return Err(JsValue::from_str("Expected a function")), + }; let observer = observer::Observer::new(f); self.0 .travel_change_ancestors( @@ -1652,6 +1662,30 @@ impl LoroDoc { )?; Ok(JsValue::from(obj).into()) } + + /// Gets container IDs modified in the given ID range. + /// + /// **NOTE:** This method will implicitly commit. + /// + /// This method identifies which containers were affected by changes in a given range of operations. + /// It can be used together with `doc.travelChangeAncestors()` to analyze the history of changes + /// and determine which containers were modified by each change. + /// + /// @param id - The starting ID of the change range + /// @param len - The length of the change range to check + /// @returns An array of container IDs that were modified in the given range + pub fn getChangedContainersIn(&self, id: JsID, len: usize) -> JsResult> { + let id = js_id_to_id(id)?; + Ok(self + .0 + .get_changed_containers_in(id, len) + .into_iter() + .map(|cid| { + let v: JsValue = (&cid).into(); + v.into() + }) + .collect()) + } } #[allow(unused)] @@ -3271,16 +3305,12 @@ impl LoroMovableList { /// Get the last mover of the list item at the given position. pub fn getLastMoverAt(&self, pos: usize) -> Option { - self.handler - .get_last_mover_at(pos) - .map(peer_id_to_js) + self.handler.get_last_mover_at(pos).map(peer_id_to_js) } /// Get the last editor of the list item at the given position. pub fn getLastEditorAt(&self, pos: usize) -> Option { - self.handler - .get_last_editor_at(pos) - .map(peer_id_to_js) + self.handler.get_last_editor_at(pos).map(peer_id_to_js) } } diff --git a/crates/loro-wasm/tests/basic.test.ts b/crates/loro-wasm/tests/basic.test.ts index f5587f56..e7fac64f 100644 --- a/crates/loro-wasm/tests/basic.test.ts +++ b/crates/loro-wasm/tests/basic.test.ts @@ -16,6 +16,7 @@ import { encodeFrontiers, decodeFrontiers, } from "../bundler/index"; +import { ContainerID } from "loro-wasm"; it("basic example", () => { const doc = new LoroDoc(); @@ -675,3 +676,16 @@ it("can push container to movable list", () => { const map = list.pushContainer(new LoroMap()); expect(list.toJSON()).toStrictEqual([{}]); }) + +it("can query the history for changed containers", () => { + const doc = new LoroDoc(); + doc.setPeerId("0"); + doc.getText("text").insert(0, "H"); + doc.getMap("map").set("key", "H"); + const changed = doc.getChangedContainersIn({ peer: "0", counter: 0 }, 2) + const changedSet = new Set(changed); + expect(changedSet).toEqual(new Set([ + "cid:root-text:Text" as ContainerID, + "cid:root-map:Map" as ContainerID, + ])) +}) diff --git a/crates/loro/Cargo.toml b/crates/loro/Cargo.toml index 65bbf2ea..ffd911ac 100644 --- a/crates/loro/Cargo.toml +++ b/crates/loro/Cargo.toml @@ -21,6 +21,7 @@ delta = { path = "../delta", package = "loro-delta", version = "1.0.0-beta.5" } generic-btree = { version = "^0.10.5" } enum-as-inner = { workspace = true } tracing = { workspace = true } +fxhash = { workspace = true } [dev-dependencies] serde_json = "1.0.87" diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 1c42307a..7d798b3d 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -2,6 +2,7 @@ #![warn(missing_docs)] #![warn(missing_debug_implementations)] use event::{DiffEvent, Subscriber}; +use fxhash::FxHashSet; use loro_common::InternalString; pub use loro_internal::cursor::CannotFindRelativePosition; use loro_internal::cursor::Cursor; @@ -848,6 +849,21 @@ impl LoroDoc { pub fn is_shallow(&self) -> bool { self.doc.is_shallow() } + + /// Gets container IDs modified in the given ID range. + /// + /// **NOTE:** This method will implicitly commit. + /// + /// This method can be used in conjunction with `doc.travel_change_ancestors()` to traverse + /// the history and identify all changes that affected specific containers. + /// + /// # Arguments + /// + /// * `id` - The starting ID of the change range + /// * `len` - The length of the change range to check + pub fn get_changed_containers_in(&self, id: ID, len: usize) -> FxHashSet { + self.doc.get_changed_containers_in(id, len) + } } /// It's used to prevent the user from implementing the trait directly. diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index bc4641f9..cdd9f6aa 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -2190,3 +2190,35 @@ fn get_editor() { let mov_id = tree.get_last_move_id(&node_0).unwrap(); assert_eq!(mov_id.peer, 2); } + +#[test] +fn get_changed_containers_in() { + let doc = LoroDoc::new(); + doc.set_peer_id(0).unwrap(); + let text = doc.get_text("text"); + text.insert(0, "H").unwrap(); + let map = doc.get_map("map"); + map.insert("key", "value").unwrap(); + let changed_set = doc.get_changed_containers_in(ID::new(0, 0), 2); + assert_eq!( + changed_set, + vec![ + ContainerID::new_root("text", ContainerType::Text), + ContainerID::new_root("map", ContainerType::Map), + ] + .into_iter() + .collect() + ); + + map.insert("key1", "value1").unwrap(); + assert_eq!( + doc.get_deep_value().to_json_value(), + json!({ + "text": "H", + "map": { + "key": "value", + "key1": "value1" + } + }) + ) +}