Feat fork (#393)

* feat: fork

* chore: add changeset
This commit is contained in:
Zixuan Chen 2024-06-26 21:05:04 +08:00 committed by GitHub
parent b30bb18f77
commit 1e94248128
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 214 additions and 9 deletions

View file

@ -0,0 +1,6 @@
---
"loro-wasm": patch
"loro-crdt": patch
---
Add `.fork()` to duplicate the doc

View file

@ -63,6 +63,24 @@ impl SharedArena {
}
}
pub fn fork(&self) -> Self {
Self {
inner: Arc::new(InnerSharedArena {
container_idx_to_id: Mutex::new(
self.inner.container_idx_to_id.lock().unwrap().clone(),
),
depth: Mutex::new(self.inner.depth.lock().unwrap().clone()),
container_id_to_idx: Mutex::new(
self.inner.container_id_to_idx.lock().unwrap().clone(),
),
parents: Mutex::new(self.inner.parents.lock().unwrap().clone()),
values: Mutex::new(self.inner.values.lock().unwrap().clone()),
root_c_idx: Mutex::new(self.inner.root_c_idx.lock().unwrap().clone()),
str: Mutex::new(self.inner.str.lock().unwrap().clone()),
}),
}
}
pub fn register_container(&self, id: &ContainerID) -> ContainerIdx {
let mut container_id_to_idx = self.inner.container_id_to_idx.lock().unwrap();
if let Some(&idx) = container_id_to_idx.get(id) {

View file

@ -5,7 +5,7 @@ use append_only_bytes::{AppendOnlyBytes, BytesSlice};
use crate::container::richtext::richtext_state::unicode_to_utf8_index;
const INDEX_INTERVAL: u32 = 128;
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone)]
pub(crate) struct StrArena {
bytes: AppendOnlyBytes,
unicode_indexes: Vec<Index>,

View file

@ -21,6 +21,26 @@ impl Default for Configure {
}
impl Configure {
pub fn fork(&self) -> Self {
Self {
text_style_config: Arc::new(RwLock::new(
self.text_style_config.read().unwrap().clone(),
)),
record_timestamp: Arc::new(AtomicBool::new(
self.record_timestamp
.load(std::sync::atomic::Ordering::Relaxed),
)),
merge_interval: Arc::new(AtomicI64::new(
self.merge_interval
.load(std::sync::atomic::Ordering::Relaxed),
)),
tree_position_jitter: Arc::new(AtomicU8::new(
self.tree_position_jitter
.load(std::sync::atomic::Ordering::Relaxed),
)),
}
}
pub fn text_style_config(&self) -> &Arc<RwLock<StyleConfigMap>> {
&self.text_style_config
}

View file

@ -19,13 +19,22 @@ use crate::{
VersionVector,
};
#[derive(Debug, Clone)]
#[derive(Debug)]
pub(crate) struct OpGroups {
arena: SharedArena,
groups: FxHashMap<ContainerIdx, OpGroup>,
}
impl OpGroups {
pub(crate) fn fork(&self, arena: SharedArena) -> Self {
let mut groups = FxHashMap::with_capacity_and_hasher(self.groups.len(), Default::default());
for (container_idx, group) in self.groups.iter() {
groups.insert(*container_idx, group.fork(&arena));
}
Self { arena, groups }
}
pub(crate) fn new(arena: SharedArena) -> Self {
Self {
arena,
@ -108,6 +117,23 @@ pub(crate) enum OpGroup {
MovableList(MovableListOpGroup),
}
impl OpGroup {
fn fork(&self, a: &SharedArena) -> Self {
match self {
OpGroup::Map(m) => OpGroup::Map(m.clone()),
OpGroup::Tree(t) => OpGroup::Tree(TreeOpGroup {
ops: t.ops.clone(),
tree_for_diff: Arc::new(Mutex::new(Default::default())),
}),
OpGroup::MovableList(m) => OpGroup::MovableList(MovableListOpGroup {
arena: a.clone(),
elem_mappings: m.elem_mappings.clone(),
pos_to_elem: m.pos_to_elem.clone(),
}),
}
}
}
#[enum_dispatch]
trait OpGroupTrait {
fn insert(&mut self, op: &RichOp);

View file

@ -118,6 +118,41 @@ impl LoroDoc {
}
}
pub fn fork(&self) -> Self {
self.commit_then_stop();
let arena = self.arena.fork();
let config = self.config.fork();
let txn = Arc::new(Mutex::new(None));
let new_state =
self.state
.lock()
.unwrap()
.fork(arena.clone(), Arc::downgrade(&txn), config.clone());
let doc = LoroDoc {
oplog: Arc::new(Mutex::new(
self.oplog()
.lock()
.unwrap()
.fork(arena.clone(), config.clone()),
)),
state: new_state,
arena,
config,
observer: Arc::new(Observer::new(self.arena.clone())),
diff_calculator: Arc::new(Mutex::new(DiffCalculator::new())),
txn,
auto_commit: AtomicBool::new(false),
detached: AtomicBool::new(self.detached.load(std::sync::atomic::Ordering::Relaxed)),
};
if self.auto_commit.load(std::sync::atomic::Ordering::Relaxed) {
doc.start_auto_commit();
}
self.renew_txn_if_auto_commit();
doc
}
/// Set whether to record the timestamp of each change. Default is `false`.
///
/// If enabled, the Unix timestamp will be recorded for each change automatically.

View file

@ -79,18 +79,18 @@ pub struct AppDagNode {
pub(crate) len: usize,
}
impl Clone for OpLog {
fn clone(&self) -> Self {
impl OpLog {
pub(crate) fn fork(&self, arena: SharedArena, configure: Configure) -> Self {
Self {
dag: self.dag.clone(),
arena: self.arena.clone(),
op_groups: self.op_groups.fork(arena.clone()),
arena,
changes: self.changes.clone(),
op_groups: self.op_groups.clone(),
next_lamport: self.next_lamport,
latest_timestamp: self.latest_timestamp,
pending_changes: Default::default(),
batch_importing: false,
configure: self.configure.clone(),
configure,
}
}
}

View file

@ -56,7 +56,6 @@ macro_rules! get_or_create {
}};
}
#[derive(Clone)]
pub struct DocState {
pub(super) peer: PeerID,
@ -282,6 +281,28 @@ impl DocState {
})
}
pub fn fork(
&self,
arena: SharedArena,
global_txn: Weak<Mutex<Option<Transaction>>>,
config: Configure,
) -> Arc<Mutex<Self>> {
Arc::new_cyclic(|weak| {
Mutex::new(Self {
peer: DefaultRandom.next_u64(),
frontiers: self.frontiers.clone(),
states: self.states.clone(),
arena,
config,
weak_state: weak.clone(),
global_txn,
in_txn: false,
changed_idx_in_txn: FxHashSet::default(),
event_recorder: Default::default(),
})
})
}
pub fn start_recording(&mut self) {
if self.is_recording() {
return;

View file

@ -483,6 +483,30 @@ impl Loro {
self.0.is_detached()
}
/// Detach the document state from the latest known version.
///
/// After detaching, all import operations will be recorded in the `OpLog` without being applied to the `DocState`.
/// When `detached`, the document is not editable.
///
/// @example
/// ```ts
/// import { Loro } from "loro-crdt";
///
/// const doc = new Loro();
/// doc.detach();
/// console.log(doc.is_detached()); // true
/// ```
pub fn detach(&self) {
self.0.detach()
}
/// Duplicate the document with a different PeerID
///
/// The time complexity and space complexity of this operation are both O(n),
pub fn fork(&self) -> Self {
Self(Arc::new(self.0.fork()))
}
/// Checkout the `DocState` to the latest version of `OpLog`.
///
/// > The document becomes detached during a `checkout` operation.

View file

@ -43,6 +43,7 @@ pub use loro_internal::obs::SubID;
pub use loro_internal::oplog::FrontiersNotIncluded;
pub use loro_internal::undo;
pub use loro_internal::version::{Frontiers, VersionVector};
pub use loro_internal::ApplyDiff;
pub use loro_internal::JsonSchema;
pub use loro_internal::UndoManager as InnerUndoManager;
pub use loro_internal::{loro_value, to_value};
@ -76,6 +77,14 @@ impl LoroDoc {
LoroDoc { doc }
}
/// Duplicate the document with a different PeerID
///
/// The time complexity and space complexity of this operation are both O(n),
pub fn fork(&self) -> Self {
let doc = self.doc.fork();
LoroDoc { doc }
}
/// Get the configureations of the document.
pub fn config(&self) -> &Configure {
self.doc.config()

View file

@ -1,4 +1,7 @@
use std::{cmp::Ordering, sync::Arc};
use std::{
cmp::Ordering,
sync::{atomic::AtomicBool, Arc},
};
use loro::{
awareness::Awareness, FrontiersNotIncluded, LoroDoc, LoroError, LoroList, LoroMap, LoroText,
@ -42,6 +45,34 @@ fn insert_an_inserted_movable_handler() -> Result<(), LoroError> {
Ok(())
}
#[test]
fn fork_doc() -> anyhow::Result<()> {
let doc0 = LoroDoc::new();
let text = doc0.get_text("123");
text.insert(0, "123")?;
let triggered = Arc::new(AtomicBool::new(false));
let trigger_cloned = triggered.clone();
doc0.commit();
doc0.subscribe_root(Arc::new(move |e| {
for e in e.events {
let _t = e.diff.as_text().unwrap();
triggered.store(true, std::sync::atomic::Ordering::Release);
}
}));
let doc1 = doc0.fork();
let text1 = doc1.get_text("123");
assert_eq!(&text1.to_string(), "123");
text1.insert(3, "456")?;
assert_eq!(&text.to_string(), "123");
assert_eq!(&text1.to_string(), "123456");
assert!(!trigger_cloned.load(std::sync::atomic::Ordering::Acquire),);
doc0.import(&doc1.export_from(&Default::default()))?;
assert!(trigger_cloned.load(std::sync::atomic::Ordering::Acquire),);
assert_eq!(text.to_string(), text1.to_string());
assert_ne!(doc0.peer_id(), doc1.peer_id());
Ok(())
}
#[test]
fn movable_list() -> Result<(), LoroError> {
let doc = LoroDoc::new();

View file

@ -460,3 +460,18 @@ it("get elem by path", () => {
map1.set("key1", 1);
expect(doc.getByPath("map/key1")).toBe(1);
});
it("fork", () => {
const doc = new Loro();
const map = doc.getMap("map");
map.set("key", 1);
const doc2 = doc.fork();
const map2 = doc2.getMap("map");
expect(map2.get("key")).toBe(1);
expect(doc2.toJSON()).toStrictEqual({ map: { key: 1 } });
map2.set("key", 2);
expect(doc.toJSON()).toStrictEqual({ map: { key: 1 } });
expect(doc2.toJSON()).toStrictEqual({ map: { key: 2 } });
doc.import(doc2.exportSnapshot());
expect(doc.toJSON()).toStrictEqual({ map: { key: 2 } });
});