mirror of
https://github.com/loro-dev/loro.git
synced 2024-12-01 04:28:18 +00:00
parent
b30bb18f77
commit
1e94248128
12 changed files with 214 additions and 9 deletions
6
.changeset/funny-rivers-sin.md
Normal file
6
.changeset/funny-rivers-sin.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"loro-wasm": patch
|
||||
"loro-crdt": patch
|
||||
---
|
||||
|
||||
Add `.fork()` to duplicate the doc
|
|
@ -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) {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 } });
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue