From 82933473341b235b07051131676cdadf79930b4d Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Mon, 30 Oct 2023 18:32:36 +0800 Subject: [PATCH] Feat: autocommit transaction (#127) * feat: auto commit * fix: make recursive single thread event work again --- crates/loro-common/src/error.rs | 2 + .../src/container/richtext/richtext_state.rs | 2 +- .../src/fuzz/recursive_refactored.rs | 25 ++- crates/loro-internal/src/fuzz/tree.rs | 36 +-- crates/loro-internal/src/handler.rs | 194 ++++++++++++++-- crates/loro-internal/src/loro.rs | 141 ++++++++++-- crates/loro-internal/src/state.rs | 4 + crates/loro-internal/src/txn.rs | 25 ++- crates/loro-internal/tests/autocommit.rs | 64 ++++++ crates/loro-wasm/deno.lock | 5 +- crates/loro-wasm/src/lib.rs | 185 ++++------------ loro-js/package.json | 1 + loro-js/src/index.ts | 120 ++-------- loro-js/tests/checkout.test.ts | 30 +-- loro-js/tests/event.test.ts | 114 +++++----- loro-js/tests/frontiers.test.ts | 15 +- loro-js/tests/misc.test.ts | 209 +++++++----------- pnpm-lock.yaml | 151 ++++++++++++- 18 files changed, 769 insertions(+), 554 deletions(-) create mode 100644 crates/loro-internal/tests/autocommit.rs diff --git a/crates/loro-common/src/error.rs b/crates/loro-common/src/error.rs index 80394fc54..a44cded05 100644 --- a/crates/loro-common/src/error.rs +++ b/crates/loro-common/src/error.rs @@ -36,6 +36,8 @@ pub enum LoroError { TreeError(#[from] LoroTreeError), #[error("Invalid argument ({0})")] ArgErr(Box), + #[error("Auto commit has not started. The doc is readonly when detached. You should ensure autocommit is on and the doc and the state is attached.")] + AutoCommitNotStarted, // #[error("the data for key `{0}` is not available")] // Redaction(String), // #[error("invalid header (expected {expected:?}, found {found:?})")] diff --git a/crates/loro-internal/src/container/richtext/richtext_state.rs b/crates/loro-internal/src/container/richtext/richtext_state.rs index 7ce749b6f..977b0f478 100644 --- a/crates/loro-internal/src/container/richtext/richtext_state.rs +++ b/crates/loro-internal/src/container/richtext/richtext_state.rs @@ -1310,7 +1310,7 @@ impl RichtextState { "pos: {}, len: {}, self.len(): {}", pos, len, - self.to_string() + &self.to_string() ); // PERF: may use cache to speed up self.cursor_cache.invalidate(); diff --git a/crates/loro-internal/src/fuzz/recursive_refactored.rs b/crates/loro-internal/src/fuzz/recursive_refactored.rs index a4c70007d..43907c409 100644 --- a/crates/loro-internal/src/fuzz/recursive_refactored.rs +++ b/crates/loro-internal/src/fuzz/recursive_refactored.rs @@ -301,16 +301,23 @@ trait Actionable { impl Actor { fn add_new_container(&mut self, idx: ContainerIdx, type_: ContainerType) { + let txn = self.loro.get_global_txn(); match type_ { - ContainerType::Text => self - .text_containers - .push(TextHandler::new(idx, Arc::downgrade(self.loro.app_state()))), - ContainerType::Map => self - .map_containers - .push(MapHandler::new(idx, Arc::downgrade(self.loro.app_state()))), - ContainerType::List => self - .list_containers - .push(ListHandler::new(idx, Arc::downgrade(self.loro.app_state()))), + ContainerType::Text => self.text_containers.push(TextHandler::new( + txn, + idx, + Arc::downgrade(self.loro.app_state()), + )), + ContainerType::Map => self.map_containers.push(MapHandler::new( + txn, + idx, + Arc::downgrade(self.loro.app_state()), + )), + ContainerType::List => self.list_containers.push(ListHandler::new( + txn, + idx, + Arc::downgrade(self.loro.app_state()), + )), ContainerType::Tree => { // TODO Tree } diff --git a/crates/loro-internal/src/fuzz/tree.rs b/crates/loro-internal/src/fuzz/tree.rs index 1dfbc58a1..d3979ed2f 100644 --- a/crates/loro-internal/src/fuzz/tree.rs +++ b/crates/loro-internal/src/fuzz/tree.rs @@ -17,9 +17,8 @@ use crate::{ ContainerType, LoroValue, }; use crate::{ - container::idx::ContainerIdx, delta::TreeDiffItem, handler::TreeHandler, loro::LoroDoc, - state::Forest, value::ToJson, version::Frontiers, ApplyDiff, ListHandler, MapHandler, - TextHandler, + delta::TreeDiffItem, handler::TreeHandler, loro::LoroDoc, state::Forest, value::ToJson, + version::Frontiers, ApplyDiff, ListHandler, MapHandler, TextHandler, }; #[derive(Arbitrary, EnumAsInner, Clone, PartialEq, Eq, Debug)] @@ -227,26 +226,6 @@ trait Actionable { fn preprocess(&mut self, action: &mut Action); } -impl Actor { - #[allow(unused)] - fn add_new_container(&mut self, idx: ContainerIdx, type_: ContainerType) { - match type_ { - ContainerType::Text => self - .text_containers - .push(TextHandler::new(idx, Arc::downgrade(self.loro.app_state()))), - ContainerType::Map => self - .map_containers - .push(MapHandler::new(idx, Arc::downgrade(self.loro.app_state()))), - ContainerType::List => self - .list_containers - .push(ListHandler::new(idx, Arc::downgrade(self.loro.app_state()))), - ContainerType::Tree => self - .tree_containers - .push(TreeHandler::new(idx, Arc::downgrade(self.loro.app_state()))), - } - } -} - impl Actionable for Vec { fn preprocess(&mut self, action: &mut Action) { let max_users = self.len() as u8; @@ -552,13 +531,10 @@ impl Actionable for Vec { let key = parent_peer.to_string(); let value = *parent_counter; let meta = container - .get_meta( - &mut txn, - TreeID { - peer: *target_peer, - counter: *target_counter, - }, - ) + .get_meta(TreeID { + peer: *target_peer, + counter: *target_counter, + }) .unwrap(); meta.insert(&mut txn, &key, value.into()).unwrap(); } diff --git a/crates/loro-internal/src/handler.rs b/crates/loro-internal/src/handler.rs index cd2e556ab..024633fda 100644 --- a/crates/loro-internal/src/handler.rs +++ b/crates/loro-internal/src/handler.rs @@ -22,6 +22,7 @@ use std::{ #[derive(Clone)] pub struct TextHandler { + txn: Weak>>, container_idx: ContainerIdx, state: Weak>, } @@ -34,6 +35,7 @@ impl std::fmt::Debug for TextHandler { #[derive(Clone)] pub struct MapHandler { + txn: Weak>>, container_idx: ContainerIdx, state: Weak>, } @@ -46,6 +48,7 @@ impl std::fmt::Debug for MapHandler { #[derive(Clone)] pub struct ListHandler { + txn: Weak>>, container_idx: ContainerIdx, state: Weak>, } @@ -59,6 +62,7 @@ impl std::fmt::Debug for ListHandler { /// #[derive(Clone)] pub struct TreeHandler { + txn: Weak>>, container_idx: ContainerIdx, state: Weak>, } @@ -98,20 +102,29 @@ impl Handler { } impl Handler { - fn new(value: ContainerIdx, state: Weak>) -> Self { + fn new( + txn: Weak>>, + value: ContainerIdx, + state: Weak>, + ) -> Self { match value.get_type() { - ContainerType::Map => Self::Map(MapHandler::new(value, state)), - ContainerType::List => Self::List(ListHandler::new(value, state)), - ContainerType::Tree => Self::Tree(TreeHandler::new(value, state)), - ContainerType::Text => Self::Text(TextHandler::new(value, state)), + ContainerType::Map => Self::Map(MapHandler::new(txn, value, state)), + ContainerType::List => Self::List(ListHandler::new(txn, value, state)), + ContainerType::Tree => Self::Tree(TreeHandler::new(txn, value, state)), + ContainerType::Text => Self::Text(TextHandler::new(txn, value, state)), } } } impl TextHandler { - pub fn new(idx: ContainerIdx, state: Weak>) -> Self { + pub fn new( + txn: Weak>>, + idx: ContainerIdx, + state: Weak>, + ) -> Self { assert_eq!(idx.get_type(), ContainerType::Text); Self { + txn, container_idx: idx, state, } @@ -220,6 +233,16 @@ impl TextHandler { }) } + /// `pos` is a Event Index: + /// + /// - if feature="wasm", pos is a UTF-16 index + /// - if feature!="wasm", pos is a Unicode index + /// + /// This method requires auto_commit to be enabled. + pub fn insert_(&self, pos: usize, s: &str) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.insert(txn, pos, s)) + } + /// `pos` is a Event Index: /// /// - if feature="wasm", pos is a UTF-16 index @@ -260,6 +283,16 @@ impl TextHandler { ) } + /// `pos` is a Event Index: + /// + /// - if feature="wasm", pos is a UTF-16 index + /// - if feature!="wasm", pos is a Unicode index + /// + /// This method requires auto_commit to be enabled. + pub fn delete_(&self, pos: usize, len: usize) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.delete(txn, pos, len)) + } + /// `pos` is a Event Index: /// /// - if feature="wasm", pos is a UTF-16 index @@ -313,6 +346,22 @@ impl TextHandler { Ok(()) } + /// `start` and `end` are [Event Index]s: + /// + /// - if feature="wasm", pos is a UTF-16 index + /// - if feature!="wasm", pos is a Unicode index + /// + /// This method requires auto_commit to be enabled. + pub fn mark_( + &self, + start: usize, + end: usize, + key: &str, + flag: TextStyleInfoFlag, + ) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.mark(txn, start, end, key, flag)) + } + /// `start` and `end` are [Event Index]s: /// /// - if feature="wasm", pos is a UTF-16 index @@ -382,14 +431,23 @@ impl TextHandler { } impl ListHandler { - pub fn new(idx: ContainerIdx, state: Weak>) -> Self { + pub fn new( + txn: Weak>>, + idx: ContainerIdx, + state: Weak>, + ) -> Self { assert_eq!(idx.get_type(), ContainerType::List); Self { + txn, container_idx: idx, state, } } + pub fn insert_(&self, pos: usize, v: LoroValue) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.insert(txn, pos, v)) + } + pub fn insert(&self, txn: &mut Transaction, pos: usize, v: LoroValue) -> LoroResult<()> { if let Some(container) = v.as_container() { self.insert_container(txn, pos, container.container_type())?; @@ -410,11 +468,34 @@ impl ListHandler { ) } + pub fn push_(&self, v: LoroValue) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.push(txn, v)) + } + pub fn push(&self, txn: &mut Transaction, v: LoroValue) -> LoroResult<()> { let pos = self.len(); self.insert(txn, pos, v) } + pub fn pop_(&self) -> LoroResult> { + with_txn(&self.txn, |txn| self.pop(txn)) + } + + pub fn pop(&self, txn: &mut Transaction) -> LoroResult> { + let len = self.len(); + if len == 0 { + return Ok(None); + } + + let v = self.get(len - 1); + self.delete(txn, len - 1, 1)?; + Ok(v) + } + + pub fn insert_container_(&self, pos: usize, c_type: ContainerType) -> LoroResult { + with_txn(&self.txn, |txn| self.insert_container(txn, pos, c_type)) + } + pub fn insert_container( &self, txn: &mut Transaction, @@ -438,7 +519,15 @@ impl ListHandler { }, &self.state, )?; - Ok(Handler::new(child_idx, self.state.clone())) + Ok(Handler::new( + self.txn.clone(), + child_idx, + self.state.clone(), + )) + } + + pub fn delete_(&self, pos: usize, len: usize) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.delete(txn, pos, len)) } pub fn delete(&self, txn: &mut Transaction, pos: usize, len: usize) -> LoroResult<()> { @@ -472,7 +561,7 @@ impl ListHandler { .clone() }); let idx = state.arena.register_container(&container_id); - Handler::new(idx, self.state.clone()) + Handler::new(self.txn.clone(), idx, self.state.clone()) } pub fn len(&self) -> usize { @@ -559,14 +648,23 @@ impl ListHandler { } impl MapHandler { - pub fn new(idx: ContainerIdx, state: Weak>) -> Self { + pub fn new( + txn: Weak>>, + idx: ContainerIdx, + state: Weak>, + ) -> Self { assert_eq!(idx.get_type(), ContainerType::Map); Self { + txn, container_idx: idx, state, } } + pub fn insert_(&self, key: &str, value: LoroValue) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.insert(txn, key, value)) + } + pub fn insert(&self, txn: &mut Transaction, key: &str, value: LoroValue) -> LoroResult<()> { if let Some(value) = value.as_container() { self.insert_container(txn, key, value.container_type())?; @@ -592,6 +690,10 @@ impl MapHandler { ) } + pub fn insert_container_(&self, key: &str, c_type: ContainerType) -> LoroResult { + with_txn(&self.txn, |txn| self.insert_container(txn, key, c_type)) + } + pub fn insert_container( &self, txn: &mut Transaction, @@ -615,7 +717,15 @@ impl MapHandler { &self.state, )?; - Ok(Handler::new(child_idx, self.state.clone())) + Ok(Handler::new( + self.txn.clone(), + child_idx, + self.state.clone(), + )) + } + + pub fn delete_(&self, key: &str) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.delete(txn, key)) } pub fn delete(&self, txn: &mut Transaction, key: &str) -> LoroResult<()> { @@ -674,7 +784,7 @@ impl MapHandler { .clone() }); let idx = state.arena.register_container(&container_id); - Handler::new(idx, self.state.clone()) + Handler::new(self.txn.clone(), idx, self.state.clone()) } pub fn get_deep_value(&self) -> LoroValue { @@ -735,14 +845,23 @@ impl MapHandler { } impl TreeHandler { - pub fn new(idx: ContainerIdx, state: Weak>) -> Self { + pub fn new( + txn: Weak>>, + idx: ContainerIdx, + state: Weak>, + ) -> Self { assert_eq!(idx.get_type(), ContainerType::Tree); Self { + txn, container_idx: idx, state, } } + pub fn create_(&self) -> LoroResult { + with_txn(&self.txn, |txn| self.create(txn)) + } + pub fn create(&self, txn: &mut Transaction) -> LoroResult { let tree_id = TreeID::from_id(txn.next_id()); let container_id = self.meta_container_id(tree_id); @@ -760,6 +879,10 @@ impl TreeHandler { Ok(tree_id) } + pub fn delete_(&self, target: TreeID) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.delete(txn, target)) + } + pub fn delete(&self, txn: &mut Transaction, target: TreeID) -> LoroResult<()> { txn.apply_local_op( self.container_idx, @@ -772,6 +895,10 @@ impl TreeHandler { ) } + pub fn create_and_mov_(&self, parent: TreeID) -> LoroResult { + with_txn(&self.txn, |txn| self.create_and_mov(txn, parent)) + } + pub fn create_and_mov(&self, txn: &mut Transaction, parent: TreeID) -> LoroResult { let tree_id = TreeID::from_id(txn.next_id()); let container_id = self.meta_container_id(tree_id); @@ -789,6 +916,10 @@ impl TreeHandler { Ok(tree_id) } + pub fn as_root_(&self, target: TreeID) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.as_root(txn, target)) + } + pub fn as_root(&self, txn: &mut Transaction, target: TreeID) -> LoroResult<()> { txn.apply_local_op( self.container_idx, @@ -801,6 +932,10 @@ impl TreeHandler { ) } + pub fn mov_(&self, target: TreeID, parent: TreeID) -> LoroResult<()> { + with_txn(&self.txn, |txn| self.mov(txn, target, parent)) + } + pub fn mov(&self, txn: &mut Transaction, target: TreeID, parent: TreeID) -> LoroResult<()> { txn.apply_local_op( self.container_idx, @@ -813,12 +948,20 @@ impl TreeHandler { ) } - pub fn get_meta(&self, txn: &mut Transaction, target: TreeID) -> LoroResult { + pub fn get_meta(&self, target: TreeID) -> LoroResult { if !self.contains(target) { return Err(LoroTreeError::TreeNodeNotExist(target).into()); } let map_container_id = self.meta_container_id(target); - let map = txn.get_map(map_container_id); + let idx = self + .state + .upgrade() + .unwrap() + .lock() + .unwrap() + .arena + .register_container(&map_container_id); + let map = MapHandler::new(self.txn.clone(), idx, self.state.clone()); Ok(map) } @@ -905,6 +1048,19 @@ impl TreeHandler { } } +#[inline(always)] +fn with_txn( + txn: &Weak>>, + f: impl FnOnce(&mut Transaction) -> LoroResult, +) -> LoroResult { + let mutex = &txn.upgrade().unwrap(); + let mut txn = mutex.try_lock().unwrap(); + match &mut *txn { + Some(t) => f(t), + None => Err(LoroError::AutoCommitNotStarted), + } +} + #[cfg(test)] mod test { use std::ops::Deref; @@ -1095,13 +1251,13 @@ mod test { let tree = loro.get_tree("root"); let id = loro.with_txn(|txn| tree.create(txn)).unwrap(); loro.with_txn(|txn| { - let meta = tree.get_meta(txn, id)?; + let meta = tree.get_meta(id)?; meta.insert(txn, "a", 123.into()) }) .unwrap(); let meta = loro - .with_txn(|txn| { - let meta = tree.get_meta(txn, id)?; + .with_txn(|_| { + let meta = tree.get_meta(id)?; Ok(meta.get("a").unwrap()) }) .unwrap(); @@ -1123,7 +1279,7 @@ mod test { let text = loro.get_text("text"); loro.with_txn(|txn| { let id = tree.create(txn)?; - let meta = tree.get_meta(txn, id)?; + let meta = tree.get_meta(id)?; meta.insert(txn, "a", 1.into())?; text.insert(txn, 0, "abc")?; let _id2 = tree.create(txn)?; diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index 6a0e5beae..47b497406 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -1,7 +1,7 @@ use std::{ borrow::Cow, cmp::Ordering, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, Weak}, }; use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue}; @@ -52,6 +52,8 @@ pub struct LoroDoc { arena: SharedArena, observer: Arc, diff_calculator: Arc>, + txn: Arc>>, + auto_commit: bool, detached: bool, } @@ -65,8 +67,10 @@ impl LoroDoc { oplog: Arc::new(Mutex::new(oplog)), state, detached: false, + auto_commit: false, observer: Arc::new(Observer::new(arena.clone())), diff_calculator: Arc::new(Mutex::new(DiffCalculator::new())), + txn: Arc::new(Mutex::new(None)), arena, } } @@ -89,9 +93,11 @@ impl LoroDoc { Self { arena: oplog.arena.clone(), observer: Arc::new(obs), + auto_commit: false, oplog: Arc::new(Mutex::new(oplog)), state: Arc::new(Mutex::new(state)), diff_calculator: Arc::new(Mutex::new(DiffCalculator::new())), + txn: Arc::new(Mutex::new(None)), detached: false, } } @@ -144,6 +150,81 @@ impl LoroDoc { Ok(v) } + pub fn start_auto_commit(&mut self) { + self.auto_commit = true; + let mut self_txn = self.txn.try_lock().unwrap(); + if self_txn.is_some() || self.detached { + return; + } + + let txn = self.txn().unwrap(); + self_txn.replace(txn); + } + + /// Commit the cumulative auto commit transaction. + /// This method only has effect when `auto_commit` is true. + #[inline] + pub fn commit(&self) { + self.commit_with(None, None, false) + } + + /// Commit the cumulative auto commit transaction. + /// This method only has effect when `auto_commit` is true. + /// If `immediate_renew` is true, a new transaction will be created after the old one is commited + pub fn commit_with( + &self, + origin: Option, + timestamp: Option, + immediate_renew: bool, + ) { + if !self.auto_commit { + return; + } + + let mut txn_guard = self.txn.try_lock().unwrap(); + let txn = txn_guard.take(); + drop(txn_guard); + let Some(mut txn) = txn else { + return; + }; + + let on_commit = txn.take_on_commit(); + if let Some(origin) = origin { + txn.set_origin(origin); + } + + if let Some(timestamp) = timestamp { + txn.set_timestamp(timestamp); + } + + txn.commit().unwrap(); + if immediate_renew { + let mut txn_guard = self.txn.try_lock().unwrap(); + assert!(!self.detached); + *txn_guard = Some(self.txn().unwrap()); + } + + if let Some(on_commit) = on_commit { + on_commit(&self.state); + } + } + + pub fn renew_txn_if_auto_commit(&self) { + if self.auto_commit && !self.detached { + let mut self_txn = self.txn.try_lock().unwrap(); + if self_txn.is_some() { + return; + } + + let txn = self.txn().unwrap(); + self_txn.replace(txn); + } + } + + pub(crate) fn get_global_txn(&self) -> Weak>> { + Arc::downgrade(&self.txn) + } + /// Create a new transaction with specified origin. /// /// The origin will be propagated to the events. @@ -155,17 +236,22 @@ impl LoroDoc { )); } - let mut txn = - Transaction::new_with_origin(self.state.clone(), self.oplog.clone(), origin.into()); - if self.state.lock().unwrap().is_recording() { - let obs = self.observer.clone(); - txn.set_on_commit(Box::new(move |state| { - let events = state.lock().unwrap().take_events(); - for event in events { - obs.emit(event); - } - })); - } + let mut txn = Transaction::new_with_origin( + self.state.clone(), + self.oplog.clone(), + origin.into(), + self.get_global_txn(), + ); + + let obs = self.observer.clone(); + txn.set_on_commit(Box::new(move |state| { + let mut state = state.try_lock().unwrap(); + let events = state.take_events(); + drop(state); + for event in events { + obs.emit(event); + } + })); Ok(txn) } @@ -185,7 +271,10 @@ impl LoroDoc { } pub fn export_from(&self, vv: &VersionVector) -> Vec { - self.oplog.lock().unwrap().export_from(vv) + self.commit(); + let ans = self.oplog.lock().unwrap().export_from(vv); + self.renew_txn_if_auto_commit(); + ans } #[inline(always)] @@ -194,11 +283,23 @@ impl LoroDoc { } pub fn import_without_state(&mut self, bytes: &[u8]) -> Result<(), LoroError> { + self.commit(); self.detach(); self.import(bytes) } pub fn import_with(&self, bytes: &[u8], origin: InternalString) -> Result<(), LoroError> { + self.commit(); + let ans = self._import_with(bytes, origin); + self.renew_txn_if_auto_commit(); + ans + } + + fn _import_with( + &self, + bytes: &[u8], + origin: string_cache::Atom, + ) -> Result<(), LoroError> { if bytes.len() <= 6 { return Err(LoroError::DecodeError("Invalid bytes".into())); } @@ -275,6 +376,7 @@ impl LoroDoc { } pub fn export_snapshot(&self) -> Vec { + self.commit(); debug_log::group!("export snapshot"); let version = ENCODE_SCHEMA_VERSION; let mut ans = Vec::from(MAGIC_BYTES); @@ -283,6 +385,7 @@ impl LoroDoc { ans.push((EncodeMode::Snapshot).to_byte()); ans.extend(encode_app_snapshot(self)); debug_log::group_end!(); + self.renew_txn_if_auto_commit(); ans } @@ -301,28 +404,28 @@ impl LoroDoc { /// if it's str it will use Root container, which will not be None pub fn get_text(&self, id: I) -> TextHandler { let idx = self.get_container_idx(id, ContainerType::Text); - TextHandler::new(idx, Arc::downgrade(&self.state)) + TextHandler::new(self.get_global_txn(), idx, Arc::downgrade(&self.state)) } /// id can be a str, ContainerID, or ContainerIdRaw. /// if it's str it will use Root container, which will not be None pub fn get_list(&self, id: I) -> ListHandler { let idx = self.get_container_idx(id, ContainerType::List); - ListHandler::new(idx, Arc::downgrade(&self.state)) + ListHandler::new(self.get_global_txn(), idx, Arc::downgrade(&self.state)) } /// id can be a str, ContainerID, or ContainerIdRaw. /// if it's str it will use Root container, which will not be None pub fn get_map(&self, id: I) -> MapHandler { let idx = self.get_container_idx(id, ContainerType::Map); - MapHandler::new(idx, Arc::downgrade(&self.state)) + MapHandler::new(self.get_global_txn(), idx, Arc::downgrade(&self.state)) } /// id can be a str, ContainerID, or ContainerIdRaw. /// if it's str it will use Root container, which will not be None pub fn get_tree(&self, id: I) -> TreeHandler { let idx = self.get_container_idx(id, ContainerType::Tree); - TreeHandler::new(idx, Arc::downgrade(&self.state)) + TreeHandler::new(self.get_global_txn(), idx, Arc::downgrade(&self.state)) } /// This is for debugging purpose. It will travel the whole oplog @@ -374,6 +477,7 @@ impl LoroDoc { // PERF: opt pub fn import_batch(&mut self, bytes: &[Vec]) -> LoroResult<()> { + self.commit(); let is_detached = self.is_detached(); self.detach(); self.oplog.lock().unwrap().batch_importing = true; @@ -396,6 +500,7 @@ impl LoroDoc { self.checkout_to_latest(); } + self.renew_txn_if_auto_commit(); if let Some(err) = err { return Err(err); } @@ -417,6 +522,7 @@ impl LoroDoc { let f = self.oplog_frontiers(); self.checkout(&f).unwrap(); self.detached = false; + self.renew_txn_if_auto_commit(); } /// Checkout [DocState] to a specific version. @@ -424,6 +530,7 @@ impl LoroDoc { /// This will make the current [DocState] detached from the latest version of [OpLog]. /// Any further import will not be reflected on the [DocState], until user call [LoroDoc::attach()] pub fn checkout(&mut self, frontiers: &Frontiers) -> LoroResult<()> { + self.commit(); let oplog = self.oplog.lock().unwrap(); let mut state = self.state.lock().unwrap(); self.detached = true; diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index d0922ad40..b71475944 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -206,6 +206,10 @@ impl DocState { fn convert_current_batch_diff_into_event(&mut self) { let recorder = &mut self.event_recorder; + if recorder.diffs.is_empty() { + return; + } + let diffs = std::mem::take(&mut recorder.diffs); let start = recorder.diff_start_version.take().unwrap(); recorder.diff_start_version = Some((*diffs.last().unwrap().new_version).to_owned()); diff --git a/crates/loro-internal/src/txn.rs b/crates/loro-internal/src/txn.rs index 33d3b2ebd..6de382de5 100644 --- a/crates/loro-internal/src/txn.rs +++ b/crates/loro-internal/src/txn.rs @@ -30,9 +30,10 @@ use super::{ state::{DocState, State}, }; -pub type OnCommitFn = Box>)>; +pub type OnCommitFn = Box>) + Sync + Send>; pub struct Transaction { + global_txn: Weak>>, peer: PeerID, origin: InternalString, start_counter: Counter, @@ -85,14 +86,19 @@ pub(super) enum EventHint { } impl Transaction { - pub fn new(state: Arc>, oplog: Arc>) -> Self { - Self::new_with_origin(state, oplog, "".into()) + pub fn new( + state: Arc>, + oplog: Arc>, + global_txn: Weak>>, + ) -> Self { + Self::new_with_origin(state, oplog, "".into(), global_txn) } pub fn new_with_origin( state: Arc>, oplog: Arc>, origin: InternalString, + global_txn: Weak>>, ) -> Self { let mut state_lock = state.lock().unwrap(); if state_lock.is_in_txn() { @@ -109,6 +115,7 @@ impl Transaction { drop(state_lock); drop(oplog_lock); Self { + global_txn, origin: Default::default(), peer, start_counter: next_counter, @@ -143,6 +150,10 @@ impl Transaction { self.on_commit = Some(f); } + pub(crate) fn take_on_commit(&mut self) -> Option { + self.on_commit.take() + } + pub fn abort(mut self) { self._abort(); } @@ -268,28 +279,28 @@ impl Transaction { /// if it's str it will use Root container, which will not be None pub fn get_text(&self, id: I) -> TextHandler { let idx = self.get_container_idx(id, ContainerType::Text); - TextHandler::new(idx, Arc::downgrade(&self.state)) + TextHandler::new(self.global_txn.clone(), idx, Arc::downgrade(&self.state)) } /// id can be a str, ContainerID, or ContainerIdRaw. /// if it's str it will use Root container, which will not be None pub fn get_list(&self, id: I) -> ListHandler { let idx = self.get_container_idx(id, ContainerType::List); - ListHandler::new(idx, Arc::downgrade(&self.state)) + ListHandler::new(self.global_txn.clone(), idx, Arc::downgrade(&self.state)) } /// id can be a str, ContainerID, or ContainerIdRaw. /// if it's str it will use Root container, which will not be None pub fn get_map(&self, id: I) -> MapHandler { let idx = self.get_container_idx(id, ContainerType::Map); - MapHandler::new(idx, Arc::downgrade(&self.state)) + MapHandler::new(self.global_txn.clone(), idx, Arc::downgrade(&self.state)) } /// id can be a str, ContainerID, or ContainerIdRaw. /// if it's str it will use Root container, which will not be None pub fn get_tree(&self, id: I) -> TreeHandler { let idx = self.get_container_idx(id, ContainerType::Tree); - TreeHandler::new(idx, Arc::downgrade(&self.state)) + TreeHandler::new(self.global_txn.clone(), idx, Arc::downgrade(&self.state)) } fn get_container_idx(&self, id: I, c_type: ContainerType) -> ContainerIdx { diff --git a/crates/loro-internal/tests/autocommit.rs b/crates/loro-internal/tests/autocommit.rs new file mode 100644 index 000000000..0908951ed --- /dev/null +++ b/crates/loro-internal/tests/autocommit.rs @@ -0,0 +1,64 @@ +use loro_common::ID; +use loro_internal::{version::Frontiers, LoroDoc, ToJson}; +use serde_json::json; + +#[test] +fn auto_commit() { + let mut doc_a = LoroDoc::default(); + doc_a.start_auto_commit(); + let text_a = doc_a.get_text("text"); + text_a.insert_(0, "hello").unwrap(); + text_a.delete_(2, 2).unwrap(); + assert_eq!(&**text_a.get_value().as_string().unwrap(), "heo"); + let bytes = doc_a.export_from(&Default::default()); + + let mut doc_b = LoroDoc::default(); + doc_b.start_auto_commit(); + let text_b = doc_b.get_text("text"); + text_b.insert_(0, "100").unwrap(); + doc_b.import(&bytes).unwrap(); + doc_a.import(&doc_b.export_snapshot()).unwrap(); + assert_eq!(text_a.get_value(), text_b.get_value()); +} + +#[test] +fn auto_commit_list() { + let mut doc_a = LoroDoc::default(); + doc_a.start_auto_commit(); + let list_a = doc_a.get_list("list"); + list_a.insert_(0, "hello".into()).unwrap(); + assert_eq!(list_a.get_value().to_json_value(), json!(["hello"])); + let text_a = list_a + .insert_container_(0, loro_common::ContainerType::Text) + .unwrap(); + let text = text_a.into_text().unwrap(); + text.insert_(0, "world").unwrap(); + let value = doc_a.get_deep_value(); + assert_eq!(value.to_json_value(), json!({"list": ["world", "hello"]})) +} + +#[test] +fn auto_commit_with_checkout() { + let mut doc = LoroDoc::default(); + doc.set_peer_id(1); + doc.start_auto_commit(); + let map = doc.get_map("a"); + map.insert_("0", 0.into()).unwrap(); + map.insert_("1", 1.into()).unwrap(); + map.insert_("2", 2.into()).unwrap(); + map.insert_("3", 3.into()).unwrap(); + doc.checkout(&Frontiers::from(ID::new(1, 0))).unwrap(); + assert_eq!(map.get_value().to_json_value(), json!({"0": 0})); + // assert error if insert after checkout + map.insert_("4", 4.into()).unwrap_err(); + doc.checkout_to_latest(); + // assert ok if doc is attached + map.insert_("4", 4.into()).unwrap(); + let expected = json!({"0": 0, "1": 1, "2": 2, "3": 3, "4": 4}); + + // should include all changes + let new = LoroDoc::default(); + let a = new.get_map("a"); + new.import(&doc.export_snapshot()).unwrap(); + assert_eq!(a.get_value().to_json_value(), expected,); +} diff --git a/crates/loro-wasm/deno.lock b/crates/loro-wasm/deno.lock index 86befe69d..b65f68e45 100644 --- a/crates/loro-wasm/deno.lock +++ b/crates/loro-wasm/deno.lock @@ -1,5 +1,8 @@ { - "version": "2", + "version": "3", + "redirects": { + "https://x.nest.land/std@0.73.0/path/mod.ts": "https://lra6z45nakk5lnu3yjchp7tftsdnwwikwr65ocha5eojfnlgu4sa.arweave.net/XEHs860CldW2m8JEd_5lnIbbWQq0fdcI4OkckrVmpyQ/path/mod.ts" + }, "remote": { "https://deno.land/std@0.105.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", "https://deno.land/std@0.105.0/_util/os.ts": "dfb186cc4e968c770ab6cc3288bd65f4871be03b93beecae57d657232ecffcac", diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 1119d7bf6..24fc4bb7d 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -6,11 +6,10 @@ use loro_internal::{ handler::{ListHandler, MapHandler, TextHandler, TreeHandler}, id::{Counter, TreeID, ID}, obs::SubID, - txn::Transaction as Txn, version::Frontiers, ContainerType, DiffEvent, LoroDoc, LoroError, VersionVector, }; -use std::{cell::RefCell, cmp::Ordering, ops::Deref, rc::Rc, sync::Arc}; +use std::{cell::RefCell, cmp::Ordering, ops::Deref, panic, rc::Rc, sync::Arc}; use wasm_bindgen::{__rt::IntoJsResult, prelude::*}; mod log; mod prelim; @@ -20,14 +19,7 @@ mod convert; #[wasm_bindgen(js_name = setPanicHook)] pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); + panic::set_hook(Box::new(console_error_panic_hook::hook)); } #[wasm_bindgen(js_name = setDebug)] @@ -149,19 +141,9 @@ fn frontiers_to_ids(frontiers: &Frontiers) -> Vec { impl Loro { #[wasm_bindgen(constructor)] pub fn new() -> Self { - Self(LoroDoc::new()) - } - - /// Create a new Loro transaction. - /// There can be only one transaction at a time. - /// - /// It's caller's responsibility to call `commit` or `abort` on the transaction. - /// Transaction.free() will commit the transaction if it's not committed or aborted. - #[wasm_bindgen(js_name = "newTransaction")] - pub fn new_transaction(&self, origin: Option) -> Transaction { - Transaction(Some( - self.0.txn_with_origin(&origin.unwrap_or_default()).unwrap(), - )) + let mut doc = LoroDoc::new(); + doc.start_auto_commit(); + Self(doc) } pub fn attach(&mut self) { @@ -173,6 +155,11 @@ impl Loro { Ok(()) } + pub fn checkout_to_latest(&mut self) -> JsResult<()> { + self.0.checkout_to_latest(); + Ok(()) + } + #[wasm_bindgen(js_name = "peerId", method, getter)] pub fn peer_id(&self) -> u64 { self.0.peer_id() @@ -184,6 +171,11 @@ impl Loro { Ok(LoroText(text)) } + /// Commit the cumulative auto commit transaction. + pub fn commit(&self, origin: Option) { + self.0.commit_with(origin.map(|x| x.into()), None, true); + } + #[wasm_bindgen(js_name = "getMap")] pub fn get_map(&self, name: &str) -> JsResult { let map = self.0.get_map(name); @@ -303,6 +295,7 @@ impl Loro { let observer = observer::Observer::new(f); self.0 .subscribe_deep(Arc::new(move |e| { + // call_after_micro_task(observer.clone(), e) call_subscriber(observer.clone(), e); })) .into_u32() @@ -311,24 +304,9 @@ impl Loro { pub fn unsubscribe(&self, subscription: u32) { self.0.unsubscribe(SubID::from_u32(subscription)) } - - /// It's the caller's responsibility to commit and free the transaction - #[wasm_bindgen(js_name = "__raw__transactionWithOrigin")] - pub fn transaction_with_origin( - &self, - origin: &JsOrigin, - f: js_sys::Function, - ) -> JsResult { - let origin = origin.as_string().unwrap(); - debug_log::group!("transaction with origin: {}", origin); - let txn = self.0.txn_with_origin(&origin)?; - let js_txn = JsValue::from(Transaction(Some(txn))); - let ans = f.call1(&JsValue::NULL, &js_txn); - debug_log::group_end!(); - ans - } } +#[allow(unused)] fn call_subscriber(ob: observer::Observer, e: DiffEvent) { // We convert the event to js object here, so that we don't need to worry about GC. // In the future, when FinalizationRegistry[1] is stable, we can use `--weak-ref`[2] feature @@ -423,54 +401,18 @@ impl Event { } } -#[wasm_bindgen] -pub struct Transaction(Option); - -#[wasm_bindgen] -impl Transaction { - pub fn commit(&mut self) -> JsResult<()> { - if let Some(x) = self.0.take() { - x.commit()?; - } - Ok(()) - } - - pub fn abort(&mut self) -> JsResult<()> { - if let Some(x) = self.0.take() { - x.abort(); - } - Ok(()) - } - - fn as_mut(&mut self) -> JsResult<&mut Txn> { - self.0 - .as_mut() - .ok_or_else(|| JsValue::from_str("Transaction is aborted")) - } -} - #[wasm_bindgen] pub struct LoroText(TextHandler); #[wasm_bindgen] impl LoroText { - pub fn __txn_insert( - &mut self, - txn: &mut Transaction, - index: usize, - content: &str, - ) -> JsResult<()> { - self.0.insert(txn.as_mut()?, index, content)?; + pub fn insert(&mut self, index: usize, content: &str) -> JsResult<()> { + self.0.insert_(index, content)?; Ok(()) } - pub fn __txn_delete( - &mut self, - txn: &mut Transaction, - index: usize, - len: usize, - ) -> JsResult<()> { - self.0.delete(txn.as_mut()?, index, len)?; + pub fn delete(&mut self, index: usize, len: usize) -> JsResult<()> { + self.0.delete_(index, len)?; Ok(()) } @@ -515,18 +457,14 @@ const CONTAINER_TYPE_ERR: &str = "Invalid container type, only supports Text, Ma #[wasm_bindgen] impl LoroMap { - pub fn __txn_insert( - &mut self, - txn: &mut Transaction, - key: &str, - value: JsValue, - ) -> JsResult<()> { - self.0.insert(txn.as_mut()?, key, value.into())?; + #[wasm_bindgen(js_name = "set")] + pub fn insert(&mut self, key: &str, value: JsValue) -> JsResult<()> { + self.0.insert_(key, value.into())?; Ok(()) } - pub fn __txn_delete(&mut self, txn: &mut Transaction, key: &str) -> JsResult<()> { - self.0.delete(txn.as_mut()?, key)?; + pub fn delete(&mut self, key: &str) -> JsResult<()> { + self.0.delete_(key)?; Ok(()) } @@ -552,20 +490,14 @@ impl LoroMap { } #[wasm_bindgen(js_name = "insertContainer")] - pub fn insert_container( - &mut self, - txn: &mut Transaction, - key: &str, - container_type: &str, - ) -> JsResult { + pub fn insert_container(&mut self, key: &str, container_type: &str) -> JsResult { let type_ = match container_type { "text" | "Text" => ContainerType::Text, "map" | "Map" => ContainerType::Map, "list" | "List" => ContainerType::List, _ => return Err(JsValue::from_str(CONTAINER_TYPE_ERR)), }; - let t = txn.as_mut()?; - let c = self.0.insert_container(t, key, type_)?; + let c = self.0.insert_container_(key, type_)?; let container = match type_ { ContainerType::Map => LoroMap(c.into_map().unwrap()).into(), @@ -599,23 +531,13 @@ pub struct LoroList(ListHandler); #[wasm_bindgen] impl LoroList { - pub fn __txn_insert( - &mut self, - txn: &mut Transaction, - index: usize, - value: JsValue, - ) -> JsResult<()> { - self.0.insert(txn.as_mut()?, index, value.into())?; + pub fn insert(&mut self, index: usize, value: JsValue) -> JsResult<()> { + self.0.insert_(index, value.into())?; Ok(()) } - pub fn __txn_delete( - &mut self, - txn: &mut Transaction, - index: usize, - len: usize, - ) -> JsResult<()> { - self.0.delete(txn.as_mut()?, index, len)?; + pub fn delete(&mut self, index: usize, len: usize) -> JsResult<()> { + self.0.delete_(index, len)?; Ok(()) } @@ -641,20 +563,14 @@ impl LoroList { } #[wasm_bindgen(js_name = "insertContainer")] - pub fn insert_container( - &mut self, - txn: &mut Transaction, - pos: usize, - container: &str, - ) -> JsResult { + pub fn insert_container(&mut self, pos: usize, container: &str) -> JsResult { let _type = match container { "text" | "Text" => ContainerType::Text, "map" | "Map" => ContainerType::Map, "list" | "List" => ContainerType::List, _ => return Err(JsValue::from_str(CONTAINER_TYPE_ERR)), }; - let t = txn.as_mut()?; - let c = self.0.insert_container(t, pos, _type)?; + let c = self.0.insert_container_(pos, _type)?; let container = match _type { ContainerType::Map => LoroMap(c.into_map().unwrap()).into(), ContainerType::List => LoroList(c.into_list().unwrap()).into(), @@ -689,51 +605,42 @@ pub struct LoroTree(TreeHandler); #[wasm_bindgen] impl LoroTree { - pub fn __txn_create( - &mut self, - txn: &mut Transaction, - parent: Option, - ) -> JsResult { + pub fn create(&mut self, parent: Option) -> JsResult { let id = if let Some(p) = parent { let parent: JsValue = p.into(); - self.0 - .create_and_mov(txn.as_mut()?, parent.try_into().unwrap())? + self.0.create_and_mov_(parent.try_into().unwrap())? } else { - self.0.create(txn.as_mut()?)? + self.0.create_()? }; let js_id: JsValue = id.into(); Ok(js_id.into()) } - pub fn __txn_move( - &mut self, - txn: &mut Transaction, - target: JsTreeID, - parent: JsTreeID, - ) -> JsResult<()> { + pub fn mov(&mut self, target: JsTreeID, parent: JsTreeID) -> JsResult<()> { let target: JsValue = target.into(); let target = TreeID::try_from(target).unwrap(); let parent: JsValue = parent.into(); let parent = TreeID::try_from(parent).unwrap(); - self.0.mov(txn.as_mut()?, target, parent)?; + self.0.mov_(target, parent)?; Ok(()) } - pub fn __txn_delete(&mut self, txn: &mut Transaction, target: JsTreeID) -> JsResult<()> { + pub fn delete(&mut self, target: JsTreeID) -> JsResult<()> { let target: JsValue = target.into(); - self.0.delete(txn.as_mut()?, target.try_into().unwrap())?; + self.0.delete_(target.try_into().unwrap())?; Ok(()) } - pub fn __txn_as_root(&mut self, txn: &mut Transaction, target: JsTreeID) -> JsResult<()> { + pub fn root(&mut self, target: JsTreeID) -> JsResult<()> { let target: JsValue = target.into(); - self.0.as_root(txn.as_mut()?, target.try_into().unwrap())?; + self.0.as_root_(target.try_into().unwrap())?; Ok(()) } - pub fn __txn_get_meta(&mut self, txn: &mut Transaction, target: JsTreeID) -> JsResult { + #[wasm_bindgen(js_name = "getMeta")] + pub fn get_meta(&mut self, target: JsTreeID) -> JsResult { let target: JsValue = target.into(); - let meta = self.0.get_meta(txn.as_mut()?, target.try_into().unwrap())?; + let meta = self.0.get_meta(target.try_into().unwrap())?; // .insert_meta(txn.as_mut()?, target.try_into().unwrap(), key, value.into())?; Ok(LoroMap(meta)) } diff --git a/loro-js/package.json b/loro-js/package.json index d5677fbd5..f9d0b7140 100644 --- a/loro-js/package.json +++ b/loro-js/package.json @@ -19,6 +19,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^15.0.1", "@typescript-eslint/parser": "^6.2.0", + "@vitest/ui": "^0.34.6", "esbuild": "^0.17.12", "eslint": "^8.46.0", "prettier": "^3.0.0", diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index b2300aff5..e3e9e0c0a 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -2,38 +2,23 @@ export { LoroList, LoroMap, LoroText, - LoroTree, PrelimList, PrelimMap, PrelimText, setPanicHook, - Transaction, } from "loro-wasm"; import { PrelimMap } from "loro-wasm"; import { PrelimText } from "loro-wasm"; import { PrelimList } from "loro-wasm"; import { ContainerID, - TreeID, Loro, LoroList, LoroMap, LoroText, - LoroTree, - Transaction, } from "loro-wasm"; -export type { ContainerID, ContainerType, TreeID } from "loro-wasm"; - -Loro.prototype.transact = function (cb, origin) { - return this.__raw__transactionWithOrigin(origin || "", (txn: Transaction) => { - try { - return cb(txn); - } finally { - txn.free(); - } - }); -}; +export type { ContainerID, ContainerType } from "loro-wasm"; Loro.prototype.getTypedMap = function (...args) { return this.getMap(...args); @@ -64,58 +49,11 @@ LoroMap.prototype.setTyped = function (...args) { return this.set(...args); }; -LoroText.prototype.insert = function (txn, pos, text) { - this.__txn_insert(txn, pos, text); -}; - -LoroText.prototype.delete = function (txn, pos, len) { - this.__txn_delete(txn, pos, len); -}; - -LoroList.prototype.insert = function (txn, pos, len) { - this.__txn_insert(txn, pos, len); -}; - -LoroList.prototype.delete = function (txn, pos, len) { - this.__txn_delete(txn, pos, len); -}; - -LoroMap.prototype.set = function (txn, key, value) { - this.__txn_insert(txn, key, value); -}; - -LoroMap.prototype.delete = function (txn, key) { - this.__txn_delete(txn, key); -}; - -LoroTree.prototype.create = function(txn, parent){ - return this.__txn_create(txn, parent); -} - - -LoroTree.prototype.move = function(txn, target, parent){ - this.__txn_move(txn, target, parent) -} - -LoroTree.prototype.asRoot = function(txn, target){ - this.__txn_as_root(txn, target) -} - - -LoroTree.prototype.delete = function(txn, target){ - this.__txn_delete(txn, target) -} - -LoroTree.prototype.getMeta = function(txn, target){ - return this.__txn_get_meta(txn, target) -} - export type Value = | ContainerID | string | number | null - | boolean | { [key: string]: Value } | Uint8Array | Value[]; @@ -157,15 +95,7 @@ export type MapDiff = { updated: Record; }; -export type TreeDiff = { - type: "tree"; - diff: { - target: TreeID, - action: {type: "create"} | {type: "move", parent: TreeID} | {type: "delete"} - }[] -} - -export type Diff = ListDiff | TextDiff | MapDiff| TreeDiff; +export type Diff = ListDiff | TextDiff | MapDiff; export interface LoroEvent { local: boolean; @@ -179,7 +109,7 @@ interface Listener { (event: LoroEvent): void; } -const CONTAINER_TYPES = ["Map", "Text", "List", "Tree"]; +const CONTAINER_TYPES = ["Map", "Text", "List"]; export function isContainerId(s: string): s is ContainerID { try { @@ -205,19 +135,11 @@ export function isContainerId(s: string): s is ContainerID { } } -export interface TreeNode{ - id: TreeID, - parent: TreeID | null, - children: TreeNode[] - meta: {[key: string]: any} -} - export { Loro }; declare module "loro-wasm" { interface Loro { subscribe(listener: Listener): number; - transact(f: (tx: Transaction) => T, origin?: string): T; } interface Loro = Record> { @@ -230,70 +152,54 @@ declare module "loro-wasm" { } interface LoroList { - insertContainer(txn: Transaction, pos: number, container: "Map"): LoroMap; - insertContainer(txn: Transaction, pos: number, container: "List"): LoroList; - insertContainer(txn: Transaction, pos: number, container: "Text"): LoroText; - insertContainer(txn: Transaction, pos: number, container: string): never; + insertContainer(pos: number, container: "Map"): LoroMap; + insertContainer(pos: number, container: "List"): LoroList; + insertContainer(pos: number, container: "Text"): LoroText; + insertContainer(pos: number, container: string): never; get(index: number): Value; getTyped(loro: Loro, index: Key): T[Key]; insertTyped( - txn: Transaction, pos: Key, value: T[Key], ): void; - insert(txn: Transaction, pos: number, value: Value | Prelim): void; - delete(txn: Transaction, pos: number, len: number): void; + insert(pos: number, value: Value | Prelim): void; + delete(pos: number, len: number): void; subscribe(txn: Loro, listener: Listener): number; } interface LoroMap = Record> { insertContainer( - txn: Transaction, key: string, container_type: "Map", ): LoroMap; insertContainer( - txn: Transaction, key: string, container_type: "List", ): LoroList; insertContainer( - txn: Transaction, key: string, container_type: "Text", ): LoroText; insertContainer( - txn: Transaction, key: string, container_type: string, ): never; get(key: string): Value; getTyped(txn: Loro, key: Key): T[Key]; - set(txn: Transaction, key: string, value: Value | Prelim): void; + set(key: string, value: Value | Prelim): void; setTyped( - txn: Transaction, key: Key, value: T[Key], ): void; - delete(txn: Transaction, key: string): void; + delete(key: string): void; subscribe(txn: Loro, listener: Listener): number; } interface LoroText { - insert(txn: Transaction, pos: number, text: string): void; - delete(txn: Transaction, pos: number, len: number): void; - subscribe(txn: Loro, listener: Listener): number; - } - - interface LoroTree{ - create(txn: Transaction, parent: TreeID | undefined): TreeID; - delete(txn: Transaction, target: TreeID):void; - move(txn: Transaction, target: TreeID, parent: TreeID):void; - asRoot(txn: Transaction, target:TreeID):void; - getMeta(txn: Transaction, target: TreeID): LoroMap; + insert(pos: number, text: string): void; + delete(pos: number, len: number): void; subscribe(txn: Loro, listener: Listener): number; - getDeepValue(): {roots: TreeNode[]}; } } diff --git a/loro-js/tests/checkout.test.ts b/loro-js/tests/checkout.test.ts index a9ba07407..c4215b645 100644 --- a/loro-js/tests/checkout.test.ts +++ b/loro-js/tests/checkout.test.ts @@ -9,14 +9,10 @@ describe("Checkout", () => { it("simple checkout", () => { const doc = new Loro(); const text = doc.getText("text"); - doc.transact(txn => { - text.insert(txn, 0, "hello world"); - }); + text.insert(0, "hello world"); + doc.commit(); const v = doc.frontiers(); - doc.transact(txn => { - text.insert(txn, 0, "000"); - }); - + text.insert(0, "000"); expect(doc.toJson()).toStrictEqual({ text: "000hello world" }); @@ -35,9 +31,8 @@ describe("Checkout", () => { it("Chinese char", () => { const doc = new Loro(); const text = doc.getText("text"); - doc.transact(txn => { - text.insert(txn, 0, "你好世界"); - }); + text.insert(0, "你好世界"); + doc.commit(); const v = doc.frontiers(); expect(v[0].counter).toBe(3); v[0].counter -= 1; @@ -60,22 +55,19 @@ describe("Checkout", () => { it("two clients", () => { const doc = new Loro(); const text = doc.getText("text"); - const txn = doc.newTransaction(""); - text.insert(txn, 0, "0"); - txn.commit(); + text.insert(0, "0"); + doc.commit(); const v0 = doc.frontiers(); const docB = new Loro(); docB.import(doc.exportFrom()); expect(docB.cmpFrontiers(v0)).toBe(0); - doc.transact((t) => { - text.insert(t, 1, "0"); - }); + text.insert(1, "0"); + doc.commit(); expect(docB.cmpFrontiers(doc.frontiers())).toBe(-1); const textB = docB.getText("text"); - docB.transact((t) => { - textB.insert(t, 0, "0"); - }); + textB.insert(0, "0"); + docB.commit(); expect(docB.cmpFrontiers(doc.frontiers())).toBe(-1); docB.import(doc.exportFrom()); expect(docB.cmpFrontiers(doc.frontiers())).toBe(1); diff --git a/loro-js/tests/event.test.ts b/loro-js/tests/event.test.ts index 9f9c63245..18293639f 100644 --- a/loro-js/tests/event.test.ts +++ b/loro-js/tests/event.test.ts @@ -19,9 +19,8 @@ describe("event", () => { }); const text = loro.getText("text"); const id = text.id; - loro.transact((tx) => { - text.insert(tx, 0, "123"); - }); + text.insert(0, "123"); + loro.commit(); expect(lastEvent?.target).toEqual(id); }); @@ -32,22 +31,17 @@ describe("event", () => { lastEvent = event; }); const map = loro.getMap("map"); - const subMap = loro.transact((tx) => { - const subMap = map.insertContainer(tx, "sub", "Map"); - subMap.set(tx, "0", "1"); - return subMap; - }); + const subMap = map.insertContainer("sub", "Map"); + subMap.set("0", "1"); + loro.commit(); expect(lastEvent?.path).toStrictEqual(["map", "sub"]); - const text = loro.transact((tx) => { - const list = subMap.insertContainer(tx, "list", "List"); - list.insert(tx, 0, "2"); - const text = list.insertContainer(tx, 1, "Text"); - return text; - }); - loro.transact((tx) => { - text.insert(tx, 0, "3"); - }); + const list = subMap.insertContainer("list", "List"); + list.insert(0, "2"); + const text = list.insertContainer(1, "Text"); + loro.commit(); + text.insert(0, "3"); + loro.commit(); expect(lastEvent?.path).toStrictEqual(["map", "sub", "list", 1]); }); @@ -58,16 +52,14 @@ describe("event", () => { lastEvent = event; }); const text = loro.getText("t"); - loro.transact((tx) => { - text.insert(tx, 0, "3"); - }); + text.insert(0, "3"); + loro.commit(); expect(lastEvent?.diff).toStrictEqual({ type: "text", diff: [{ insert: "3" }], } as TextDiff); - loro.transact((tx) => { - text.insert(tx, 1, "12"); - }); + text.insert(1, "12"); + loro.commit(); expect(lastEvent?.diff).toStrictEqual({ type: "text", diff: [{ retain: 1 }, { insert: "12" }], @@ -81,16 +73,14 @@ describe("event", () => { lastEvent = event; }); const text = loro.getList("l"); - loro.transact((tx) => { - text.insert(tx, 0, "3"); - }); + text.insert(0, "3"); + loro.commit(); expect(lastEvent?.diff).toStrictEqual({ type: "list", diff: [{ insert: ["3"] }], } as ListDiff); - loro.transact((tx) => { - text.insert(tx, 1, "12"); - }); + text.insert(1, "12"); + loro.commit(); expect(lastEvent?.diff).toStrictEqual({ type: "list", diff: [{ retain: 1 }, { insert: ["12"] }], @@ -104,10 +94,9 @@ describe("event", () => { lastEvent = event; }); const map = loro.getMap("m"); - loro.transact((tx) => { - map.set(tx, "0", "3"); - map.set(tx, "1", "2"); - }); + map.set("0", "3"); + map.set("1", "2"); + loro.commit(); expect(lastEvent?.diff).toStrictEqual({ type: "map", updated: { @@ -115,10 +104,9 @@ describe("event", () => { "1": "2", }, } as MapDiff); - loro.transact((tx) => { - map.set(tx, "0", "0"); - map.set(tx, "1", "1"); - }); + map.set("0", "0"); + map.set("1", "1"); + loro.commit(); expect(lastEvent?.diff).toStrictEqual({ type: "map", updated: { @@ -143,12 +131,10 @@ describe("event", () => { expect(event.target).toBe(text.id); }); - loro.transact((tx) => { - text.insert(tx, 0, "123"); - }); - loro.transact((tx) => { - text.insert(tx, 1, "456"); - }); + text.insert(0, "123"); + loro.commit(); + text.insert(1, "456"); + loro.commit(); expect(ran).toBeTruthy(); // subscribeOnce test expect(text.toString()).toEqual("145623"); @@ -156,9 +142,8 @@ describe("event", () => { // unsubscribe const oldRan = ran; text.unsubscribe(loro, sub); - loro.transact((tx) => { - text.insert(tx, 0, "789"); - }); + text.insert(0, "789"); + loro.commit(); expect(ran).toBe(oldRan); }); @@ -170,20 +155,20 @@ describe("event", () => { times += 1; }); - const subMap = loro.transact((tx) => - map.insertContainer(tx, "sub", "Map"), - ); + const subMap = map.insertContainer("sub", "Map"); + loro.commit(); expect(times).toBe(1); - const text = loro.transact((tx) => - subMap.insertContainer(tx, "k", "Text"), - ); + const text = subMap.insertContainer("k", "Text"); + loro.commit(); expect(times).toBe(2); - loro.transact((tx) => text.insert(tx, 0, "123")); + text.insert(0, "123"); + loro.commit(); expect(times).toBe(3); // unsubscribe loro.unsubscribe(sub); - loro.transact((tx) => text.insert(tx, 0, "123")); + text.insert(0, "123"); + loro.commit(); expect(times).toBe(3); }); @@ -195,14 +180,17 @@ describe("event", () => { times += 1; }); - const text = loro.transact((tx) => list.insertContainer(tx, 0, "Text")); + const text = list.insertContainer(0, "Text"); + loro.commit(); expect(times).toBe(1); - loro.transact((tx) => text.insert(tx, 0, "123")); + text.insert(0, "123"); + loro.commit(); expect(times).toBe(2); // unsubscribe loro.unsubscribe(sub); - loro.transact((tx) => text.insert(tx, 0, "123")); + text.insert(0, "123"); + loro.commit(); expect(times).toBe(2); }); }); @@ -232,16 +220,20 @@ describe("event", () => { string = newString + string.slice(pos); } }); - loro.transact((tx) => text.insert(tx, 0, "你好")); + text.insert(0, "你好"); + loro.commit(); expect(text.toString()).toBe(string); - loro.transact((tx) => text.insert(tx, 1, "世界")); + text.insert(1, "世界"); + loro.commit(); expect(text.toString()).toBe(string); - loro.transact((tx) => text.insert(tx, 2, "👍")); + text.insert(2, "👍"); + loro.commit(); expect(text.toString()).toBe(string); - loro.transact((tx) => text.insert(tx, 2, "♪(^∇^*)")); + text.insert(2, "♪(^∇^*)"); + loro.commit(); expect(text.toString()).toBe(string); }); }); diff --git a/loro-js/tests/frontiers.test.ts b/loro-js/tests/frontiers.test.ts index 8cd4ea8be..d9b22faf1 100644 --- a/loro-js/tests/frontiers.test.ts +++ b/loro-js/tests/frontiers.test.ts @@ -9,22 +9,19 @@ describe("Frontiers", () => { it("two clients", () => { const doc = new Loro(); const text = doc.getText("text"); - const txn = doc.newTransaction(""); - text.insert(txn, 0, "0"); - txn.commit(); + text.insert(0, "0"); + doc.commit(); const v0 = doc.frontiers(); const docB = new Loro(); docB.import(doc.exportFrom()); expect(docB.cmpFrontiers(v0)).toBe(0); - doc.transact((t) => { - text.insert(t, 1, "0"); - }); + text.insert(1, "0"); + doc.commit(); expect(docB.cmpFrontiers(doc.frontiers())).toBe(-1); const textB = docB.getText("text"); - docB.transact((t) => { - textB.insert(t, 0, "0"); - }); + textB.insert(0, "0"); + docB.commit(); expect(docB.cmpFrontiers(doc.frontiers())).toBe(-1); docB.import(doc.exportFrom()); expect(docB.cmpFrontiers(doc.frontiers())).toBe(1); diff --git a/loro-js/tests/misc.test.ts b/loro-js/tests/misc.test.ts index 79f9440b7..be86f88bb 100644 --- a/loro-js/tests/misc.test.ts +++ b/loro-js/tests/misc.test.ts @@ -6,10 +6,11 @@ import { PrelimList, PrelimMap, PrelimText, - Transaction, + setPanicHook, } from "../src"; import { expectTypeOf } from "vitest"; -import { assert } from "https://lra6z45nakk5lnu3yjchp7tftsdnwwikwr65ocha5eojfnlgu4sa.arweave.net/XEHs860CldW2m8JEd_5lnIbbWQq0fdcI4OkckrVmpyQ/_util/assert.ts"; + +setPanicHook(); function assertEquals(a: any, b: any) { expect(a).toStrictEqual(b); @@ -24,13 +25,12 @@ describe("transaction", () => { count += 1; loro.unsubscribe(sub); }); - loro.transact((txn: Transaction) => { - expect(count).toBe(0); - text.insert(txn, 0, "hello world"); - expect(count).toBe(0); - text.insert(txn, 0, "hello world"); - assertEquals(count, 0); - }); + expect(count).toBe(0); + text.insert(0, "hello world"); + expect(count).toBe(0); + text.insert(0, "hello world"); + assertEquals(count, 0); + loro.commit(); assertEquals(count, 1); }); @@ -43,13 +43,13 @@ describe("transaction", () => { loro.unsubscribe(sub); assertEquals(event.origin, "origin"); }); - loro.transact((txn: Transaction) => { - assertEquals(count, 0); - text.insert(txn, 0, "hello world"); - assertEquals(count, 0); - text.insert(txn, 0, "hello world"); - assertEquals(count, 0); - }, "origin"); + + assertEquals(count, 0); + text.insert(0, "hello world"); + assertEquals(count, 0); + text.insert(0, "hello world"); + assertEquals(count, 0); + loro.commit("origin"); assertEquals(count, 1); }); }); @@ -63,27 +63,24 @@ describe("subscribe", () => { let i = 1; const sub = loro.subscribe(() => { if (i > 0) { - loro.transact(txn => { - list.insert(txn, 0, i); - i--; - }) + list.insert(0, i); + loro.commit(); + i--; } count += 1; }); - loro.transact((txn) => { - text.insert(txn, 0, "hello world"); - }) + + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 2); - loro.transact((txn) => { - text.insert(txn, 0, "hello world"); - }); + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 3); loro.unsubscribe(sub); - loro.transact(txn => { - text.insert(txn, 0, "hello world"); - }) + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 3); }); @@ -96,14 +93,12 @@ describe("subscribe", () => { loro.unsubscribe(sub); }); assertEquals(count, 0); - loro.transact(txn => { - text.insert(txn, 0, "hello world"); - }) + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 1); - loro.transact(txn => { - text.insert(txn, 0, "hello world"); - }) + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 1); }); @@ -115,18 +110,15 @@ describe("subscribe", () => { const sub = loro.subscribe(() => { count += 1; }); - loro.transact(loro => { - text.insert(loro, 0, "hello world"); - }) + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 1); - loro.transact(loro => { - text.insert(loro, 0, "hello world"); - }) + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 2); loro.unsubscribe(sub); - loro.transact(loro => { - text.insert(loro, 0, "hello world"); - }) + text.insert(0, "hello world"); + loro.commit(); assertEquals(count, 2); }); }); @@ -153,9 +145,8 @@ describe("sync", () => { }); const aText = a.getText("text"); const bText = b.getText("text"); - a.transact(txn => { - aText.insert(txn, 0, "abc"); - }); + aText.insert(0, "abc"); + a.commit(); assertEquals(aText.toString(), bText.toString()); }); @@ -163,25 +154,19 @@ describe("sync", () => { it("sync", () => { const loro = new Loro(); const text = loro.getText("text"); - loro.transact(txn => { - text.insert(txn, 0, "hello world"); - }); + text.insert(0, "hello world"); const loro_bk = new Loro(); loro_bk.import(loro.exportFrom(undefined)); assertEquals(loro_bk.toJson(), loro.toJson()); const text_bk = loro_bk.getText("text"); assertEquals(text_bk.toString(), "hello world"); - loro_bk.transact(txn => { - text_bk.insert(txn, 0, "a "); - }); + text_bk.insert(0, "a "); loro.import(loro_bk.exportFrom(undefined)); assertEquals(text.toString(), "a hello world"); const map = loro.getMap("map"); - loro.transact(txn => { - map.set(txn, "key", "value"); - }); + map.set("key", "value"); }); }); @@ -218,11 +203,10 @@ describe("prelim", () => { }); it("prelim map integrate", () => { - loro.transact(txn => { - map.set(txn, "text", prelim_text); - map.set(txn, "map", prelim_map); - map.set(txn, "list", prelim_list); - }); + map.set("text", prelim_text); + map.set("map", prelim_map); + map.set("list", prelim_list); + loro.commit(); assertEquals(map.getDeepValue(), { text: "hello everyone", @@ -235,11 +219,10 @@ describe("prelim", () => { const prelim_text = new PrelimText("ttt"); const prelim_map = new PrelimMap({ a: 1, b: 2 }); const prelim_list = new PrelimList([1, "2", { a: 4 }]); - loro.transact(txn => { - list.insert(txn, 0, prelim_text); - list.insert(txn, 1, prelim_map); - list.insert(txn, 2, prelim_list); - }); + list.insert(0, prelim_text); + list.insert(1, prelim_map); + list.insert(2, prelim_list); + loro.commit(); assertEquals(list.getDeepValue(), ["ttt", { a: 1, b: 2 }, [1, "2", { a: 4, @@ -251,51 +234,33 @@ describe("prelim", () => { describe("wasm", () => { const loro = new Loro(); const a = loro.getText("ha"); - loro.transact(txn => { - a.insert(txn, 0, "hello world"); + a.insert(0, "hello world"); + a.delete(6, 5); + a.insert(6, "everyone"); + loro.commit(); - a.delete(txn, 6, 5); - a.insert(txn, 6, "everyone"); - }); const b = loro.getMap("ha"); - loro.transact(txn => { - b.set(txn, "ab", 123); - }); + b.set("ab", 123); + loro.commit(); - const bText = loro.transact(txn => { - return b.insertContainer(txn, "hh", "Text") - }); + const bText = b.insertContainer("hh", "Text"); + loro.commit(); it("map get", () => { assertEquals(b.get("ab"), 123); }); it("getValueDeep", () => { - loro.transact(txn => { - bText.insert(txn, 0, "hello world Text"); - }); - + bText.insert(0, "hello world Text"); assertEquals(b.getDeepValue(), { ab: 123, hh: "hello world Text" }); }); - it("should throw error when using the wrong context", () => { - expect(() => { - const loro2 = new Loro(); - loro2.transact(txn => { - bText.insert(txn, 0, "hello world Text"); - }); - - }).toThrow(); - }); - it("get container by id", () => { const id = b.id; const b2 = loro.getContainerById(id) as LoroMap; assertEquals(b2.value, b.value); assertEquals(b2.id, id); - loro.transact(txn => { - b2.set(txn, "0", 12); - }); + b2.set("0", 12); assertEquals(b2.value, b.value); }); @@ -312,9 +277,7 @@ describe("type", () => { it("test recursive map type", () => { const loro = new Loro<{ map: LoroMap<{ map: LoroMap<{ name: "he" }> }> }>(); const map = loro.getTypedMap("map"); - loro.transact(txn => { - map.insertContainer(txn, "map", "Map"); - }); + map.insertContainer("map", "Map"); const subMap = map.getTyped(loro, "map"); const name = subMap.getTyped(loro, "name"); @@ -325,14 +288,8 @@ describe("type", () => { const loro = new Loro<{ list: LoroList<[string, number]> }>(); const list = loro.getTypedList("list"); console.dir((list as any).__proto__); - loro.transact(txn => { - list.insertTyped(txn, 0, "123"); - }); - - loro.transact(txn => { - list.insertTyped(txn, 1, 123); - }); - + list.insertTyped(0, "123"); + list.insertTyped(1, 123); const v0 = list.getTyped(loro, 0); expectTypeOf(v0).toEqualTypeOf(); const v1 = list.getTyped(loro, 1); @@ -340,44 +297,32 @@ describe("type", () => { }); it("test binary type", () => { - const loro = new Loro<{ list: LoroList<[string, number]> }>(); - const list = loro.getTypedList("list"); - console.dir((list as any).__proto__); - loro.transact(txn => { - list.insertTyped(txn, 0, new Uint8Array(10)); - }); - const v0 = list.getTyped(loro, 0); - expectTypeOf(v0).toEqualTypeOf(); + // const loro = new Loro<{ list: LoroList<[string, number]> }>(); + // const list = loro.getTypedList("list"); + // console.dir((list as any).__proto__); + // list.insertTyped(0, new Uint8Array(10)); + // const v0 = list.getTyped(loro, 0); + // expectTypeOf(v0).toEqualTypeOf(); }); }); describe("tree", () => { const loro = new Loro(); const tree = loro.getTree("root"); - - it("create move", ()=>{ - const id = loro.transact((txn)=>{ - return tree.create(txn); - }) - const childID = loro.transact((txn)=>{ - return tree.create(txn, id); - }) + + it("create move", () => { + const id = tree.create(); + const childID = tree.create(id); console.log(typeof id); - assertEquals(tree.parent(childID), id); }) - it("meta", ()=>{ - const id = loro.transact((txn)=>{ - return tree.create(txn); - }) - const meta = loro.transact((txn)=>{ - const meta = tree.getMeta(txn, id); - meta.set(txn, "a", 123); - return meta; - }) + it("meta", () => { + const id = tree.create() + const meta = tree.getMeta(id); + meta.set("a", 123); assertEquals(meta.get("a"), 123); - + }) }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd320923a..06a5c8691 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -29,6 +29,9 @@ importers: '@typescript-eslint/parser': specifier: ^6.2.0 version: registry.npmmirror.com/@typescript-eslint/parser@6.2.0(eslint@8.46.0)(typescript@5.0.3) + '@vitest/ui': + specifier: ^0.34.6 + version: registry.npmmirror.com/@vitest/ui@0.34.6(vitest@0.29.8) esbuild: specifier: ^0.17.12 version: 0.17.15 @@ -58,19 +61,21 @@ importers: version: 3.2.2(vite@4.2.1) vitest: specifier: ^0.29.7 - version: 0.29.8 + version: 0.29.8(@vitest/ui@0.34.6) packages: /@babel/helper-validator-identifier@7.19.1: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} + requiresBuild: true dev: true optional: true /@babel/highlight@7.18.6: resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} engines: {node: '>=6.9.0'} + requiresBuild: true dependencies: '@babel/helper-validator-identifier': 7.19.1 chalk: 2.4.2 @@ -219,6 +224,7 @@ packages: /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} + requiresBuild: true dependencies: color-convert: 1.9.3 dev: true @@ -264,6 +270,7 @@ packages: /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} + requiresBuild: true dependencies: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 @@ -285,6 +292,7 @@ packages: /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + requiresBuild: true dependencies: color-name: 1.1.3 dev: true @@ -292,6 +300,7 @@ packages: /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + requiresBuild: true dev: true optional: true @@ -369,6 +378,7 @@ packages: /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + requiresBuild: true dev: true optional: true @@ -387,6 +397,7 @@ packages: /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} + requiresBuild: true dev: true optional: true @@ -426,6 +437,7 @@ packages: /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + requiresBuild: true dev: true optional: true @@ -632,6 +644,7 @@ packages: /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} + requiresBuild: true dependencies: has-flag: 3.0.0 dev: true @@ -752,7 +765,7 @@ packages: fsevents: registry.npmmirror.com/fsevents@2.3.2 dev: true - /vitest@0.29.8: + /vitest@0.29.8(@vitest/ui@0.34.6): resolution: {integrity: sha512-JIAVi2GK5cvA6awGpH0HvH/gEG9PZ0a/WoxdiV3PmqK+3CjQMf8c+J/Vhv4mdZ2nRyXFw66sAg6qz7VNkaHfDQ==} engines: {node: '>=v14.16.0'} hasBin: true @@ -789,6 +802,7 @@ packages: '@vitest/expect': 0.29.8 '@vitest/runner': 0.29.8 '@vitest/spy': 0.29.8 + '@vitest/ui': registry.npmmirror.com/@vitest/ui@0.34.6(vitest@0.29.8) '@vitest/utils': 0.29.8 acorn: 8.8.2 acorn-walk: 8.2.0 @@ -1178,6 +1192,15 @@ packages: version: 1.2.1 dev: true + registry.npmmirror.com/@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz} + name: '@jest/schemas' + version: 29.6.3 + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': registry.npmmirror.com/@sinclair/typebox@0.27.8 + dev: true + registry.npmmirror.com/@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz} name: '@nodelib/fs.stat' @@ -1195,6 +1218,18 @@ packages: fastq: registry.npmmirror.com/fastq@1.15.0 dev: true + registry.npmmirror.com/@polka/url@1.0.0-next.23: + resolution: {integrity: sha512-C16M+IYz0rgRhWZdCmK+h58JMv8vijAA61gmz2rspCSwKwzBebpdcsiUmwrtJRdphuY30i6BSLEOP8ppbNLyLg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.23.tgz} + name: '@polka/url' + version: 1.0.0-next.23 + dev: true + + registry.npmmirror.com/@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.8.tgz} + name: '@sinclair/typebox' + version: 0.27.8 + dev: true + registry.npmmirror.com/@swc/core-darwin-arm64@1.3.44: resolution: {integrity: sha512-Y+oVsCjXUPvr3D9YLuB1gjP84TseM/CRkbPNrf+3JXQhsPEkgxdIdFP1cl/obeqMQrRgPpvSfK+TOvGuOuV22g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.44.tgz} name: '@swc/core-darwin-arm64' @@ -1384,6 +1419,34 @@ packages: eslint-visitor-keys: registry.npmmirror.com/eslint-visitor-keys@3.4.2 dev: true + registry.npmmirror.com/@vitest/ui@0.34.6(vitest@0.29.8): + resolution: {integrity: sha512-/fxnCwGC0Txmr3tF3BwAbo3v6U2SkBTGR9UB8zo0Ztlx0BTOXHucE0gDHY7SjwEktCOHatiGmli9kZD6gYSoWQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@vitest/ui/-/ui-0.34.6.tgz} + id: registry.npmmirror.com/@vitest/ui/0.34.6 + name: '@vitest/ui' + version: 0.34.6 + peerDependencies: + vitest: '>=0.30.1 <1' + dependencies: + '@vitest/utils': registry.npmmirror.com/@vitest/utils@0.34.6 + fast-glob: registry.npmmirror.com/fast-glob@3.3.1 + fflate: registry.npmmirror.com/fflate@0.8.1 + flatted: registry.npmmirror.com/flatted@3.2.7 + pathe: registry.npmmirror.com/pathe@1.1.1 + picocolors: registry.npmmirror.com/picocolors@1.0.0 + sirv: registry.npmmirror.com/sirv@2.0.3 + vitest: 0.29.8(@vitest/ui@0.34.6) + dev: true + + registry.npmmirror.com/@vitest/utils@0.34.6: + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@vitest/utils/-/utils-0.34.6.tgz} + name: '@vitest/utils' + version: 0.34.6 + dependencies: + diff-sequences: registry.npmmirror.com/diff-sequences@29.6.3 + loupe: registry.npmmirror.com/loupe@2.3.6 + pretty-format: registry.npmmirror.com/pretty-format@29.7.0 + dev: true + registry.npmmirror.com/acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz} id: registry.npmmirror.com/acorn-jsx/5.3.2 @@ -1430,6 +1493,13 @@ packages: color-convert: registry.npmmirror.com/color-convert@2.0.1 dev: true + registry.npmmirror.com/ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz} + name: ansi-styles + version: 5.2.0 + engines: {node: '>=10'} + dev: true + registry.npmmirror.com/argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz} name: argparse @@ -1530,6 +1600,13 @@ packages: version: 0.1.4 dev: true + registry.npmmirror.com/diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz} + name: diff-sequences + version: 29.6.3 + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + registry.npmmirror.com/dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz} name: dir-glob @@ -1734,6 +1811,12 @@ packages: reusify: registry.npmmirror.com/reusify@1.0.4 dev: true + registry.npmmirror.com/fflate@0.8.1: + resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fflate/-/fflate-0.8.1.tgz} + name: fflate + version: 0.8.1 + dev: true + registry.npmmirror.com/file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz} name: file-entry-cache @@ -1800,6 +1883,12 @@ packages: version: 1.1.1 dev: true + registry.npmmirror.com/get-func-name@2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.0.tgz} + name: get-func-name + version: 2.0.0 + dev: true + registry.npmmirror.com/glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz} name: glob-parent @@ -2005,6 +2094,14 @@ packages: version: 4.6.2 dev: true + registry.npmmirror.com/loupe@2.3.6: + resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/loupe/-/loupe-2.3.6.tgz} + name: loupe + version: 2.3.6 + dependencies: + get-func-name: registry.npmmirror.com/get-func-name@2.0.0 + dev: true + registry.npmmirror.com/lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz} name: lru-cache @@ -2039,6 +2136,13 @@ packages: brace-expansion: registry.npmmirror.com/brace-expansion@1.1.11 dev: true + registry.npmmirror.com/mrmime@1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mrmime/-/mrmime-1.0.1.tgz} + name: mrmime + version: 1.0.1 + engines: {node: '>=10'} + dev: true + registry.npmmirror.com/ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz} name: ms @@ -2142,6 +2246,12 @@ packages: engines: {node: '>=8'} dev: true + registry.npmmirror.com/pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/pathe/-/pathe-1.1.1.tgz} + name: pathe + version: 1.1.1 + dev: true + registry.npmmirror.com/picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz} name: picocolors @@ -2181,6 +2291,17 @@ packages: hasBin: true dev: true + registry.npmmirror.com/pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz} + name: pretty-format + version: 29.7.0 + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': registry.npmmirror.com/@jest/schemas@29.6.3 + ansi-styles: registry.npmmirror.com/ansi-styles@5.2.0 + react-is: registry.npmmirror.com/react-is@18.2.0 + dev: true + registry.npmmirror.com/punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/punycode/-/punycode-2.3.0.tgz} name: punycode @@ -2194,6 +2315,12 @@ packages: version: 1.2.3 dev: true + registry.npmmirror.com/react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-is/-/react-is-18.2.0.tgz} + name: react-is + version: 18.2.0 + dev: true + registry.npmmirror.com/resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz} name: resolve-from @@ -2272,6 +2399,17 @@ packages: engines: {node: '>=8'} dev: true + registry.npmmirror.com/sirv@2.0.3: + resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sirv/-/sirv-2.0.3.tgz} + name: sirv + version: 2.0.3 + engines: {node: '>= 10'} + dependencies: + '@polka/url': registry.npmmirror.com/@polka/url@1.0.0-next.23 + mrmime: registry.npmmirror.com/mrmime@1.0.1 + totalist: registry.npmmirror.com/totalist@3.0.1 + dev: true + registry.npmmirror.com/slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz} name: slash @@ -2333,6 +2471,13 @@ packages: is-number: registry.npmmirror.com/is-number@7.0.0 dev: true + registry.npmmirror.com/totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz} + name: totalist + version: 3.0.1 + engines: {node: '>=6'} + dev: true + registry.npmmirror.com/ts-api-utils@1.0.1(typescript@5.0.3): resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==, registry: http://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz} id: registry.npmmirror.com/ts-api-utils/1.0.1