mirror of
https://github.com/loro-dev/loro.git
synced 2025-01-22 12:57:20 +00:00
Feat: autocommit transaction (#127)
* feat: auto commit * fix: make recursive single thread event work again
This commit is contained in:
parent
734b832c00
commit
8293347334
18 changed files with 769 additions and 554 deletions
|
@ -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:?})")]
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
64
crates/loro-internal/tests/autocommit.rs
Normal file
64
crates/loro-internal/tests/autocommit.rs
Normal 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,);
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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[]};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
|
|
151
pnpm-lock.yaml
151
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
|
||||
|
|
Loading…
Reference in a new issue