From afac34755f3f9a099c2985ff22dc9f2534d72290 Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Thu, 13 Jun 2024 14:43:19 +0800 Subject: [PATCH] feat: implementing Counter and expose to js side (#384) --- .changeset/silly-schools-arrive.md | 6 + Cargo.lock | 30 ++--- crates/fuzz/Cargo.toml | 6 +- crates/fuzz/src/actor.rs | 4 +- crates/fuzz/src/container/counter.rs | 8 +- crates/fuzz/tests/json.rs | 14 +- crates/fuzz/tests/test.rs | 3 +- crates/loro-common/src/lib.rs | 4 +- crates/loro-internal/src/diff_calc/counter.rs | 4 +- .../src/encoding/encode_reordered.rs | 24 +++- .../loro-internal/src/encoding/json_schema.rs | 123 +++++------------- crates/loro-internal/src/encoding/value.rs | 40 +----- crates/loro-internal/src/event.rs | 8 +- crates/loro-internal/src/handler.rs | 21 ++- crates/loro-internal/src/op/content.rs | 4 +- crates/loro-internal/src/state.rs | 12 +- .../loro-internal/src/state/counter_state.rs | 24 ++-- crates/loro-internal/src/state/list_state.rs | 3 +- crates/loro-internal/src/state/map_state.rs | 3 +- .../src/state/movable_list_state.rs | 3 +- .../loro-internal/src/state/richtext_state.rs | 3 +- crates/loro-internal/src/state/tree_state.rs | 3 +- .../loro-internal/src/state/unknown_state.rs | 4 +- crates/loro-internal/src/txn.rs | 2 +- crates/loro-internal/src/value.rs | 2 +- crates/loro-internal/src/version.rs | 1 - crates/loro-internal/tests/test.rs | 13 ++ crates/loro-wasm/Cargo.toml | 1 + crates/loro-wasm/scripts/build.ts | 2 +- crates/loro-wasm/src/convert.rs | 21 ++- crates/loro-wasm/src/counter.rs | 121 +++++++++++++++++ crates/loro-wasm/src/lib.rs | 32 ++++- crates/loro/src/counter.rs | 11 +- crates/loro/src/event.rs | 2 +- loro-js/src/index.ts | 11 +- loro-js/tests/counter.test.ts | 60 +++++++++ 36 files changed, 420 insertions(+), 213 deletions(-) create mode 100644 .changeset/silly-schools-arrive.md create mode 100644 crates/loro-wasm/src/counter.rs create mode 100644 loro-js/tests/counter.test.ts diff --git a/.changeset/silly-schools-arrive.md b/.changeset/silly-schools-arrive.md new file mode 100644 index 000000000..b74d43ecb --- /dev/null +++ b/.changeset/silly-schools-arrive.md @@ -0,0 +1,6 @@ +--- +"loro-wasm": patch +"loro-crdt": patch +--- + +feat: implement `Counter` and expose it to js side diff --git a/Cargo.lock b/Cargo.lock index a808b6cb3..113920bfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,8 +661,8 @@ dependencies = [ "fxhash", "itertools 0.12.1", "loro 0.16.2", - "loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", - "loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", + "loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", + "loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", "rand", "serde_json", "tabled 0.10.0", @@ -999,13 +999,13 @@ dependencies = [ [[package]] name = "loro" version = "0.16.2" -source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9" +source = "git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7#052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" dependencies = [ "either", "enum-as-inner 0.6.0", "generic-btree", - "loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", - "loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", + "loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", + "loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", "tracing", ] @@ -1029,12 +1029,12 @@ dependencies = [ [[package]] name = "loro-common" version = "0.16.2" -source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9" +source = "git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7#052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" dependencies = [ "arbitrary", "enum-as-inner 0.6.0", "fxhash", - "loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", + "loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", "nonmax", "serde", "serde_columnar", @@ -1061,7 +1061,7 @@ dependencies = [ [[package]] name = "loro-delta" version = "0.16.2" -source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9" +source = "git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7#052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" dependencies = [ "arrayvec", "enum-as-inner 0.5.1", @@ -1124,7 +1124,7 @@ dependencies = [ [[package]] name = "loro-internal" version = "0.16.2" -source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9" +source = "git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7#052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" dependencies = [ "append-only-bytes", "arref", @@ -1137,10 +1137,10 @@ dependencies = [ "im", "itertools 0.12.1", "leb128", - "loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", - "loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", - "loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", - "loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9)", + "loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", + "loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", + "loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", + "loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7)", "md5", "num", "num-derive", @@ -1176,7 +1176,7 @@ dependencies = [ [[package]] name = "loro-rle" version = "0.16.2" -source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9" +source = "git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7#052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" dependencies = [ "append-only-bytes", "arref", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "loro_fractional_index" version = "0.16.2" -source = "git+https://github.com/loro-dev/loro.git?rev=83938290ab2666d85c0c72169127611585a05cf9#83938290ab2666d85c0c72169127611585a05cf9" +source = "git+https://github.com/loro-dev/loro.git?rev=052ca29dc398eba150c8d9a4cebd6f506f1fb2e7#052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" dependencies = [ "imbl", "rand", diff --git a/crates/fuzz/Cargo.toml b/crates/fuzz/Cargo.toml index 4dab2e359..cca58e3b1 100644 --- a/crates/fuzz/Cargo.toml +++ b/crates/fuzz/Cargo.toml @@ -10,15 +10,15 @@ publish = false loro-without-counter = { path = "../loro", package = "loro" } loro = { git = "https://github.com/loro-dev/loro.git", features = [ "counter", -], rev = "83938290ab2666d85c0c72169127611585a05cf9" } +], rev = "052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" } loro-common = { git = "https://github.com/loro-dev/loro.git", features = [ "counter", -], rev = "83938290ab2666d85c0c72169127611585a05cf9" } +], rev = "052ca29dc398eba150c8d9a4cebd6f506f1fb2e7" } # loro = { path = "../loro", package = "loro", features = ["counter"] } # loro-common = { path = "../loro-common", package = "loro-common", features = [ # "counter", # ] } -# loro-without-counter = { git = "https://github.com/loro-dev/loro.git", rev = "eb6daf4f064238cbc5c3d357615f5ed73767e98c", package = "loro" } +# loro-without-counter = { git = "https://github.com/loro-dev/loro.git", rev = "43cc07daf7aada060d01505da5efcbc4e6cc2de8", package = "loro" } fxhash = { workspace = true } enum_dispatch = { workspace = true } enum-as-inner = { workspace = true } diff --git a/crates/fuzz/src/actor.rs b/crates/fuzz/src/actor.rs index c98e6f090..8a620bab8 100644 --- a/crates/fuzz/src/actor.rs +++ b/crates/fuzz/src/actor.rs @@ -362,7 +362,7 @@ pub fn assert_value_eq(a: &LoroValue, b: &LoroValue) { continue; } - if !eq(v, b.get(k).unwrap_or(&LoroValue::I64(0))) { + if !eq(v, b.get(k).unwrap_or(&LoroValue::Double(0.))) { return false; } } @@ -378,7 +378,7 @@ pub fn assert_value_eq(a: &LoroValue, b: &LoroValue) { continue; } - if !eq(v, a.get(k).unwrap_or(&LoroValue::I64(0))) { + if !eq(v, a.get(k).unwrap_or(&LoroValue::Double(0.))) { return false; } } diff --git a/crates/fuzz/src/container/counter.rs b/crates/fuzz/src/container/counter.rs index 8fb6eca3e..ed4a8eb8b 100644 --- a/crates/fuzz/src/container/counter.rs +++ b/crates/fuzz/src/container/counter.rs @@ -113,7 +113,7 @@ impl Actionable for CounterAction { fn apply(&self, actor: &mut ActionExecutor, container: usize) -> Option { let actor = actor.as_counter_actor_mut().unwrap(); let counter = actor.containers.get(container).unwrap(); - counter.increment(self.0 as i64).unwrap(); + counter.increment(self.0 as f64).unwrap(); None } @@ -145,13 +145,13 @@ impl FromGenericAction for CounterAction { #[derive(Debug)] pub struct CounterTracker { - v: i64, + v: f64, id: ContainerID, } impl ApplyDiff for CounterTracker { fn empty(id: ContainerID) -> Self { - Self { v: 0, id } + Self { v: 0., id } } fn id(&self) -> &ContainerID { @@ -165,6 +165,6 @@ impl ApplyDiff for CounterTracker { } fn to_value(&self) -> LoroValue { - LoroValue::I64(self.v) + LoroValue::Double(self.v) } } diff --git a/crates/fuzz/tests/json.rs b/crates/fuzz/tests/json.rs index 70158ec3d..e6d1aae0c 100644 --- a/crates/fuzz/tests/json.rs +++ b/crates/fuzz/tests/json.rs @@ -14,8 +14,8 @@ fn unknown_json() { let doc = loro::LoroDoc::new(); let doc_with_unknown = loro_without_counter::LoroDoc::new(); let counter = doc.get_counter("counter"); - counter.increment(5).unwrap(); - counter.increment(1).unwrap(); + counter.increment(5.).unwrap(); + counter.increment(1.).unwrap(); // json format with counter let json = doc.export_json_updates(&Default::default()); // Test1: old version import newer version json @@ -46,13 +46,11 @@ fn unknown_json() { let _json_with_binary_unknown = doc3_without_counter .export_json_updates(&Default::default(), &doc3_without_counter.oplog_vv()); let new_doc = loro::LoroDoc::new(); - // Test4: newer version import older version json with binary unknown - if new_doc + // Test4: newer version import older version json with counter unknown + // TODO: need one more test case for binary unknown + new_doc .import_json_updates(serde_json::to_string(&unknown_json_from_snapshot).unwrap()) - .is_ok() - { - panic!("json schema don't support forward compatibility"); - } + .unwrap(); } #[test] diff --git a/crates/fuzz/tests/test.rs b/crates/fuzz/tests/test.rs index fb796e40e..73739092f 100644 --- a/crates/fuzz/tests/test.rs +++ b/crates/fuzz/tests/test.rs @@ -5580,14 +5580,13 @@ fn unknown_container() { &list.id(), Arc::new(|e| { assert_eq!(e.events.len(), 2); - assert!(e.events[1].is_unknown) }), ); let doc2 = LoroDoc::new(); let list2 = doc2.get_list("list"); let counter = list2.insert_container(0, LoroCounter::new()).unwrap(); - counter.increment(2).unwrap(); + counter.increment(2.).unwrap(); doc.import(&doc2.export_snapshot()).unwrap(); } diff --git a/crates/loro-common/src/lib.rs b/crates/loro-common/src/lib.rs index 4c42b31da..f94125a93 100644 --- a/crates/loro-common/src/lib.rs +++ b/crates/loro-common/src/lib.rs @@ -221,7 +221,7 @@ impl ContainerType { ContainerType::Tree => LoroValue::List(Arc::new(Default::default())), ContainerType::MovableList => LoroValue::List(Arc::new(Default::default())), #[cfg(feature = "counter")] - ContainerType::Counter => LoroValue::I64(0), + ContainerType::Counter => LoroValue::Double(0.), ContainerType::Unknown(_) => unreachable!(), } } @@ -386,6 +386,8 @@ mod container { "Text" | "text" => Ok(ContainerType::Text), "Tree" | "tree" => Ok(ContainerType::Tree), "MovableList" | "movableList" => Ok(ContainerType::MovableList), + #[cfg(feature = "counter")] + "Counter" | "counter" => Ok(ContainerType::Counter), a => { if a.ends_with(')') { let start = a.find('(').ok_or_else(|| { diff --git a/crates/loro-internal/src/diff_calc/counter.rs b/crates/loro-internal/src/diff_calc/counter.rs index 1abdbb5c7..780d9dad9 100644 --- a/crates/loro-internal/src/diff_calc/counter.rs +++ b/crates/loro-internal/src/diff_calc/counter.rs @@ -9,7 +9,7 @@ use super::DiffCalculatorTrait; #[derive(Debug)] pub(crate) struct CounterDiffCalculator { idx: ContainerIdx, - ops: BTreeMap, + ops: BTreeMap, } impl CounterDiffCalculator { @@ -46,7 +46,7 @@ impl DiffCalculatorTrait for CounterDiffCalculator { to: &crate::VersionVector, _on_new_container: impl FnMut(&ContainerID), ) -> InternalDiff { - let mut diff = 0; + let mut diff = 0.; let (b, a) = from.diff_iter(to); for sub in b { diff --git a/crates/loro-internal/src/encoding/encode_reordered.rs b/crates/loro-internal/src/encoding/encode_reordered.rs index 0dee9e089..a793e1dc0 100644 --- a/crates/loro-internal/src/encoding/encode_reordered.rs +++ b/crates/loro-internal/src/encoding/encode_reordered.rs @@ -32,6 +32,9 @@ use super::{ ImportBlobMetadata, }; +#[allow(unused_imports)] +use super::value::FutureValue; + /// If any section of the document is longer than this, we will not decode it. /// It will return an data corruption error instead. pub(super) const MAX_DECODED_SIZE: usize = 1 << 30; @@ -862,7 +865,7 @@ fn decode_snapshot_states( mode: crate::encoding::EncodeMode::Snapshot, peers: &peers.peer_ids, }, - ); + )?; } let s = take(&mut state.states); @@ -1138,7 +1141,7 @@ mod encode { fn get_future_op_prop(op: &FutureInnerContent) -> i32 { match &op { #[cfg(feature = "counter")] - FutureInnerContent::Counter(c) => *c as i32, + FutureInnerContent::Counter(_) => 0, FutureInnerContent::Unknown { prop, .. } => *prop, } } @@ -1269,7 +1272,14 @@ mod encode { } crate::op::InnerContent::Future(f) => match f { #[cfg(feature = "counter")] - FutureInnerContent::Counter(_) => Value::Future(FutureValue::Counter), + FutureInnerContent::Counter(c) => { + let c_abs = c.abs(); + if c_abs.fract() < std::f64::EPSILON && (c_abs as i64) < (2 << 26) { + Value::I64(*c as i64) + } else { + Value::F64(*c) + } + } FutureInnerContent::Unknown { prop: _, value } => Value::from_owned(value), }, }; @@ -1443,9 +1453,11 @@ fn decode_op( } } #[cfg(feature = "counter")] - ContainerType::Counter => { - crate::op::InnerContent::Future(FutureInnerContent::Counter(prop as i64)) - } + ContainerType::Counter => match value { + Value::F64(c) => crate::op::InnerContent::Future(FutureInnerContent::Counter(c)), + Value::I64(c) => crate::op::InnerContent::Future(FutureInnerContent::Counter(c as f64)), + _ => unreachable!(), + }, // NOTE: The future container type need also try to parse the unknown type ContainerType::Unknown(_) => crate::op::InnerContent::Future(FutureInnerContent::Unknown { prop, diff --git a/crates/loro-internal/src/encoding/json_schema.rs b/crates/loro-internal/src/encoding/json_schema.rs index 8ba569438..d989073a3 100644 --- a/crates/loro-internal/src/encoding/json_schema.rs +++ b/crates/loro-internal/src/encoding/json_schema.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, sync::Arc}; use loro_common::{ContainerID, ContainerType, IdLp, LoroResult, LoroValue, PeerID, TreeID, ID}; -use rle::{HasLength, Sliceable}; +use rle::{HasLength, RleVec, Sliceable}; use crate::{ arena::SharedArena, @@ -60,7 +60,7 @@ pub(crate) fn export_json<'a, 'c: 'a>( } pub(crate) fn import_json(oplog: &mut OpLog, json: JsonSchema) -> LoroResult<()> { - let changes = decode_changes(json, &oplog.arena); + let changes = decode_changes(json, &oplog.arena)?; let (latest_ids, pending_changes) = import_changes_to_oplog(changes, oplog)?; if oplog.try_apply_pending(latest_ids).should_update && !oplog.batch_importing { oplog.dag.refresh_frontiers(); @@ -378,10 +378,8 @@ fn encode_changes( match f { FutureInnerContent::Counter(x) => { JsonOpContent::Future(op::FutureOpWrapper { - prop: *x as i32, - value: op::FutureOp::Counter(super::value::OwnedValue::Future( - super::value::OwnedFutureValue::Counter, - )), + prop: 0, + value: op::FutureOp::Counter(super::OwnedValue::F64(*x)), }) } _ => unreachable!(), @@ -411,7 +409,7 @@ fn encode_changes( changes } -fn decode_changes(json: JsonSchema, arena: &SharedArena) -> Vec { +fn decode_changes(json: JsonSchema, arena: &SharedArena) -> LoroResult> { let JsonSchema { peers, changes, .. } = json; let mut ans = Vec::with_capacity(changes.len()); for op::Change { @@ -420,14 +418,15 @@ fn decode_changes(json: JsonSchema, arena: &SharedArena) -> Vec { deps, lamport, msg: _, - ops, + ops: json_ops, } in changes { let id = convert_id(&id, &peers); - let ops = ops - .into_iter() - .map(|op| decode_op(op, arena, &peers)) - .collect(); + let mut ops: RleVec<[Op; 1]> = RleVec::new(); + for op in json_ops { + ops.push(decode_op(op, arena, &peers)?); + } + let change = Change { id, timestamp, @@ -438,10 +437,10 @@ fn decode_changes(json: JsonSchema, arena: &SharedArena) -> Vec { }; ans.push(change); } - ans + Ok(ans) } -fn decode_op(op: op::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> Op { +fn decode_op(op: op::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> LoroResult { let op::JsonOp { counter, container, @@ -618,24 +617,28 @@ fn decode_op(op: op::JsonOp, arena: &SharedArena, peers: &[PeerID]) -> Op { }, #[cfg(feature = "counter")] ContainerType::Counter => { - let JsonOpContent::Future(op::FutureOpWrapper { prop, value }) = content else { + let JsonOpContent::Future(op::FutureOpWrapper { prop: _, value }) = content else { unreachable!() }; + use crate::encoding::OwnedValue; match value { - op::FutureOp::Counter(_) => { - InnerContent::Future(FutureInnerContent::Counter(prop as i64)) + op::FutureOp::Counter(OwnedValue::F64(c)) + | op::FutureOp::Unknown(OwnedValue::F64(c)) => { + InnerContent::Future(FutureInnerContent::Counter(c)) } - op::FutureOp::Unknown(_) => { - InnerContent::Future(FutureInnerContent::Counter(prop as i64)) + op::FutureOp::Counter(OwnedValue::I64(c)) + | op::FutureOp::Unknown(OwnedValue::I64(c)) => { + InnerContent::Future(FutureInnerContent::Counter(c as f64)) } + _ => unreachable!(), } } // Note: The Future Type need try to parse Op from the unknown content }; - Op { + Ok(Op { counter, container: idx, content, - } + }) } impl TryFrom<&str> for JsonSchema { @@ -831,6 +834,9 @@ pub mod op { Deserialize, Deserializer, Serialize, Serializer, }; + #[allow(unused_imports)] + use crate::encoding::OwnedValue; + impl Serialize for super::JsonOp { fn serialize(&self, serializer: S) -> Result where @@ -898,14 +904,11 @@ pub mod op { } #[cfg(feature = "counter")] ContainerType::Counter => { - let (_key, v) = map.next_entry::()?.unwrap(); + let (_key, v) = + map.next_entry::()?.unwrap(); super::JsonOpContent::Future(super::FutureOpWrapper { - prop: v as i32, - value: super::FutureOp::Counter( - crate::encoding::value::OwnedValue::Future( - crate::encoding::value::OwnedFutureValue::Counter, - ), - ), + prop: 0, + value: super::FutureOp::Counter(v), }) } _ => unreachable!(), @@ -924,70 +927,6 @@ pub mod op { } } - // pub mod future_op { - - // use serde::{Deserialize, Deserializer}; - - // use crate::encoding::json_schema::op::FutureOp; - - // impl<'de> Deserialize<'de> for FutureOp { - // fn deserialize(d: D) -> Result - // where - // D: Deserializer<'de>, - // { - // enum Field { - // #[cfg(feature = "counter")] - // Counter, - // Unknown, - // } - // struct FieldVisitor; - // impl<'de> serde::de::Visitor<'de> for FieldVisitor { - // type Value = Field; - // fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - // f.write_str("field identifier") - // } - // fn visit_str(self, value: &str) -> Result - // where - // E: serde::de::Error, - // { - // match value { - // #[cfg(feature = "counter")] - // "counter" => Ok(Field::Counter), - // _ => Ok(Field::Unknown), - // } - // } - // } - // impl<'de> Deserialize<'de> for Field { - // fn deserialize(deserializer: D) -> Result - // where - // D: Deserializer<'de>, - // { - // deserializer.deserialize_identifier(FieldVisitor) - // } - // } - // let (tag, content) = d.deserialize_any( - // serde::__private::de::TaggedContentVisitor::::new( - // "type", - // "internally tagged enum FutureOp", - // ), - // )?; - // let __deserializer = - // serde::__private::de::ContentDeserializer::::new(content); - // match tag { - // #[cfg(feature = "counter")] - // Field::Counter => { - // let v = serde::Deserialize::deserialize(__deserializer)?; - // Ok(FutureOp::Counter(v)) - // } - // _ => { - // let v = serde::Deserialize::deserialize(__deserializer)?; - // Ok(FutureOp::Unknown(v)) - // } - // } - // } - // } - // } - pub mod id { use loro_common::ID; use serde::{Deserialize, Deserializer, Serializer}; diff --git a/crates/loro-internal/src/encoding/value.rs b/crates/loro-internal/src/encoding/value.rs index 66419c94e..ef57af187 100644 --- a/crates/loro-internal/src/encoding/value.rs +++ b/crates/loro-internal/src/encoding/value.rs @@ -85,8 +85,6 @@ impl LoroValueKind { #[derive(Debug)] pub enum FutureValueKind { - #[cfg(feature = "counter")] - Counter, // 16 Unknown(u8), } @@ -110,8 +108,6 @@ impl ValueKind { ValueKind::ListMove => 14, ValueKind::ListSet => 15, ValueKind::Future(future_value_kind) => match future_value_kind { - #[cfg(feature = "counter")] - FutureValueKind::Counter => 16, FutureValueKind::Unknown(u8) => *u8 | 0x80, }, } @@ -136,8 +132,6 @@ impl ValueKind { 13 => ValueKind::TreeMove, 14 => ValueKind::ListMove, 15 => ValueKind::ListSet, - #[cfg(feature = "counter")] - 16 => ValueKind::Future(FutureValueKind::Counter), _ => ValueKind::Future(FutureValueKind::Unknown(kind)), } } @@ -175,13 +169,8 @@ pub enum Value<'a> { #[derive(Debug)] pub enum FutureValue<'a> { - #[cfg(feature = "counter")] - Counter, // The future value cannot depend on the arena for encoding. - Unknown { - kind: u8, - data: &'a [u8], - }, + Unknown { kind: u8, data: &'a [u8] }, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -218,13 +207,8 @@ pub enum OwnedValue { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "value_type", content = "value")] pub enum OwnedFutureValue { - #[cfg(feature = "counter")] - Counter, // The future value cannot depend on the arena for encoding. - Unknown { - kind: u8, - data: Arc>, - }, + Unknown { kind: u8, data: Arc> }, } impl<'a> Value<'a> { @@ -263,8 +247,6 @@ impl<'a> Value<'a> { value: value.clone(), }, OwnedValue::Future(value) => match value { - #[cfg(feature = "counter")] - OwnedFutureValue::Counter => Value::Future(FutureValue::Counter), OwnedFutureValue::Unknown { kind, data } => Value::Future(FutureValue::Unknown { kind: *kind, data: data.as_slice(), @@ -308,8 +290,6 @@ impl<'a> Value<'a> { value, }, Value::Future(value) => match value { - #[cfg(feature = "counter")] - FutureValue::Counter => OwnedValue::Future(OwnedFutureValue::Counter), FutureValue::Unknown { kind, data } => { OwnedValue::Future(OwnedFutureValue::Unknown { kind, @@ -326,11 +306,11 @@ impl<'a> Value<'a> { ) -> LoroResult { let bytes = value_reader.read_binary()?; let value = match future_kind { - #[cfg(feature = "counter")] - FutureValueKind::Counter => FutureValue::Counter, - FutureValueKind::Unknown(kind) => FutureValue::Unknown { kind, data: bytes }, + FutureValueKind::Unknown(kind) => { + Value::Future(FutureValue::Unknown { kind, data: bytes }) + } }; - Ok(Value::Future(value)) + Ok(value) } pub(super) fn decode<'r: 'a>( @@ -393,12 +373,6 @@ impl<'a> Value<'a> { // when decoding, we will use reader.read_binary() to read the binary data. // So such as FutureValue::Counter, we should write 0 as the length of binary data first. match value { - #[cfg(feature = "counter")] - FutureValue::Counter => { - // write bytes length - value_writer.write_u8(0); - (FutureValueKind::Counter, 1) - } FutureValue::Unknown { kind, data } => ( FutureValueKind::Unknown(kind), value_writer.write_binary(data), @@ -860,7 +834,7 @@ impl<'a> ValueReader<'a> { .map_err(|_| LoroError::DecodeDataCorruptionError) } - fn read_f64(&mut self) -> LoroResult { + pub fn read_f64(&mut self) -> LoroResult { if self.raw.len() < 8 { return Err(LoroError::DecodeDataCorruptionError); } diff --git a/crates/loro-internal/src/event.rs b/crates/loro-internal/src/event.rs index 991a98c18..3fd1ea464 100644 --- a/crates/loro-internal/src/event.rs +++ b/crates/loro-internal/src/event.rs @@ -228,7 +228,7 @@ pub(crate) enum InternalDiff { Tree(TreeDelta), MovableList(MovableListInnerDelta), #[cfg(feature = "counter")] - Counter(i64), + Counter(f64), Unknown, } @@ -314,7 +314,7 @@ pub enum Diff { Map(ResolvedMapDelta), Tree(TreeDiff), #[cfg(feature = "counter")] - Counter(i64), + Counter(f64), Unknown, } @@ -333,7 +333,7 @@ impl InternalDiff { InternalDiff::Tree(t) => t.is_empty(), InternalDiff::MovableList(t) => t.is_empty(), #[cfg(feature = "counter")] - InternalDiff::Counter(c) => *c == 0, + InternalDiff::Counter(c) => c.abs() < f64::EPSILON, InternalDiff::Unknown => true, } } @@ -423,7 +423,7 @@ impl Diff { Diff::Map(m) => m.updated.is_empty(), Diff::Tree(t) => t.diff.is_empty(), #[cfg(feature = "counter")] - Diff::Counter(c) => *c == 0, + Diff::Counter(c) => c.abs() < f64::EPSILON, Diff::Unknown => true, } } diff --git a/crates/loro-internal/src/handler.rs b/crates/loro-internal/src/handler.rs index 5e3f7e990..2ab70c3b3 100644 --- a/crates/loro-internal/src/handler.rs +++ b/crates/loro-internal/src/handler.rs @@ -3239,17 +3239,17 @@ pub mod counter { #[derive(Clone)] pub struct CounterHandler { - pub(super) inner: MaybeDetached, + pub(super) inner: MaybeDetached, } impl CounterHandler { pub fn new_detached() -> Self { Self { - inner: MaybeDetached::new_detached(0), + inner: MaybeDetached::new_detached(0.), } } - pub fn increment(&self, n: i64) -> LoroResult<()> { + pub fn increment(&self, n: f64) -> LoroResult<()> { match &self.inner { MaybeDetached::Detached(d) => { let d = &mut d.try_lock().unwrap().value; @@ -3260,7 +3260,18 @@ pub mod counter { } } - fn increment_with_txn(&self, txn: &mut Transaction, n: i64) -> LoroResult<()> { + pub fn decrement(&self, n: f64) -> LoroResult<()> { + match &self.inner { + MaybeDetached::Detached(d) => { + let d = &mut d.try_lock().unwrap().value; + *d -= n; + Ok(()) + } + MaybeDetached::Attached(a) => a.with_txn(|txn| self.increment_with_txn(txn, -n)), + } + } + + fn increment_with_txn(&self, txn: &mut Transaction, n: f64) -> LoroResult<()> { let inner = self.inner.try_attached_state()?; txn.apply_local_op( inner.container_idx, @@ -3340,7 +3351,7 @@ pub mod counter { MaybeDetached::Attached(a) => { let new_inner = create_handler(a, self_id); let ans = new_inner.into_counter().unwrap(); - let delta = *self.get_value().as_i64().unwrap(); + let delta = *self.get_value().as_double().unwrap(); ans.increment_with_txn(txn, delta)?; Ok(ans) } diff --git a/crates/loro-internal/src/op/content.rs b/crates/loro-internal/src/op/content.rs index 57f73b374..eef39ac98 100644 --- a/crates/loro-internal/src/op/content.rs +++ b/crates/loro-internal/src/op/content.rs @@ -67,7 +67,7 @@ impl InnerContent { #[derive(EnumAsInner, Debug, Clone)] pub enum FutureInnerContent { #[cfg(feature = "counter")] - Counter(i64), + Counter(f64), Unknown { prop: i32, value: OwnedValue, @@ -82,7 +82,7 @@ pub enum RawOpContent<'a> { List(ListOp<'a>), Tree(TreeOp), #[cfg(feature = "counter")] - Counter(i64), + Counter(f64), Unknown { prop: i32, value: OwnedValue, diff --git a/crates/loro-internal/src/state.rs b/crates/loro-internal/src/state.rs index a499b48cc..8f92ccc36 100644 --- a/crates/loro-internal/src/state.rs +++ b/crates/loro-internal/src/state.rs @@ -136,7 +136,7 @@ pub(crate) trait ContainerState: Clone { fn encode_snapshot(&self, encoder: StateSnapshotEncoder) -> Vec; /// Restore the state to the state represented by the ops and the blob that exported by `get_snapshot_ops` - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext); + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()>; } impl ContainerState for Box { @@ -216,7 +216,7 @@ impl ContainerState for Box { } #[doc = r" Restore the state to the state represented by the ops and the blob that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { self.as_mut().import_from_snapshot_ops(ctx) } } @@ -591,10 +591,10 @@ impl DocState { &mut self, cid: ContainerID, decode_ctx: StateSnapshotDecodeContext, - ) { + ) -> LoroResult<()> { let idx = self.arena.register_container(&cid); let state = get_or_create!(self, idx); - state.import_from_snapshot_ops(decode_ctx); + state.import_from_snapshot_ops(decode_ctx) } pub(crate) fn init_unknown_container(&mut self, cid: ContainerID) { @@ -1089,8 +1089,8 @@ impl DocState { } #[cfg(feature = "counter")] if id.container_type() == ContainerType::Counter { - if let LoroValue::I64(c) = value { - if c == 0 { + if let LoroValue::Double(c) = value { + if c.abs() < f64::EPSILON { return None; } } diff --git a/crates/loro-internal/src/state/counter_state.rs b/crates/loro-internal/src/state/counter_state.rs index c68eed77d..b900cee6a 100644 --- a/crates/loro-internal/src/state/counter_state.rs +++ b/crates/loro-internal/src/state/counter_state.rs @@ -1,6 +1,6 @@ use std::sync::{Mutex, Weak}; -use loro_common::{ContainerID, LoroResult, LoroValue}; +use loro_common::{ContainerID, LoroError, LoroResult, LoroValue}; use crate::{ arena::SharedArena, @@ -17,12 +17,12 @@ use super::ContainerState; #[derive(Debug, Clone)] pub struct CounterState { idx: ContainerIdx, - value: i64, + value: f64, } impl CounterState { pub(crate) fn new(idx: ContainerIdx) -> Self { - Self { idx, value: 0 } + Self { idx, value: 0. } } } @@ -85,7 +85,7 @@ impl ContainerState for CounterState { } fn get_value(&mut self) -> LoroValue { - LoroValue::I64(self.value) + LoroValue::Double(self.value) } #[doc = " Get the index of the child container"] @@ -105,15 +105,19 @@ impl ContainerState for CounterState { #[doc = " The ops should be encoded into the snapshot as well as the blob."] #[doc = " The users then can use the ops and the blob to restore the state to the current state."] fn encode_snapshot(&self, _encoder: StateSnapshotEncoder) -> Vec { - let mut ans = vec![]; - leb128::write::signed(&mut ans, self.value).unwrap(); - ans + self.value.to_be_bytes().to_vec() } #[doc = " Restore the state to the state represented by the ops and the blob that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { - let mut reader = ctx.blob; - self.value = leb128::read::signed(&mut reader).unwrap(); + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { + let reader = ctx.blob; + let Some(bytes) = reader.get(0..8) else { + return Err(LoroError::DecodeDataCorruptionError); + }; + let mut buf = [0; 8]; + buf.copy_from_slice(bytes); + self.value = f64::from_be_bytes(buf); + Ok(()) } #[allow(unused)] diff --git a/crates/loro-internal/src/state/list_state.rs b/crates/loro-internal/src/state/list_state.rs index 9d6eb4dcd..96e306740 100644 --- a/crates/loro-internal/src/state/list_state.rs +++ b/crates/loro-internal/src/state/list_state.rs @@ -505,7 +505,7 @@ impl ContainerState for ListState { } #[doc = "Restore the state to the state represented by the ops that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { assert_eq!(ctx.mode, EncodeMode::Snapshot); let mut index = 0; for op in ctx.ops { @@ -518,6 +518,7 @@ impl ContainerState for ListState { self.insert_batch(index, list, op.id_full()); index += len; } + Ok(()) } } diff --git a/crates/loro-internal/src/state/map_state.rs b/crates/loro-internal/src/state/map_state.rs index 8d9c9ab23..3eb018bbb 100644 --- a/crates/loro-internal/src/state/map_state.rs +++ b/crates/loro-internal/src/state/map_state.rs @@ -173,7 +173,7 @@ impl ContainerState for MapState { } #[doc = " Restore the state to the state represented by the ops that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { assert_eq!(ctx.mode, EncodeMode::Snapshot); for op in ctx.ops { debug_assert_eq!( @@ -192,6 +192,7 @@ impl ContainerState for MapState { }, ); } + Ok(()) } } diff --git a/crates/loro-internal/src/state/movable_list_state.rs b/crates/loro-internal/src/state/movable_list_state.rs index de0b6dc76..dd98c27af 100644 --- a/crates/loro-internal/src/state/movable_list_state.rs +++ b/crates/loro-internal/src/state/movable_list_state.rs @@ -1351,7 +1351,7 @@ impl ContainerState for MovableListState { serde_columnar::to_vec(&out).unwrap() } - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { let iter = serde_columnar::iter_from_bytes::(ctx.blob).unwrap(); let item_iter = iter.items; let mut item_ids = iter.ids; @@ -1437,6 +1437,7 @@ impl ContainerState for MovableListState { assert!(item_ids.next().is_none()); assert!(last_set_op_iter.next().is_none()); + Ok(()) } } diff --git a/crates/loro-internal/src/state/richtext_state.rs b/crates/loro-internal/src/state/richtext_state.rs index 200a0ceef..d20c25fd9 100644 --- a/crates/loro-internal/src/state/richtext_state.rs +++ b/crates/loro-internal/src/state/richtext_state.rs @@ -655,7 +655,7 @@ impl ContainerState for RichtextState { } #[doc = " Restore the state to the state represented by the ops that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { self.update_version(); assert_eq!(ctx.mode, EncodeMode::Snapshot); let mut loader = RichtextStateLoader::default(); @@ -692,6 +692,7 @@ impl ContainerState for RichtextState { self.state = LazyLoad::Src(loader); // self.check_consistency_between_content_and_style_ranges(); + Ok(()) } } diff --git a/crates/loro-internal/src/state/tree_state.rs b/crates/loro-internal/src/state/tree_state.rs index 73ec07ada..caa55897c 100644 --- a/crates/loro-internal/src/state/tree_state.rs +++ b/crates/loro-internal/src/state/tree_state.rs @@ -1068,7 +1068,7 @@ impl ContainerState for TreeState { } #[doc = " Restore the state to the state represented by the ops that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) { + fn import_from_snapshot_ops(&mut self, ctx: StateSnapshotDecodeContext) -> LoroResult<()> { assert_eq!(ctx.mode, EncodeMode::Snapshot); for op in ctx.ops { assert_eq!(op.op.atom_len(), 1); @@ -1095,6 +1095,7 @@ impl ContainerState for TreeState { } }; } + Ok(()) } } diff --git a/crates/loro-internal/src/state/unknown_state.rs b/crates/loro-internal/src/state/unknown_state.rs index 5fc5c3088..8d5a58497 100644 --- a/crates/loro-internal/src/state/unknown_state.rs +++ b/crates/loro-internal/src/state/unknown_state.rs @@ -101,5 +101,7 @@ impl ContainerState for UnknownState { } #[doc = r" Restore the state to the state represented by the ops and the blob that exported by `get_snapshot_ops`"] - fn import_from_snapshot_ops(&mut self, _ctx: StateSnapshotDecodeContext) {} + fn import_from_snapshot_ops(&mut self, _ctx: StateSnapshotDecodeContext) -> LoroResult<()> { + Ok(()) + } } diff --git a/crates/loro-internal/src/txn.rs b/crates/loro-internal/src/txn.rs index 747578d6a..54e9cad5d 100644 --- a/crates/loro-internal/src/txn.rs +++ b/crates/loro-internal/src/txn.rs @@ -126,7 +126,7 @@ pub(super) enum EventHint { Tree(TreeDiffItem), MarkEnd, #[cfg(feature = "counter")] - Counter(i64), + Counter(f64), } impl generic_btree::rle::HasLength for EventHint { diff --git a/crates/loro-internal/src/value.rs b/crates/loro-internal/src/value.rs index 852780997..c3da5f5ea 100644 --- a/crates/loro-internal/src/value.rs +++ b/crates/loro-internal/src/value.rs @@ -482,7 +482,7 @@ impl ApplyDiff for LoroValue { TypeHint::List => LoroValue::List(Default::default()), TypeHint::Tree => LoroValue::List(Default::default()), #[cfg(feature = "counter")] - TypeHint::Counter => LoroValue::I64(0), + TypeHint::Counter => LoroValue::Double(0.), }) } Index::Seq(index) => { diff --git a/crates/loro-internal/src/version.rs b/crates/loro-internal/src/version.rs index 4788e154c..691ea1d3b 100644 --- a/crates/loro-internal/src/version.rs +++ b/crates/loro-internal/src/version.rs @@ -3,7 +3,6 @@ use smallvec::smallvec; use std::{ cmp::Ordering, ops::{Deref, DerefMut}, - sync::Arc, }; use fxhash::{FxHashMap, FxHashSet}; diff --git a/crates/loro-internal/tests/test.rs b/crates/loro-internal/tests/test.rs index 90d103572..fd6fcd6a3 100644 --- a/crates/loro-internal/tests/test.rs +++ b/crates/loro-internal/tests/test.rs @@ -960,3 +960,16 @@ fn tree_attach() { json!({"key":"value"}) ) } + +#[test] +#[cfg(feature = "counter")] +fn counter() { + let doc = LoroDoc::new_auto_commit(); + let counter = doc.get_counter("counter"); + counter.increment(1.).unwrap(); + counter.increment(2.).unwrap(); + counter.decrement(1.).unwrap(); + let json = doc.export_json_updates(&Default::default()); + let doc2 = LoroDoc::new_auto_commit(); + doc2.import_json_updates(json).unwrap(); +} diff --git a/crates/loro-wasm/Cargo.toml b/crates/loro-wasm/Cargo.toml index 3342257e7..dc210c34d 100644 --- a/crates/loro-wasm/Cargo.toml +++ b/crates/loro-wasm/Cargo.toml @@ -24,3 +24,4 @@ serde_json = "1" [features] default = ["console_error_panic_hook"] +counter = ["loro-internal/counter"] diff --git a/crates/loro-wasm/scripts/build.ts b/crates/loro-wasm/scripts/build.ts index ddce6f93a..9cff1d108 100644 --- a/crates/loro-wasm/scripts/build.ts +++ b/crates/loro-wasm/scripts/build.ts @@ -59,7 +59,7 @@ async function build() { async function cargoBuild() { const cmd = - `cargo build --target wasm32-unknown-unknown --profile ${profile}`; + `cargo build --features counter --target wasm32-unknown-unknown --profile ${profile}`; console.log(cmd); const status = await Deno.run({ cmd: cmd.split(" "), diff --git a/crates/loro-wasm/src/convert.rs b/crates/loro-wasm/src/convert.rs index 65b157ea2..6f97eabde 100644 --- a/crates/loro-wasm/src/convert.rs +++ b/crates/loro-wasm/src/convert.rs @@ -15,6 +15,9 @@ use crate::{ use wasm_bindgen::__rt::IntoJsResult; use wasm_bindgen::convert::RefFromWasmAbi; +#[cfg(feature = "counter")] +use crate::LoroCounter; + /// Convert a `JsValue` to `T` by constructor's name. /// /// more details can be found in https://github.com/rustwasm/wasm-bindgen/issues/2231#issuecomment-656293288 @@ -133,6 +136,22 @@ pub(crate) fn resolved_diff_to_js(value: &Diff, doc: &Arc) -> JsValue { ) .unwrap(); } + + #[cfg(feature = "counter")] + Diff::Counter(v) => { + js_sys::Reflect::set( + &obj, + &JsValue::from_str("type"), + &JsValue::from_str("counter"), + ) + .unwrap(); + js_sys::Reflect::set( + &obj, + &JsValue::from_str("increment"), + &JsValue::from_f64(*v), + ) + .unwrap(); + } _ => unreachable!(), }; @@ -327,7 +346,7 @@ pub(crate) fn handler_to_js_value(handler: Handler, doc: Option>) - Handler::Tree(t) => LoroTree { handler: t, doc }.into(), Handler::MovableList(m) => LoroMovableList { handler: m, doc }.into(), #[cfg(feature = "counter")] - Handler::Counter(c) => unimplemented!(), + Handler::Counter(c) => LoroCounter { handler: c, doc }.into(), Handler::Unknown(_) => unreachable!(), } } diff --git a/crates/loro-wasm/src/counter.rs b/crates/loro-wasm/src/counter.rs new file mode 100644 index 000000000..553fd2289 --- /dev/null +++ b/crates/loro-wasm/src/counter.rs @@ -0,0 +1,121 @@ +use std::sync::Arc; + +use loro_internal::{ + handler::{counter::CounterHandler, Handler}, + obs::SubID, + HandlerTrait, LoroDoc, +}; +use wasm_bindgen::prelude::*; + +use crate::{ + call_after_micro_task, convert::handler_to_js_value, observer, JsContainerOrUndefined, + JsLoroTreeOrUndefined, JsResult, +}; + +/// The handler of a tree(forest) container. +#[derive(Clone)] +#[wasm_bindgen] +pub struct LoroCounter { + pub(crate) handler: CounterHandler, + pub(crate) doc: Option>, +} + +impl Default for LoroCounter { + fn default() -> Self { + Self::new() + } +} + +#[wasm_bindgen] +impl LoroCounter { + /// Create a new LoroCounter. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + handler: CounterHandler::new_detached(), + doc: None, + } + } + + /// Increment the counter by the given value. + pub fn increment(&self, value: f64) -> JsResult<()> { + self.handler.increment(value)?; + Ok(()) + } + + /// Decrement the counter by the given value. + pub fn decrement(&self, value: f64) -> JsResult<()> { + self.handler.decrement(value)?; + Ok(()) + } + + /// Get the value of the counter. + #[wasm_bindgen(js_name = "value", getter)] + pub fn get_value(&self) -> f64 { + self.handler.get_value().into_double().unwrap() + } + + /// Subscribe to the changes of the counter. + pub fn subscribe(&self, f: js_sys::Function) -> JsResult { + let observer = observer::Observer::new(f); + let doc = self + .doc + .clone() + .ok_or_else(|| JsError::new("Document is not attached"))?; + let doc_clone = doc.clone(); + let ans = doc.subscribe( + &self.handler.id(), + Arc::new(move |e| { + call_after_micro_task(observer.clone(), e, &doc_clone); + }), + ); + Ok(ans.into_u32()) + } + + /// Unsubscribe by the subscription id. + pub fn unsubscribe(&self, subscription: u32) -> JsResult<()> { + self.doc + .as_ref() + .ok_or_else(|| JsError::new("Document is not attached"))? + .unsubscribe(SubID::from_u32(subscription)); + Ok(()) + } + + /// Get the parent container of the counter container. + /// + /// - The parent container of the root counter is `undefined`. + /// - The object returned is a new js object each time because it need to cross + /// the WASM boundary. + pub fn parent(&self) -> JsContainerOrUndefined { + if let Some(p) = HandlerTrait::parent(&self.handler) { + handler_to_js_value(p, self.doc.clone()).into() + } else { + JsContainerOrUndefined::from(JsValue::UNDEFINED) + } + } + + /// Whether the container is attached to a docuemnt. + /// + /// If it's detached, the operations on the container will not be persisted. + #[wasm_bindgen(js_name = "isAttached")] + pub fn is_attached(&self) -> bool { + self.handler.is_attached() + } + + /// Get the attached container associated with this. + /// + /// Returns an attached `Container` that equals to this or created by this, otherwise `undefined`. + #[wasm_bindgen(js_name = "getAttached")] + pub fn get_attached(&self) -> JsLoroTreeOrUndefined { + if self.is_attached() { + let value: JsValue = self.clone().into(); + return value.into(); + } + + if let Some(h) = self.handler.get_attached() { + handler_to_js_value(Handler::Counter(h), self.doc.clone()).into() + } else { + JsValue::UNDEFINED.into() + } + } +} diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 2a05df1cc..5fb09f6f2 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -29,6 +29,12 @@ use std::{cell::RefCell, cmp::Ordering, rc::Rc, sync::Arc}; use wasm_bindgen::{__rt::IntoJsResult, prelude::*, throw_val}; use wasm_bindgen_derive::TryFromJsValue; +#[cfg(feature = "counter")] +mod counter; +#[cfg(feature = "counter")] +pub use counter::LoroCounter; +#[cfg(feature = "counter")] +use loro_internal::handler::counter::CounterHandler; mod awareness; mod log; @@ -665,6 +671,19 @@ impl Loro { }) } + /// Get a LoroCounter by container id + #[cfg(feature = "counter")] + #[wasm_bindgen(js_name = "getCounter")] + pub fn get_counter(&self, cid: &JsIntoContainerID) -> JsResult { + let counter = self + .0 + .get_counter(js_value_to_container_id(cid, ContainerType::Counter)?); + Ok(LoroCounter { + handler: counter, + doc: Some(self.0.clone()), + }) + } + /// Get a LoroTree by container id /// /// The object returned is a new js object each time because it need to cross @@ -746,6 +765,15 @@ impl Loro { } .into() } + #[cfg(feature = "counter")] + ContainerType::Counter => { + let counter = self.0.get_counter(container_id); + LoroCounter { + handler: counter, + doc: Some(self.0.clone()), + } + .into() + } ContainerType::Unknown(_) => { return Err(JsValue::from_str( "You are attempting to get an unknown container", @@ -893,7 +921,7 @@ impl Loro { json_end_vv = vv.0; } let json_schema = self.0.export_json_updates(&json_start_vv, &json_end_vv); - let s = serde_wasm_bindgen::Serializer::new(); + let s = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true); let v = json_schema .serialize(&s) .map_err(std::convert::Into::::into)?; @@ -3127,7 +3155,6 @@ impl LoroTree { /// const node2 = node.createNode(); /// console.log(tree.nodes()); /// ``` - #[wasm_bindgen] pub fn nodes(&mut self) -> Vec { self.handler .nodes() @@ -3137,7 +3164,6 @@ impl LoroTree { } /// Get the root nodes of the forest. - #[wasm_bindgen] pub fn roots(&self) -> Vec { self.handler .roots() diff --git a/crates/loro/src/counter.rs b/crates/loro/src/counter.rs index 298b6d1bd..83d9745a1 100644 --- a/crates/loro/src/counter.rs +++ b/crates/loro/src/counter.rs @@ -4,6 +4,7 @@ use loro_internal::{ use crate::{Container, ContainerTrait, SealedTrait}; +/// A counter that can be incremented or decremented. #[derive(Debug, Clone)] pub struct LoroCounter { pub(crate) handler: CounterHandler, @@ -16,6 +17,7 @@ impl Default for LoroCounter { } impl LoroCounter { + /// Create a new Counter. pub fn new() -> Self { Self { handler: CounterHandler::new_detached(), @@ -27,10 +29,17 @@ impl LoroCounter { self.handler.id().clone() } - pub fn increment(&self, value: i64) -> LoroResult<()> { + /// Increment the counter by the given value. + pub fn increment(&self, value: f64) -> LoroResult<()> { self.handler.increment(value) } + /// Decrement the counter by the given value. + pub fn decrement(&self, value: f64) -> LoroResult<()> { + self.handler.decrement(value) + } + + /// Get the current value of the counter. pub fn get_value(&self) -> LoroValue { self.handler.get_value() } diff --git a/crates/loro/src/event.rs b/crates/loro/src/event.rs index 848c29bcd..329950810 100644 --- a/crates/loro/src/event.rs +++ b/crates/loro/src/event.rs @@ -55,7 +55,7 @@ pub enum Diff<'a> { Tree(&'a TreeDiff), #[cfg(feature = "counter")] /// A counter diff. - Counter(i64), + Counter(f64), /// An unknown diff. Unknown, } diff --git a/loro-js/src/index.ts b/loro-js/src/index.ts index 70b912c88..682503175 100644 --- a/loro-js/src/index.ts +++ b/loro-js/src/index.ts @@ -8,6 +8,7 @@ import { LoroMap, LoroText, LoroTree, + LoroCounter, OpId, TreeID, Value, @@ -103,13 +104,18 @@ export type TreeDiff = { diff: TreeDiffItem[]; }; -export type Diff = ListDiff | TextDiff | MapDiff | TreeDiff; +export type CounterDiff = { + type: "counter"; + increment: number; +} + +export type Diff = ListDiff | TextDiff | MapDiff | TreeDiff | CounterDiff; interface Listener { (event: LoroEventBatch): void; } -const CONTAINER_TYPES = ["Map", "Text", "List", "Tree", "MovableList"]; +const CONTAINER_TYPES = ["Map", "Text", "List", "Tree", "MovableList", "Counter"]; export function isContainerId(s: string): s is ContainerID { return s.startsWith("cid:"); @@ -173,6 +179,7 @@ export function getType( ? "Tree" : T extends LoroList ? "List" + :T extends LoroCounter?"Counter" : "Json" { if (isContainer(value)) { return value.kind() as unknown as any; diff --git a/loro-js/tests/counter.test.ts b/loro-js/tests/counter.test.ts new file mode 100644 index 000000000..7223b244d --- /dev/null +++ b/loro-js/tests/counter.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import { CounterDiff, Loro } from "../src"; + +function oneMs(): Promise { + return new Promise((r) => setTimeout(r)); +} + +describe("counter", () => { + it("increment", () => { + const doc = new Loro(); + const counter = doc.getCounter("counter"); + counter.increment(1); + counter.increment(2); + counter.decrement(1); + expect(counter.value).toBe(2); + }); + + it("encode", async () => { + const doc = new Loro(); + const counter = doc.getCounter("counter"); + counter.increment(1); + counter.increment(2); + counter.decrement(4); + + const updates = doc.exportFrom(); + const snapshot = doc.exportSnapshot(); + const json = doc.exportJsonUpdates(); + const doc2 = new Loro(); + doc2.import(updates); + expect(doc2.toJSON()).toStrictEqual(doc.toJSON()); + const doc3 = new Loro(); + doc3.import(snapshot); + expect(doc3.toJSON()).toStrictEqual(doc.toJSON()); + const doc4 = new Loro(); + doc4.importJsonUpdates(json); + expect(doc4.toJSON()).toStrictEqual(doc.toJSON()); + }); +}); + +describe("counter event", () => { + it("event", async () => { + const doc = new Loro(); + let triggered = false; + doc.subscribe((e) => { + triggered = true; + const diff = e.events[0].diff as CounterDiff; + expect(diff.type).toBe("counter"); + expect(diff.increment).toStrictEqual(-1); + }); + const counter = doc.getCounter("counter"); + + counter.increment(1); + counter.increment(2); + counter.decrement(4); + doc.commit(); + await oneMs(); + expect(triggered).toBe(true); + }); +}); +