feat: export diff batch to ffi

This commit is contained in:
Leon Zhao 2025-01-15 11:53:20 +08:00
parent 07500dab34
commit db2353192a
5 changed files with 300 additions and 41 deletions

View file

@ -34,6 +34,7 @@
"pointee", "pointee",
"reparent", "reparent",
"RUSTFLAGS", "RUSTFLAGS",
"serde",
"smstring", "smstring",
"sstable", "sstable",
"Stewen", "Stewen",

View file

@ -13,10 +13,10 @@ use loro::{
}; };
use crate::{ use crate::{
event::{DiffEvent, Subscriber}, event::{DiffBatch, DiffEvent, Subscriber},
AbsolutePosition, Configure, ContainerID, ContainerIdLike, Cursor, Frontiers, Index, AbsolutePosition, Configure, ContainerID, ContainerIdLike, Cursor, Frontiers, Index,
LoroCounter, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, LoroValue, StyleConfigMap, LoroCounter, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, LoroValue, StyleConfigMap,
ValueOrContainer, VersionVector, ValueOrContainer, VersionVector, VersionVectorDiff,
}; };
/// Decodes the metadata for an imported blob from the provided bytes. /// Decodes the metadata for an imported blob from the provided bytes.
@ -88,10 +88,13 @@ impl LoroDoc {
self.doc.set_record_timestamp(record); self.doc.set_record_timestamp(record);
} }
/// Set the interval of mergeable changes, in milliseconds. /// Set the interval of mergeable changes, **in seconds**.
/// ///
/// If two continuous local changes are within the interval, they will be merged into one change. /// If two continuous local changes are within the interval, they will be merged into one change.
/// The default value is 1000 seconds. /// The default value is 1000 seconds.
///
/// By default, we record timestamps in seconds for each change. So if the merge interval is 1, and changes A and B
/// have timestamps of 3 and 4 respectively, then they will be merged into one change
#[inline] #[inline]
pub fn set_change_merge_interval(&self, interval: i64) { pub fn set_change_merge_interval(&self, interval: i64) {
self.doc.set_change_merge_interval(interval); self.doc.set_change_merge_interval(interval);
@ -249,6 +252,8 @@ impl LoroDoc {
} }
/// Set commit message for the current uncommitted changes /// Set commit message for the current uncommitted changes
///
/// It will be persisted.
pub fn set_next_commit_message(&self, msg: &str) { pub fn set_next_commit_message(&self, msg: &str) {
self.doc.set_next_commit_message(msg) self.doc.set_next_commit_message(msg)
} }
@ -291,6 +296,32 @@ impl LoroDoc {
serde_json::to_string(&json).unwrap() serde_json::to_string(&json).unwrap()
} }
/// Export the current state with json-string format of the document, without peer compression.
///
/// Compared to [`export_json_updates`], this method does not compress the peer IDs in the updates.
/// So the operations are easier to be processed by application code.
#[inline]
pub fn export_json_updates_without_peer_compression(
&self,
start_vv: &VersionVector,
end_vv: &VersionVector,
) -> String {
let json = self
.doc
.export_json_updates_without_peer_compression(&start_vv.into(), &end_vv.into());
serde_json::to_string(&json).unwrap()
}
/// Export the readable [`Change`]s in the given [`IdSpan`]
// TODO: swift type
pub fn export_json_in_id_span(&self, id_span: IdSpan) -> Vec<String> {
self.doc
.export_json_in_id_span(id_span)
.into_iter()
.map(|x| serde_json::to_string(&x).unwrap())
.collect()
}
// TODO: add export method // TODO: add export method
/// Export all the ops not included in the given `VersionVector` /// Export all the ops not included in the given `VersionVector`
#[inline] #[inline]
@ -464,6 +495,58 @@ impl LoroDoc {
.map(|x| Arc::new(x) as Arc<dyn ValueOrContainer>) .map(|x| Arc::new(x) as Arc<dyn ValueOrContainer>)
} }
///
/// The path can be specified in different ways depending on the container type:
///
/// For Tree:
/// 1. Using node IDs: `tree/{node_id}/property`
/// 2. Using indices: `tree/0/1/property`
///
/// For List and MovableList:
/// - Using indices: `list/0` or `list/1/property`
///
/// For Map:
/// - Using keys: `map/key` or `map/nested/property`
///
/// For tree structures, index-based paths follow depth-first traversal order.
/// The indices start from 0 and represent the position of a node among its siblings.
///
/// # Examples
/// ```
/// # use loro::{LoroDoc, LoroValue};
/// let doc = LoroDoc::new();
///
/// // Tree example
/// let tree = doc.get_tree("tree");
/// let root = tree.create(None).unwrap();
/// tree.get_meta(root).unwrap().insert("name", "root").unwrap();
/// // Access tree by ID or index
/// let name1 = doc.get_by_str_path(&format!("tree/{}/name", root)).unwrap().into_value().unwrap();
/// let name2 = doc.get_by_str_path("tree/0/name").unwrap().into_value().unwrap();
/// assert_eq!(name1, name2);
///
/// // List example
/// let list = doc.get_list("list");
/// list.insert(0, "first").unwrap();
/// list.insert(1, "second").unwrap();
/// // Access list by index
/// let item = doc.get_by_str_path("list/0");
/// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "first".into());
///
/// // Map example
/// let map = doc.get_map("map");
/// map.insert("key", "value").unwrap();
/// // Access map by key
/// let value = doc.get_by_str_path("map/key");
/// assert_eq!(value.unwrap().into_value().unwrap().into_string().unwrap(), "value".into());
///
/// // MovableList example
/// let mlist = doc.get_movable_list("mlist");
/// mlist.insert(0, "item").unwrap();
/// // Access movable list by index
/// let item = doc.get_by_str_path("mlist/0");
/// assert_eq!(item.unwrap().into_value().unwrap().into_string().unwrap(), "item".into());
/// ```
pub fn get_by_str_path(&self, path: &str) -> Option<Arc<dyn ValueOrContainer>> { pub fn get_by_str_path(&self, path: &str) -> Option<Arc<dyn ValueOrContainer>> {
self.doc self.doc
.get_by_str_path(path) .get_by_str_path(path)
@ -616,6 +699,38 @@ impl LoroDoc {
pub fn get_pending_txn_len(&self) -> u32 { pub fn get_pending_txn_len(&self) -> u32 {
self.doc.get_pending_txn_len() as u32 self.doc.get_pending_txn_len() as u32
} }
/// 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.into(), &to.into())
.into()
}
/// Revert the current document state back to the target version
///
/// Internally, it will generate a series of local operations that can revert the
/// current doc to the target version. It will calculate the diff between the current
/// state and the target state, and apply the diff to the current state.
#[inline]
pub fn revert_to(&self, version: &Frontiers) -> LoroResult<()> {
self.doc.revert_to(&version.into())
}
/// Apply a diff to the current document state.
///
/// Internally, it will apply the diff to the current state.
#[inline]
pub fn apply_diff(&self, diff: DiffBatch) -> LoroResult<()> {
self.doc.apply_diff(diff.into())
}
/// Calculate the diff between two versions
#[inline]
pub fn diff(&self, a: &Frontiers, b: &Frontiers) -> LoroResult<DiffBatch> {
self.doc.diff(&a.into(), &b.into()).map(|x| x.into())
}
} }
pub trait ChangeAncestorsTraveler: Sync + Send { pub trait ChangeAncestorsTraveler: Sync + Send {

View file

@ -1,8 +1,14 @@
use std::{collections::HashMap, sync::Arc}; use std::{
borrow::Cow,
collections::HashMap,
sync::{Arc, Mutex},
};
use loro::{EventTriggerKind, TreeID}; use loro::{EventTriggerKind, FractionalIndex, TreeID};
use crate::{ContainerID, LoroValue, TreeParentId, ValueOrContainer}; use crate::{
convert_trait_to_v_or_container, ContainerID, LoroValue, TreeParentId, ValueOrContainer,
};
pub trait Subscriber: Sync + Send { pub trait Subscriber: Sync + Send {
fn on_diff(&self, diff: DiffEvent); fn on_diff(&self, diff: DiffEvent);
@ -135,6 +141,76 @@ impl From<loro::TextDelta> for TextDelta {
} }
} }
impl From<ListDiffItem> for loro::event::ListDiffItem {
fn from(value: ListDiffItem) -> Self {
match value {
ListDiffItem::Insert { insert, is_move } => loro::event::ListDiffItem::Insert {
insert: insert
.into_iter()
.map(convert_trait_to_v_or_container)
.collect(),
is_move,
},
ListDiffItem::Delete { delete } => loro::event::ListDiffItem::Delete {
delete: delete as usize,
},
ListDiffItem::Retain { retain } => loro::event::ListDiffItem::Retain {
retain: retain as usize,
},
}
}
}
impl From<MapDelta> for loro::event::MapDelta<'static> {
fn from(value: MapDelta) -> Self {
loro::event::MapDelta {
updated: value
.updated
.into_iter()
.map(|(k, v)| (Cow::Owned(k), v.map(convert_trait_to_v_or_container)))
.collect(),
}
}
}
impl From<TreeDiffItem> for loro::TreeDiffItem {
fn from(value: TreeDiffItem) -> Self {
let target: TreeID = value.target;
let action = match value.action {
TreeExternalDiff::Create {
parent,
index,
fractional_index,
} => loro::TreeExternalDiff::Create {
parent: parent.into(),
index: index as usize,
position: FractionalIndex::from_hex_string(fractional_index),
},
TreeExternalDiff::Move {
parent,
index,
fractional_index,
old_parent,
old_index,
} => loro::TreeExternalDiff::Move {
parent: parent.into(),
index: index as usize,
position: FractionalIndex::from_hex_string(fractional_index),
old_parent: old_parent.into(),
old_index: old_index as usize,
},
TreeExternalDiff::Delete {
old_parent,
old_index,
} => loro::TreeExternalDiff::Delete {
old_parent: old_parent.into(),
old_index: old_index as usize,
},
};
loro::TreeDiffItem { target, action }
}
}
pub enum ListDiffItem { pub enum ListDiffItem {
/// Insert a new element into the list. /// Insert a new element into the list.
Insert { Insert {
@ -263,40 +339,9 @@ impl From<&loro::event::Diff<'_>> for Diff {
} }
Diff::List { diff: ans } Diff::List { diff: ans }
} }
loro::event::Diff::Text(t) => { loro::event::Diff::Text(t) => Diff::Text {
let mut ans = Vec::new(); diff: t.iter().map(|i| i.clone().into()).collect(),
for item in t.iter() { },
match item {
loro::TextDelta::Retain { retain, attributes } => {
ans.push(TextDelta::Retain {
retain: *retain as u32,
attributes: attributes.as_ref().map(|a| {
a.iter()
.map(|(k, v)| (k.to_string(), v.clone().into()))
.collect()
}),
});
}
loro::TextDelta::Insert { insert, attributes } => {
ans.push(TextDelta::Insert {
insert: insert.to_string(),
attributes: attributes.as_ref().map(|a| {
a.iter()
.map(|(k, v)| (k.to_string(), v.clone().into()))
.collect()
}),
});
}
loro::TextDelta::Delete { delete } => {
ans.push(TextDelta::Delete {
delete: *delete as u32,
});
}
}
}
Diff::Text { diff: ans }
}
loro::event::Diff::Map(m) => { loro::event::Diff::Map(m) => {
let mut updated = HashMap::new(); let mut updated = HashMap::new();
for (key, value) in m.updated.iter() { for (key, value) in m.updated.iter() {
@ -359,3 +404,60 @@ impl From<&loro::event::Diff<'_>> for Diff {
} }
} }
} }
impl From<Diff> for loro::event::Diff<'static> {
fn from(value: Diff) -> Self {
match value {
Diff::List { diff } => {
loro::event::Diff::List(diff.into_iter().map(|i| i.into()).collect())
}
Diff::Text { diff } => {
loro::event::Diff::Text(diff.into_iter().map(|i| i.into()).collect())
}
Diff::Map { diff } => loro::event::Diff::Map(diff.into()),
Diff::Tree { diff } => loro::event::Diff::Tree(Cow::Owned(loro::TreeDiff {
diff: diff.diff.into_iter().map(|i| i.into()).collect(),
})),
Diff::Counter { diff } => loro::event::Diff::Counter(diff),
Diff::Unknown => loro::event::Diff::Unknown,
}
}
}
#[derive(Debug, Default)]
pub struct DiffBatch(Mutex<loro::event::DiffBatch>);
impl DiffBatch {
pub fn new() -> Self {
Self(Default::default())
}
pub fn push(&self, cid: ContainerID, diff: Diff) -> Option<Diff> {
let mut batch = self.0.lock().unwrap();
if let Err(diff) = batch.push(cid.into(), diff.into()) {
Some((&diff).into())
} else {
None
}
}
pub fn diffs(&self) -> Vec<(ContainerID, Diff)> {
let batch = self.0.lock().unwrap();
batch
.iter()
.map(|(id, diff)| (id.into(), diff.into()))
.collect()
}
}
impl From<DiffBatch> for loro::event::DiffBatch {
fn from(value: DiffBatch) -> Self {
value.0.into_inner().unwrap()
}
}
impl From<loro::event::DiffBatch> for DiffBatch {
fn from(value: loro::event::DiffBatch) -> Self {
Self(Mutex::new(value))
}
}

View file

@ -41,6 +41,7 @@ pub trait ValueOrContainer: Send + Sync {
fn is_value(&self) -> bool; fn is_value(&self) -> bool;
fn is_container(&self) -> bool; fn is_container(&self) -> bool;
fn as_value(&self) -> Option<LoroValue>; fn as_value(&self) -> Option<LoroValue>;
fn container_type(&self) -> Option<ContainerType>;
fn as_container(&self) -> Option<ContainerID>; fn as_container(&self) -> Option<ContainerID>;
fn as_loro_list(&self) -> Option<Arc<LoroList>>; fn as_loro_list(&self) -> Option<Arc<LoroList>>;
fn as_loro_text(&self) -> Option<Arc<LoroText>>; fn as_loro_text(&self) -> Option<Arc<LoroText>>;
@ -48,6 +49,7 @@ pub trait ValueOrContainer: Send + Sync {
fn as_loro_movable_list(&self) -> Option<Arc<LoroMovableList>>; fn as_loro_movable_list(&self) -> Option<Arc<LoroMovableList>>;
fn as_loro_tree(&self) -> Option<Arc<LoroTree>>; fn as_loro_tree(&self) -> Option<Arc<LoroTree>>;
fn as_loro_counter(&self) -> Option<Arc<LoroCounter>>; fn as_loro_counter(&self) -> Option<Arc<LoroCounter>>;
fn as_unknown(&self) -> Option<Arc<LoroUnknown>>;
} }
impl ValueOrContainer for loro::ValueOrContainer { impl ValueOrContainer for loro::ValueOrContainer {
@ -59,12 +61,17 @@ impl ValueOrContainer for loro::ValueOrContainer {
loro::ValueOrContainer::is_container(self) loro::ValueOrContainer::is_container(self)
} }
fn container_type(&self) -> Option<ContainerType> {
loro::ValueOrContainer::as_container(self).map(|c| c.id().container_type().into())
}
fn as_value(&self) -> Option<LoroValue> { fn as_value(&self) -> Option<LoroValue> {
loro::ValueOrContainer::as_value(self) loro::ValueOrContainer::as_value(self)
.cloned() .cloned()
.map(LoroValue::from) .map(LoroValue::from)
} }
// TODO: pass Container to Swift
fn as_container(&self) -> Option<ContainerID> { fn as_container(&self) -> Option<ContainerID> {
loro::ValueOrContainer::as_container(self).map(|c| c.id().into()) loro::ValueOrContainer::as_container(self).map(|c| c.id().into())
} }
@ -122,4 +129,37 @@ impl ValueOrContainer for loro::ValueOrContainer {
_ => None, _ => None,
} }
} }
fn as_unknown(&self) -> Option<Arc<LoroUnknown>> {
match self {
loro::ValueOrContainer::Container(Container::Unknown(c)) => {
Some(Arc::new(LoroUnknown { unknown: c.clone() }))
}
_ => None,
}
}
}
fn convert_trait_to_v_or_container<T: AsRef<dyn ValueOrContainer>>(i: T) -> loro::ValueOrContainer {
let v = i.as_ref();
if v.is_value() {
loro::ValueOrContainer::Value(v.as_value().unwrap().into())
} else {
let container = match v.container_type().unwrap() {
ContainerType::List => Container::List((*v.as_loro_list().unwrap()).clone().list),
ContainerType::Text => Container::Text((*v.as_loro_text().unwrap()).clone().text),
ContainerType::Map => Container::Map((*v.as_loro_map().unwrap()).clone().map),
ContainerType::MovableList => {
Container::MovableList((*v.as_loro_movable_list().unwrap()).clone().list)
}
ContainerType::Tree => Container::Tree((*v.as_loro_tree().unwrap()).clone().tree),
ContainerType::Counter => {
Container::Counter((*v.as_loro_counter().unwrap()).clone().counter)
}
ContainerType::Unknown { kind: _ } => {
Container::Unknown((*v.as_unknown().unwrap()).clone().unknown)
}
};
loro::ValueOrContainer::Container(container)
}
} }

View file

@ -4,7 +4,8 @@ use delta::DeltaRope;
use enum_as_inner::EnumAsInner; use enum_as_inner::EnumAsInner;
use loro_common::IdLp; use loro_common::IdLp;
use loro_internal::container::ContainerID; use loro_internal::container::ContainerID;
use loro_internal::delta::{ResolvedMapDelta, ResolvedMapValue, TreeDiff}; pub use loro_internal::delta::TreeDiff;
use loro_internal::delta::{ResolvedMapDelta, ResolvedMapValue};
use loro_internal::event::{EventTriggerKind, ListDeltaMeta}; use loro_internal::event::{EventTriggerKind, ListDeltaMeta};
use loro_internal::handler::{TextDelta, ValueOrHandler}; use loro_internal::handler::{TextDelta, ValueOrHandler};
use loro_internal::undo::DiffBatch as InnerDiffBatch; use loro_internal::undo::DiffBatch as InnerDiffBatch;