Skip to content

Commit

Permalink
feat: find id spans between (#607)
Browse files Browse the repository at this point in the history
* feat: add a method to find id spans between frontiers

* feat(wasm): expose frontiers (from&to) in event

* test: fix test

* chore: changeset

* refactor: rename to findIdSpansBetween

* test: fix test err

* refactor: rename the fields of version vector diff

replace `left` and `right` with `retreat` and `forward`

* docs: add more details about find_id_spans_between
  • Loading branch information
zxch3n authored Jan 6, 2025
1 parent ac51ceb commit 8039e44
Show file tree
Hide file tree
Showing 14 changed files with 440 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .changeset/tame-spies-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"loro-crdt": minor
---

feat: find id spans between #607
4 changes: 2 additions & 2 deletions crates/loro-ffi/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ pub struct VersionVectorDiff {
impl From<loro::VersionVectorDiff> for VersionVectorDiff {
fn from(value: loro::VersionVectorDiff) -> Self {
Self {
left: value.left.into_iter().collect(),
right: value.right.into_iter().collect(),
left: value.retreat.into_iter().collect(),
right: value.forward.into_iter().collect(),
}
}
}
Expand Down
9 changes: 4 additions & 5 deletions crates/loro-internal/src/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ impl<T: Dag + ?Sized> DagUtils for T {

fn find_path(&self, from: &Frontiers, to: &Frontiers) -> VersionVectorDiff {
let mut ans = VersionVectorDiff::default();
trace!("find_path from={:?} to={:?}", from, to);
if from == to {
return ans;
}
Expand All @@ -112,12 +111,12 @@ impl<T: Dag + ?Sized> DagUtils for T {
let to_span = self.get(to).unwrap();
if from_span.id_start() == to_span.id_start() {
if from.counter < to.counter {
ans.right.insert(
ans.forward.insert(
from.peer,
CounterSpan::new(from.counter + 1, to.counter + 1),
);
} else {
ans.left.insert(
ans.retreat.insert(
from.peer,
CounterSpan::new(to.counter + 1, from.counter + 1),
);
Expand All @@ -128,7 +127,7 @@ impl<T: Dag + ?Sized> DagUtils for T {
if from_span.deps().len() == 1
&& to_span.contains_id(from_span.deps().as_single().unwrap())
{
ans.left.insert(
ans.retreat.insert(
from.peer,
CounterSpan::new(to.counter + 1, from.counter + 1),
);
Expand All @@ -138,7 +137,7 @@ impl<T: Dag + ?Sized> DagUtils for T {
if to_span.deps().len() == 1
&& from_span.contains_id(to_span.deps().as_single().unwrap())
{
ans.right.insert(
ans.forward.insert(
from.peer,
CounterSpan::new(from.counter + 1, to.counter + 1),
);
Expand Down
4 changes: 2 additions & 2 deletions crates/loro-internal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ pub use encoding::json_schema::json;
pub use fractional_index::FractionalIndex;
pub use loro_common::{loro_value, to_value};
pub use loro_common::{
Counter, CounterSpan, IdLp, IdSpan, Lamport, LoroEncodeError, LoroError, LoroResult,
LoroTreeError, PeerID, TreeID, ID,
Counter, CounterSpan, IdLp, IdSpan, IdSpanVector, Lamport, LoroEncodeError, LoroError,
LoroResult, LoroTreeError, PeerID, TreeID, ID,
};
pub use loro_common::{LoroBinaryValue, LoroListValue, LoroMapValue, LoroStringValue};
#[cfg(feature = "wasm")]
Expand Down
9 changes: 7 additions & 2 deletions crates/loro-internal/src/loro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use crate::{
IntoContainerId,
},
cursor::{AbsolutePosition, CannotFindRelativePosition, Cursor, PosQueryResult},
dag::Dag,
dag::{Dag, DagUtils},
diff_calc::DiffCalculator,
encoding::{
self, decode_snapshot, export_fast_snapshot, export_fast_updates,
Expand All @@ -49,7 +49,7 @@ use crate::{
txn::Transaction,
undo::DiffBatch,
utils::subscription::{SubscriberSetWithQueue, Subscription},
version::{shrink_frontiers, Frontiers, ImVersionVector, VersionRange},
version::{shrink_frontiers, Frontiers, ImVersionVector, VersionRange, VersionVectorDiff},
ChangeMeta, DocDiff, HandlerTrait, InternalString, ListHandler, LoroError, MapHandler,
VersionVector,
};
Expand Down Expand Up @@ -1635,6 +1635,11 @@ impl LoroDoc {
0
}
}

#[inline]
pub fn find_id_spans_between(&self, from: &Frontiers, to: &Frontiers) -> VersionVectorDiff {
self.oplog().try_lock().unwrap().dag.find_path(from, to)
}
}

#[derive(Debug, thiserror::Error)]
Expand Down
2 changes: 1 addition & 1 deletion crates/loro-internal/src/oplog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ impl OpLog {

let common_ancestors_vv = self.dag.frontiers_to_vv(&common_ancestors).unwrap();
// go from lca to merged_vv
let diff = common_ancestors_vv.diff(&merged_vv).right;
let diff = common_ancestors_vv.diff(&merged_vv).forward;
let mut iter = self.dag.iter_causal(common_ancestors, diff);
let mut node = iter.next();
let mut cur_cnt = 0;
Expand Down
32 changes: 18 additions & 14 deletions crates/loro-internal/src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,42 +300,46 @@ impl Deref for VersionVector {

#[derive(Default, Debug, PartialEq, Eq)]
pub struct VersionVectorDiff {
/// need to add these spans to move from right to left
pub left: IdSpanVector,
/// need to add these spans to move from left to right
pub right: IdSpanVector,
/// The spans that the `left` side needs to retreat to reach the `right` side
///
/// these spans are included in the left, but not in the right
pub retreat: IdSpanVector,
/// The spans that the `left` side needs to forward to reach the `right` side
///
/// these spans are included in the right, but not in the left
pub forward: IdSpanVector,
}

impl VersionVectorDiff {
#[inline]
pub fn merge_left(&mut self, span: IdSpan) {
merge(&mut self.left, span);
merge(&mut self.retreat, span);
}

#[inline]
pub fn merge_right(&mut self, span: IdSpan) {
merge(&mut self.right, span);
merge(&mut self.forward, span);
}

#[inline]
pub fn subtract_start_left(&mut self, span: IdSpan) {
subtract_start(&mut self.left, span);
subtract_start(&mut self.retreat, span);
}

#[inline]
pub fn subtract_start_right(&mut self, span: IdSpan) {
subtract_start(&mut self.right, span);
subtract_start(&mut self.forward, span);
}

pub fn get_id_spans_left(&self) -> impl Iterator<Item = IdSpan> + '_ {
self.left.iter().map(|(peer, span)| IdSpan {
self.retreat.iter().map(|(peer, span)| IdSpan {
peer: *peer,
counter: *span,
})
}

pub fn get_id_spans_right(&self) -> impl Iterator<Item = IdSpan> + '_ {
self.right.iter().map(|(peer, span)| IdSpan {
self.forward.iter().map(|(peer, span)| IdSpan {
peer: *peer,
counter: *span,
})
Expand Down Expand Up @@ -457,7 +461,7 @@ impl VersionVector {
if let Some(&rhs_counter) = rhs.get(client_id) {
match counter.cmp(&rhs_counter) {
Ordering::Less => {
ans.right.insert(
ans.forward.insert(
*client_id,
CounterSpan {
start: counter,
Expand All @@ -466,7 +470,7 @@ impl VersionVector {
);
}
Ordering::Greater => {
ans.left.insert(
ans.retreat.insert(
*client_id,
CounterSpan {
start: rhs_counter,
Expand All @@ -477,7 +481,7 @@ impl VersionVector {
Ordering::Equal => {}
}
} else {
ans.left.insert(
ans.retreat.insert(
*client_id,
CounterSpan {
start: 0,
Expand All @@ -488,7 +492,7 @@ impl VersionVector {
}
for (client_id, &rhs_counter) in rhs.iter() {
if !self.contains_key(client_id) {
ans.right.insert(
ans.forward.insert(
*client_id,
CounterSpan {
start: 0,
Expand Down
79 changes: 48 additions & 31 deletions crates/loro-wasm/deno_tests/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,60 @@
import init, { initSync, LoroDoc, LoroMap } from "../web/loro_wasm.js";
import init, { initSync, LoroDoc, LoroMap, LoroText } from "../web/index.js";
import { expect } from "npm:expect";

await init();

Deno.test("basic", () => {
const doc = new LoroDoc();
doc.getText("text").insert(0, "Hello, world!");
expect(doc.getText("text").toString()).toBe("Hello, world!");
const doc = new LoroDoc();
doc.getText("text").insert(0, "Hello, world!");
expect(doc.getText("text").toString()).toBe("Hello, world!");
});

Deno.test("fork when detached", () => {
const doc = new LoroDoc();
doc.setPeerId("0");
doc.getText("text").insert(0, "Hello, world!");
doc.checkout([{ peer: "0", counter: 5 }]);
const newDoc = doc.fork();
newDoc.setPeerId("1");
newDoc.getText("text").insert(6, " Alice!");
// ┌───────────────┐ ┌───────────────┐
// │ Hello, │◀─┬──│ world! │
// └───────────────┘ │ └───────────────┘
// │
// │ ┌───────────────┐
// └──│ Alice! │
// └───────────────┘
doc.import(newDoc.export({ mode: "update" }));
doc.checkoutToLatest();
console.log(doc.getText("text").toString()); // "Hello, world! Alice!"
const doc: LoroDoc = new LoroDoc();
doc.setPeerId("0");
doc.getText("text").insert(0, "Hello, world!");
doc.checkout([{ peer: "0", counter: 5 }]);
const newDoc = doc.fork();
newDoc.setPeerId("1");
newDoc.getText("text").insert(6, " Alice!");
// ┌───────────────┐ ┌───────────────┐
// │ Hello, │◀─┬──│ world! │
// └───────────────┘ │ └───────────────┘
// │
// │ ┌───────────────┐
// └──│ Alice! │
// └───────────────┘
doc.import(newDoc.export({ mode: "update" }));
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);
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);
});

Deno.test("toJsonWithReplacer", () => {
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
text.mark({ start: 0, end: 2 }, "bold", true);

// Use delta to represent text
// @ts-ignore: deno is not very clever
const json = doc.toJsonWithReplacer((key, value) => {
if (value instanceof LoroText) {
return value.toDelta();
}

return value;
});
});
Loading

0 comments on commit 8039e44

Please sign in to comment.