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
This commit is contained in:
Zixuan Chen 2024-11-09 21:00:07 +08:00 committed by GitHub
parent 6e878d216a
commit 778ca5452d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 122 additions and 7 deletions

View file

@ -0,0 +1,5 @@
---
"loro-crdt": minor
---
Feat: allow users to query the changed containers in the target id range

1
Cargo.lock generated
View file

@ -1069,6 +1069,7 @@ dependencies = [
"ctor 0.2.6", "ctor 0.2.6",
"dev-utils", "dev-utils",
"enum-as-inner 0.6.0", "enum-as-inner 0.6.0",
"fxhash",
"generic-btree", "generic-btree",
"loro-common 1.0.0-beta.5", "loro-common 1.0.0-beta.5",
"loro-delta 1.0.0-beta.5", "loro-delta 1.0.0-beta.5",

View file

@ -1653,6 +1653,18 @@ impl LoroDoc {
Ok(()) Ok(())
} }
pub fn get_changed_containers_in(&self, id: ID, len: usize) -> FxHashSet<ContainerID> {
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 // FIXME: PERF: This method is quite slow because it iterates all the changes

View file

@ -328,6 +328,10 @@ impl<'a> RichOp<'a> {
self.peer self.peer
} }
pub fn container(&self) -> ContainerIdx {
self.op.container
}
pub fn timestamp(&self) -> i64 { pub fn timestamp(&self) -> i64 {
self.timestamp self.timestamp
} }

View file

@ -190,6 +190,8 @@ extern "C" {
pub type JsCommitOption; pub type JsCommitOption;
#[wasm_bindgen(typescript_type = "ImportStatus")] #[wasm_bindgen(typescript_type = "ImportStatus")]
pub type JsImportStatus; pub type JsImportStatus;
#[wasm_bindgen(typescript_type = "(change: ChangeMeta) => boolean")]
pub type JsTravelChangeFunction;
} }
mod observer { mod observer {
@ -606,7 +608,15 @@ impl LoroDoc {
/// @param ids - the changes to visit /// @param ids - the changes to visit
/// @param f - the callback function, return `true` to continue visiting, return `false` to stop /// @param f - the callback function, return `true` to continue visiting, return `false` to stop
#[wasm_bindgen(js_name = "travelChangeAncestors")] #[wasm_bindgen(js_name = "travelChangeAncestors")]
pub fn travel_change_ancestors(&self, ids: Vec<JsID>, f: js_sys::Function) -> JsResult<()> { pub fn travel_change_ancestors(
&self,
ids: Vec<JsID>,
f: JsTravelChangeFunction,
) -> JsResult<()> {
let f: js_sys::Function = match f.dyn_into::<js_sys::Function>() {
Ok(f) => f,
Err(_) => return Err(JsValue::from_str("Expected a function")),
};
let observer = observer::Observer::new(f); let observer = observer::Observer::new(f);
self.0 self.0
.travel_change_ancestors( .travel_change_ancestors(
@ -1652,6 +1662,30 @@ impl LoroDoc {
)?; )?;
Ok(JsValue::from(obj).into()) 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<Vec<JsContainerID>> {
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)] #[allow(unused)]
@ -3271,16 +3305,12 @@ impl LoroMovableList {
/// Get the last mover of the list item at the given position. /// Get the last mover of the list item at the given position.
pub fn getLastMoverAt(&self, pos: usize) -> Option<JsStrPeerID> { pub fn getLastMoverAt(&self, pos: usize) -> Option<JsStrPeerID> {
self.handler self.handler.get_last_mover_at(pos).map(peer_id_to_js)
.get_last_mover_at(pos)
.map(peer_id_to_js)
} }
/// Get the last editor of the list item at the given position. /// Get the last editor of the list item at the given position.
pub fn getLastEditorAt(&self, pos: usize) -> Option<JsStrPeerID> { pub fn getLastEditorAt(&self, pos: usize) -> Option<JsStrPeerID> {
self.handler self.handler.get_last_editor_at(pos).map(peer_id_to_js)
.get_last_editor_at(pos)
.map(peer_id_to_js)
} }
} }

View file

@ -16,6 +16,7 @@ import {
encodeFrontiers, encodeFrontiers,
decodeFrontiers, decodeFrontiers,
} from "../bundler/index"; } from "../bundler/index";
import { ContainerID } from "loro-wasm";
it("basic example", () => { it("basic example", () => {
const doc = new LoroDoc(); const doc = new LoroDoc();
@ -675,3 +676,16 @@ it("can push container to movable list", () => {
const map = list.pushContainer(new LoroMap()); const map = list.pushContainer(new LoroMap());
expect(list.toJSON()).toStrictEqual([{}]); 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,
]))
})

View file

@ -21,6 +21,7 @@ delta = { path = "../delta", package = "loro-delta", version = "1.0.0-beta.5" }
generic-btree = { version = "^0.10.5" } generic-btree = { version = "^0.10.5" }
enum-as-inner = { workspace = true } enum-as-inner = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
fxhash = { workspace = true }
[dev-dependencies] [dev-dependencies]
serde_json = "1.0.87" serde_json = "1.0.87"

View file

@ -2,6 +2,7 @@
#![warn(missing_docs)] #![warn(missing_docs)]
#![warn(missing_debug_implementations)] #![warn(missing_debug_implementations)]
use event::{DiffEvent, Subscriber}; use event::{DiffEvent, Subscriber};
use fxhash::FxHashSet;
use loro_common::InternalString; use loro_common::InternalString;
pub use loro_internal::cursor::CannotFindRelativePosition; pub use loro_internal::cursor::CannotFindRelativePosition;
use loro_internal::cursor::Cursor; use loro_internal::cursor::Cursor;
@ -848,6 +849,21 @@ impl LoroDoc {
pub fn is_shallow(&self) -> bool { pub fn is_shallow(&self) -> bool {
self.doc.is_shallow() 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<ContainerID> {
self.doc.get_changed_containers_in(id, len)
}
} }
/// It's used to prevent the user from implementing the trait directly. /// It's used to prevent the user from implementing the trait directly.

View file

@ -2190,3 +2190,35 @@ fn get_editor() {
let mov_id = tree.get_last_move_id(&node_0).unwrap(); let mov_id = tree.get_last_move_id(&node_0).unwrap();
assert_eq!(mov_id.peer, 2); 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"
}
})
)
}