Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: export diff batch to ffi #619

Merged
merged 6 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"pointee",
"reparent",
"RUSTFLAGS",
"serde",
"smstring",
"sstable",
"Stewen",
Expand Down
121 changes: 118 additions & 3 deletions crates/loro-ffi/src/doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ use loro::{
};

use crate::{
event::{DiffEvent, Subscriber},
event::{DiffBatch, DiffEvent, Subscriber},
AbsolutePosition, Configure, ContainerID, ContainerIdLike, Cursor, Frontiers, Index,
LoroCounter, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, LoroValue, StyleConfigMap,
ValueOrContainer, VersionVector,
ValueOrContainer, VersionVector, VersionVectorDiff,
};

/// Decodes the metadata for an imported blob from the provided bytes.
Expand Down Expand Up @@ -88,10 +88,13 @@ impl LoroDoc {
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.
/// 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]
pub fn set_change_merge_interval(&self, interval: i64) {
self.doc.set_change_merge_interval(interval);
Expand Down Expand Up @@ -249,6 +252,8 @@ impl LoroDoc {
}

/// Set commit message for the current uncommitted changes
///
/// It will be persisted.
pub fn set_next_commit_message(&self, msg: &str) {
self.doc.set_next_commit_message(msg)
}
Expand Down Expand Up @@ -291,6 +296,32 @@ impl LoroDoc {
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

#[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
/// Export all the ops not included in the given `VersionVector`
#[inline]
Expand Down Expand Up @@ -464,6 +495,58 @@ impl LoroDoc {
.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>> {
self.doc
.get_by_str_path(path)
Expand Down Expand Up @@ -616,6 +699,38 @@ impl LoroDoc {
pub fn get_pending_txn_len(&self) -> 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 {
Expand Down
176 changes: 139 additions & 37 deletions crates/loro-ffi/src/event.rs
Original file line number Diff line number Diff line change
@@ -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 {
fn on_diff(&self, diff: DiffEvent);
Expand Down Expand Up @@ -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 {
/// Insert a new element into the list.
Insert {
Expand Down Expand Up @@ -263,40 +339,9 @@ impl From<&loro::event::Diff<'_>> for Diff {
}
Diff::List { diff: ans }
}
loro::event::Diff::Text(t) => {
let mut ans = Vec::new();
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::Text(t) => Diff::Text {
diff: t.iter().map(|i| i.clone().into()).collect(),
},
loro::event::Diff::Map(m) => {
let mut updated = HashMap::new();
for (key, value) in m.updated.iter() {
Expand Down Expand Up @@ -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))
}
}
Loading
Loading