mirror of
https://github.com/loro-dev/loro.git
synced 2025-02-05 12:14:43 +00:00
feat: find id spans between (#607)
* 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
This commit is contained in:
parent
ac51ceb2f9
commit
8039e446a8
14 changed files with 440 additions and 64 deletions
5
.changeset/tame-spies-attack.md
Normal file
5
.changeset/tame-spies-attack.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"loro-crdt": minor
|
||||
---
|
||||
|
||||
feat: find id spans between #607
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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)]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
@ -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,
|
||||
|
@ -466,7 +470,7 @@ impl VersionVector {
|
|||
);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
ans.left.insert(
|
||||
ans.retreat.insert(
|
||||
*client_id,
|
||||
CounterSpan {
|
||||
start: rhs_counter,
|
||||
|
@ -477,7 +481,7 @@ impl VersionVector {
|
|||
Ordering::Equal => {}
|
||||
}
|
||||
} else {
|
||||
ans.left.insert(
|
||||
ans.retreat.insert(
|
||||
*client_id,
|
||||
CounterSpan {
|
||||
start: 0,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,10 +22,10 @@ use loro_internal::{
|
|||
id::{Counter, PeerID, TreeID, ID},
|
||||
json::JsonSchema,
|
||||
loro::{CommitOptions, ExportMode},
|
||||
loro_common::check_root_container_name,
|
||||
loro_common::{check_root_container_name, IdSpanVector},
|
||||
undo::{UndoItemMeta, UndoOrRedo},
|
||||
version::Frontiers,
|
||||
ContainerType, DiffEvent, FxHashMap, HandlerTrait, LoroDoc as LoroDocInner, LoroResult,
|
||||
ContainerType, DiffEvent, FxHashMap, HandlerTrait, IdSpan, LoroDoc as LoroDocInner, LoroResult,
|
||||
LoroValue, MovableListHandler, Subscription, TreeNodeWithChildren, TreeParentId,
|
||||
UndoManager as InnerUndoManager, VersionVector as InternalVersionVector,
|
||||
};
|
||||
|
@ -210,6 +210,8 @@ extern "C" {
|
|||
pub type JsLoroRootShallowValue;
|
||||
#[wasm_bindgen(typescript_type = "{ peer: PeerID, counter: number, length: number }")]
|
||||
pub type JsIdSpan;
|
||||
#[wasm_bindgen(typescript_type = "VersionVectorDiff")]
|
||||
pub type JsVersionVectorDiff;
|
||||
}
|
||||
|
||||
mod observer {
|
||||
|
@ -673,6 +675,90 @@ impl LoroDoc {
|
|||
.map_err(|e| JsValue::from(e.to_string()))
|
||||
}
|
||||
|
||||
/// Find the op id spans that between the `from` version and the `to` version.
|
||||
///
|
||||
/// You can combine it with `exportJsonInIdSpan` to get the changes between two versions.
|
||||
///
|
||||
/// You can use it to travel all the changes from `from` to `to`. `from` and `to` are frontiers,
|
||||
/// and they can be concurrent to each other. You can use it to find all the changes related to an event:
|
||||
///
|
||||
/// @example
|
||||
/// ```ts
|
||||
/// import { LoroDoc } from "loro-crdt";
|
||||
///
|
||||
/// const docA = new LoroDoc();
|
||||
/// docA.setPeerId("1");
|
||||
/// const docB = new LoroDoc();
|
||||
///
|
||||
/// docA.getText("text").update("Hello");
|
||||
/// docA.commit();
|
||||
/// const snapshot = docA.export({ mode: "snapshot" });
|
||||
/// let done = false;
|
||||
/// docB.subscribe(e => {
|
||||
/// const spans = docB.findIdSpansBetween(e.from, e.to);
|
||||
/// const changes = docB.exportJsonInIdSpan(spans.forward[0]);
|
||||
/// console.log(changes);
|
||||
/// // [{
|
||||
/// // id: "0@1",
|
||||
/// // timestamp: expect.any(Number),
|
||||
/// // deps: [],
|
||||
/// // lamport: 0,
|
||||
/// // msg: undefined,
|
||||
/// // ops: [{
|
||||
/// // container: "cid:root-text:Text",
|
||||
/// // counter: 0,
|
||||
/// // content: {
|
||||
/// // type: "insert",
|
||||
/// // pos: 0,
|
||||
/// // text: "Hello"
|
||||
/// // }
|
||||
/// // }]
|
||||
/// // }]
|
||||
/// });
|
||||
/// docB.import(snapshot);
|
||||
/// ```
|
||||
#[wasm_bindgen(js_name = "findIdSpansBetween")]
|
||||
pub fn find_id_spans_between(
|
||||
&self,
|
||||
from: Vec<JsID>,
|
||||
to: Vec<JsID>,
|
||||
) -> JsResult<JsVersionVectorDiff> {
|
||||
fn id_span_to_js(v: IdSpan) -> JsValue {
|
||||
let obj = Object::new();
|
||||
js_sys::Reflect::set(&obj, &"peer".into(), &JsValue::from(v.peer.to_string())).unwrap();
|
||||
js_sys::Reflect::set(&obj, &"counter".into(), &JsValue::from(v.counter.start)).unwrap();
|
||||
js_sys::Reflect::set(
|
||||
&obj,
|
||||
&"length".into(),
|
||||
&JsValue::from(v.counter.end - v.counter.start),
|
||||
)
|
||||
.unwrap();
|
||||
obj.into()
|
||||
}
|
||||
|
||||
fn id_span_vector_to_js(v: IdSpanVector) -> JsValue {
|
||||
let arr = Array::new();
|
||||
for (peer, span) in v.iter() {
|
||||
let v = id_span_to_js(IdSpan {
|
||||
peer: *peer,
|
||||
counter: *span,
|
||||
});
|
||||
arr.push(&v);
|
||||
}
|
||||
arr.into()
|
||||
}
|
||||
|
||||
let from = ids_to_frontiers(from)?;
|
||||
let to = ids_to_frontiers(to)?;
|
||||
let diff = self.0.find_id_spans_between(&from, &to);
|
||||
let obj = Object::new();
|
||||
|
||||
js_sys::Reflect::set(&obj, &"retreat".into(), &id_span_vector_to_js(diff.retreat)).unwrap();
|
||||
js_sys::Reflect::set(&obj, &"forward".into(), &id_span_vector_to_js(diff.forward)).unwrap();
|
||||
let v: JsValue = obj.into();
|
||||
Ok(v.into())
|
||||
}
|
||||
|
||||
/// Checkout the `DocState` to a specific version.
|
||||
///
|
||||
/// > The document becomes detached during a `checkout` operation.
|
||||
|
@ -1401,7 +1487,7 @@ impl LoroDoc {
|
|||
///
|
||||
/// @example
|
||||
/// ```ts
|
||||
/// import { LoroDoc, LoroText } from "loro-crdt";
|
||||
/// import { LoroDoc, LoroText, LoroMap } from "loro-crdt";
|
||||
///
|
||||
/// const doc = new LoroDoc();
|
||||
/// const list = doc.getList("list");
|
||||
|
@ -1841,6 +1927,18 @@ fn diff_event_to_js_value(event: DiffEvent, doc: &Arc<LoroDocInner>) -> JsValue
|
|||
}
|
||||
|
||||
Reflect::set(&obj, &"events".into(), &events.into()).unwrap();
|
||||
Reflect::set(
|
||||
&obj,
|
||||
&"from".into(),
|
||||
&frontiers_to_ids(&event.event_meta.from).into(),
|
||||
)
|
||||
.unwrap();
|
||||
Reflect::set(
|
||||
&obj,
|
||||
&"to".into(),
|
||||
&frontiers_to_ids(&event.event_meta.to).into(),
|
||||
)
|
||||
.unwrap();
|
||||
obj.into()
|
||||
}
|
||||
|
||||
|
@ -2745,7 +2843,7 @@ impl LoroMap {
|
|||
///
|
||||
/// @example
|
||||
/// ```ts
|
||||
/// import { LoroDoc } from "loro-crdt";
|
||||
/// import { LoroDoc, LoroText } from "loro-crdt";
|
||||
///
|
||||
/// const doc = new LoroDoc();
|
||||
/// doc.setPeerId("1");
|
||||
|
@ -4997,7 +5095,7 @@ interface LoroDoc {
|
|||
* const doc = new LoroDoc();
|
||||
* const text = doc.getText("text");
|
||||
* text.insert(0, "Hello");
|
||||
* text.mark(0, 2, {bold: true});
|
||||
* text.mark({ start: 0, end: 2 }, "bold", true);
|
||||
*
|
||||
* // Use delta to represent text
|
||||
* const json = doc.toJsonWithReplacer((key, value) => {
|
||||
|
@ -5089,6 +5187,27 @@ export type Value =
|
|||
| Value[]
|
||||
| undefined;
|
||||
|
||||
export type IdSpan = {
|
||||
peer: PeerID,
|
||||
counter: number,
|
||||
length: number,
|
||||
}
|
||||
|
||||
export type VersionVectorDiff = {
|
||||
/**
|
||||
* The spans that the `from` side needs to retreat to reach the `to` side
|
||||
*
|
||||
* These spans are included in the `from`, but not in the `to`
|
||||
*/
|
||||
retreat: IdSpan[],
|
||||
/**
|
||||
* The spans that the `from` side needs to forward to reach the `to` side
|
||||
*
|
||||
* These spans are included in the `to`, but not in the `from`
|
||||
*/
|
||||
forward: IdSpan[],
|
||||
}
|
||||
|
||||
export type UndoConfig = {
|
||||
mergeInterval?: number,
|
||||
maxUndoSteps?: number,
|
||||
|
@ -5453,6 +5572,8 @@ export interface LoroEventBatch {
|
|||
*/
|
||||
currentTarget?: ContainerID;
|
||||
events: LoroEvent[];
|
||||
from: Frontiers;
|
||||
to: Frontiers;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
Frontiers,
|
||||
encodeFrontiers,
|
||||
decodeFrontiers,
|
||||
OpId,
|
||||
} from "../bundler/index";
|
||||
import { ContainerID } from "loro-wasm";
|
||||
|
||||
|
@ -1023,3 +1024,130 @@ it("export json in id span #602", () => {
|
|||
expect(changes).toStrictEqual([]);
|
||||
}
|
||||
})
|
||||
|
||||
it("find spans between versions", () => {
|
||||
const doc = new LoroDoc();
|
||||
doc.setPeerId("1");
|
||||
|
||||
// Make some changes to create version history
|
||||
doc.getText("text").insert(0, "Hello");
|
||||
doc.commit({ message: "a" });
|
||||
const f1 = doc.oplogFrontiers();
|
||||
|
||||
doc.getText("text").insert(5, " World");
|
||||
doc.commit({ message: "b" });
|
||||
const f2 = doc.oplogFrontiers();
|
||||
|
||||
// Test finding spans between frontiers (f1 -> f2)
|
||||
let diff = doc.findIdSpansBetween(f1, f2);
|
||||
expect(diff.retreat).toHaveLength(0); // No changes needed to go from f2 to f1
|
||||
expect(diff.forward).toHaveLength(1); // One change needed to go from f1 to f2
|
||||
expect(diff.forward[0]).toEqual({
|
||||
peer: "1",
|
||||
counter: 5,
|
||||
length: 6,
|
||||
});
|
||||
|
||||
// Test empty frontiers
|
||||
const emptyFrontiers: OpId[] = [];
|
||||
diff = doc.findIdSpansBetween(emptyFrontiers, f2);
|
||||
expect(diff.retreat).toHaveLength(0); // No changes needed to go from f2 to empty
|
||||
expect(diff.forward).toHaveLength(1); // One change needed to go from empty to f2
|
||||
expect(diff.forward[0]).toEqual({
|
||||
peer: "1",
|
||||
counter: 0,
|
||||
length: 11,
|
||||
});
|
||||
|
||||
// Test with multiple peers
|
||||
const doc2 = new LoroDoc();
|
||||
doc2.setPeerId("2");
|
||||
doc2.getText("text").insert(0, "Hi");
|
||||
doc2.commit();
|
||||
doc.import(doc2.export({ mode: "snapshot" }));
|
||||
const f3 = doc.oplogFrontiers();
|
||||
|
||||
// Test finding spans between f2 and f3
|
||||
diff = doc.findIdSpansBetween(f2, f3);
|
||||
expect(diff.retreat).toHaveLength(0); // No changes needed to go from f3 to f2
|
||||
expect(diff.forward).toHaveLength(1); // One change needed to go from f2 to f3
|
||||
expect(diff.forward[0]).toEqual({
|
||||
peer: "2",
|
||||
counter: 0,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
// Test spans in both directions between f1 and f3
|
||||
diff = doc.findIdSpansBetween(f1, f3);
|
||||
expect(diff.retreat).toHaveLength(0); // No changes needed to go from f3 to f1
|
||||
expect(diff.forward).toHaveLength(2); // Two changes needed to go from f1 to f3
|
||||
const forwardSpans = new Map(diff.forward.map(span => [span.peer, span]));
|
||||
expect(forwardSpans.get("1")).toEqual({
|
||||
peer: "1",
|
||||
counter: 5,
|
||||
length: 6,
|
||||
});
|
||||
expect(forwardSpans.get("2")).toEqual({
|
||||
peer: "2",
|
||||
counter: 0,
|
||||
length: 2,
|
||||
});
|
||||
|
||||
// Test spans in reverse direction (f3 -> f1)
|
||||
diff = doc.findIdSpansBetween(f3, f1);
|
||||
expect(diff.forward).toHaveLength(0); // No changes needed to go from f3 to f1
|
||||
expect(diff.retreat).toHaveLength(2); // Two changes needed to go from f1 to f3
|
||||
const retreatSpans = new Map(diff.retreat.map(span => [span.peer, span]));
|
||||
expect(retreatSpans.get("1")).toEqual({
|
||||
peer: "1",
|
||||
counter: 5,
|
||||
length: 6,
|
||||
});
|
||||
expect(retreatSpans.get("2")).toEqual({
|
||||
peer: "2",
|
||||
counter: 0,
|
||||
length: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("can travel changes from event", async () => {
|
||||
const docA = new LoroDoc();
|
||||
docA.setPeerId("1");
|
||||
const docB = new LoroDoc();
|
||||
|
||||
docA.getText("text").update("Hello");
|
||||
docA.commit();
|
||||
const snapshot = docA.export({ mode: "snapshot" });
|
||||
let done = false;
|
||||
docB.subscribe(e => {
|
||||
const spans = docB.findIdSpansBetween(e.from, e.to);
|
||||
expect(spans.retreat).toHaveLength(0);
|
||||
expect(spans.forward).toHaveLength(1);
|
||||
expect(spans.forward[0]).toEqual({
|
||||
peer: "1",
|
||||
counter: 0,
|
||||
length: 5,
|
||||
});
|
||||
const changes = docB.exportJsonInIdSpan(spans.forward[0]);
|
||||
expect(changes).toStrictEqual([{
|
||||
id: "0@1",
|
||||
timestamp: expect.any(Number),
|
||||
deps: [],
|
||||
lamport: 0,
|
||||
msg: undefined,
|
||||
ops: [{
|
||||
container: "cid:root-text:Text",
|
||||
counter: 0,
|
||||
content: {
|
||||
type: "insert",
|
||||
pos: 0,
|
||||
text: "Hello"
|
||||
}
|
||||
}]
|
||||
}]);
|
||||
done = true;
|
||||
});
|
||||
docB.import(snapshot);
|
||||
await Promise.resolve();
|
||||
expect(done).toBe(true);
|
||||
})
|
||||
|
|
|
@ -937,6 +937,12 @@ impl LoroDoc {
|
|||
pub fn get_changed_containers_in(&self, id: ID, len: usize) -> FxHashSet<ContainerID> {
|
||||
self.doc.get_changed_containers_in(id, len)
|
||||
}
|
||||
|
||||
/// Find the operation id spans that between the `from` version and the `to` version.
|
||||
#[inline]
|
||||
pub fn find_id_spans_between(&self, from: &Frontiers, to: &Frontiers) -> VersionVectorDiff {
|
||||
self.doc.find_id_spans_between(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
/// It's used to prevent the user from implementing the trait directly.
|
||||
|
|
|
@ -2653,3 +2653,90 @@ fn test_export_json_in_id_span_with_complex_operations() -> LoroResult<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_spans_between() -> LoroResult<()> {
|
||||
let doc = LoroDoc::new();
|
||||
doc.set_peer_id(1)?;
|
||||
|
||||
// Make some changes to create version history
|
||||
doc.get_text("text").insert(0, "Hello")?;
|
||||
doc.set_next_commit_message("a");
|
||||
doc.commit();
|
||||
let f1 = doc.state_frontiers();
|
||||
|
||||
doc.get_text("text").insert(5, " World")?;
|
||||
doc.set_next_commit_message("b");
|
||||
doc.commit();
|
||||
let f2 = doc.state_frontiers();
|
||||
|
||||
// Test finding spans between frontiers (f1 -> f2)
|
||||
let diff = doc.find_id_spans_between(&f1, &f2);
|
||||
assert!(diff.retreat.is_empty()); // No changes needed to go from f2 to f1
|
||||
assert_eq!(diff.forward.len(), 1); // One change needed to go from f1 to f2
|
||||
let span = diff.forward.get(&1).unwrap();
|
||||
assert_eq!(span.start, 5); // First change ends at counter 3
|
||||
assert_eq!(span.end, 11); // Second change ends at counter 6
|
||||
|
||||
// Test empty frontiers
|
||||
let empty_frontiers = Frontiers::default();
|
||||
let diff = doc.find_id_spans_between(&empty_frontiers, &f2);
|
||||
assert!(diff.retreat.is_empty()); // No changes needed to go from f2 to empty
|
||||
assert_eq!(diff.forward.len(), 1); // One change needed to go from empty to f2
|
||||
let span = diff.forward.get(&1).unwrap();
|
||||
assert_eq!(span.start, 0); // From beginning
|
||||
assert_eq!(span.end, 11); // To latest change
|
||||
|
||||
// Test with multiple peers
|
||||
let doc2 = LoroDoc::new();
|
||||
doc2.set_peer_id(2)?;
|
||||
doc2.get_text("text").insert(0, "Hi")?;
|
||||
doc2.commit();
|
||||
doc.import(&doc2.export_snapshot())?;
|
||||
let f3 = doc.state_frontiers();
|
||||
|
||||
// Test finding spans between f2 and f3
|
||||
let diff = doc.find_id_spans_between(&f2, &f3);
|
||||
assert!(diff.retreat.is_empty()); // No changes needed to go from f3 to f2
|
||||
assert_eq!(diff.forward.len(), 1); // One change needed to go from f2 to f3
|
||||
let span = diff.forward.get(&2).unwrap();
|
||||
assert_eq!(span.start, 0);
|
||||
assert_eq!(span.end, 2);
|
||||
|
||||
// Test spans in both directions between f1 and f3
|
||||
let diff = doc.find_id_spans_between(&f1, &f3);
|
||||
assert!(diff.retreat.is_empty()); // No changes needed to go from f3 to f1
|
||||
assert_eq!(diff.forward.len(), 2); // Two changes needed to go from f1 to f3
|
||||
for (peer, span) in diff.forward.iter() {
|
||||
match peer {
|
||||
1 => {
|
||||
assert_eq!(span.start, 5);
|
||||
assert_eq!(span.end, 11);
|
||||
}
|
||||
2 => {
|
||||
assert_eq!(span.start, 0);
|
||||
assert_eq!(span.end, 2);
|
||||
}
|
||||
_ => panic!("Unexpected peer ID"),
|
||||
}
|
||||
}
|
||||
|
||||
let diff = doc.find_id_spans_between(&f3, &f1);
|
||||
assert!(diff.forward.is_empty()); // No changes needed to go from f3 to f1
|
||||
assert_eq!(diff.retreat.len(), 2); // Two changes needed to go from f1 to f3
|
||||
for (peer, span) in diff.retreat.iter() {
|
||||
match peer {
|
||||
1 => {
|
||||
assert_eq!(span.start, 5);
|
||||
assert_eq!(span.end, 11);
|
||||
}
|
||||
2 => {
|
||||
assert_eq!(span.start, 0);
|
||||
assert_eq!(span.end, 2);
|
||||
}
|
||||
_ => panic!("Unexpected peer ID"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"jsr:@std/toml@^1.0.1": "1.0.1",
|
||||
"npm:expect@29.7.0": "29.7.0",
|
||||
"npm:loro-crdt@1.0.7": "1.0.7",
|
||||
"npm:loro-crdt@1.2.1": "1.2.1"
|
||||
"npm:loro-crdt@1.2.1": "1.2.1",
|
||||
"npm:loro-crdt@1.2.7": "1.2.7"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/collections@1.0.5": {
|
||||
|
@ -260,6 +261,9 @@
|
|||
"loro-crdt@1.2.1": {
|
||||
"integrity": "sha512-KP06MrpH2Yh2Tl7VHSCS78/ko575nVS5tEp/0NsIf4Q8XfJ0on+Zj6SVHnKpOq0iwKB7xt/mU2yVroe5jPr4SQ=="
|
||||
},
|
||||
"loro-crdt@1.2.7": {
|
||||
"integrity": "sha512-eqVxUtLr7YBv5QaSW4obs01HIMzzJGfBC6MVnKHzMAEdaRitTP6Dj+BRvLFPqLEJthRtjohKez8wZgwzAUDiTg=="
|
||||
},
|
||||
"loro-wasm@1.0.7": {
|
||||
"integrity": "sha512-WFIpGGzc6I7zRMDoRGxa3AHhno7gVnOgwqcrTfmpKWOtktZQ7BvhIV4kYgsdyuIBcMSrQEJTfOY/80xQSjUKTw=="
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const LORO_VERSION = "1.2.1";
|
||||
const LORO_VERSION = "1.2.7";
|
||||
|
||||
export interface CodeBlock {
|
||||
filename: string;
|
||||
|
|
Loading…
Reference in a new issue