feat: event & wasm

This commit is contained in:
Zixuan Chen 2023-07-29 02:03:51 +08:00
parent f63c346e5c
commit 15be521777
34 changed files with 1812 additions and 640 deletions

24
Cargo.lock generated
View file

@ -564,6 +564,16 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "364ff57c5031fee39b026dcdfdc9c7dc1d1d79451bfdacba90f040524b766254"
[[package]]
name = "debug-log"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d13e7dd03f70e6b332a2b42a9cdfa5b04f1015098c9656e77995c09772c947"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.1"
@ -721,13 +731,15 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.8"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
@ -999,13 +1011,14 @@ dependencies = [
"crdt-list",
"criterion 0.4.0",
"ctor",
"debug-log",
"debug-log 0.1.4",
"dhat",
"enum-as-inner 0.5.1",
"enum_dispatch",
"flate2",
"fxhash",
"generic-btree",
"getrandom",
"im",
"itertools",
"js-sys",
@ -1019,7 +1032,6 @@ dependencies = [
"proptest",
"proptest-derive",
"rand",
"ring",
"rle",
"serde",
"serde-wasm-bindgen",
@ -1060,6 +1072,8 @@ name = "loro-wasm"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"debug-log 0.2.1",
"getrandom",
"js-sys",
"loro-internal",
"serde-wasm-bindgen",
@ -1725,7 +1739,7 @@ dependencies = [
"color-backtrace",
"crdt-list",
"ctor",
"debug-log",
"debug-log 0.1.4",
"enum-as-inner 0.5.1",
"fxhash",
"heapless",

View file

@ -13,7 +13,6 @@ loro-common = { path = "../loro-common" }
smallvec = { version = "1.8.0", features = ["serde"] }
smartstring = { version = "1.0.1" }
fxhash = "0.2.1"
ring = "0.16.20"
serde = { version = "1.0.140", features = ["derive"] }
thiserror = "1.0.31"
enum-as-inner = "0.5.1"
@ -40,6 +39,7 @@ generic-btree = "0.4.0"
compact-bytes = { path = "../compact-bytes" }
lz4_flex = "0.11.1"
miniz_oxide = "0.7.1"
getrandom = "0.2.10"
[dev-dependencies]
serde_json = "1.0.87"

View file

@ -61,8 +61,8 @@ mod run {
let text = txn.get_text("text");
for TextAction { pos, ins, del } in actions.iter() {
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
txn.commit().unwrap();

View file

@ -20,8 +20,8 @@ mod run {
let mut txn = loro.txn().unwrap();
for TextAction { pos, ins, del } in actions.iter() {
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
})
});
@ -35,8 +35,8 @@ mod run {
}));
let mut txn = loro.txn().unwrap();
for TextAction { pos, ins, del } in actions.iter() {
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
})
});
@ -54,8 +54,8 @@ mod run {
txn = loro.txn().unwrap();
}
n += 1;
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
txn.commit().unwrap();
@ -77,8 +77,8 @@ mod run {
txn = loro.txn().unwrap();
}
n += 1;
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
txn.commit().unwrap();
@ -99,8 +99,8 @@ mod run {
txn = loro.txn().unwrap();
}
n += 1;
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
txn.commit().unwrap();
@ -124,8 +124,8 @@ mod run {
txn = loro.txn().unwrap();
}
n += 1;
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
txn.commit().unwrap();
@ -143,8 +143,8 @@ mod run {
let mut txn = loro.txn().unwrap();
for TextAction { pos, ins, del } in actions.iter() {
text.delete_utf16(&mut txn, *pos, *del);
text.insert_utf16(&mut txn, *pos, ins);
text.delete_utf16(&mut txn, *pos, *del).unwrap();
text.insert_utf16(&mut txn, *pos, ins).unwrap();
}
})
});
@ -162,8 +162,8 @@ mod run {
txn = loro.txn().unwrap();
}
n += 1;
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
})
});
@ -175,8 +175,8 @@ mod run {
{
for TextAction { pos, ins, del } in actions.iter() {
let mut txn = loro.txn().unwrap();
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
txn.commit().unwrap();
}
}
@ -193,8 +193,8 @@ mod run {
{
for TextAction { pos, ins, del } in actions.iter() {
let mut txn = loro.txn().unwrap();
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
txn.commit().unwrap();
}
}
@ -209,8 +209,8 @@ mod run {
for TextAction { pos, ins, del } in actions.iter() {
{
let mut txn = loro.txn().unwrap();
text.delete(&mut txn, *pos, *del);
text.insert(&mut txn, *pos, ins);
text.delete(&mut txn, *pos, *del).unwrap();
text.insert(&mut txn, *pos, ins).unwrap();
}
loro_b
@ -239,14 +239,14 @@ mod run {
{
let mut txn = loro.txn().unwrap();
text.delete(&mut txn, pos, del);
text.insert(&mut txn, pos, ins);
text.delete(&mut txn, pos, del).unwrap();
text.insert(&mut txn, pos, ins).unwrap();
}
{
let mut txn = loro_b.txn().unwrap();
text2.delete(&mut txn, pos, del);
text2.insert(&mut txn, pos, ins);
text2.delete(&mut txn, pos, del).unwrap();
text2.insert(&mut txn, pos, ins).unwrap();
}
loro_b
.import(&loro.export_from(&loro_b.vv_cloned()))

View file

@ -1,7 +1,6 @@
use std::{fmt::Debug, sync::Arc};
use crate::{log_store::GcConfig, Timestamp};
use ring::rand::{SecureRandom, SystemRandom};
#[derive(Clone)]
pub struct Configure {
@ -19,6 +18,14 @@ impl Debug for Configure {
}
}
pub struct DefaultRandom;
impl SecureRandomGenerator for DefaultRandom {
fn fill_byte(&self, dest: &mut [u8]) {
getrandom::getrandom(dest).unwrap();
}
}
pub trait SecureRandomGenerator: Send + Sync {
fn fill_byte(&self, dest: &mut [u8]);
fn next_u64(&self) -> u64 {
@ -46,18 +53,12 @@ pub trait SecureRandomGenerator: Send + Sync {
}
}
impl SecureRandomGenerator for SystemRandom {
fn fill_byte(&self, dest: &mut [u8]) {
self.fill(dest).unwrap();
}
}
impl Default for Configure {
fn default() -> Self {
Self {
gc: GcConfig::default(),
get_time: || 0,
rand: Arc::new(SystemRandom::new()),
rand: Arc::new(DefaultRandom),
}
}
}

View file

@ -5,6 +5,7 @@
//! Every [Container] can take a [Snapshot], which contains [crate::LoroValue] that describes the state.
//!
use crate::{
arena::SharedArena,
event::{Observer, ObserverHandler, SubscriptionID},
hierarchy::Hierarchy,
log_store::ImportContext,
@ -121,6 +122,41 @@ pub enum ContainerIdRaw {
Normal { id: ID },
}
pub trait IntoContainerId {
fn into_container_id(self, arena: &SharedArena, kind: ContainerType) -> ContainerID;
}
impl IntoContainerId for String {
fn into_container_id(self, _arena: &SharedArena, kind: ContainerType) -> ContainerID {
ContainerID::Root {
name: InternalString::from(self.as_str()),
container_type: kind,
}
}
}
impl<'a> IntoContainerId for &'a str {
fn into_container_id(self, _arena: &SharedArena, kind: ContainerType) -> ContainerID {
ContainerID::Root {
name: InternalString::from(self),
container_type: kind,
}
}
}
impl IntoContainerId for ContainerID {
fn into_container_id(self, _arena: &SharedArena, _kind: ContainerType) -> ContainerID {
self
}
}
impl IntoContainerId for ContainerIdx {
fn into_container_id(self, arena: &SharedArena, kind: ContainerType) -> ContainerID {
assert_eq!(self.get_type(), kind);
arena.get_container_id(self).unwrap()
}
}
impl From<String> for ContainerIdRaw {
fn from(value: String) -> Self {
ContainerIdRaw::Root { name: value.into() }

View file

@ -582,7 +582,6 @@ impl List {
self.container_idx
}
/// Inserts an element at position index within the List
pub fn insert<T: Transact, P: Prelim>(
&mut self,
txn: &T,

View file

@ -213,13 +213,13 @@ impl Actionable for Vec<LoroDoc> {
let site = &mut self[*site as usize];
let mut txn = site.txn().unwrap();
let text = txn.get_text("text");
text.insert(&mut txn, *pos, &content.to_string());
text.insert(&mut txn, *pos, &content.to_string()).unwrap();
}
Action::Del { pos, len, site } => {
let site = &mut self[*site as usize];
let mut txn = site.txn().unwrap();
let text = txn.get_text("text");
text.delete(&mut txn, *pos, *len);
text.delete(&mut txn, *pos, *len).unwrap();
}
Action::Sync { from, to } => {
if from != to {

View file

@ -504,13 +504,17 @@ impl Actionable for Vec<Actor> {
let mut txn = actor.loro.txn().unwrap();
match value {
FuzzValue::Null => {
container.delete(&mut txn, &key.to_string());
container.delete(&mut txn, &key.to_string()).unwrap();
}
FuzzValue::I32(i) => {
container.insert(&mut txn, &key.to_string(), LoroValue::from(*i));
container
.insert(&mut txn, &key.to_string(), LoroValue::from(*i))
.unwrap();
}
FuzzValue::Container(c) => {
let idx = container.insert_container(&mut txn, &key.to_string(), *c);
let idx = container
.insert_container(&mut txn, &key.to_string(), *c)
.unwrap();
actor.add_new_container(idx, *c);
}
};
@ -534,13 +538,17 @@ impl Actionable for Vec<Actor> {
let mut txn = actor.loro.txn().unwrap();
match value {
FuzzValue::Null => {
container.delete(&mut txn, *key as usize, 1);
container.delete(&mut txn, *key as usize, 1).unwrap();
}
FuzzValue::I32(i) => {
container.insert(&mut txn, *key as usize, LoroValue::from(*i));
container
.insert(&mut txn, *key as usize, LoroValue::from(*i))
.unwrap();
}
FuzzValue::Container(c) => {
let idx = container.insert_container(&mut txn, *key as usize, *c);
let idx = container
.insert_container(&mut txn, *key as usize, *c)
.unwrap();
actor.add_new_container(idx, *c);
}
};
@ -563,9 +571,13 @@ impl Actionable for Vec<Actor> {
};
let mut txn = actor.loro.txn().unwrap();
if *is_del {
container.delete(&mut txn, *pos as usize, *value as usize);
container
.delete(&mut txn, *pos as usize, *value as usize)
.unwrap();
} else {
container.insert(&mut txn, *pos as usize, &(format!("[{}]", value)));
container
.insert(&mut txn, *pos as usize, &(format!("[{}]", value)))
.unwrap();
}
drop(txn);
}

View file

@ -16,6 +16,7 @@ pub mod log_store;
pub mod op;
pub mod refactor;
pub mod version;
pub use refactor::*;
mod error;
#[cfg(feature = "test_utils")]

View file

@ -17,7 +17,7 @@ pub struct ContainerDiff {
pub diff: Diff,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DiffEvent<'a> {
/// whether the event comes from the children of the container.
pub from_children: bool,
@ -73,3 +73,30 @@ impl<'a> InternalDocDiff<'a> {
self.origin == other.origin && self.local == other.local
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use crate::LoroDoc;
#[test]
fn test_text_event() {
let loro = LoroDoc::new();
loro.subscribe_deep(Arc::new(|event| {
assert_eq!(
&event.container.diff.as_text().unwrap().vec[0]
.as_insert()
.unwrap()
.0,
&"h223ello"
);
dbg!(event);
}));
let mut txn = loro.txn().unwrap();
let text = loro.get_text("id");
text.insert(&mut txn, 0, "hello").unwrap();
text.insert(&mut txn, 1, "223").unwrap();
txn.commit().unwrap();
}
}

View file

@ -1,10 +1,13 @@
use super::{state::DocState, txn::Transaction};
use crate::container::{
list::list_op::{DeleteSpan, ListOp},
registry::ContainerIdx,
text::text_content::ListSlice,
use crate::{
container::{
list::list_op::{DeleteSpan, ListOp},
registry::ContainerIdx,
text::text_content::ListSlice,
},
txn::EventHint,
};
use loro_common::{ContainerID, ContainerType, LoroValue};
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue};
use std::{
borrow::Cow,
sync::{Mutex, Weak},
@ -81,15 +84,15 @@ impl TextHandler {
}
}
#[cfg(not(features = "wasm"))]
#[cfg(not(feature = "wasm"))]
impl TextHandler {
#[inline(always)]
pub fn insert(&self, txn: &mut Transaction, pos: usize, s: &str) {
pub fn insert(&self, txn: &mut Transaction, pos: usize, s: &str) -> LoroResult<()> {
self.insert_utf8(txn, pos, s)
}
#[inline(always)]
pub fn delete(&self, txn: &mut Transaction, pos: usize, len: usize) {
pub fn delete(&self, txn: &mut Transaction, pos: usize, len: usize) -> LoroResult<()> {
self.delete_utf8(txn, pos, len)
}
@ -98,9 +101,9 @@ impl TextHandler {
self.len_utf8()
}
pub fn insert_utf8(&self, txn: &mut Transaction, pos: usize, s: &str) {
pub fn insert_utf8(&self, txn: &mut Transaction, pos: usize, s: &str) -> LoroResult<()> {
if s.is_empty() {
return;
return Ok(());
}
txn.apply_local_op(
@ -110,12 +113,12 @@ impl TextHandler {
pos,
}),
None,
);
)
}
pub fn delete_utf8(&self, txn: &mut Transaction, pos: usize, len: usize) {
pub fn delete_utf8(&self, txn: &mut Transaction, pos: usize, len: usize) -> LoroResult<()> {
if len == 0 {
return;
return Ok(());
}
txn.apply_local_op(
@ -125,12 +128,12 @@ impl TextHandler {
len: len as isize,
})),
None,
);
)
}
pub fn insert_utf16(&self, txn: &mut Transaction, pos: usize, s: &str) {
pub fn insert_utf16(&self, txn: &mut Transaction, pos: usize, s: &str) -> LoroResult<()> {
if s.is_empty() {
return;
return Ok(());
}
let start =
@ -152,12 +155,14 @@ impl TextHandler {
pos: start,
}),
None,
);
)?;
Ok(())
}
pub fn delete_utf16(&self, txn: &mut Transaction, pos: usize, del: usize) {
pub fn delete_utf16(&self, txn: &mut Transaction, pos: usize, del: usize) -> LoroResult<()> {
if del == 0 {
return;
return Ok(());
}
let (start, end) =
@ -178,11 +183,11 @@ impl TextHandler {
len: (end - start) as isize,
})),
None,
);
)
}
}
#[cfg(features = "wasm")]
#[cfg(feature = "wasm")]
impl TextHandler {
#[inline(always)]
pub fn len(&self) -> usize {
@ -190,29 +195,18 @@ impl TextHandler {
}
#[inline(always)]
pub fn delete(&self, txn: &mut Transaction, pos: usize, del: usize) {
pub fn delete(&self, txn: &mut Transaction, pos: usize, del: usize) -> LoroResult<()> {
self.delete_utf16(txn, pos, del)
}
#[inline(always)]
pub fn insert(&self, txn: &mut Transaction, pos: usize, s: &str) {
pub fn insert(&self, txn: &mut Transaction, pos: usize, s: &str) -> LoroResult<()> {
self.insert_utf16(txn, pos, s)
}
pub fn len_utf16(&self) -> usize {
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state(self.container_idx, |state| {
state.as_text_state().as_ref().unwrap().len_wchars()
})
}
pub fn insert_utf16(&self, txn: &mut Transaction, pos: usize, s: &str) {
pub fn insert_utf16(&self, txn: &mut Transaction, pos: usize, s: &str) -> LoroResult<()> {
if s.is_empty() {
return;
return Ok(());
}
let start =
@ -234,12 +228,12 @@ impl TextHandler {
pos: start,
}),
Some(EventHint::Utf16 { pos, len: 0 }),
);
)
}
pub fn delete_utf16(&self, txn: &mut Transaction, pos: usize, del: usize) {
pub fn delete_utf16(&self, txn: &mut Transaction, pos: usize, del: usize) -> LoroResult<()> {
if del == 0 {
return;
return Ok(());
}
let (start, end) =
@ -260,7 +254,7 @@ impl TextHandler {
len: (end - start) as isize,
})),
Some(EventHint::Utf16 { pos, len: del }),
);
)
}
}
@ -273,10 +267,10 @@ impl ListHandler {
}
}
pub fn insert(&self, txn: &mut Transaction, pos: usize, v: LoroValue) {
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());
return;
self.insert_container(txn, pos, container.container_type())?;
return Ok(());
}
txn.apply_local_op(
@ -286,7 +280,7 @@ impl ListHandler {
pos,
}),
None,
);
)
}
pub fn insert_container(
@ -294,7 +288,7 @@ impl ListHandler {
txn: &mut Transaction,
pos: usize,
c_type: ContainerType,
) -> ContainerIdx {
) -> LoroResult<ContainerIdx> {
let id = txn.next_id();
let container_id = ContainerID::new_normal(id, c_type);
let child_idx = txn.arena.register_container(&container_id);
@ -307,13 +301,13 @@ impl ListHandler {
pos,
}),
None,
);
child_idx
)?;
Ok(child_idx)
}
pub fn delete(&self, txn: &mut Transaction, pos: usize, len: usize) {
pub fn delete(&self, txn: &mut Transaction, pos: usize, len: usize) -> LoroResult<()> {
if len == 0 {
return;
return Ok(());
}
txn.apply_local_op(
@ -323,10 +317,10 @@ impl ListHandler {
len: len as isize,
})),
None,
);
)
}
pub(crate) fn len(&self) -> usize {
pub fn len(&self) -> usize {
self.state
.upgrade()
.unwrap()
@ -337,7 +331,7 @@ impl ListHandler {
})
}
pub(crate) fn is_empty(&self) -> bool {
pub fn is_empty(&self) -> bool {
self.len() == 0
}
@ -360,6 +354,18 @@ impl ListHandler {
.idx_to_id(self.container_idx)
.unwrap()
}
pub fn get(&self, index: usize) -> Option<LoroValue> {
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state(self.container_idx, |state| {
let a = state.as_list_state().unwrap();
a.get(index).cloned()
})
}
}
impl MapHandler {
@ -371,10 +377,10 @@ impl MapHandler {
}
}
pub fn insert(&self, txn: &mut Transaction, key: &str, value: LoroValue) {
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());
return;
self.insert_container(txn, key, value.container_type())?;
return Ok(());
}
txn.apply_local_op(
@ -384,7 +390,7 @@ impl MapHandler {
value,
}),
None,
);
)
}
pub fn insert_container(
@ -392,7 +398,7 @@ impl MapHandler {
txn: &mut Transaction,
key: &str,
c_type: ContainerType,
) -> ContainerIdx {
) -> LoroResult<ContainerIdx> {
let id = txn.next_id();
let container_id = ContainerID::new_normal(id, c_type);
let child_idx = txn.arena.register_container(&container_id);
@ -404,11 +410,11 @@ impl MapHandler {
value: LoroValue::Container(container_id),
}),
None,
);
child_idx
)?;
Ok(child_idx)
}
pub fn delete(&self, txn: &mut Transaction, key: &str) {
pub fn delete(&self, txn: &mut Transaction, key: &str) -> LoroResult<()> {
txn.apply_local_op(
self.container_idx,
crate::op::RawOpContent::Map(crate::container::map::MapSet {
@ -417,7 +423,7 @@ impl MapHandler {
value: LoroValue::Null,
}),
None,
);
)
}
pub fn get_value(&self) -> LoroValue {
@ -429,6 +435,18 @@ impl MapHandler {
.get_value_by_idx(self.container_idx)
}
pub fn get(&self, key: &str) -> Option<LoroValue> {
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state(self.container_idx, |state| {
let a = state.as_map_state().unwrap();
a.get(key).cloned()
})
}
pub fn id(&self) -> ContainerID {
self.state
.upgrade()
@ -439,6 +457,21 @@ impl MapHandler {
.idx_to_id(self.container_idx)
.unwrap()
}
pub fn len(&self) -> usize {
self.state
.upgrade()
.unwrap()
.lock()
.unwrap()
.with_state(self.container_idx, |state| {
state.as_map_state().as_ref().unwrap().len()
})
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
@ -451,14 +484,14 @@ mod test {
let loro = LoroDoc::new();
let mut txn = loro.txn().unwrap();
let text = txn.get_text("hello");
text.insert(&mut txn, 0, "hello");
text.insert(&mut txn, 0, "hello").unwrap();
assert_eq!(&**text.get_value().as_string().unwrap(), "hello");
text.insert(&mut txn, 2, " kk ");
text.insert(&mut txn, 2, " kk ").unwrap();
assert_eq!(&**text.get_value().as_string().unwrap(), "he kk llo");
txn.abort();
let mut txn = loro.txn().unwrap();
assert_eq!(&**text.get_value().as_string().unwrap(), "");
text.insert(&mut txn, 0, "hi");
text.insert(&mut txn, 0, "hi").unwrap();
txn.commit().unwrap();
assert_eq!(&**text.get_value().as_string().unwrap(), "hi");
}
@ -472,14 +505,14 @@ mod test {
let mut txn = loro.txn().unwrap();
let text = txn.get_text("hello");
text.insert(&mut txn, 0, "hello");
text.insert(&mut txn, 0, "hello").unwrap();
txn.commit().unwrap();
let exported = loro.export_from(&Default::default());
loro2.import(&exported).unwrap();
let mut txn = loro2.txn().unwrap();
let text = txn.get_text("hello");
assert_eq!(&**text.get_value().as_string().unwrap(), "hello");
text.insert(&mut txn, 5, " world");
text.insert(&mut txn, 5, " world").unwrap();
assert_eq!(&**text.get_value().as_string().unwrap(), "hello world");
txn.commit().unwrap();
loro.import(&loro2.export_from(&Default::default()))

View file

@ -1,15 +1,18 @@
use std::{
borrow::Cow,
cmp::Ordering,
sync::{Arc, Mutex},
};
use debug_log::debug_dbg;
use loro_common::{ContainerID, ContainerType, LoroValue};
use loro_common::{ContainerID, ContainerType, LoroResult, LoroValue};
use crate::{
container::{registry::ContainerIdx, ContainerIdRaw},
arena::SharedArena,
container::{registry::ContainerIdx, IntoContainerId},
id::PeerID,
log_store::encoding::{ConcreteEncodeMode, ENCODE_SCHEMA_VERSION, MAGIC_BYTES},
version::Frontiers,
EncodeMode, InternalString, LoroError, VersionVector,
};
@ -44,6 +47,7 @@ use super::{
pub struct LoroDoc {
oplog: Arc<Mutex<OpLog>>,
state: Arc<Mutex<DocState>>,
arena: SharedArena,
observer: Arc<Observer>,
detached: bool,
}
@ -58,7 +62,8 @@ impl LoroDoc {
oplog: Arc::new(Mutex::new(oplog)),
state,
detached: false,
observer: Arc::new(Observer::new(arena)),
observer: Arc::new(Observer::new(arena.clone())),
arena,
}
}
@ -69,6 +74,7 @@ impl LoroDoc {
pub(super) fn from_existing(oplog: OpLog, state: DocState) -> Self {
let obs = Observer::new(oplog.arena.clone());
Self {
arena: oplog.arena.clone(),
observer: Arc::new(obs),
oplog: Arc::new(Mutex::new(oplog)),
state: Arc::new(Mutex::new(state)),
@ -76,6 +82,10 @@ impl LoroDoc {
}
}
pub fn peer_id(&self) -> PeerID {
self.state.lock().unwrap().peer
}
pub fn set_peer_id(&self, peer: PeerID) {
self.state.lock().unwrap().peer = peer;
}
@ -105,18 +115,23 @@ impl LoroDoc {
});
}
#[inline(always)]
pub fn txn(&self) -> Result<Transaction, LoroError> {
if self.state.lock().unwrap().is_in_txn() {
return Err(LoroError::DuplicatedTransactionError);
self.txn_with_origin("")
}
pub fn txn_with_origin(&self, origin: &str) -> Result<Transaction, LoroError> {
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(self.state.clone(), self.oplog.clone());
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);
}
}));
Ok(txn)
}
@ -228,21 +243,21 @@ impl LoroDoc {
/// 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_text<I: Into<ContainerIdRaw>>(&self, id: I) -> TextHandler {
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))
}
/// 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: Into<ContainerIdRaw>>(&self, id: I) -> ListHandler {
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))
}
/// 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: Into<ContainerIdRaw>>(&self, id: I) -> MapHandler {
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))
}
@ -251,26 +266,20 @@ impl LoroDoc {
self.oplog().lock().unwrap().diagnose_size();
}
fn get_container_idx<I: Into<ContainerIdRaw>>(
&self,
id: I,
c_type: ContainerType,
) -> ContainerIdx {
let id: ContainerIdRaw = id.into();
match id {
ContainerIdRaw::Root { name } => self.oplog().lock().unwrap().arena.register_container(
&crate::container::ContainerID::Root {
name,
container_type: c_type,
},
),
ContainerIdRaw::Normal { id: _ } => self
.oplog()
.lock()
.unwrap()
.arena
.register_container(&id.with_type(c_type)),
}
fn get_container_idx<I: IntoContainerId>(&self, id: I, c_type: ContainerType) -> ContainerIdx {
let id = id.into_container_id(&self.arena, c_type);
self.arena.register_container(&id)
}
pub fn frontiers(&self) -> Frontiers {
self.oplog().lock().unwrap().frontiers().clone()
}
/// - Ordering::Less means self is less than target or parallel
/// - Ordering::Equal means versions equal
/// - Ordering::Greater means self's version is greater than target
pub fn cmp_frontiers(&self, other: &Frontiers) -> Ordering {
self.oplog().lock().unwrap().cmp_frontiers(other)
}
pub fn subscribe_deep(&self, callback: Subscriber) -> SubID {
@ -294,6 +303,19 @@ impl LoroDoc {
pub fn unsubscribe(&self, id: SubID) {
self.observer.unsubscribe(id);
}
// PERF: opt
pub fn import_batch(&self, bytes: &[Vec<u8>]) -> LoroResult<()> {
for data in bytes.iter() {
self.import(data)?;
}
Ok(())
}
pub fn to_json(&self) -> LoroValue {
self.state.lock().unwrap().get_deep_value()
}
}
impl Default for LoroDoc {

View file

@ -4,7 +4,11 @@ pub(super) mod arena;
mod container;
pub(super) mod diff_calc;
pub mod handler;
pub use event::{ContainerDiff, DiffEvent, DocDiff};
pub use handler::{ListHandler, MapHandler, TextHandler};
pub use loro::LoroDoc;
pub use oplog::OpLog;
pub use state::DocState;
pub mod event;
pub mod loro;
pub mod obs;

View file

@ -1,5 +1,5 @@
use std::sync::{
atomic::{AtomicUsize, Ordering},
atomic::{AtomicU32, AtomicUsize, Ordering},
Arc, Mutex,
};
@ -25,12 +25,22 @@ struct ObserverInner {
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SubID(usize);
pub struct SubID(u32);
impl SubID {
pub fn into_u32(self) -> u32 {
self.0
}
pub fn from_u32(id: u32) -> Self {
Self(id)
}
}
pub struct Observer {
inner: Mutex<ObserverInner>,
arena: SharedArena,
next_sub_id: AtomicUsize,
next_sub_id: AtomicU32,
taken_times: AtomicUsize,
}
@ -38,7 +48,7 @@ impl Observer {
pub fn new(arena: SharedArena) -> Self {
Self {
arena,
next_sub_id: AtomicUsize::new(0),
next_sub_id: AtomicU32::new(0),
taken_times: AtomicUsize::new(0),
inner: Mutex::new(ObserverInner {
subscribers: Default::default(),
@ -223,14 +233,14 @@ mod test {
if text.get_value().as_string().unwrap().len() > 10 {
return;
}
text.insert(&mut txn, 0, "123");
text.insert(&mut txn, 0, "123").unwrap();
txn.commit().unwrap();
}));
let loro = loro_cp;
let mut txn = loro.txn().unwrap();
let text = loro.get_text("id");
text.insert(&mut txn, 0, "123");
text.insert(&mut txn, 0, "123").unwrap();
txn.commit().unwrap();
let count = count.load(Ordering::SeqCst);
assert!(count > 2, "{}", count);
@ -250,20 +260,20 @@ mod test {
assert_eq!(count.load(Ordering::SeqCst), 0);
{
let mut txn = loro.txn().unwrap();
text.insert(&mut txn, 0, "123");
text.insert(&mut txn, 0, "123").unwrap();
txn.commit().unwrap();
}
assert_eq!(count.load(Ordering::SeqCst), 1);
{
let mut txn = loro.txn().unwrap();
text.insert(&mut txn, 0, "123");
text.insert(&mut txn, 0, "123").unwrap();
txn.commit().unwrap();
}
assert_eq!(count.load(Ordering::SeqCst), 2);
loro.unsubscribe(sub);
{
let mut txn = loro.txn().unwrap();
text.insert(&mut txn, 0, "123");
text.insert(&mut txn, 0, "123").unwrap();
txn.commit().unwrap();
}
assert_eq!(count.load(Ordering::SeqCst), 2);

View file

@ -2,6 +2,7 @@ pub(crate) mod dag;
use std::borrow::Cow;
use std::cell::RefCell;
use std::cmp::Ordering;
use std::rc::Rc;
use fxhash::FxHashMap;
@ -234,6 +235,13 @@ impl OpLog {
&self.dag.frontiers
}
/// - Ordering::Less means self is less than target or parallel
/// - Ordering::Equal means versions equal
/// - Ordering::Greater means self's version is greater than target
pub fn cmp_frontiers(&self, other: &Frontiers) -> Ordering {
self.dag.cmp_frontiers(other)
}
pub(crate) fn export_changes_from(&self, from: &VersionVector) -> RemoteClientChanges {
let mut changes = RemoteClientChanges::default();
for (&peer, &cnt) in self.vv().iter() {

View file

@ -200,4 +200,17 @@ impl AppDag {
pub fn get_frontiers(&self) -> &Frontiers {
&self.frontiers
}
/// - Ordering::Less means self is less than target or parallel
/// - Ordering::Equal means versions equal
/// - Ordering::Greater means self's version is greater than target
pub fn cmp_frontiers(&self, other: &Frontiers) -> Ordering {
if &self.frontiers == other {
Ordering::Equal
} else if other.iter().all(|id| self.vv.includes_id(*id)) {
Ordering::Greater
} else {
Ordering::Less
}
}
}

View file

@ -800,7 +800,7 @@ mod test {
let app = LoroDoc::new();
let mut txn = app.txn().unwrap();
let text = txn.get_text("id");
text.insert(&mut txn, 0, "hello");
text.insert(&mut txn, 0, "hello").unwrap();
txn.commit().unwrap();
let snapshot = app.export_snapshot();
let app2 = LoroDoc::new();
@ -818,7 +818,7 @@ mod test {
// test import snapshot to a LoroApp that is already changed
let mut txn = app2.txn().unwrap();
let text = txn.get_text("id");
text.insert(&mut txn, 2, " ");
text.insert(&mut txn, 2, " ").unwrap();
txn.commit().unwrap();
debug_log::group!("app2 export");
let snapshot = app2.export_snapshot();

View file

@ -4,11 +4,10 @@ use debug_log::debug_dbg;
use enum_as_inner::EnumAsInner;
use enum_dispatch::enum_dispatch;
use fxhash::{FxHashMap, FxHashSet};
use loro_common::ContainerID;
use ring::rand::SystemRandom;
use loro_common::{ContainerID, LoroResult};
use crate::{
configure::SecureRandomGenerator,
configure::{DefaultRandom, SecureRandomGenerator},
container::{registry::ContainerIdx, ContainerIdRaw},
delta::{Delta, DeltaItem},
event::{Diff, Index, Utf16Meta},
@ -105,7 +104,7 @@ impl State {
impl DocState {
#[inline]
pub fn new(arena: SharedArena) -> Self {
let peer = SystemRandom::new().next_u64();
let peer = DefaultRandom.next_u64();
// TODO: maybe we should switch to certain version in oplog?
Self {
peer,
@ -242,7 +241,7 @@ impl DocState {
debug_dbg!(self.get_deep_value());
}
pub fn apply_local_op(&mut self, op: RawOp) {
pub fn apply_local_op(&mut self, op: RawOp) -> LoroResult<()> {
let state = self
.states
.entry(op.container)
@ -253,7 +252,9 @@ impl DocState {
self.changed_idx_in_txn.insert(op.container);
}
// TODO: make apply_op return a result
state.apply_op(op, &self.arena);
Ok(())
}
pub(crate) fn start_txn(&mut self, origin: InternalString, local: bool) {
@ -539,7 +540,6 @@ impl DocState {
match idx.get_type() {
ContainerType::Text => {
let state = self.states.get(&idx).unwrap().as_text_state().unwrap();
let mut ans: Delta<String, Utf16Meta> = Delta::new();
let mut index = 0;
for span in seq.iter() {
@ -554,10 +554,12 @@ impl DocState {
crate::delta::DeltaItem::Insert { value, .. } => {
let len = value.0.iter().fold(0, |acc, cur| acc + cur.0.len());
let mut s = String::with_capacity(len);
for sub in state.slice(index..index + len) {
s.push_str(sub);
for slice in value.0.iter() {
let bytes = self
.arena
.slice_bytes(slice.0.start as usize..slice.0.end as usize);
s.push_str(std::str::from_utf8(&bytes).unwrap());
}
ans.push(DeltaItem::Insert {
value: s,
meta: Utf16Meta { utf16_len: None },

View file

@ -247,6 +247,15 @@ impl ListState {
}
ans
}
pub fn get(&self, index: usize) -> Option<&LoroValue> {
let result = self.list.query::<LengthFinder>(&index);
if result.found {
Some(result.elem(&self.list).unwrap())
} else {
None
}
}
}
impl ContainerState for ListState {

View file

@ -142,7 +142,7 @@ impl MapState {
self.map.iter()
}
fn len(&self) -> usize {
pub fn len(&self) -> usize {
self.map.len()
}
@ -160,4 +160,14 @@ impl MapState {
}
ans
}
pub fn get(&self, k: &str) -> Option<&LoroValue> {
match self.map.get(&k.into()) {
Some(value) => match &value.value {
Some(v) => Some(v),
None => None,
},
None => None,
}
}
}

View file

@ -233,7 +233,7 @@ impl TextState {
self.rope.slice_substrings(range)
}
#[cfg(not(features = "wasm"))]
#[cfg(not(feature = "wasm"))]
fn apply_seq_raw(
&mut self,
delta: &mut Delta<SliceRanges>,
@ -261,7 +261,7 @@ impl TextState {
None
}
#[cfg(features = "wasm")]
#[cfg(feature = "wasm")]
fn apply_seq_raw(
&mut self,
delta: &mut Delta<SliceRanges>,
@ -283,14 +283,14 @@ impl TextState {
let start_utf16_len = self.len_wchars();
for value in value.0.iter() {
let s = arena.slice_bytes(value.0.start as usize..value.0.end as usize);
self.insert(index, std::str::from_utf8(&s).unwrap());
self.insert_utf8(index, std::str::from_utf8(&s).unwrap());
index += s.len();
}
utf16_index += self.len_wchars() - start_utf16_len;
}
DeltaItem::Delete { len, .. } => {
let start_utf16_len = self.len_wchars();
self.delete(index..index + len);
self.delete_utf8(index..index + len);
new_delta = new_delta.delete(start_utf16_len - self.len_wchars());
}
}

View file

@ -4,9 +4,10 @@ use std::{
sync::{Arc, Mutex},
};
use debug_log::debug_dbg;
use enum_as_inner::EnumAsInner;
use fxhash::FxHashMap;
use loro_common::ContainerType;
use loro_common::{ContainerType, LoroResult};
use rle::{HasLength, RleVec};
use smallvec::smallvec;
@ -14,7 +15,7 @@ use crate::{
change::{Change, Lamport},
container::{
list::list_op::InnerListOp, registry::ContainerIdx, text::text_content::SliceRanges,
ContainerIdRaw,
IntoContainerId,
},
delta::{Delta, MapValue},
event::Diff,
@ -68,6 +69,10 @@ impl Transaction {
origin: InternalString,
) -> Self {
let mut state_lock = state.lock().unwrap();
if state_lock.is_in_txn() {
panic!("Cannot start a transaction while another one is in progress");
}
let oplog_lock = oplog.lock().unwrap();
state_lock.start_txn(origin, true);
let arena = state_lock.arena.clone();
@ -186,7 +191,7 @@ impl Transaction {
content: RawOpContent,
// we need extra hint to reduce calculation for utf16 text op
hint: Option<EventHint>,
) {
) -> LoroResult<()> {
let len = content.content_len();
let op = RawOp {
id: ID {
@ -198,14 +203,16 @@ impl Transaction {
content,
};
let mut state = self.state.lock().unwrap();
state.apply_local_op(op.clone())?;
drop(state);
if let Some(hint) = hint {
self.event_hints.insert(op.id.counter, hint);
}
self.push_local_op_to_log(&op);
let mut state = self.state.lock().unwrap();
state.apply_local_op(op);
self.next_counter += len as Counter;
self.next_lamport += len as Lamport;
Ok(())
}
fn push_local_op_to_log(&mut self, op: &RawOp) {
@ -215,43 +222,28 @@ impl Transaction {
/// 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_text<I: Into<ContainerIdRaw>>(&self, id: I) -> TextHandler {
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))
}
/// 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: Into<ContainerIdRaw>>(&self, id: I) -> ListHandler {
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))
}
/// 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: Into<ContainerIdRaw>>(&self, id: I) -> MapHandler {
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))
}
fn get_container_idx<I: Into<ContainerIdRaw>>(
&self,
id: I,
c_type: ContainerType,
) -> ContainerIdx {
let id: ContainerIdRaw = id.into();
match id {
ContainerIdRaw::Root { name } => {
self.arena
.register_container(&crate::container::ContainerID::Root {
name,
container_type: c_type,
})
}
ContainerIdRaw::Normal { id: _ } => {
self.arena.register_container(&id.with_type(c_type))
}
}
fn get_container_idx<I: IntoContainerId>(&self, id: I, c_type: ContainerType) -> ContainerIdx {
let id = id.into_container_id(&self.arena, c_type);
self.arena.register_container(&id)
}
pub fn get_value_by_idx(&self, idx: ContainerIdx) -> LoroValue {
@ -351,5 +343,7 @@ fn change_to_diff(
lamport += op.content_len() as Lamport;
diff.push(diff_op);
}
debug_dbg!(&diff);
diff
}

View file

@ -288,7 +288,7 @@ pub mod wasm {
use wasm_bindgen::{JsValue, __rt::IntoJsResult};
use crate::{
delta::{Delta, DeltaItem, MapDiff},
delta::{Delta, DeltaItem, MapDelta, MapDiff},
event::{Diff, Index, Utf16Meta},
LoroValue,
};
@ -385,12 +385,7 @@ pub mod wasm {
)
.unwrap();
js_sys::Reflect::set(
&obj,
&JsValue::from_str("diff"),
&serde_wasm_bindgen::to_value(&map).unwrap(),
)
.unwrap();
js_sys::Reflect::set(&obj, &JsValue::from_str("updated"), &map.into()).unwrap();
}
Diff::SeqRaw(text) => {
// set type as "text"
@ -431,6 +426,22 @@ pub mod wasm {
}
}
impl From<MapDelta> for JsValue {
fn from(value: MapDelta) -> Self {
let obj = Object::new();
for (key, value) in value.updated.iter() {
js_sys::Reflect::set(
&obj,
&JsValue::from_str(key),
&JsValue::from(value.value.clone()),
)
.unwrap();
}
obj.into_js_result().unwrap()
}
}
impl From<MapDiff<LoroValue>> for JsValue {
fn from(value: MapDiff<LoroValue>) -> Self {
let obj = Object::new();
@ -508,7 +519,7 @@ pub mod wasm {
fn from(value: DeltaItem<String, Utf16Meta>) -> Self {
let obj = Object::new();
match value {
DeltaItem::Retain { len: _len, meta } => {
DeltaItem::Retain { len, meta: _ } => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("type"),
@ -518,7 +529,7 @@ pub mod wasm {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("len"),
&JsValue::from_f64(meta.utf16_len.unwrap() as f64),
&JsValue::from_f64(len as f64),
)
.unwrap();
}
@ -537,7 +548,7 @@ pub mod wasm {
)
.unwrap();
}
DeltaItem::Delete { len: _len, meta } => {
DeltaItem::Delete { len, meta: _ } => {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("type"),
@ -547,7 +558,7 @@ pub mod wasm {
js_sys::Reflect::set(
&obj,
&JsValue::from_str("len"),
&JsValue::from_f64(meta.utf16_len.unwrap() as f64),
&JsValue::from_f64(len as f64),
)
.unwrap();
}

View file

@ -12,6 +12,8 @@ loro-internal = { path = "../loro-internal", features = ["wasm"] }
wasm-bindgen = "0.2.83"
serde-wasm-bindgen = { version = "0.5.0" }
console_error_panic_hook = { version = "0.1.6", optional = true }
getrandom = { version = "0.2.10", features = ["js"] }
debug-log = { version = "0.2.1", features = ["wasm"] }
[features]
default = ["console_error_panic_hook"]

View file

@ -19,12 +19,13 @@ const LoroWasmDir = resolve(__dirname, "..");
console.log(LoroWasmDir);
async function build() {
await cargoBuild();
if (Deno.args[1] != null) {
if (!TARGETS.includes(Deno.args[1])) {
throw new Error(`Invalid target ${Deno.args[1]}`);
const target = Deno.args[1];
if (target != null) {
if (!TARGETS.includes(target)) {
throw new Error(`Invalid target ${target}`);
}
buildTarget(Deno.args[1]);
buildTarget(target);
return;
}
@ -74,13 +75,29 @@ async function buildTarget(target: string) {
try {
await Deno.remove(targetDirPath, { recursive: true });
console.log("Clear directory " + targetDirPath);
} catch (e) {}
} catch (_e) {
//
}
const cmd =
`wasm-bindgen --weak-refs --target ${target} --out-dir ${target} ../../target/wasm32-unknown-unknown/${profileDir}/loro_wasm.wasm`;
console.log(">", cmd);
await Deno.run({ cmd: cmd.split(" "), cwd: LoroWasmDir }).status();
console.log();
if (target === "nodejs") {
console.log("🔨 Patching nodejs target");
const patch = await Deno.readTextFile(
resolve(__dirname, "./nodejs_patch.js"),
);
const wasm = await Deno.readTextFile(
resolve(targetDirPath, "loro_wasm.js"),
);
await Deno.writeTextFile(
resolve(targetDirPath, "loro_wasm.js"),
wasm + "\n" + patch,
);
}
}
build();

View file

@ -0,0 +1,2 @@
const { webcrypto } = require("crypto");
globalThis.crypto = webcrypto;

View file

@ -1,23 +1,20 @@
use js_sys::{Array, Object, Promise, Reflect, Uint8Array};
use js_sys::{Array, Promise, Uint8Array};
use loro_internal::{
configure::{Configure, SecureRandomGenerator},
container::{registry::ContainerWrapper, ContainerID},
context::Context,
configure::SecureRandomGenerator,
container::ContainerID,
event::{Diff, Path},
log_store::GcConfig,
obs::SubID,
refactor::handler::{ListHandler, MapHandler, TextHandler},
refactor::txn::Transaction as Txn,
version::Frontiers,
ContainerType, List, LoroCore, Map, Origin, Text, Transact, TransactionWrap, VersionVector,
ContainerType, DiffEvent, LoroDoc, VersionVector,
};
use std::{cell::RefCell, cmp::Ordering, ops::Deref, rc::Rc, sync::Arc};
use wasm_bindgen::{
__rt::{IntoJsResult, RefMut},
prelude::*,
};
use wasm_bindgen::{__rt::IntoJsResult, prelude::*};
mod log;
mod prelim;
pub use prelim::{PrelimList, PrelimMap, PrelimText};
use crate::convert::js_try_to_prelim;
mod convert;
#[wasm_bindgen(js_name = setPanicHook)]
@ -32,13 +29,18 @@ pub fn set_panic_hook() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen(js_name = setDebug)]
pub fn set_debug(filter: &str) {
debug_log::set_debug(filter)
}
type JsResult<T> = Result<T, JsValue>;
#[wasm_bindgen]
pub struct Loro(RefCell<LoroCore>);
pub struct Loro(RefCell<LoroDoc>);
impl Deref for Loro {
type Target = RefCell<LoroCore>;
type Target = RefCell<LoroDoc>;
fn deref(&self) -> &Self::Target {
&self.0
@ -79,6 +81,8 @@ mod observer {
use wasm_bindgen::JsValue;
use crate::JsResult;
/// We need to wrap the observer function in a struct so that we can implement Send for it.
/// But it's not Send essentially, so we need to check it manually in runtime.
#[derive(Clone)]
@ -95,9 +99,9 @@ mod observer {
}
}
pub fn call1(&self, arg: &JsValue) {
pub fn call1(&self, arg: &JsValue) -> JsResult<JsValue> {
if std::thread::current().id() == self.thread {
self.f.call1(&JsValue::NULL, arg).unwrap();
self.f.call1(&JsValue::NULL, arg)
} else {
panic!("Observer called from different thread")
}
@ -111,17 +115,20 @@ mod observer {
impl Loro {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
let cfg: Configure = Configure {
gc: GcConfig::default().with_gc(false),
get_time: || js_sys::Date::now() as i64,
rand: Arc::new(MathRandom),
};
Self(RefCell::new(LoroCore::new(cfg, None)))
Self(RefCell::new(LoroDoc::new()))
}
pub fn txn(&self) -> Transaction {
Transaction(self.0.borrow().txn().unwrap())
}
pub fn txn_with_origin(&self, origin: &str) -> Transaction {
Transaction(self.0.borrow().txn_with_origin(origin).unwrap())
}
#[wasm_bindgen(js_name = "clientId", method, getter)]
pub fn client_id(&self) -> u64 {
self.0.borrow().client_id()
self.0.borrow().peer_id()
}
#[wasm_bindgen(js_name = "getText")]
@ -146,26 +153,20 @@ impl Loro {
pub fn get_container_by_id(&self, container_id: JsContainerID) -> JsResult<JsValue> {
let container_id: ContainerID = container_id.to_owned().try_into()?;
let ty = container_id.container_type();
let container = self.0.borrow_mut().get_container(&container_id);
if let Some(container) = container {
let client_id = self.0.borrow().client_id();
Ok(match ty {
ContainerType::Text => {
let text: Text = Text::from_instance(container, client_id);
LoroText(text).into()
}
ContainerType::Map => {
let map: Map = Map::from_instance(container, client_id);
LoroMap(map).into()
}
ContainerType::List => {
let list: List = List::from_instance(container, client_id);
LoroList(list).into()
}
})
} else {
Err(JsValue::from_str("Container not found"))
}
Ok(match ty {
ContainerType::Text => {
let text = self.0.borrow().get_text(container_id);
LoroText(text).into()
}
ContainerType::Map => {
let map = self.0.borrow().get_map(container_id);
LoroMap(map).into()
}
ContainerType::List => {
let list = self.0.borrow().get_list(container_id);
LoroList(list).into()
}
})
}
#[inline(always)]
@ -194,7 +195,7 @@ impl Loro {
#[wasm_bindgen(js_name = "exportSnapshot")]
pub fn export_snapshot(&self) -> JsResult<Vec<u8>> {
Ok(self.0.borrow().encode_all())
Ok(self.0.borrow().export_snapshot())
}
#[wasm_bindgen(skip_typescript, js_name = "exportFrom")]
@ -211,11 +212,11 @@ impl Loro {
None => Default::default(),
};
Ok(self.0.borrow().encode_from(vv))
Ok(self.0.borrow().export_from(&vv))
}
pub fn import(&self, update_or_snapshot: &[u8]) -> JsResult<()> {
self.0.borrow_mut().decode(update_or_snapshot)?;
self.0.borrow_mut().import(update_or_snapshot)?;
Ok(())
}
@ -231,7 +232,7 @@ impl Loro {
if data.is_empty() {
return Ok(());
}
Ok(self.0.borrow_mut().decode_batch(&data)?)
Ok(self.0.borrow_mut().import_batch(&data)?)
}
#[wasm_bindgen(js_name = "toJson")]
@ -243,45 +244,55 @@ impl Loro {
// TODO: convert event and event sub config
pub fn subscribe(&self, f: js_sys::Function) -> u32 {
let observer = observer::Observer::new(f);
self.0.borrow_mut().subscribe_deep(Box::new(move |e| {
call_after_micro_task(observer.clone(), e);
}))
self.0
.borrow_mut()
.subscribe_deep(Arc::new(move |e| {
call_after_micro_task(observer.clone(), e);
}))
.into_u32()
}
pub fn unsubscribe(&self, subscription: u32) {
self.0.borrow_mut().unsubscribe_deep(subscription)
self.0
.borrow_mut()
.unsubscribe(SubID::from_u32(subscription))
}
/// It's the caller's responsibility to commit and free the transaction
#[wasm_bindgen(js_name = "__raw__transactionWithOrigin")]
pub fn transaction_with_origin(&self, origin: &JsOrigin, f: js_sys::Function) -> JsResult<()> {
let origin = origin.as_string().map(Origin::from);
let txn = self.0.borrow().transact_with(origin);
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.borrow().txn_with_origin(&origin)?;
let js_txn = JsValue::from(Transaction(txn));
f.call1(&JsValue::NULL, &js_txn)?;
Ok(())
let ans = f.call1(&JsValue::NULL, &js_txn);
debug_log::group_end!();
ans
}
}
fn call_after_micro_task(ob: observer::Observer, e: &loro_internal::event::Event) {
let e = e.clone();
fn call_after_micro_task(ob: observer::Observer, e: DiffEvent) {
let promise = Promise::resolve(&JsValue::NULL);
type C = Closure<dyn FnMut(JsValue)>;
let drop_handler: Rc<RefCell<Option<C>>> = Rc::new(RefCell::new(None));
let copy = drop_handler.clone();
let event = Event {
local: e.doc.local,
origin: e.doc.origin.to_string(),
target: e.container.id.clone(),
diff: Either::A(e.container.diff.to_owned()),
path: Either::A(e.container.path.iter().map(|x| x.1.clone()).collect()),
};
let closure = Closure::once(move |_: JsValue| {
ob.call1(
&Event {
local: e.local,
origin: e.origin.clone(),
target: e.target.clone(),
diff: Either::A(e.diff),
path: Either::A(e.absolute_path.clone()),
}
.into(),
);
let ans = ob.call1(&event.into());
drop(copy);
if let Err(e) = ans {
console_log!("Error when calling observer: {:#?}", e);
}
});
let _ = promise.then(&closure);
drop_handler.borrow_mut().replace(closure);
@ -301,7 +312,7 @@ enum Either<A, B> {
#[wasm_bindgen]
pub struct Event {
pub local: bool,
origin: Option<Origin>,
origin: String,
target: ContainerID,
diff: Either<Diff, JsValue>,
path: Either<Path, JsValue>,
@ -310,10 +321,8 @@ pub struct Event {
#[wasm_bindgen]
impl Event {
#[wasm_bindgen(js_name = "origin", method, getter)]
pub fn origin(&self) -> Option<JsOrigin> {
self.origin
.as_ref()
.map(|o| JsValue::from_str(o.as_str()).into())
pub fn origin(&self) -> String {
self.origin.clone()
}
#[wasm_bindgen(getter)]
@ -351,62 +360,38 @@ impl Event {
}
#[wasm_bindgen]
pub struct Transaction(TransactionWrap);
pub struct Transaction(Txn);
#[wasm_bindgen]
impl Transaction {
pub fn commit(&self) -> JsResult<()> {
pub fn commit(self) -> JsResult<()> {
self.0.commit()?;
Ok(())
}
}
fn get_transaction_mut(txn: &JsTransaction) -> TransactionWrap {
use wasm_bindgen::convert::RefMutFromWasmAbi;
let js: &JsValue = txn.as_ref();
if js.is_undefined() || js.is_null() {
panic!("you should input Transaction");
} else {
let ctor_name = Object::get_prototype_of(js).constructor().name();
if ctor_name == "Transaction" {
let ptr = Reflect::get(js, &JsValue::from_str("ptr")).unwrap();
let ptr = ptr.as_f64().ok_or(JsValue::NULL).unwrap() as u32;
let txn: RefMut<Transaction> = unsafe { Transaction::ref_mut_from_abi(ptr) };
txn.0.transact()
} else if ctor_name == "Loro" {
let ptr = Reflect::get(js, &JsValue::from_str("ptr")).unwrap();
let ptr = ptr.as_f64().ok_or(JsValue::NULL).unwrap() as u32;
let loro: RefMut<Loro> = unsafe { Loro::ref_mut_from_abi(ptr) };
let loro = loro.0.borrow();
loro.transact()
} else {
panic!("you should input Transaction");
}
}
}
#[wasm_bindgen]
pub struct LoroText(Text);
pub struct LoroText(TextHandler);
#[wasm_bindgen]
impl LoroText {
pub fn __loro_insert(&mut self, txn: &Loro, index: usize, content: &str) -> JsResult<()> {
self.0.insert_utf16(&*txn.0.borrow(), index, content)?;
pub fn __txn_insert(
&mut self,
txn: &mut Transaction,
index: usize,
content: &str,
) -> JsResult<()> {
self.0.insert_utf16(&mut txn.0, index, content)?;
Ok(())
}
pub fn __loro_delete(&mut self, txn: &Loro, index: usize, len: usize) -> JsResult<()> {
self.0.delete_utf16(&*txn.0.borrow(), index, len)?;
Ok(())
}
pub fn __txn_insert(&mut self, txn: &Transaction, index: usize, content: &str) -> JsResult<()> {
self.0.insert_utf16(&txn.0, index, content)?;
Ok(())
}
pub fn __txn_delete(&mut self, txn: &Transaction, index: usize, len: usize) -> JsResult<()> {
self.0.delete_utf16(&txn.0, index, len)?;
pub fn __txn_delete(
&mut self,
txn: &mut Transaction,
index: usize,
len: usize,
) -> JsResult<()> {
self.0.delete_utf16(&mut txn.0, index, len)?;
Ok(())
}
@ -427,69 +412,44 @@ impl LoroText {
self.0.len()
}
pub fn subscribe(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
pub fn subscribe(&self, loro: &Loro, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe(
&txn,
Box::new(move |e| {
let ans = loro.0.borrow_mut().subscribe(
&self.0.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
);
Ok(ans.into_u32())
}
#[wasm_bindgen(js_name = "subscribeOnce")]
pub fn subscribe_once(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe_once(
&txn,
Box::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
pub fn unsubscribe(&self, txn: &JsTransaction, subscription: u32) -> JsResult<()> {
let txn = get_transaction_mut(txn);
self.0.unsubscribe(&txn, subscription)?;
pub fn unsubscribe(&self, loro: &Loro, subscription: u32) -> JsResult<()> {
loro.0
.borrow_mut()
.unsubscribe(SubID::from_u32(subscription));
Ok(())
}
}
#[wasm_bindgen]
pub struct LoroMap(Map);
pub struct LoroMap(MapHandler);
const CONTAINER_TYPE_ERR: &str = "Invalid container type, only supports Text, Map, List";
#[wasm_bindgen]
impl LoroMap {
pub fn __loro_insert(&mut self, txn: &Loro, key: &str, value: JsValue) -> JsResult<()> {
if let Some(v) = js_try_to_prelim(&value) {
self.0.insert(&*txn.0.borrow(), key, v)?;
} else {
self.0.insert(&*txn.0.borrow(), key, value)?;
};
pub fn __txn_insert(
&mut self,
txn: &mut Transaction,
key: &str,
value: JsValue,
) -> JsResult<()> {
self.0.insert(&mut txn.0, key, value.into())?;
Ok(())
}
pub fn __txn_insert(&mut self, txn: &Transaction, key: &str, value: JsValue) -> JsResult<()> {
if let Some(v) = js_try_to_prelim(&value) {
self.0.insert(&txn.0, key, v)?;
} else {
self.0.insert(&txn.0, key, value)?;
};
Ok(())
}
pub fn __loro_delete(&mut self, txn: &Loro, key: &str) -> JsResult<()> {
self.0.delete(&*txn.0.borrow(), key)?;
Ok(())
}
pub fn __txn_delete(&mut self, txn: &Transaction, key: &str) -> JsResult<()> {
self.0.delete(&txn.0, key)?;
pub fn __txn_delete(&mut self, txn: &mut Transaction, key: &str) -> JsResult<()> {
self.0.delete(&mut txn.0, key)?;
Ok(())
}
@ -510,85 +470,44 @@ impl LoroMap {
}
#[wasm_bindgen(js_name = "getValueDeep")]
pub fn get_value_deep(&self, ctx: &Loro) -> JsValue {
self.0.get_value_deep(ctx.deref()).into()
pub fn get_value_deep(&self) -> JsValue {
todo!()
// self.0.get_value_deep(ctx.deref()).into()
}
#[wasm_bindgen(js_name = "insertContainer")]
pub fn insert_container(
&mut self,
txn: &JsTransaction,
txn: &mut Transaction,
key: &str,
container_type: &str,
) -> JsResult<JsValue> {
let txn = get_transaction_mut(txn);
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 idx = self.0.insert(&txn, key, type_)?.unwrap();
let idx = self.0.insert_container(&mut txn.0, key, type_)?;
let container = match type_ {
ContainerType::Text => {
let x = txn.get_text_by_idx(idx).unwrap();
LoroText(x).into()
}
ContainerType::Map => {
let x = txn.get_map_by_idx(idx).unwrap();
LoroMap(x).into()
}
ContainerType::List => {
let x = txn.get_list_by_idx(idx).unwrap();
LoroList(x).into()
}
ContainerType::Text => LoroText(txn.0.get_text(idx)).into(),
ContainerType::Map => LoroMap(txn.0.get_map(idx)).into(),
ContainerType::List => LoroList(txn.0.get_list(idx)).into(),
};
Ok(container)
}
pub fn subscribe(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
pub fn subscribe(&self, loro: &Loro, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe(
&txn,
Box::new(move |e| {
let id = loro.0.borrow_mut().subscribe(
&self.0.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
);
#[wasm_bindgen(js_name = "subscribeOnce")]
pub fn subscribe_once(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe_once(
&txn,
Box::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
#[wasm_bindgen(js_name = "subscribeDeep")]
pub fn subscribe_deep(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe_deep(
&txn,
Box::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
pub fn unsubscribe(&self, txn: &JsTransaction, subscription: u32) -> JsResult<()> {
let txn = get_transaction_mut(txn);
self.0.unsubscribe(&txn, subscription)?;
Ok(())
Ok(id.into_u32())
}
#[wasm_bindgen(js_name = "size", method, getter)]
@ -598,40 +517,27 @@ impl LoroMap {
}
#[wasm_bindgen]
pub struct LoroList(List);
pub struct LoroList(ListHandler);
#[wasm_bindgen]
impl LoroList {
pub fn __loro_insert(&mut self, loro: &Loro, index: usize, value: JsValue) -> JsResult<()> {
if let Some(v) = js_try_to_prelim(&value) {
self.0.insert(&*loro.0.borrow(), index, v)?;
} else {
self.0.insert(&*loro.0.borrow(), index, value)?;
};
Ok(())
}
pub fn __txn_insert(
&mut self,
txn: &Transaction,
txn: &mut Transaction,
index: usize,
value: JsValue,
) -> JsResult<()> {
if let Some(v) = js_try_to_prelim(&value) {
self.0.insert(&txn.0, index, v)?;
} else {
self.0.insert(&txn.0, index, value)?;
};
self.0.insert(&mut txn.0, index, value.into())?;
Ok(())
}
pub fn __loro_delete(&mut self, loro: &Loro, index: usize, len: usize) -> JsResult<()> {
self.0.delete(&*loro.0.borrow(), index, len)?;
Ok(())
}
pub fn __txn_delete(&mut self, loro: &Transaction, index: usize, len: usize) -> JsResult<()> {
self.0.delete(&loro.0, index, len)?;
pub fn __txn_delete(
&mut self,
txn: &mut Transaction,
index: usize,
len: usize,
) -> JsResult<()> {
self.0.delete(&mut txn.0, index, len)?;
Ok(())
}
@ -651,85 +557,43 @@ impl LoroList {
}
#[wasm_bindgen(js_name = "getValueDeep")]
pub fn get_value_deep(&self, ctx: &Loro) -> JsValue {
let value = self.0.get_value_deep(ctx.deref());
value.into()
pub fn get_value_deep(&self) -> JsValue {
todo!()
// let value = self.0.get_value_deep(ctx.deref());
// value.into()
}
#[wasm_bindgen(js_name = "insertContainer")]
pub fn insert_container(
&mut self,
txn: &JsTransaction,
txn: &mut Transaction,
pos: usize,
container: &str,
) -> JsResult<JsValue> {
let txn = get_transaction_mut(txn);
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 idx = self.0.insert(&txn, pos, _type)?.unwrap();
let idx = self.0.insert_container(&mut txn.0, pos, _type)?;
let container = match _type {
ContainerType::Text => {
let x = txn.get_text_by_idx(idx).unwrap();
LoroText(x).into()
}
ContainerType::Map => {
let x = txn.get_map_by_idx(idx).unwrap();
LoroMap(x).into()
}
ContainerType::List => {
let x = txn.get_list_by_idx(idx).unwrap();
LoroList(x).into()
}
ContainerType::Text => LoroText(txn.0.get_text(idx)).into(),
ContainerType::Map => LoroMap(txn.0.get_map(idx)).into(),
ContainerType::List => LoroList(txn.0.get_list(idx)).into(),
};
Ok(container)
}
pub fn subscribe(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
pub fn subscribe(&self, loro: &Loro, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe(
&txn,
Box::new(move |e| {
let ans = loro.0.borrow_mut().subscribe(
&self.0.id(),
Arc::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
#[wasm_bindgen(js_name = "subscribeOnce")]
pub fn subscribe_once(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe_once(
&txn,
Box::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
#[wasm_bindgen(js_name = "subscribeDeep")]
pub fn subscribe_deep(&self, txn: &JsTransaction, f: js_sys::Function) -> JsResult<u32> {
let observer = observer::Observer::new(f);
let txn = get_transaction_mut(txn);
let ans = self.0.subscribe_deep(
&txn,
Box::new(move |e| {
call_after_micro_task(observer.clone(), e);
}),
)?;
Ok(ans)
}
pub fn unsubscribe(&self, txn: &JsTransaction, subscription: u32) -> JsResult<()> {
let txn = get_transaction_mut(txn);
self.0.unsubscribe(&txn, subscription)?;
Ok(())
);
Ok(ans.into_u32())
}
#[wasm_bindgen(js_name = "length", method, getter)]

View file

@ -1,3 +1,8 @@
{
"deno.enable": false
"deno.enable": false,
"editor.defaultFormatter": "vscode.typescript-language-features",
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}

View file

@ -18,7 +18,10 @@
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.0.1",
"@typescript-eslint/parser": "^6.2.0",
"esbuild": "^0.17.12",
"eslint": "^8.46.0",
"prettier": "^3.0.0",
"rollup": "^3.20.1",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-esbuild": "^5.0.0",

View file

@ -23,11 +23,10 @@ import {
export type { ContainerID, ContainerType } from "loro-wasm";
Loro.prototype.transact = function (cb, origin) {
this.__raw__transactionWithOrigin(origin, (txn: Transaction) => {
return this.__raw__transactionWithOrigin(origin || "", (txn: Transaction) => {
try {
cb(txn);
return cb(txn);
} finally {
txn.commit();
txn.free();
}
});
@ -55,51 +54,27 @@ LoroMap.prototype.getTyped = function (loro, key) {
LoroMap.prototype.setTyped = LoroMap.prototype.set;
LoroText.prototype.insert = function (txn, pos, text) {
if (txn instanceof Loro) {
this.__loro_insert(txn, pos, text);
} else {
this.__txn_insert(txn, pos, text);
}
this.__txn_insert(txn, pos, text);
};
LoroText.prototype.delete = function (txn, pos, len) {
if (txn instanceof Loro) {
this.__loro_delete(txn, pos, len);
} else {
this.__txn_delete(txn, pos, len);
}
this.__txn_delete(txn, pos, len);
};
LoroList.prototype.insert = function (txn, pos, len) {
if (txn instanceof Loro) {
this.__loro_insert(txn, pos, len);
} else {
this.__txn_insert(txn, pos, len);
}
this.__txn_insert(txn, pos, len);
};
LoroList.prototype.delete = function (txn, pos, len) {
if (txn instanceof Loro) {
this.__loro_delete(txn, pos, len);
} else {
this.__txn_delete(txn, pos, len);
}
this.__txn_delete(txn, pos, len);
};
LoroMap.prototype.set = function (txn, key, value) {
if (txn instanceof Loro) {
this.__loro_insert(txn, key, value);
} else {
this.__txn_insert(txn, key, value);
}
this.__txn_insert(txn, key, value);
};
LoroMap.prototype.delete = function (txn, key) {
if (txn instanceof Loro) {
this.__loro_delete(txn, key);
} else {
this.__txn_delete(txn, key);
}
this.__txn_delete(txn, key);
};
export type Value =
@ -136,14 +111,7 @@ export type TextDiff = {
export type MapDiff = {
type: "map";
diff: {
added: Record<string, Value>;
deleted: Record<string, Value>;
updated: Record<string, {
old: Value;
new: Value;
}>;
};
updated: Record<string, Value | undefined>;
};
export type Diff = ListDiff | TextDiff | MapDiff;
@ -191,7 +159,7 @@ export { Loro };
declare module "loro-wasm" {
interface Loro {
subscribe(listener: Listener): number;
transact(f: (tx: Transaction) => void, origin?: string): void;
transact<T>(f: (tx: Transaction) => T, origin?: string): T;
}
interface Loro<T extends Record<string, any> = Record<string, any>> {
@ -205,22 +173,22 @@ declare module "loro-wasm" {
interface LoroList<T extends any[] = any[]> {
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
pos: number,
container: "Map",
): LoroMap;
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
pos: number,
container: "List",
): LoroList;
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
pos: number,
container: "Text",
): LoroText;
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
pos: number,
container: string,
): never;
@ -228,35 +196,33 @@ declare module "loro-wasm" {
get(index: number): Value;
getTyped<Key extends (keyof T) & number>(loro: Loro, index: Key): T[Key];
insertTyped<Key extends (keyof T) & number>(
txn: Transaction | Loro,
txn: Transaction,
pos: Key,
value: T[Key],
): void;
insert(txn: Transaction | Loro, pos: number, value: Value | Prelim): void;
delete(txn: Transaction | Loro, pos: number, len: number): void;
subscribe(txn: Transaction | Loro, listener: Listener): number;
subscribeDeep(txn: Transaction | Loro, listener: Listener): number;
subscribeOnce(txn: Transaction | Loro, listener: Listener): number;
insert(txn: Transaction, pos: number, value: Value | Prelim): void;
delete(txn: Transaction, pos: number, len: number): void;
subscribe(txn: Loro, listener: Listener): number;
}
interface LoroMap<T extends Record<string, any> = Record<string, any>> {
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
key: string,
container_type: "Map",
): LoroMap;
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
key: string,
container_type: "List",
): LoroList;
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
key: string,
container_type: "Text",
): LoroText;
insertContainer(
txn: Transaction | Loro,
txn: Transaction,
key: string,
container_type: string,
): never;
@ -266,23 +232,19 @@ declare module "loro-wasm" {
txn: Loro,
key: Key,
): T[Key];
set(txn: Transaction | Loro, key: string, value: Value | Prelim): void;
set(txn: Transaction, key: string, value: Value | Prelim): void;
setTyped<Key extends (keyof T) & string>(
txn: Transaction | Loro,
txn: Transaction,
key: Key,
value: T[Key],
): void;
delete(txn: Transaction | Loro, key: string): void;
subscribe(txn: Transaction | Loro, listener: Listener): number;
subscribeDeep(txn: Transaction | Loro, listener: Listener): number;
subscribeOnce(txn: Transaction | Loro, listener: Listener): number;
delete(txn: Transaction, key: string): void;
subscribe(txn: Loro, listener: Listener): number;
}
interface LoroText {
insert(txn: Transaction | Loro, pos: number, text: string): void;
delete(txn: Transaction | Loro, pos: number, len: number): void;
subscribe(txn: Transaction | Loro, listener: Listener): number;
subscribeDeep(txn: Transaction | Loro, listener: Listener): number;
subscribeOnce(txn: Transaction | Loro, listener: Listener): number;
insert(txn: Transaction, pos: number, text: string): void;
delete(txn: Transaction, pos: number, len: number): void;
subscribe(txn: Loro, listener: Listener): number;
}
}

View file

@ -6,8 +6,10 @@ import {
LoroEvent,
MapDiff as MapDiff,
TextDiff,
setPanicHook
} from "../src";
setPanicHook();
describe("event", () => {
it("target", async () => {
const loro = new Loro();
@ -17,7 +19,9 @@ describe("event", () => {
});
const text = loro.getText("text");
const id = text.id;
text.insert(loro, 0, "123");
loro.transact((tx) => {
text.insert(tx, 0, "123");
});
await zeroMs();
expect(lastEvent?.target).toEqual(id);
});
@ -29,15 +33,24 @@ describe("event", () => {
lastEvent = event;
});
const map = loro.getMap("map");
const subMap = map.insertContainer(loro, "sub", "Map");
subMap.set(loro, "0", "1");
const subMap = loro.transact((tx) => {
const subMap = map.insertContainer(tx, "sub", "Map");
subMap.set(tx, "0", "1");
return subMap;
});
await zeroMs();
expect(lastEvent?.path).toStrictEqual(["map", "sub"]);
const list = subMap.insertContainer(loro, "list", "List");
list.insert(loro, 0, "2");
const text = list.insertContainer(loro, 1, "Text");
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;
});
await zeroMs();
text.insert(loro, 0, "3");
loro.transact((tx) => {
text.insert(tx, 0, "3");
});
await zeroMs();
expect(lastEvent?.path).toStrictEqual(["map", "sub", "list", 1]);
});
@ -49,12 +62,16 @@ describe("event", () => {
lastEvent = event;
});
const text = loro.getText("t");
text.insert(loro, 0, "3");
loro.transact(tx => {
text.insert(tx, 0, "3");
})
await zeroMs();
expect(lastEvent?.diff).toStrictEqual(
{ type: "text", diff: [{ type: "insert", value: "3" }] } as TextDiff,
);
text.insert(loro, 1, "12");
loro.transact(tx => {
text.insert(tx, 1, "12");
})
await zeroMs();
expect(lastEvent?.diff).toStrictEqual(
{
@ -71,12 +88,16 @@ describe("event", () => {
lastEvent = event;
});
const text = loro.getList("l");
text.insert(loro, 0, "3");
loro.transact(tx => {
text.insert(tx, 0, "3");
})
await zeroMs();
expect(lastEvent?.diff).toStrictEqual(
{ type: "list", diff: [{ type: "insert", value: ["3"] }] } as ListDiff,
);
text.insert(loro, 1, "12");
loro.transact(tx => {
text.insert(tx, 1, "12");
})
await zeroMs();
expect(lastEvent?.diff).toStrictEqual(
{
@ -101,14 +122,10 @@ describe("event", () => {
expect(lastEvent?.diff).toStrictEqual(
{
type: "map",
diff: {
added: {
"0": "3",
"1": "2",
},
deleted: {},
updated: {},
},
updated: {
"0": "3",
"1": "2",
}
} as MapDiff,
);
loro.transact((tx) => {
@ -119,13 +136,9 @@ describe("event", () => {
expect(lastEvent?.diff).toStrictEqual(
{
type: "map",
diff: {
added: {},
updated: {
"0": { old: "3", new: "0" },
"1": { old: "2", new: "1" },
},
deleted: {},
updated: {
"0": "0",
"1": "1"
},
} as MapDiff,
);
@ -136,10 +149,6 @@ describe("event", () => {
const loro = new Loro();
const text = loro.getText("text");
let ran = 0;
let oneTimeRan = 0;
text.subscribeOnce(loro, (_) => {
oneTimeRan += 1;
});
const sub = text.subscribe(loro, (event) => {
if (!ran) {
expect(event.diff.diff).toStrictEqual(
@ -149,18 +158,24 @@ describe("event", () => {
ran += 1;
expect(event.target).toBe(text.id);
});
text.insert(loro, 0, "123");
text.insert(loro, 1, "456");
loro.transact(tx => {
text.insert(tx, 0, "123");
});
loro.transact(tx => {
text.insert(tx, 1, "456");
});
await zeroMs();
expect(ran).toBeTruthy();
// subscribeOnce test
expect(oneTimeRan).toBe(1);
expect(text.toString()).toEqual("145623");
// unsubscribe
const oldRan = ran;
text.unsubscribe(loro, sub);
text.insert(loro, 0, "789");
loro.transact(tx => {
text.insert(tx, 0, "789");
})
expect(ran).toBe(oldRan);
});
@ -168,23 +183,27 @@ describe("event", () => {
const loro = new Loro();
const map = loro.getMap("map");
let times = 0;
const sub = map.subscribeDeep(loro, (event) => {
const sub = map.subscribe(loro, (event) => {
times += 1;
});
const subMap = map.insertContainer(loro, "sub", "Map");
const subMap =
loro.transact(tx =>
map.insertContainer(tx, "sub", "Map")
);
await zeroMs();
expect(times).toBe(1);
const text = subMap.insertContainer(loro, "k", "Text");
const text = loro.transact(tx => subMap.insertContainer(tx, "k", "Text"));
await zeroMs();
expect(times).toBe(2);
text.insert(loro, 0, "123");
loro.transact(tx => text.insert(tx, 0, "123"));
await zeroMs();
expect(times).toBe(3);
// unsubscribe
map.unsubscribe(loro, sub);
text.insert(loro, 0, "123");
loro.unsubscribe(sub);
loro.transact(tx => text.insert(tx, 0, "123"));
await zeroMs();
expect(times).toBe(3);
});
@ -193,20 +212,20 @@ describe("event", () => {
const loro = new Loro();
const list = loro.getList("list");
let times = 0;
const sub = list.subscribeDeep(loro, (_) => {
const sub = list.subscribe(loro, (_) => {
times += 1;
});
const text = list.insertContainer(loro, 0, "Text");
const text = loro.transact(tx => list.insertContainer(tx, 0, "Text"));
await zeroMs();
expect(times).toBe(1);
text.insert(loro, 0, "123");
loro.transact(tx => text.insert(tx, 0, "123"));
await zeroMs();
expect(times).toBe(2);
// unsubscribe
list.unsubscribe(loro, sub);
text.insert(loro, 0, "123");
loro.unsubscribe(sub);
loro.transact(tx => text.insert(tx, 0, "123"));
await zeroMs();
expect(times).toBe(2);
});
@ -237,19 +256,19 @@ describe("event", () => {
string = newString + string.slice(pos);
}
});
text.insert(loro, 0, "你好");
loro.transact(tx => text.insert(tx, 0, "你好"));
await zeroMs();
expect(text.toString()).toBe(string);
text.insert(loro, 1, "世界");
loro.transact(tx => text.insert(tx, 1, "世界"));
await zeroMs();
expect(text.toString()).toBe(string);
text.insert(loro, 2, "👍");
loro.transact(tx => text.insert(tx, 2, "👍"));
await zeroMs();
expect(text.toString()).toBe(string);
text.insert(loro, 4, "♪(^∇^*)");
loro.transact(tx => text.insert(tx, 2, "♪(^∇^*)"));
await zeroMs();
expect(text.toString()).toBe(string);
});

View file

@ -5,22 +5,31 @@ import {
Loro,
LoroEvent,
MapDiff as MapDiff,
setPanicHook,
TextDiff,
} from "../src";
setPanicHook();
describe("Frontiers", () => {
it("two clients", () => {
const doc = new Loro();
const text = doc.getText("text");
text.insert(doc, 0, "0");
const txn = doc.txn();
text.insert(txn, 0, "0");
txn.commit();
const v0 = doc.frontiers();
const docB = new Loro();
docB.import(doc.exportFrom());
expect(docB.cmpFrontiers(v0)).toBe(0);
text.insert(doc, 1, "0");
doc.transact((t) => {
text.insert(t, 1, "0");
});
expect(docB.cmpFrontiers(doc.frontiers())).toBe(-1);
const textB = docB.getText("text");
textB.insert(docB, 0, "0");
docB.transact((t) => {
textB.insert(t, 0, "0");
});
expect(docB.cmpFrontiers(doc.frontiers())).toBe(-1);
docB.import(doc.exportFrom());
expect(docB.cmpFrontiers(doc.frontiers())).toBe(1);

File diff suppressed because it is too large Load diff