Refactor ID (#274)

* refactor: add idlp and add lamport info to snapshot enc

* fix: fix warnings

* fix: idlp err due to incorrect merge

* fix: comments

* test: fix fuzz
This commit is contained in:
Zixuan Chen 2024-02-27 23:36:17 +08:00 committed by GitHub
parent 1f4a59e85c
commit b8eb57f4a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 640 additions and 299 deletions

View file

@ -156,7 +156,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
@ -167,14 +167,14 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
name = "debug-log"
version = "0.2.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b90f9d9c0c144c4aa35a874e362392ec6aef3b9291c882484235069540b26c73"
checksum = "caf861b629ec23fc562cccc8b47cc98a54d192ecf4e7b575ce30659d8c814c3a"
dependencies = [
"once_cell",
]
@ -187,7 +187,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
@ -223,7 +223,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
@ -235,7 +235,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
@ -246,6 +246,7 @@ dependencies = [
"bench-utils",
"debug-log",
"loro",
"serde_json",
"tabled",
]
@ -406,7 +407,7 @@ dependencies = [
[[package]]
name = "loro"
version = "0.2.2"
version = "0.3.0"
dependencies = [
"either",
"enum-as-inner 0.6.0",
@ -415,7 +416,7 @@ dependencies = [
[[package]]
name = "loro-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
@ -429,7 +430,7 @@ dependencies = [
[[package]]
name = "loro-internal"
version = "0.2.2"
version = "0.3.0"
dependencies = [
"append-only-bytes",
"arref",
@ -455,13 +456,12 @@ dependencies = [
"serde_columnar",
"serde_json",
"smallvec",
"string_cache",
"thiserror",
]
[[package]]
name = "loro-preload"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bytes",
"loro-common",
@ -471,7 +471,7 @@ dependencies = [
[[package]]
name = "loro-rle"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"append-only-bytes",
"arref",
@ -695,18 +695,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.71"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
@ -788,9 +788,9 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
@ -817,25 +817,25 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]
name = "serde_json"
version = "1.0.108"
version = "1.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
dependencies = [
"itoa",
"ryu",
@ -915,9 +915,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.43"
version = "2.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53"
checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c"
dependencies = [
"proc-macro2",
"quote",
@ -965,7 +965,7 @@ checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.43",
"syn 2.0.51",
]
[[package]]

View file

@ -3,6 +3,4 @@
use bench_utils::{draw::DrawAction, Action};
use libfuzzer_sys::fuzz_target;
fuzz_target!(|actions: Vec<Action<DrawAction>>| {
examples::draw::run_actions_fuzz_in_async_mode(5, 100, &actions)
});
fuzz_target!(|actions: Vec<Action<DrawAction>>| examples::draw::fuzz(5, 100, &actions));

View file

@ -1,4 +1,4 @@
use crate::{span::IdSpan, CounterSpan};
use crate::{span::IdSpan, CounterSpan, IdFull, IdLp, IdLpSpan, Lamport};
use super::{Counter, LoroError, PeerID, ID};
const UNKNOWN: PeerID = 404;
@ -13,12 +13,24 @@ impl Debug for ID {
}
}
impl Debug for IdLp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(format!("L{}@{}", self.lamport, self.peer).as_str())
}
}
impl Display for ID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(format!("{}@{}", self.counter, self.peer).as_str())
}
}
impl Display for IdLp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(format!("L{}@{}", self.lamport, self.peer).as_str())
}
}
impl TryFrom<&str> for ID {
type Error = LoroError;
@ -151,3 +163,69 @@ impl RangeBounds<ID> for (ID, ID) {
std::ops::Bound::Excluded(&self.1)
}
}
impl IdLp {
pub const NONE_ID: IdLp = IdLp::new(u64::MAX, 0);
#[inline]
pub const fn new(peer: PeerID, lp: Lamport) -> Self {
Self { peer, lamport: lp }
}
pub fn inc(&self, offset: i32) -> IdLp {
IdLp {
peer: self.peer,
lamport: (self.lamport as i32 + offset) as Lamport,
}
}
}
impl From<IdLp> for IdLpSpan {
fn from(value: IdLp) -> Self {
IdLpSpan {
peer: value.peer,
lamport: crate::LamportSpan {
start: value.lamport,
end: value.lamport + 1,
},
}
}
}
impl IdFull {
pub const NONE_ID: IdFull = IdFull {
peer: PeerID::MAX,
lamport: 0,
counter: 0,
};
pub fn new(peer: PeerID, counter: Counter, lamport: Lamport) -> Self {
Self {
peer,
lamport,
counter,
}
}
pub fn inc(&self, offset: i32) -> IdFull {
IdFull {
peer: self.peer,
lamport: (self.lamport as i32 + offset) as Lamport,
counter: self.counter + offset as Counter,
}
}
pub fn id(&self) -> ID {
ID {
peer: self.peer,
counter: self.counter,
}
}
pub fn idlp(&self) -> IdLp {
IdLp {
peer: self.peer,
lamport: self.lamport,
}
}
}

View file

@ -18,17 +18,36 @@ pub use internal_string::InternalString;
pub use span::*;
pub use value::{to_value, LoroValue};
/// Unique id for each peer. It's usually random
/// Unique id for each peer. It's a random u64 by default.
pub type PeerID = u64;
/// If it's the nth Op of a peer, the counter will be n.
pub type Counter = i32;
/// It's the [Lamport clock](https://en.wikipedia.org/wiki/Lamport_timestamp)
pub type Lamport = u32;
/// It's the unique ID of an Op represented by [PeerID] and [Counter].
#[derive(PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ID {
pub peer: PeerID,
pub counter: Counter,
}
/// It's the unique ID of an Op represented by [PeerID] and [Lamport] clock.
/// It's used to define the total order of Ops.
#[derive(PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, PartialOrd, Ord)]
pub struct IdLp {
pub lamport: Lamport,
pub peer: PeerID,
}
/// It's the unique ID of an Op represented by [PeerID], [Lamport] clock and [Counter].
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct IdFull {
pub peer: PeerID,
pub lamport: Lamport,
pub counter: Counter,
}
/// [ContainerID] includes the Op's [ID] and the type. So it's impossible to have
/// the same [ContainerID] with conflict [ContainerType].
///

View file

@ -236,6 +236,36 @@ impl Mergable for CounterSpan {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LamportSpan {
pub start: Lamport,
pub end: Lamport,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct IdLpSpan {
pub peer: PeerID,
pub lamport: LamportSpan,
}
impl HasLength for IdLpSpan {
fn content_len(&self) -> usize {
(self.lamport.end - self.lamport.start) as usize
}
}
impl IdLpSpan {
pub fn new(peer: PeerID, from: Lamport, to: Lamport) -> Self {
Self {
peer,
lamport: LamportSpan {
start: from,
end: to,
},
}
}
}
/// This struct supports reverse repr: [CounterSpan]'s from can be less than to. But we should use it conservatively.
/// We need this because it'll make merging deletions easier.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]

View file

@ -342,7 +342,7 @@ dependencies = [
[[package]]
name = "loro-common"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"arbitrary",
"enum-as-inner 0.6.0",
@ -356,7 +356,7 @@ dependencies = [
[[package]]
name = "loro-internal"
version = "0.2.2"
version = "0.3.0"
dependencies = [
"append-only-bytes",
"arbitrary",
@ -398,7 +398,7 @@ dependencies = [
[[package]]
name = "loro-preload"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bytes",
"loro-common",
@ -408,7 +408,7 @@ dependencies = [
[[package]]
name = "loro-rle"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"append-only-bytes",
"arref",

View file

@ -19,7 +19,7 @@ mod tracker;
use crate::{change::Lamport, delta::StyleMeta, utils::string_slice::StringSlice, InternalString};
use fugue_span::*;
use loro_common::{Counter, LoroValue, PeerID, ID};
use loro_common::{Counter, IdLp, LoroValue, PeerID, ID};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
@ -99,6 +99,10 @@ impl StyleOp {
pub fn id(&self) -> ID {
ID::new(self.peer, self.cnt)
}
pub fn idlp(&self) -> IdLp {
IdLp::new(self.peer, self.lamport)
}
}
impl PartialOrd for StyleOp {

View file

@ -1,7 +1,7 @@
use std::ops::Range;
use generic_btree::rle::{HasLength, Mergeable, Sliceable};
use loro_common::{Counter, HasId, IdSpan, ID};
use loro_common::{Counter, HasId, IdFull, IdSpan, Lamport, ID};
use serde::{Deserialize, Serialize};
use super::AnchorType;
@ -170,7 +170,7 @@ impl Sliceable for RichtextChunk {
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub(super) struct FugueSpan {
pub id: ID,
pub id: IdFull,
/// The status at the current version
pub status: Status,
/// The status at the `new` version.
@ -223,7 +223,7 @@ impl Sliceable for FugueSpan {
origin_left: if range.start == 0 {
self.origin_left
} else {
Some(self.id.inc((range.start - 1) as Counter))
Some(self.id.inc((range.start - 1) as Counter).id())
},
origin_right: self.origin_right,
content: self.content._slice(range),
@ -244,6 +244,7 @@ impl Mergeable for FugueSpan {
&& self.status == rhs.status
&& self.diff_status == rhs.diff_status
&& self.id.counter + self.content.len() as Counter == rhs.id.counter
&& self.id.lamport + self.content.len() as Lamport == rhs.id.lamport
&& rhs.origin_left.is_some()
&& rhs.origin_left.unwrap().peer == self.id.peer
&& rhs.origin_left.unwrap().counter
@ -265,7 +266,7 @@ impl Mergeable for FugueSpan {
impl FugueSpan {
#[allow(unused)]
pub fn new(id: ID, content: RichtextChunk) -> Self {
pub fn new(id: IdFull, content: RichtextChunk) -> Self {
Self {
id,
status: Status::default(),
@ -293,7 +294,7 @@ impl FugueSpan {
impl HasId for FugueSpan {
fn id_start(&self) -> ID {
self.id
self.id.id()
}
}

View file

@ -4,7 +4,7 @@ use generic_btree::{
rle::{HasLength, Mergeable, Sliceable},
BTree, BTreeTrait, Cursor,
};
use loro_common::{IdSpan, LoroValue, ID};
use loro_common::{IdFull, IdLpSpan, Lamport, LoroValue};
use serde::{ser::SerializeStruct, Serialize};
use std::{
fmt::{Display, Formatter},
@ -68,18 +68,18 @@ mod text_chunk {
use std::ops::Range;
use append_only_bytes::BytesSlice;
use loro_common::ID;
use loro_common::{IdFull, IdLp};
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TextChunk {
bytes: BytesSlice,
unicode_len: i32,
utf16_len: i32,
start_op_id: ID,
id: IdFull,
}
impl TextChunk {
pub fn new(bytes: BytesSlice, id: ID) -> Self {
pub fn new(bytes: BytesSlice, id: IdFull) -> Self {
let mut utf16_len = 0;
let mut unicode_len = 0;
for c in std::str::from_utf8(&bytes).unwrap().chars() {
@ -91,35 +91,42 @@ mod text_chunk {
unicode_len,
bytes,
utf16_len: utf16_len as i32,
start_op_id: id,
id,
}
}
pub fn id(&self) -> ID {
self.start_op_id
#[inline]
pub fn idlp(&self) -> IdLp {
IdLp::new(self.id.peer, self.id.lamport)
}
#[inline]
pub fn bytes(&self) -> &BytesSlice {
&self.bytes
}
#[inline]
pub fn as_str(&self) -> &str {
// SAFETY: We know that the text is valid UTF-8
unsafe { std::str::from_utf8_unchecked(&self.bytes) }
}
#[inline]
pub fn len(&self) -> i32 {
self.unicode_len
}
#[inline]
pub fn unicode_len(&self) -> i32 {
self.unicode_len
}
#[inline]
pub fn utf16_len(&self) -> i32 {
self.utf16_len
}
#[inline]
pub fn event_len(&self) -> i32 {
if cfg!(feature = "wasm") {
self.utf16_len
@ -151,7 +158,7 @@ mod text_chunk {
utf16_len: 0,
// This is a dummy value.
// It's fine because the length is 0. We never actually use this value.
start_op_id: ID::NONE_ID,
id: IdFull::NONE_ID,
}
}
@ -199,7 +206,7 @@ mod text_chunk {
}
(true, false) => {
self.bytes.slice_(end_byte..);
self.start_op_id = self.start_op_id.inc(end_unicode_index as i32);
self.id = self.id.inc(end_unicode_index as i32);
None
}
(false, true) => {
@ -208,7 +215,7 @@ mod text_chunk {
}
(false, false) => {
let next = self.bytes.slice_clone(end_byte..);
let next = Self::new(next, self.start_op_id.inc(end_unicode_index as i32));
let next = Self::new(next, self.id.inc(end_unicode_index as i32));
self.unicode_len -= next.unicode_len;
self.utf16_len -= next.utf16_len;
self.bytes.slice_(..start_byte);
@ -307,7 +314,7 @@ mod text_chunk {
unicode_len: range.len() as i32,
bytes: self.bytes.slice_clone(start..end),
utf16_len: utf16_len as i32,
start_op_id: self.start_op_id.inc(range.start as i32),
id: self.id.inc(range.start as i32),
};
ans.check();
ans
@ -328,7 +335,7 @@ mod text_chunk {
unicode_len: self.unicode_len - pos as i32,
bytes: self.bytes.slice_clone(byte_offset..),
utf16_len: self.utf16_len - utf16_len as i32,
start_op_id: self.start_op_id.inc(pos as i32),
id: self.id.inc(pos as i32),
};
self.unicode_len = pos as i32;
@ -342,8 +349,7 @@ mod text_chunk {
impl generic_btree::rle::Mergeable for TextChunk {
fn can_merge(&self, rhs: &Self) -> bool {
self.bytes.can_merge(&rhs.bytes)
&& self.start_op_id.inc(self.unicode_len) == rhs.start_op_id
self.bytes.can_merge(&rhs.bytes) && self.id.inc(self.unicode_len) == rhs.id
}
fn merge_right(&mut self, rhs: &Self) {
@ -359,7 +365,7 @@ mod text_chunk {
self.bytes = new;
self.utf16_len += left.utf16_len;
self.unicode_len += left.unicode_len;
self.start_op_id = left.start_op_id;
self.id = left.id;
self.check();
}
}
@ -376,7 +382,7 @@ pub(crate) enum RichtextStateChunk {
}
impl RichtextStateChunk {
pub fn new_text(s: BytesSlice, id: ID) -> Self {
pub fn new_text(s: BytesSlice, id: IdFull) -> Self {
Self::Text(TextChunk::new(s, id))
}
@ -384,17 +390,17 @@ impl RichtextStateChunk {
Self::Style { style, anchor_type }
}
pub(crate) fn get_id_span(&self) -> loro_common::IdSpan {
pub(crate) fn get_id_lp_span(&self) -> IdLpSpan {
match self {
RichtextStateChunk::Text(t) => {
let id = t.id();
IdSpan::new(id.peer, id.counter, id.counter + t.unicode_len())
let id = t.idlp();
IdLpSpan::new(id.peer, id.lamport, id.lamport + t.unicode_len() as Lamport)
}
RichtextStateChunk::Style { style, anchor_type } => match anchor_type {
AnchorType::Start => style.id().into(),
AnchorType::Start => style.idlp().into(),
AnchorType::End => {
let id = style.id();
IdSpan::new(id.peer, id.counter + 1, id.counter + 2)
let id = style.idlp();
IdLpSpan::new(id.peer, id.lamport + 1, id.lamport + 2)
}
},
}
@ -453,7 +459,7 @@ impl Serialize for RichtextStateChunk {
}
impl RichtextStateChunk {
pub fn try_new(s: BytesSlice, id: ID) -> Result<Self, Utf8Error> {
pub fn try_new(s: BytesSlice, id: IdFull) -> Result<Self, Utf8Error> {
std::str::from_utf8(&s)?;
Ok(RichtextStateChunk::Text(TextChunk::new(s, id)))
}
@ -1238,7 +1244,12 @@ impl RichtextState {
}
/// This is used to accept changes from DiffCalculator
pub(crate) fn insert_at_entity_index(&mut self, entity_index: usize, text: BytesSlice, id: ID) {
pub(crate) fn insert_at_entity_index(
&mut self,
entity_index: usize,
text: BytesSlice,
id: IdFull,
) {
let elem = RichtextStateChunk::try_new(text, id).unwrap();
self.style_ranges
.as_mut()
@ -2291,7 +2302,7 @@ mod test {
let state = &mut self.state;
let text = self.bytes.slice(start..);
let entity_index = state.get_entity_index_for_text_insert(pos, PosType::Unicode);
state.insert_at_entity_index(entity_index, text, ID::new(0, 0));
state.insert_at_entity_index(entity_index, text, IdFull::new(0, 0, 0));
};
}

View file

@ -1,5 +1,5 @@
use generic_btree::{rle::Sliceable, LeafIndex};
use loro_common::{Counter, HasId, HasIdSpan, IdSpan, PeerID, ID};
use loro_common::{Counter, HasId, HasIdSpan, IdFull, IdSpan, PeerID, ID};
use rle::HasLength;
use crate::VersionVector;
@ -41,7 +41,7 @@ impl Tracker {
let result = this.rope.tree.push(FugueSpan {
content: RichtextChunk::new_unknown(u32::MAX / 4),
id: ID::new(UNKNOWN_PEER_ID, 0),
id: IdFull::new(UNKNOWN_PEER_ID, 0, 0),
status: Status::default(),
diff_status: None,
origin_left: None,
@ -69,14 +69,14 @@ impl Tracker {
&self.applied_vv
}
pub(crate) fn insert(&mut self, mut op_id: ID, mut pos: usize, mut content: RichtextChunk) {
pub(crate) fn insert(&mut self, mut op_id: IdFull, mut pos: usize, mut content: RichtextChunk) {
// debug_log::group!("TrackerInsert");
// debug_log::debug_dbg!(&op_id, pos, content);
// debug_log::debug_dbg!(&self);
let last_id = op_id.inc(content.len() as Counter - 1);
let applied_counter_end = self.applied_vv.get(&last_id.peer).copied().unwrap_or(0);
if applied_counter_end > op_id.counter {
if !self.current_vv.includes_id(last_id) {
if !self.current_vv.includes_id(last_id.id()) {
// PERF: may be slow
let mut updates = Default::default();
let cnt_start = self.current_vv.get(&op_id.peer).copied().unwrap_or(0);
@ -93,7 +93,7 @@ impl Tracker {
if applied_counter_end > last_id.counter {
// the op is included in the applied vv
self.current_vv.extend_to_include_last_id(last_id);
self.current_vv.extend_to_include_last_id(last_id.id());
// debug_log::debug_log!("Ops are already included {:#?}", &self);
return;
}
@ -122,15 +122,15 @@ impl Tracker {
|id| self.id_to_cursor.get_insert(id).unwrap(),
);
self.id_to_cursor.push(
op_id,
op_id.id(),
id_to_cursor::Cursor::new_insert(result.leaf, content.len()),
);
self.update_insert_by_split(&result.splitted.arr);
let end_id = op_id.inc(content.len() as Counter);
self.current_vv.extend_to_include_end_id(end_id);
self.applied_vv.extend_to_include_end_id(end_id);
self.current_vv.extend_to_include_end_id(end_id.id());
self.applied_vv.extend_to_include_end_id(end_id.id());
// debug_log::debug_dbg!(&self);
}
@ -381,11 +381,11 @@ mod test {
#[test]
fn test_len() {
let mut t = Tracker::new();
t.insert(ID::new(1, 0), 0, RichtextChunk::new_text(0..2));
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..2));
assert_eq!(t.rope.len(), 2);
t.checkout(&Default::default());
assert_eq!(t.rope.len(), 0);
t.insert(ID::new(2, 0), 0, RichtextChunk::new_text(2..4));
t.insert(IdFull::new(2, 0, 0), 0, RichtextChunk::new_text(2..4));
let v = vv!(1 => 2, 2 => 2);
t.checkout(&v);
assert_eq!(&t.applied_vv, &v);
@ -395,7 +395,7 @@ mod test {
#[test]
fn test_retreat_and_forward_delete() {
let mut t = Tracker::new();
t.insert(ID::new(1, 0), 0, RichtextChunk::new_text(0..10));
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10));
t.delete(ID::new(2, 0), 0, 10, true);
t.checkout(&vv!(1 => 10, 2=>5));
assert_eq!(t.rope.len(), 5);
@ -410,7 +410,7 @@ mod test {
#[test]
fn test_checkout_in_doc_with_del_span() {
let mut t = Tracker::new();
t.insert(ID::new(1, 0), 0, RichtextChunk::new_text(0..10));
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10));
t.delete(ID::new(2, 0), 0, 10, false);
t.checkout(&vv!(1 => 10, 2=>4));
let v: Vec<FugueSpan> = t.rope.tree().iter().copied().collect();
@ -424,7 +424,7 @@ mod test {
#[test]
fn test_checkout_in_doc_with_reversed_del_span() {
let mut t = Tracker::new();
t.insert(ID::new(1, 0), 0, RichtextChunk::new_text(0..10));
t.insert(IdFull::new(1, 0, 0), 0, RichtextChunk::new_text(0..10));
t.delete(ID::new(2, 0), 0, 10, true);
t.checkout(&vv!(1 => 10, 2=>4));
let v: Vec<FugueSpan> = t.rope.tree().iter().copied().collect();

View file

@ -5,7 +5,7 @@ use generic_btree::{
BTree, BTreeTrait, Cursor, FindResult, LeafIndex, Query, SplittedLeaves,
};
use itertools::Itertools;
use loro_common::{Counter, HasCounter, HasCounterSpan, HasIdSpan, IdSpan, ID};
use loro_common::{Counter, HasCounter, HasCounterSpan, HasIdSpan, IdFull, IdSpan, ID};
use smallvec::SmallVec;
use crate::container::richtext::{fugue_span::DiffStatus, FugueSpan, RichtextChunk, Status};
@ -74,7 +74,8 @@ impl CrdtRope {
left_node
.elem()
.id
.inc(left_node.elem().rle_len() as Counter - 1),
.inc(left_node.elem().rle_len() as Counter - 1)
.id(),
)
} else {
None
@ -82,7 +83,7 @@ impl CrdtRope {
} else {
let left_node = self.tree.get_leaf(start.leaf().into());
assert!(left_node.elem().rle_len() >= start.offset());
Some(left_node.elem().id.inc(start.offset() as Counter - 1))
Some(left_node.elem().id.inc(start.offset() as Counter - 1).id())
};
let (origin_right, parent_right_leaf, in_between) = {
@ -97,7 +98,8 @@ impl CrdtRope {
}
if !iter.elem.status.future {
origin_right = Some(iter.elem.id.inc(iter.start.unwrap_or(0) as Counter));
origin_right =
Some(iter.elem.id.inc(iter.start.unwrap_or(0) as Counter).id());
let parent_right = match iter.start {
Some(offset) if offset > 0 => {
// It's guaranteed that origin_right's origin_left == this.origin_left.
@ -177,7 +179,7 @@ impl CrdtRope {
let elem_idx = find_elem(other_origin_right);
let elem = self.tree.get_elem(elem_idx).unwrap();
// It must be the start of the elem
assert_eq!(elem.id, other_origin_right);
assert_eq!(elem.id.id(), other_origin_right);
if elem.origin_left == content.origin_left {
Some(elem_idx)
} else {
@ -408,7 +410,7 @@ impl CrdtRope {
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub(crate) enum CrdtRopeDelta {
Retain(usize),
Insert { chunk: RichtextChunk, id: ID },
Insert { chunk: RichtextChunk, id: IdFull },
Delete(usize),
}
@ -627,7 +629,7 @@ mod test {
fn span(id: u32, range: Range<u32>) -> FugueSpan {
FugueSpan::new(
ID::new(id as PeerID, 0 as Counter),
IdFull::new(id as PeerID, 0 as Counter, 0),
RichtextChunk::new_text(range),
)
}
@ -635,14 +637,14 @@ mod test {
#[allow(unused)]
fn unknown_span(id: u32, len: usize) -> FugueSpan {
FugueSpan::new(
ID::new(id as PeerID, 0 as Counter),
IdFull::new(id as PeerID, 0 as Counter, 0),
RichtextChunk::new_unknown(len as u32),
)
}
fn future_span(id: u32, range: Range<u32>) -> FugueSpan {
let mut fugue = FugueSpan::new(
ID::new(id as PeerID, 0 as Counter),
IdFull::new(id as PeerID, 0 as Counter, 0),
RichtextChunk::new_text(range),
);
@ -652,7 +654,7 @@ mod test {
fn dead_span(id: u32, range: Range<u32>) -> FugueSpan {
let mut span = FugueSpan::new(
ID::new(id as PeerID, 0 as Counter),
IdFull::new(id as PeerID, 0 as Counter, 0),
RichtextChunk::new_text(range),
);
@ -825,7 +827,7 @@ mod test {
CrdtRopeDelta::Retain(2),
CrdtRopeDelta::Insert {
chunk: RichtextChunk::new_text(10..13),
id: ID::new(1, 0)
id: IdFull::new(1, 0, 0)
}
],
vec,
@ -849,7 +851,7 @@ mod test {
assert_eq!(
vec![CrdtRopeDelta::Insert {
chunk: RichtextChunk::new_text(2..10),
id: ID::new(0, 2)
id: IdFull::new(0, 2, 2)
}],
vec,
);

View file

@ -4,16 +4,12 @@ use std::{
};
use fxhash::FxHashMap;
use loro_common::IdLp;
use serde::{ser::SerializeStruct, Serialize};
use crate::{
arena::SharedArena,
change::Lamport,
handler::ValueOrContainer,
id::{Counter, PeerID, ID},
span::{HasId, HasLamport},
txn::Transaction,
DocState, InternalString, LoroValue,
arena::SharedArena, change::Lamport, handler::ValueOrContainer, id::PeerID, span::HasLamport,
txn::Transaction, DocState, InternalString, LoroValue,
};
#[derive(Default, Debug, Clone, Serialize)]
@ -23,7 +19,6 @@ pub struct MapDelta {
#[derive(Debug, Clone)]
pub struct MapValue {
pub counter: Counter,
pub value: Option<LoroValue>,
pub lamp: Lamport,
pub peer: PeerID,
@ -52,8 +47,8 @@ impl PartialEq for MapValue {
impl Eq for MapValue {}
impl MapValue {
pub fn id(&self) -> ID {
ID::new(self.peer, self.counter)
pub fn idlp(&self) -> IdLp {
IdLp::new(self.peer, self.lamp)
}
}
@ -64,9 +59,8 @@ pub struct ResolvedMapDelta {
#[derive(Debug, Clone)]
pub struct ResolvedMapValue {
pub counter: Counter,
pub value: Option<ValueOrContainer>,
pub lamport: (Lamport, PeerID),
pub idlp: IdLp,
}
impl ResolvedMapValue {
@ -77,8 +71,7 @@ impl ResolvedMapValue {
state: &Weak<Mutex<DocState>>,
) -> Self {
ResolvedMapValue {
counter: v.counter,
lamport: (v.lamp, v.peer),
idlp: IdLp::new(v.peer, v.lamp),
value: v
.value
.map(|v| ValueOrContainer::from_value(v, arena, txn, state)),
@ -120,7 +113,7 @@ impl ResolvedMapDelta {
let mut updated = self.updated.clone();
for (k, v) in x.updated.into_iter() {
if let Some(old) = updated.get_mut(&k) {
if v.lamport > old.lamport {
if v.idlp > old.idlp {
*old = v;
}
} else {
@ -147,17 +140,11 @@ impl ResolvedMapDelta {
impl Hash for MapValue {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
// value is not being hashed
self.counter.hash(state);
self.peer.hash(state);
self.lamp.hash(state);
}
}
impl HasId for MapValue {
fn id_start(&self) -> crate::id::ID {
ID::new(self.peer, self.counter)
}
}
impl HasLamport for MapValue {
fn lamport(&self) -> Lamport {
self.lamp
@ -172,7 +159,7 @@ impl Serialize for MapValue {
let mut s = serializer.serialize_struct("MapValue", 2)?;
s.serialize_field("value", &self.value)?;
s.serialize_field("lamport", &self.lamp)?;
s.serialize_field("id", &self.id())?;
s.serialize_field("id", &self.idlp())?;
s.end()
}
}

View file

@ -4,7 +4,7 @@ use std::{
};
use fxhash::{FxHashMap, FxHashSet};
use loro_common::{ContainerType, LoroValue, TreeID, ID};
use loro_common::{ContainerType, IdFull, LoroValue, TreeID};
use serde::Serialize;
use crate::state::TreeParentId;
@ -70,7 +70,7 @@ pub struct TreeDelta {
pub struct TreeDeltaItem {
pub target: TreeID,
pub action: TreeInternalDiff,
pub last_effective_move_op_id: ID,
pub last_effective_move_op_id: IdFull,
}
/// The action of [`TreeDiff`]. It's the same as [`crate::container::tree::tree_op::TreeOp`], but semantic.
@ -97,7 +97,7 @@ impl TreeDeltaItem {
target: TreeID,
parent: TreeParentId,
old_parent: TreeParentId,
op_id: ID,
op_id: IdFull,
is_new_parent_deleted: bool,
is_old_parent_deleted: bool,
) -> Self {

View file

@ -18,7 +18,6 @@ use crate::{
},
delta::{Delta, MapDelta, MapValue},
event::InternalDiff,
id::Counter,
op::{RichOp, SliceRange, SliceRanges},
span::{HasId, HasLamport},
version::Frontiers,
@ -416,14 +415,12 @@ impl DiffCalculatorTrait for MapDiffCalculator {
}
MapValue {
counter: v.counter,
value,
lamp: v.lamport,
peer: v.peer,
}
})
.unwrap_or_else(|| MapValue {
counter: 0,
value: None,
lamp: 0,
peer: 0,
@ -439,7 +436,6 @@ impl DiffCalculatorTrait for MapDiffCalculator {
struct CompactMapValue {
lamport: Lamport,
peer: PeerID,
counter: Counter,
value: Option<LoroValue>,
}
@ -457,12 +453,6 @@ impl PartialOrd for CompactMapValue {
}
}
impl HasId for CompactMapValue {
fn id_start(&self) -> ID {
ID::new(self.peer, self.counter)
}
}
use rle::{HasLength, Sliceable};
#[derive(Default)]
@ -503,7 +493,7 @@ impl DiffCalculatorTrait for ListDiffCalculator {
crate::op::InnerContent::List(l) => match l {
crate::container::list::list_op::InnerListOp::Insert { slice, pos } => {
self.tracker.insert(
op.id_start(),
op.id_full(),
*pos,
RichtextChunk::new_text(slice.0.clone()),
);
@ -603,7 +593,7 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
pos,
} => {
self.tracker.insert(
op.id_start(),
op.id_full(),
*pos as usize,
RichtextChunk::new_text(*unicode_start..*unicode_start + *len),
);
@ -634,12 +624,12 @@ impl DiffCalculatorTrait for RichtextDiffCalculator {
info: *info,
});
self.tracker.insert(
op.id_start(),
op.id_full(),
*start as usize,
RichtextChunk::new_style_anchor(style_id as u32, AnchorType::Start),
);
self.tracker.insert(
op.id_start().inc(1),
op.id_full().inc(1),
// need to shift 1 because we insert the start style anchor before this pos
*end as usize + 1,
RichtextChunk::new_style_anchor(style_id as u32, AnchorType::End),

View file

@ -2,7 +2,7 @@ use std::collections::BTreeSet;
use fxhash::FxHashMap;
use itertools::Itertools;
use loro_common::{ContainerID, HasId, IdSpan, Lamport, TreeID, ID};
use loro_common::{ContainerID, HasId, IdFull, IdSpan, Lamport, TreeID, ID};
use crate::{
container::idx::ContainerIdx,
@ -243,7 +243,7 @@ impl TreeDiffCalculator {
op.target,
op.parent,
old_parent,
op.id,
op.id_full(),
is_parent_deleted,
is_old_parent_deleted,
);
@ -300,6 +300,16 @@ pub struct MoveLamportAndID {
pub(crate) effected: bool,
}
impl MoveLamportAndID {
fn id_full(&self) -> IdFull {
IdFull {
peer: self.id.peer,
lamport: self.lamport,
counter: self.id.counter,
}
}
}
impl PartialOrd for MoveLamportAndID {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
@ -374,12 +384,12 @@ impl TreeCacheForDiff {
}
/// get the parent of the first effected op and its id
fn get_parent_with_id(&self, tree_id: TreeID) -> (TreeParentId, ID) {
let mut ans = (TreeParentId::Unexist, ID::NONE_ID);
fn get_parent_with_id(&self, tree_id: TreeID) -> (TreeParentId, IdFull) {
let mut ans = (TreeParentId::Unexist, IdFull::NONE_ID);
if let Some(cache) = self.tree.get(&tree_id) {
for op in cache.iter().rev() {
if op.effected {
ans = (op.parent, op.id);
ans = (op.parent, op.id_full());
break;
}
}
@ -406,7 +416,7 @@ impl TreeCacheForDiff {
ans
}
fn get_children_with_id(&self, parent: TreeParentId) -> Vec<(TreeID, ID)> {
fn get_children_with_id(&self, parent: TreeParentId) -> Vec<(TreeID, IdFull)> {
let mut ans = vec![];
for (tree_id, _) in self.tree.iter() {
let Some(op) = self.get_last_effective_move(*tree_id) else {
@ -414,7 +424,7 @@ impl TreeCacheForDiff {
};
if op.parent == parent {
ans.push((*tree_id, op.id));
ans.push((*tree_id, op.id_full()));
}
}

View file

@ -3,7 +3,7 @@ mod encode_reordered;
use crate::op::OpWithId;
use crate::LoroDoc;
use crate::{oplog::OpLog, LoroError, VersionVector};
use loro_common::{HasCounter, IdSpan, LoroResult};
use loro_common::{IdLpSpan, LoroResult};
use num_traits::{FromPrimitive, ToPrimitive};
use rle::{HasLength, Sliceable};
const MAGIC_BYTES: [u8; 4] = *b"loro";
@ -86,24 +86,24 @@ pub(crate) struct StateSnapshotEncoder<'a> {
/// The `check_idspan` function is used to check if the id span is valid.
/// If the id span is invalid, the function should return an error that
/// contains the missing id span.
check_idspan: &'a dyn Fn(IdSpan) -> Result<(), IdSpan>,
check_idspan: &'a dyn Fn(IdLpSpan) -> Result<(), IdLpSpan>,
/// The `encoder_by_op` function is used to encode an operation.
encoder_by_op: &'a mut dyn FnMut(OpWithId),
/// The `record_idspan` function is used to record the id span to track the
/// encoded order.
record_idspan: &'a mut dyn FnMut(IdSpan),
record_idspan: &'a mut dyn FnMut(IdLpSpan),
#[allow(unused)]
mode: EncodeMode,
}
impl StateSnapshotEncoder<'_> {
pub fn encode_op(&mut self, id_span: IdSpan, get_op: impl FnOnce() -> OpWithId) {
pub fn encode_op(&mut self, id_span: IdLpSpan, get_op: impl FnOnce() -> OpWithId) {
if let Err(span) = (self.check_idspan)(id_span) {
let mut op = get_op();
if span == id_span {
(self.encoder_by_op)(op);
} else {
debug_assert_eq!(span.ctr_start(), id_span.ctr_start());
debug_assert_eq!(span.lamport.start, id_span.lamport.start);
op.op = op.op.slice(span.atom_len(), op.op.atom_len());
(self.encoder_by_op)(op);
}
@ -121,6 +121,7 @@ impl StateSnapshotEncoder<'_> {
pub(crate) struct StateSnapshotDecodeContext<'a> {
pub oplog: &'a OpLog,
pub ops: &'a mut dyn Iterator<Item = OpWithId>,
#[allow(unused)]
pub blob: &'a [u8],
pub mode: EncodeMode,
}

View file

@ -4,7 +4,7 @@ use fxhash::{FxHashMap, FxHashSet};
use generic_btree::rle::Sliceable;
use itertools::Itertools;
use loro_common::{
ContainerID, ContainerType, Counter, HasCounterSpan, HasId, HasIdSpan, HasLamportSpan, IdSpan,
ContainerID, ContainerType, Counter, HasCounterSpan, HasId, HasIdSpan, HasLamportSpan, IdLp,
InternalString, LoroError, LoroResult, PeerID, ID,
};
use num_traits::FromPrimitive;
@ -13,7 +13,7 @@ use serde_columnar::columnar;
use crate::{
arena::SharedArena,
change::Change,
change::{Change, Lamport},
container::{idx::ContainerIdx, list::list_op::DeleteSpan, richtext::TextStyleInfoFlag},
encoding::{
encode_reordered::value::{ValueKind, ValueWriter},
@ -315,6 +315,7 @@ fn extract_ops(
ops.push(OpWithId {
peer,
op: op.clone(),
lamport: None,
});
}
@ -381,15 +382,16 @@ pub(crate) fn encode_snapshot(oplog: &OpLog, state: &DocState, vv: &VersionVecto
let mut op_len = 0;
let bytes = state.encode_snapshot(super::StateSnapshotEncoder {
check_idspan: &|id_span| {
if let Some(counter) = vv.intersect_span(id_span) {
Err(IdSpan {
client_id: id_span.client_id,
counter,
})
} else {
Ok(())
}
check_idspan: &|_id_span| {
// TODO: todo!("check intersection by vv that defined by idlp");
// if let Some(counter) = vv.intersect_span(id_span) {
// Err(IdSpan {
// client_id: id_span.peer,
// counter,
// })
// } else {
Ok(())
// }
},
encoder_by_op: &mut |op| {
origin_ops.push(TempOp {
@ -398,15 +400,16 @@ pub(crate) fn encode_snapshot(oplog: &OpLog, state: &DocState, vv: &VersionVecto
peer_id: op.peer,
container_index,
prop_that_used_for_sort: -1,
// lamport value is fake, but it's only used for sorting and will not be encoded
lamport: 0,
lamport: op.lamport.unwrap(),
});
},
record_idspan: &mut |id_span| {
let len = id_span.atom_len();
op_len += len;
pos_mapping_heap.push(PosMappingItem {
start_id: id_span.id_start(),
start_id: oplog
.idlp_to_id(IdLp::new(id_span.peer, id_span.lamport.start))
.unwrap(),
len,
target_value: pos_target_value,
});
@ -475,6 +478,12 @@ pub(crate) fn encode_snapshot(oplog: &OpLog, state: &DocState, vv: &VersionVecto
serde_columnar::to_vec(&doc).unwrap()
}
#[derive(Clone, Copy, PartialEq, Debug, Eq, PartialOrd, Ord)]
struct IdWithLamport {
peer: PeerID,
lamport: Lamport,
}
#[derive(Clone, Copy, PartialEq, Debug, Eq)]
struct PosMappingItem {
start_id: ID,
@ -500,7 +509,10 @@ impl PosMappingItem {
let new_len = self.len - pos;
self.len = pos;
PosMappingItem {
start_id: self.start_id.inc(pos as i32),
start_id: ID {
peer: self.start_id.peer,
counter: self.start_id.counter + pos as Counter,
},
len: new_len,
target_value: self.target_value + pos as i32,
}
@ -513,12 +525,14 @@ fn calc_sorted_ops_for_snapshot<'a>(
) -> Vec<TempOp<'a>> {
origin_ops.sort_unstable();
pos_mapping_heap.sort_unstable();
debug_log::debug_dbg!(&origin_ops, &pos_mapping_heap);
let mut ops: Vec<TempOp<'a>> = Vec::with_capacity(origin_ops.len());
let ops_len: usize = origin_ops.iter().map(|x| x.atom_len()).sum();
let mut origin_top = origin_ops.pop();
let mut pos_top = pos_mapping_heap.pop();
while origin_top.is_some() || pos_top.is_some() {
debug_log::debug_dbg!(&origin_top, &pos_top);
let Some(mut inner_origin_top) = origin_top else {
unreachable!()
};
@ -639,7 +653,7 @@ pub(crate) fn decode_snapshot(doc: &LoroDoc, bytes: &[u8]) -> LoroResult<()> {
let ExtractedOps {
ops_map,
ops,
mut ops,
containers,
} = extract_ops(
&iter.raw_values,
@ -651,6 +665,14 @@ pub(crate) fn decode_snapshot(doc: &LoroDoc, bytes: &[u8]) -> LoroResult<()> {
true,
)?;
let changes = decode_changes(iter.changes, iter.start_counters, peer_ids, deps, ops_map)?;
let (new_ids, pending_changes) = import_changes_to_oplog(changes, &mut oplog)?;
for op in ops.iter_mut() {
// update op's lamport
op.lamport = oplog.get_lamport_at(op.id());
}
decode_snapshot_states(
&mut state,
frontiers,
@ -661,8 +683,6 @@ pub(crate) fn decode_snapshot(doc: &LoroDoc, bytes: &[u8]) -> LoroResult<()> {
&oplog,
)
.unwrap();
let changes = decode_changes(iter.changes, iter.start_counters, peer_ids, deps, ops_map)?;
let (new_ids, pending_changes) = import_changes_to_oplog(changes, &mut oplog)?;
assert!(pending_changes.is_empty());
// we cannot assert this because frontiers of oplog is not updated yet when batch_importing
// assert_eq!(&state.frontiers, oplog.frontiers());
@ -820,6 +840,7 @@ mod encode {
self.op.atom_len()
}
}
impl<'a> generic_btree::rle::HasLength for TempOp<'a> {
#[inline(always)]
fn rle_len(&self) -> usize {
@ -827,6 +848,22 @@ mod encode {
}
}
impl TempOp<'_> {
pub fn idlp(&self) -> IdWithLamport {
IdWithLamport {
peer: self.peer_id,
lamport: self.lamport,
}
}
pub fn idlp_end(&self) -> IdWithLamport {
IdWithLamport {
peer: self.peer_id,
lamport: self.lamport + self.op.atom_len() as Lamport,
}
}
}
impl<'a> generic_btree::rle::Sliceable for TempOp<'a> {
fn _slice(&self, range: std::ops::Range<usize>) -> TempOp<'a> {
Self {
@ -916,8 +953,8 @@ mod encode {
msg_len: 0,
});
for (i, op) in change.ops().iter().enumerate() {
let lamport = i as Lamport + change.lamport();
for op in change.ops().iter() {
let lamport = (op.counter - change.id.counter) as Lamport + change.lamport();
push_op(TempOp {
op: Cow::Borrowed(op),
lamport,
@ -936,7 +973,7 @@ mod encode {
use super::{
value::{MarkStart, Value, ValueKind},
EncodedChange, EncodedOp,
EncodedChange, EncodedOp, IdWithLamport,
};
mod value_register {
use fxhash::FxHashMap;

View file

@ -4345,6 +4345,208 @@ mod failed_tests {
)
}
#[test]
fn fuzz_15() {
test_multi_sites(
5,
&mut [
List {
site: 90,
container_idx: 90,
key: 90,
value: I32(1515870810),
},
List {
site: 90,
container_idx: 175,
key: 165,
value: I32(1515890085),
},
List {
site: 90,
container_idx: 90,
key: 131,
value: I32(1520805286),
},
Sync { from: 122, to: 90 },
Sync { from: 165, to: 165 },
Sync { from: 90, to: 90 },
List {
site: 26,
container_idx: 90,
key: 131,
value: I32(1515879083),
},
Sync { from: 165, to: 165 },
List {
site: 90,
container_idx: 90,
key: 90,
value: I32(1509972611),
},
List {
site: 165,
container_idx: 165,
key: 165,
value: I32(1515870810),
},
],
)
}
#[test]
fn fuzz_16() {
test_multi_sites(
5,
&mut [
List {
site: 8,
container_idx: 0,
key: 92,
value: Null,
},
Sync { from: 113, to: 7 },
Map {
site: 0,
container_idx: 0,
key: 0,
value: I32(-1077952577),
},
Map {
site: 191,
container_idx: 191,
key: 191,
value: Container(C::Text),
},
Sync { from: 61, to: 58 },
List {
site: 58,
container_idx: 58,
key: 58,
value: I32(1617542919),
},
List {
site: 191,
container_idx: 191,
key: 191,
value: Container(C::Text),
},
Sync { from: 202, to: 202 },
List {
site: 0,
container_idx: 58,
key: 58,
value: Null,
},
List {
site: 58,
container_idx: 186,
key: 58,
value: Null,
},
Sync { from: 8, to: 92 },
Sync { from: 191, to: 28 },
List {
site: 0,
container_idx: 100,
key: 191,
value: I32(1618020287),
},
List {
site: 191,
container_idx: 191,
key: 191,
value: Container(C::Text),
},
Sync { from: 202, to: 191 },
Sync { from: 191, to: 113 },
List {
site: 191,
container_idx: 191,
key: 191,
value: Container(C::Text),
},
Map {
site: 58,
container_idx: 58,
key: 245,
value: I32(-1077976064),
},
List {
site: 58,
container_idx: 0,
key: 100,
value: Container(C::Map),
},
Map {
site: 100,
container_idx: 191,
key: 191,
value: Null,
},
],
)
}
#[test]
fn fuzz_17() {
test_multi_sites(
5,
&mut [
Text {
site: 3,
container_idx: 0,
pos: 0,
value: 27756,
is_del: false,
},
Text {
site: 3,
container_idx: 0,
pos: 2,
value: 47288,
is_del: false,
},
List {
site: 1,
container_idx: 0,
key: 0,
value: I32(1),
},
Text {
site: 3,
container_idx: 0,
pos: 10,
value: 4,
is_del: true,
},
Text {
site: 0,
container_idx: 0,
pos: 0,
value: 27756,
is_del: false,
},
Sync { from: 3, to: 4 },
Text {
site: 0,
container_idx: 0,
pos: 6,
value: 27756,
is_del: false,
},
Sync { from: 4, to: 0 },
Text {
site: 0,
container_idx: 0,
pos: 13,
value: 15476,
is_del: false,
},
],
)
}
#[test]
fn to_minify() {
minify_error(5, vec![], test_multi_sites, normalize)

View file

@ -6,7 +6,7 @@ use crate::{
};
use crate::{delta::DeltaValue, LoroValue};
use enum_as_inner::EnumAsInner;
use loro_common::IdSpan;
use loro_common::{IdFull, IdSpan};
use rle::{HasIndex, HasLength, Mergable, Sliceable};
use serde::{ser::SerializeSeq, Deserialize, Serialize};
use smallvec::SmallVec;
@ -29,6 +29,7 @@ pub struct Op {
pub(crate) struct OpWithId {
pub peer: PeerID,
pub op: Op,
pub lamport: Option<Lamport>,
}
impl OpWithId {
@ -39,6 +40,14 @@ impl OpWithId {
}
}
pub fn id_full(&self) -> IdFull {
IdFull::new(
self.peer,
self.op.counter,
self.lamport.expect("op should already be imported"),
)
}
#[allow(unused)]
pub fn id_span(&self) -> IdSpan {
IdSpan::new(
@ -66,6 +75,12 @@ pub struct RawOp<'a> {
pub content: RawOpContent<'a>,
}
impl RawOp<'_> {
pub(crate) fn id_full(&self) -> loro_common::IdFull {
IdFull::new(self.id.peer, self.id.counter, self.lamport)
}
}
/// RichOp includes lamport and timestamp info, which is used for conflict resolution.
#[derive(Debug, Clone)]
pub struct RichOp<'a> {
@ -77,15 +92,6 @@ pub struct RichOp<'a> {
pub end: usize,
}
/// RichOp includes lamport and timestamp info, which is used for conflict resolution.
#[derive(Debug, Clone)]
pub struct OwnedRichOp {
pub op: Op,
pub client_id: PeerID,
pub lamport: Lamport,
pub timestamp: Timestamp,
}
impl Op {
#[inline]
#[allow(unused)]
@ -262,15 +268,6 @@ impl<'a> RichOp<'a> {
self.op.slice(self.start, self.end)
}
pub fn as_owned(&self) -> OwnedRichOp {
OwnedRichOp {
op: self.get_sliced(),
client_id: self.peer,
lamport: self.lamport,
timestamp: self.timestamp,
}
}
pub fn op(&self) -> &Op {
self.op
}
@ -298,18 +295,9 @@ impl<'a> RichOp<'a> {
counter: self.op.counter + self.start as Counter,
}
}
}
impl OwnedRichOp {
pub fn rich_op(&self) -> RichOp {
RichOp {
op: &self.op,
peer: self.client_id,
lamport: self.lamport,
timestamp: self.timestamp,
start: 0,
end: self.op.atom_len(),
}
pub(crate) fn id_full(&self) -> IdFull {
IdFull::new(self.peer, self.op.counter, self.lamport)
}
}
@ -461,7 +449,7 @@ impl<'a> Mergable for ListSlice<'a> {
#[derive(Debug, Clone)]
pub struct SliceRanges {
pub ranges: SmallVec<[SliceRange; 2]>,
pub id: ID,
pub id: IdFull,
}
impl Serialize for SliceRanges {
@ -487,6 +475,10 @@ impl DeltaValue for SliceRanges {
return Err(other);
}
if self.id.lamport + self.length() as Lamport != other.id.lamport {
return Err(other);
}
self.ranges.extend(other.ranges);
Ok(())
}

View file

@ -460,6 +460,11 @@ impl OpLog {
self.get_change_at(id).map(|c| c.lamport).unwrap_or(0)
}
pub(crate) fn get_lamport_at(&self, id: ID) -> Option<Lamport> {
self.get_change_at(id)
.map(|c| c.lamport + (id.counter - c.id.counter) as Lamport)
}
pub(crate) fn get_max_lamport_at(&self, id: ID) -> Lamport {
self.get_change_at(id)
.map(|c| {
@ -817,6 +822,34 @@ impl OpLog {
0
}
}
pub(crate) fn idlp_to_id(&self, id: loro_common::IdLp) -> Option<ID> {
if let Some(peer_changes) = self.changes.get(&id.peer) {
let r = peer_changes.binary_search_by(|c| {
if c.lamport > id.lamport {
Ordering::Greater
} else if c.lamport + c.atom_len() as Lamport <= id.lamport {
Ordering::Less
} else {
Ordering::Equal
}
});
match r {
Ok(index) => {
let change = &peer_changes[index];
let counter = (id.lamport - change.lamport) as Counter + change.id.counter;
Some(ID::new(id.peer, counter))
}
Err(_) => {
debug_log::debug_dbg!(id, &peer_changes);
None
}
}
} else {
None
}
}
}
#[derive(Debug)]

View file

@ -22,7 +22,7 @@ use generic_btree::{
rle::{HasLength, Mergeable, Sliceable},
BTree, BTreeTrait, Cursor, LeafIndex, LengthFinder, UseLengthFinder,
};
use loro_common::{IdSpan, LoroResult, ID};
use loro_common::{IdFull, IdLpSpan, LoroResult};
#[derive(Debug)]
pub struct ListState {
@ -44,7 +44,7 @@ impl Clone for ListState {
#[derive(Debug, Clone)]
pub(crate) struct Elem {
pub v: LoroValue,
pub id: ID,
pub id: IdFull,
}
impl HasLength for Elem {
@ -161,7 +161,7 @@ impl ListState {
Some(index as usize)
}
pub fn insert(&mut self, index: usize, value: LoroValue, id: ID) {
pub fn insert(&mut self, index: usize, value: LoroValue, id: IdFull) {
if index > self.len() {
panic!("Index {index} out of range. The length is {}", self.len());
}
@ -231,7 +231,7 @@ impl ListState {
// PERF: use &[LoroValue]
// PERF: batch
pub fn insert_batch(&mut self, index: usize, values: Vec<LoroValue>, start_id: ID) {
pub fn insert_batch(&mut self, index: usize, values: Vec<LoroValue>, start_id: IdFull) {
let mut id = start_id;
for (i, value) in values.into_iter().enumerate() {
self.insert(index + i, value, id);
@ -387,10 +387,10 @@ impl ContainerState for ListState {
crate::container::list::list_op::ListOp::Insert { slice, pos } => match slice {
ListSlice::RawData(list) => match list {
std::borrow::Cow::Borrowed(list) => {
self.insert_batch(*pos, list.to_vec(), op.id);
self.insert_batch(*pos, list.to_vec(), op.id_full());
}
std::borrow::Cow::Owned(list) => {
self.insert_batch(*pos, list.clone(), op.id);
self.insert_batch(*pos, list.clone(), op.id_full());
}
},
_ => unreachable!(),
@ -445,7 +445,7 @@ impl ContainerState for ListState {
#[doc = "Get a list of ops that can be used to restore the state to the current state"]
fn encode_snapshot(&self, mut encoder: StateSnapshotEncoder) -> Vec<u8> {
for elem in self.list.iter() {
let id_span: IdSpan = elem.id.into();
let id_span: IdLpSpan = elem.id.idlp().into();
encoder.encode_op(id_span, || unimplemented!());
}
@ -463,7 +463,7 @@ impl ContainerState for ListState {
.arena
.get_values(value.0.start as usize..value.0.end as usize);
let len = list.len();
self.insert_batch(index, list, op.id());
self.insert_batch(index, list, op.id_full());
index += len;
}
}
@ -482,11 +482,11 @@ mod test {
fn id(name: &str) -> ContainerID {
ContainerID::new_root(name, crate::ContainerType::List)
}
list.insert(0, LoroValue::Container(id("abc")), ID::new(0, 0));
list.insert(0, LoroValue::Container(id("x")), ID::new(0, 0));
list.insert(0, LoroValue::Container(id("abc")), IdFull::new(0, 0, 0));
list.insert(0, LoroValue::Container(id("x")), IdFull::new(0, 0, 0));
assert_eq!(list.get_child_container_index(&id("x")), Some(0));
assert_eq!(list.get_child_container_index(&id("abc")), Some(1));
list.insert(1, LoroValue::Bool(false), ID::new(0, 0));
list.insert(1, LoroValue::Bool(false), IdFull::new(0, 0, 0));
assert_eq!(list.get_child_container_index(&id("x")), Some(0));
assert_eq!(list.get_child_container_index(&id("abc")), Some(2));
}

View file

@ -4,7 +4,7 @@ use std::{
};
use fxhash::FxHashMap;
use loro_common::{ContainerID, LoroResult};
use loro_common::{ContainerID, IdLp, LoroResult};
use rle::HasLength;
use crate::{
@ -16,7 +16,6 @@ use crate::{
handler::ValueOrContainer,
op::{Op, RawOp, RawOpContent},
txn::Transaction,
utils::delta_rle_encoded_num::DeltaRleEncodedNums,
DocState, InternalString, LoroValue,
};
@ -57,8 +56,7 @@ impl ContainerState for MapState {
resolved_delta = resolved_delta.with_entry(
key,
ResolvedMapValue {
counter: value.counter,
lamport: (value.lamp, value.peer),
idlp: IdLp::new(value.peer, value.lamp),
value: value
.value
.map(|v| ValueOrContainer::from_value(v, arena, txn, state)),
@ -88,7 +86,6 @@ impl ContainerState for MapState {
MapValue {
lamp: op.lamport,
peer: op.id.peer,
counter: op.id.counter,
value: None,
},
);
@ -100,7 +97,6 @@ impl ContainerState for MapState {
MapValue {
lamp: op.lamport,
peer: op.id.peer,
counter: op.id.counter,
value: Some(value.clone().unwrap()),
},
);
@ -163,20 +159,16 @@ impl ContainerState for MapState {
#[doc = " Get a list of ops that can be used to restore the state to the current state"]
fn encode_snapshot(&self, mut encoder: StateSnapshotEncoder) -> Vec<u8> {
let mut lamports = DeltaRleEncodedNums::new();
for v in self.map.values() {
lamports.push(v.lamp);
encoder.encode_op(v.id().into(), || unimplemented!());
encoder.encode_op(v.idlp().into(), || unimplemented!());
}
lamports.encode()
Default::default()
}
#[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) {
assert_eq!(ctx.mode, EncodeMode::Snapshot);
let lamports = DeltaRleEncodedNums::decode(ctx.blob);
let mut iter = lamports.iter();
for op in ctx.ops {
debug_assert_eq!(
op.op.atom_len(),
@ -188,9 +180,8 @@ impl ContainerState for MapState {
self.map.insert(
content.key.clone(),
MapValue {
counter: op.op.counter,
value: content.value.clone(),
lamp: iter.next().unwrap(),
lamp: op.lamport.expect("op should already be imported"),
peer: op.peer,
},
);

View file

@ -23,9 +23,7 @@ use crate::{
event::{Diff, Index, InternalDiff},
op::{Op, RawOp},
txn::Transaction,
utils::{
delta_rle_encoded_num::DeltaRleEncodedNums, lazy::LazyLoad, string_slice::StringSlice,
},
utils::{lazy::LazyLoad, string_slice::StringSlice},
DocState,
};
@ -431,7 +429,7 @@ impl ContainerState for RichtextState {
self.state.get_mut().insert_at_entity_index(
*pos as usize,
slice.clone(),
r_op.id,
r_op.id_full(),
);
}
list_op::InnerListOp::Delete(del) => {
@ -527,33 +525,22 @@ impl ContainerState for RichtextState {
}
}
let mut lamports = DeltaRleEncodedNums::new();
for chunk in iter {
match chunk {
RichtextStateChunk::Style { style, anchor_type }
if *anchor_type == AnchorType::Start =>
{
lamports.push(style.lamport);
}
_ => {}
}
let id_span = chunk.get_id_span();
debug_log::debug_dbg!(&chunk);
let id_span = chunk.get_id_lp_span();
encoder.encode_op(id_span, || unimplemented!());
}
lamports.encode()
Default::default()
}
#[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) {
assert_eq!(ctx.mode, EncodeMode::Snapshot);
let lamports = DeltaRleEncodedNums::decode(ctx.blob);
let mut lamport_iter = lamports.iter();
let mut loader = RichtextStateLoader::default();
let mut id_to_style = FxHashMap::default();
for op in ctx.ops {
let id = op.id();
let id = op.id_full();
let chunk = match op.op.content.into_list().unwrap() {
list_op::InnerListOp::InsertText { slice, .. } => {
RichtextStateChunk::new_text(slice.clone(), id)
@ -562,7 +549,7 @@ impl ContainerState for RichtextState {
key, value, info, ..
} => {
let style_op = Arc::new(StyleOp {
lamport: lamport_iter.next().unwrap(),
lamport: op.lamport.expect("op should already be imported"),
peer: op.peer,
cnt: op.op.counter,
key,

View file

@ -1,7 +1,7 @@
use enum_as_inner::EnumAsInner;
use fxhash::FxHashMap;
use itertools::Itertools;
use loro_common::{ContainerID, LoroError, LoroResult, LoroTreeError, LoroValue, TreeID, ID};
use loro_common::{ContainerID, IdFull, LoroError, LoroResult, LoroTreeError, LoroValue, TreeID};
use rle::HasLength;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
@ -68,7 +68,7 @@ pub struct TreeState {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct TreeStateNode {
pub parent: TreeParentId,
pub last_move_op: ID,
pub last_move_op: IdFull,
}
impl TreeState {
@ -79,7 +79,12 @@ impl TreeState {
}
}
pub fn mov(&mut self, target: TreeID, parent: TreeParentId, id: ID) -> Result<(), LoroError> {
pub fn mov(
&mut self,
target: TreeID,
parent: TreeParentId,
id: IdFull,
) -> Result<(), LoroError> {
if parent.is_none() {
// new root node
self.trees.insert(
@ -269,7 +274,7 @@ impl ContainerState for TreeState {
}
None => TreeParentId::None,
};
self.mov(target, parent, raw_op.id)
self.mov(target, parent, raw_op.id_full())
}
_ => unreachable!(),
}
@ -349,10 +354,10 @@ impl ContainerState for TreeState {
#[doc = " Get a list of ops that can be used to restore the state to the current state"]
fn encode_snapshot(&self, mut encoder: StateSnapshotEncoder) -> Vec<u8> {
for node in self.trees.values() {
if node.last_move_op == ID::NONE_ID {
if node.last_move_op == IdFull::NONE_ID {
continue;
}
encoder.encode_op(node.last_move_op.into(), || unimplemented!());
encoder.encode_op(node.last_move_op.idlp().into(), || unimplemented!());
}
Vec::new()
@ -381,7 +386,7 @@ impl ContainerState for TreeState {
target,
TreeStateNode {
parent,
last_move_op: op.id(),
last_move_op: op.id_full(),
},
);
}
@ -511,9 +516,9 @@ mod tests {
0,
loro_common::ContainerType::Tree,
));
state.mov(ID1, TreeParentId::None, ID::NONE_ID).unwrap();
state.mov(ID1, TreeParentId::None, IdFull::NONE_ID).unwrap();
state
.mov(ID2, TreeParentId::Node(ID1), ID::NONE_ID)
.mov(ID2, TreeParentId::Node(ID1), IdFull::NONE_ID)
.unwrap();
}
@ -523,9 +528,9 @@ mod tests {
0,
loro_common::ContainerType::Tree,
));
state.mov(ID1, TreeParentId::None, ID::NONE_ID).unwrap();
state.mov(ID1, TreeParentId::None, IdFull::NONE_ID).unwrap();
state
.mov(ID2, TreeParentId::Node(ID1), ID::NONE_ID)
.mov(ID2, TreeParentId::Node(ID1), IdFull::NONE_ID)
.unwrap();
let roots = Forest::from_tree_state(&state.trees);
let json = serde_json::to_string(&roots).unwrap();
@ -541,17 +546,19 @@ mod tests {
0,
loro_common::ContainerType::Tree,
));
state.mov(ID1, TreeParentId::None, ID::NONE_ID).unwrap();
state.mov(ID1, TreeParentId::None, IdFull::NONE_ID).unwrap();
state
.mov(ID2, TreeParentId::Node(ID1), ID::NONE_ID)
.mov(ID2, TreeParentId::Node(ID1), IdFull::NONE_ID)
.unwrap();
state
.mov(ID3, TreeParentId::Node(ID2), ID::NONE_ID)
.mov(ID3, TreeParentId::Node(ID2), IdFull::NONE_ID)
.unwrap();
state
.mov(ID4, TreeParentId::Node(ID1), ID::NONE_ID)
.mov(ID4, TreeParentId::Node(ID1), IdFull::NONE_ID)
.unwrap();
state
.mov(ID2, TreeParentId::Deleted, IdFull::NONE_ID)
.unwrap();
state.mov(ID2, TreeParentId::Deleted, ID::NONE_ID).unwrap();
let roots = Forest::from_tree_state(&state.trees);
let json = serde_json::to_string(&roots).unwrap();
assert_eq!(

View file

@ -7,7 +7,7 @@ use std::{
use enum_as_inner::EnumAsInner;
use generic_btree::rle::{HasLength as RleHasLength, Mergeable as GBSliceable};
use loro_common::{ContainerType, LoroResult};
use loro_common::{ContainerType, IdLp, LoroResult};
use rle::{HasLength, Mergable, RleVec};
use smallvec::{smallvec, SmallVec};
@ -573,10 +573,9 @@ fn change_to_diff(
diff: Diff::Map(ResolvedMapDelta::new().with_entry(
key,
ResolvedMapValue {
counter: op.counter,
value:
value.map(|v| ValueOrContainer::from_value(v, arena, txn, state)),
lamport: (lamport, peer),
idlp: IdLp::new(peer, lamport),
},
)),
}),

View file

@ -1,37 +0,0 @@
use serde_columnar::columnar;
#[columnar(vec, ser, de)]
#[derive(Debug, Clone)]
struct EncodedNum {
#[columnar(strategy = "DeltaRle")]
num: u32,
}
#[derive(Default)]
#[columnar(ser, de)]
pub struct DeltaRleEncodedNums {
#[columnar(class = "vec")]
nums: Vec<EncodedNum>,
}
impl DeltaRleEncodedNums {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, n: u32) {
self.nums.push(EncodedNum { num: n });
}
pub fn iter(&self) -> impl Iterator<Item = u32> + '_ {
self.nums.iter().map(|n| n.num)
}
pub fn encode(&self) -> Vec<u8> {
serde_columnar::to_vec(&self).unwrap()
}
pub fn decode(encoded: &[u8]) -> Self {
serde_columnar::from_bytes(encoded).unwrap()
}
}

View file

@ -1,4 +1,3 @@
pub(crate) mod delta_rle_encoded_num;
pub(crate) mod lazy;
pub mod string_slice;
pub(crate) mod utf16;