diff --git a/.vscode/settings.json b/.vscode/settings.json index b972f353..822ea821 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,6 +34,7 @@ "pointee", "reparent", "RUSTFLAGS", + "serde", "smstring", "sstable", "Stewen", diff --git a/crates/loro-ffi/src/doc.rs b/crates/loro-ffi/src/doc.rs index 28488ac4..79cb8be9 100644 --- a/crates/loro-ffi/src/doc.rs +++ b/crates/loro-ffi/src/doc.rs @@ -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. @@ -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); @@ -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) } @@ -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. + #[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 { + 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] @@ -464,6 +495,58 @@ impl LoroDoc { .map(|x| Arc::new(x) as Arc) } + /// + /// 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> { self.doc .get_by_str_path(path) @@ -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 { + self.doc.diff(&a.into(), &b.into()).map(|x| x.into()) + } } pub trait ChangeAncestorsTraveler: Sync + Send { diff --git a/crates/loro-ffi/src/event.rs b/crates/loro-ffi/src/event.rs index 5609bf96..0abb5aa2 100644 --- a/crates/loro-ffi/src/event.rs +++ b/crates/loro-ffi/src/event.rs @@ -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); @@ -135,6 +141,76 @@ impl From for TextDelta { } } +impl From 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 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 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 { @@ -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() { @@ -359,3 +404,60 @@ impl From<&loro::event::Diff<'_>> for Diff { } } } + +impl From 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); + +impl DiffBatch { + pub fn new() -> Self { + Self(Default::default()) + } + + pub fn push(&self, cid: ContainerID, diff: Diff) -> Option { + 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 for loro::event::DiffBatch { + fn from(value: DiffBatch) -> Self { + value.0.into_inner().unwrap() + } +} + +impl From for DiffBatch { + fn from(value: loro::event::DiffBatch) -> Self { + Self(Mutex::new(value)) + } +} diff --git a/crates/loro-ffi/src/lib.rs b/crates/loro-ffi/src/lib.rs index bd856b2d..3c0a124a 100644 --- a/crates/loro-ffi/src/lib.rs +++ b/crates/loro-ffi/src/lib.rs @@ -41,6 +41,7 @@ pub trait ValueOrContainer: Send + Sync { fn is_value(&self) -> bool; fn is_container(&self) -> bool; fn as_value(&self) -> Option; + fn container_type(&self) -> Option; fn as_container(&self) -> Option; fn as_loro_list(&self) -> Option>; fn as_loro_text(&self) -> Option>; @@ -48,6 +49,7 @@ pub trait ValueOrContainer: Send + Sync { fn as_loro_movable_list(&self) -> Option>; fn as_loro_tree(&self) -> Option>; fn as_loro_counter(&self) -> Option>; + fn as_unknown(&self) -> Option>; } impl ValueOrContainer for loro::ValueOrContainer { @@ -59,12 +61,17 @@ impl ValueOrContainer for loro::ValueOrContainer { loro::ValueOrContainer::is_container(self) } + fn container_type(&self) -> Option { + loro::ValueOrContainer::as_container(self).map(|c| c.id().container_type().into()) + } + fn as_value(&self) -> Option { loro::ValueOrContainer::as_value(self) .cloned() .map(LoroValue::from) } + // TODO: pass Container to Swift fn as_container(&self) -> Option { loro::ValueOrContainer::as_container(self).map(|c| c.id().into()) } @@ -122,4 +129,37 @@ impl ValueOrContainer for loro::ValueOrContainer { _ => None, } } + + fn as_unknown(&self) -> Option> { + match self { + loro::ValueOrContainer::Container(Container::Unknown(c)) => { + Some(Arc::new(LoroUnknown { unknown: c.clone() })) + } + _ => None, + } + } +} + +fn convert_trait_to_v_or_container>(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) + } } diff --git a/crates/loro/src/event.rs b/crates/loro/src/event.rs index b663330d..7711d44c 100644 --- a/crates/loro/src/event.rs +++ b/crates/loro/src/event.rs @@ -4,7 +4,8 @@ use delta::DeltaRope; use enum_as_inner::EnumAsInner; use loro_common::IdLp; 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::handler::{TextDelta, ValueOrHandler}; use loro_internal::undo::DiffBatch as InnerDiffBatch;