Feat: autocommit transaction (#127)

* feat: auto commit

* fix: make recursive single thread event work again
This commit is contained in:
Zixuan Chen 2023-10-30 18:32:36 +08:00 committed by GitHub
parent 734b832c00
commit 8293347334
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 769 additions and 554 deletions

View file

@ -36,6 +36,8 @@ pub enum LoroError {
TreeError(#[from] LoroTreeError),
#[error("Invalid argument ({0})")]
ArgErr(Box<str>),
#[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:?})")]

View file

@ -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();

View file

@ -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
}

View file

@ -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<Actor> {
fn preprocess(&mut self, action: &mut Action) {
let max_users = self.len() as u8;
@ -552,13 +531,10 @@ impl Actionable for Vec<Actor> {
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();
}

View file

@ -22,6 +22,7 @@ use std::{
#[derive(Clone)]
pub struct TextHandler {
txn: Weak<Mutex<Option<Transaction>>>,
container_idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
}
@ -34,6 +35,7 @@ impl std::fmt::Debug for TextHandler {
#[derive(Clone)]
pub struct MapHandler {
txn: Weak<Mutex<Option<Transaction>>>,
container_idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
}
@ -46,6 +48,7 @@ impl std::fmt::Debug for MapHandler {
#[derive(Clone)]
pub struct ListHandler {
txn: Weak<Mutex<Option<Transaction>>>,
container_idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
}
@ -59,6 +62,7 @@ impl std::fmt::Debug for ListHandler {
///
#[derive(Clone)]
pub struct TreeHandler {
txn: Weak<Mutex<Option<Transaction>>>,
container_idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
}
@ -98,20 +102,29 @@ impl Handler {
}
impl Handler {
fn new(value: ContainerIdx, state: Weak<Mutex<DocState>>) -> Self {
fn new(
txn: Weak<Mutex<Option<Transaction>>>,
value: ContainerIdx,
state: Weak<Mutex<DocState>>,
) -> 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<Mutex<DocState>>) -> Self {
pub fn new(
txn: Weak<Mutex<Option<Transaction>>>,
idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
) -> 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<Mutex<DocState>>) -> Self {
pub fn new(
txn: Weak<Mutex<Option<Transaction>>>,
idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
) -> 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<Option<LoroValue>> {
with_txn(&self.txn, |txn| self.pop(txn))
}
pub fn pop(&self, txn: &mut Transaction) -> LoroResult<Option<LoroValue>> {
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<Handler> {
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<Mutex<DocState>>) -> Self {
pub fn new(
txn: Weak<Mutex<Option<Transaction>>>,
idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
) -> 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<Handler> {
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<Mutex<DocState>>) -> Self {
pub fn new(
txn: Weak<Mutex<Option<Transaction>>>,
idx: ContainerIdx,
state: Weak<Mutex<DocState>>,
) -> Self {
assert_eq!(idx.get_type(), ContainerType::Tree);
Self {
txn,
container_idx: idx,
state,
}
}
pub fn create_(&self) -> LoroResult<TreeID> {
with_txn(&self.txn, |txn| self.create(txn))
}
pub fn create(&self, txn: &mut Transaction) -> LoroResult<TreeID> {
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<TreeID> {
with_txn(&self.txn, |txn| self.create_and_mov(txn, parent))
}
pub fn create_and_mov(&self, txn: &mut Transaction, parent: TreeID) -> LoroResult<TreeID> {
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<MapHandler> {
pub fn get_meta(&self, target: TreeID) -> LoroResult<MapHandler> {
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<R>(
txn: &Weak<Mutex<Option<Transaction>>>,
f: impl FnOnce(&mut Transaction) -> LoroResult<R>,
) -> LoroResult<R> {
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)?;

View file

@ -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<Observer>,
diff_calculator: Arc<Mutex<DiffCalculator>>,
txn: Arc<Mutex<Option<Transaction>>>,
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<InternalString>,
timestamp: Option<Timestamp>,
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<Mutex<Option<Transaction>>> {
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<u8> {
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<string_cache::EmptyStaticAtomSet>,
) -> 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<u8> {
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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<u8>]) -> 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;

View file

@ -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());

View file

@ -30,9 +30,10 @@ use super::{
state::{DocState, State},
};
pub type OnCommitFn = Box<dyn FnOnce(&Arc<Mutex<DocState>>)>;
pub type OnCommitFn = Box<dyn FnOnce(&Arc<Mutex<DocState>>) + Sync + Send>;
pub struct Transaction {
global_txn: Weak<Mutex<Option<Transaction>>>,
peer: PeerID,
origin: InternalString,
start_counter: Counter,
@ -85,14 +86,19 @@ pub(super) enum EventHint {
}
impl Transaction {
pub fn new(state: Arc<Mutex<DocState>>, oplog: Arc<Mutex<OpLog>>) -> Self {
Self::new_with_origin(state, oplog, "".into())
pub fn new(
state: Arc<Mutex<DocState>>,
oplog: Arc<Mutex<OpLog>>,
global_txn: Weak<Mutex<Option<Transaction>>>,
) -> Self {
Self::new_with_origin(state, oplog, "".into(), global_txn)
}
pub fn new_with_origin(
state: Arc<Mutex<DocState>>,
oplog: Arc<Mutex<OpLog>>,
origin: InternalString,
global_txn: Weak<Mutex<Option<Transaction>>>,
) -> 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<OnCommitFn> {
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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&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<I: IntoContainerId>(&self, id: I, c_type: ContainerType) -> ContainerIdx {

View file

@ -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,);
}

View file

@ -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",

View file

@ -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<JsID> {
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<String>) -> 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<String>) {
self.0.commit_with(origin.map(|x| x.into()), None, true);
}
#[wasm_bindgen(js_name = "getMap")]
pub fn get_map(&self, name: &str) -> JsResult<LoroMap> {
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<JsValue> {
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<Txn>);
#[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<JsValue> {
pub fn insert_container(&mut self, key: &str, container_type: &str) -> JsResult<JsValue> {
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<JsValue> {
pub fn insert_container(&mut self, pos: usize, container: &str) -> JsResult<JsValue> {
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<JsTreeID>,
) -> JsResult<JsTreeID> {
pub fn create(&mut self, parent: Option<JsTreeID>) -> JsResult<JsTreeID> {
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<LoroMap> {
#[wasm_bindgen(js_name = "getMeta")]
pub fn get_meta(&mut self, target: JsTreeID) -> JsResult<LoroMap> {
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))
}

View file

@ -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",

View file

@ -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<string, Value | undefined>;
};
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<T>(f: (tx: Transaction) => T, origin?: string): T;
}
interface Loro<T extends Record<string, any> = Record<string, any>> {
@ -230,70 +152,54 @@ declare module "loro-wasm" {
}
interface LoroList<T extends any[] = any[]> {
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<Key extends keyof T & number>(loro: Loro, index: Key): T[Key];
insertTyped<Key extends keyof T & number>(
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<T extends Record<string, any> = Record<string, any>> {
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<Key extends keyof T & string>(txn: Loro, key: Key): T[Key];
set(txn: Transaction, key: string, value: Value | Prelim): void;
set(key: string, value: Value | Prelim): void;
setTyped<Key extends keyof T & string>(
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;
insert(pos: number, text: string): void;
delete(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;
subscribe(txn: Loro, listener: Listener): number;
getDeepValue(): {roots: TreeNode[]};
}
}

View file

@ -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);

View file

@ -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);
});
});

View file

@ -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);

View file

@ -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<string>();
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<Uint8Array>();
// 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<Uint8Array>();
});
});
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);
})
})

View file

@ -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