mirror of
https://github.com/loro-dev/loro.git
synced 2024-11-28 01:06:50 +00:00
perf: use a priority-queue-based search for updating text (#544)
* perf: use a priority-queue-based search for updating text It tends to produce diffs with more continuous edits, which is more efficient when we need to apply them to CRDTs * fix: use better text diff calc * refactor: add text update options struct * chore: update text.update comments * chore: fix warnings * fix: rm a dumb optimization
This commit is contained in:
parent
661610165b
commit
4f0d499d4b
16 changed files with 1046 additions and 105 deletions
|
@ -8,6 +8,7 @@ use bench_utils::TextAction;
|
|||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use dev_utils::ByteSize;
|
||||
use loro::LoroDoc;
|
||||
use rand::Rng;
|
||||
|
||||
fn bench_text(c: &mut Criterion) {
|
||||
use bench_utils::TextAction;
|
||||
|
@ -125,6 +126,77 @@ fn bench_text(c: &mut Criterion) {
|
|||
});
|
||||
}
|
||||
|
||||
fn bench_update_text(c: &mut Criterion) {
|
||||
let mut g = c.benchmark_group("update_text");
|
||||
g.bench_function("Update 1024x1024 total different text", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let from = "a".repeat(1024);
|
||||
let to = "b".repeat(1024);
|
||||
(from, to)
|
||||
},
|
||||
|(from, to)| {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(&from, Default::default()).unwrap();
|
||||
text.update(&to, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), &to);
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
g.bench_function("Update 1024x1024 random diff", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
fn rand_str(len: usize, seed: u64) -> String {
|
||||
let mut rng: rand::rngs::StdRng = rand::SeedableRng::seed_from_u64(seed);
|
||||
let mut s = String::new();
|
||||
for _ in 0..len {
|
||||
s.push(rng.gen_range(b'a'..=b'z') as char);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
let from = rand_str(1024, 42);
|
||||
let mut to = from.clone();
|
||||
to.insert_str(0, &rand_str(100, 43));
|
||||
to.replace_range(100..200, &rand_str(100, 44)); // Make some differences
|
||||
to.replace_range(500..504, &rand_str(4, 45));
|
||||
to.replace_range(600..700, &rand_str(5, 46));
|
||||
(from, to)
|
||||
},
|
||||
|(from, to)| {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(&from, Default::default()).unwrap();
|
||||
text.update(&to, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), &to);
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
g.bench_function("Update 1024x1024 text with 16 inserted chars", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let from = "a".repeat(1024);
|
||||
let mut to = from.clone();
|
||||
to.insert_str(504, "b".repeat(16).as_str());
|
||||
(from, to)
|
||||
},
|
||||
|(from, to)| {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(&from, Default::default()).unwrap();
|
||||
text.update(&to, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), &to);
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn apply_text_actions(actions: &[bench_utils::TextAction], n: usize) -> LoroDoc {
|
||||
let loro = LoroDoc::new();
|
||||
let text = loro.get_text("text");
|
||||
|
@ -137,5 +209,5 @@ fn apply_text_actions(actions: &[bench_utils::TextAction], n: usize) -> LoroDoc
|
|||
loro
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_text);
|
||||
criterion_group!(benches, bench_text, bench_update_text);
|
||||
criterion_main!(benches);
|
||||
|
|
162
crates/fuzz/fuzz/Cargo.lock
generated
162
crates/fuzz/fuzz/Cargo.lock
generated
|
@ -1,6 +1,6 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
|
@ -275,13 +275,14 @@ dependencies = [
|
|||
"arbitrary",
|
||||
"bytes",
|
||||
"ensure-cov",
|
||||
"enum-as-inner 0.5.1",
|
||||
"enum-as-inner 0.6.0",
|
||||
"enum_dispatch",
|
||||
"fxhash",
|
||||
"itertools 0.12.1",
|
||||
"loro 0.16.12",
|
||||
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"loro 0.16.2 (git+https://github.com/loro-dev/loro.git?rev=90470658435ec4c62b5af59ebb82fe9e1f5aa761)",
|
||||
"loro 1.0.0-beta.5",
|
||||
"num_cpus",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
|
@ -297,7 +298,7 @@ version = "0.0.0"
|
|||
dependencies = [
|
||||
"fuzz",
|
||||
"libfuzzer-sys",
|
||||
"loro 0.16.12",
|
||||
"loro 1.0.0-beta.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -506,13 +507,13 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
dependencies = [
|
||||
"either",
|
||||
"enum-as-inner 0.6.0",
|
||||
"generic-btree",
|
||||
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"loro-internal 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
@ -532,26 +533,39 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro"
|
||||
version = "0.16.12"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"enum-as-inner 0.6.0",
|
||||
"generic-btree",
|
||||
"loro-common 0.16.12",
|
||||
"loro-delta 0.16.12",
|
||||
"loro-internal 0.16.12",
|
||||
"loro-kv-store",
|
||||
"loro-kv-store 0.16.2",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"enum-as-inner 0.6.0",
|
||||
"generic-btree",
|
||||
"loro-common 1.0.0-beta.5",
|
||||
"loro-delta 1.0.0-beta.5",
|
||||
"loro-internal 1.0.0-beta.5",
|
||||
"loro-kv-store 1.0.0-beta.5",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-common"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"enum-as-inner 0.6.0",
|
||||
"fxhash",
|
||||
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"nonmax",
|
||||
"serde",
|
||||
"serde_columnar",
|
||||
|
@ -578,6 +592,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-common"
|
||||
version = "0.16.12"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"enum-as-inner 0.6.0",
|
||||
|
@ -592,10 +607,27 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-common"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"enum-as-inner 0.6.0",
|
||||
"fxhash",
|
||||
"leb128",
|
||||
"loro-rle 1.0.0-beta.5",
|
||||
"nonmax",
|
||||
"serde",
|
||||
"serde_columnar",
|
||||
"serde_json",
|
||||
"string_cache",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-delta"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"enum-as-inner 0.5.1",
|
||||
|
@ -619,6 +651,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-delta"
|
||||
version = "0.16.12"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"enum-as-inner 0.5.1",
|
||||
|
@ -627,10 +660,20 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-delta"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"enum-as-inner 0.5.1",
|
||||
"generic-btree",
|
||||
"heapless 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-internal"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
|
@ -643,10 +686,10 @@ dependencies = [
|
|||
"im",
|
||||
"itertools 0.12.1",
|
||||
"leb128",
|
||||
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7)",
|
||||
"loro-common 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"loro-delta 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"loro-rle 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"loro_fractional_index 0.16.2 (git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7)",
|
||||
"md5",
|
||||
"num",
|
||||
"num-derive",
|
||||
|
@ -700,13 +743,14 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-internal"
|
||||
version = "0.16.12"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
"bytes",
|
||||
"either",
|
||||
"ensure-cov",
|
||||
"enum-as-inner 0.5.1",
|
||||
"enum-as-inner 0.6.0",
|
||||
"enum_dispatch",
|
||||
"fxhash",
|
||||
"generic-btree",
|
||||
|
@ -716,7 +760,7 @@ dependencies = [
|
|||
"leb128",
|
||||
"loro-common 0.16.12",
|
||||
"loro-delta 0.16.12",
|
||||
"loro-kv-store",
|
||||
"loro-kv-store 0.16.2",
|
||||
"loro-rle 0.16.12",
|
||||
"loro_fractional_index 0.16.12",
|
||||
"md5",
|
||||
|
@ -737,9 +781,49 @@ dependencies = [
|
|||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-internal"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
"bytes",
|
||||
"either",
|
||||
"ensure-cov",
|
||||
"enum-as-inner 0.6.0",
|
||||
"enum_dispatch",
|
||||
"fxhash",
|
||||
"generic-btree",
|
||||
"getrandom",
|
||||
"im",
|
||||
"itertools 0.12.1",
|
||||
"leb128",
|
||||
"loro-common 1.0.0-beta.5",
|
||||
"loro-delta 1.0.0-beta.5",
|
||||
"loro-kv-store 1.0.0-beta.5",
|
||||
"loro-rle 1.0.0-beta.5",
|
||||
"loro_fractional_index 1.0.0-beta.5",
|
||||
"md5",
|
||||
"nonmax",
|
||||
"num",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"postcard",
|
||||
"pretty_assertions",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_columnar",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-kv-store"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"ensure-cov",
|
||||
|
@ -748,13 +832,29 @@ dependencies = [
|
|||
"lz4_flex",
|
||||
"once_cell",
|
||||
"quick_cache",
|
||||
"tracing",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-kv-store"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"ensure-cov",
|
||||
"fxhash",
|
||||
"loro-common 1.0.0-beta.5",
|
||||
"lz4_flex",
|
||||
"once_cell",
|
||||
"quick_cache",
|
||||
"tracing",
|
||||
"xxhash-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-rle"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
|
@ -780,6 +880,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro-rle"
|
||||
version = "0.16.12"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"arref",
|
||||
|
@ -789,6 +890,15 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-rle"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"append-only-bytes",
|
||||
"num",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro-thunderdome"
|
||||
version = "0.6.2"
|
||||
|
@ -798,7 +908,7 @@ checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a"
|
|||
[[package]]
|
||||
name = "loro_fractional_index"
|
||||
version = "0.16.2"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt@0.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%400.16.7#d2b0520f8633f96146a49ec205bd5e7056880f1a"
|
||||
dependencies = [
|
||||
"imbl",
|
||||
"rand",
|
||||
|
@ -820,6 +930,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "loro_fractional_index"
|
||||
version = "0.16.12"
|
||||
source = "git+https://github.com/loro-dev/loro.git?tag=loro-crdt%401.0.0-alpha.4#9bfe97bce4912c6dc8439817497d18423a0e8cb7"
|
||||
dependencies = [
|
||||
"imbl",
|
||||
"once_cell",
|
||||
|
@ -828,6 +939,15 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loro_fractional_index"
|
||||
version = "1.0.0-beta.5"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"rand",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lz4_flex"
|
||||
version = "0.11.3"
|
||||
|
|
|
@ -7,9 +7,9 @@ fuzz_target!(|data: [&str; 3]| {
|
|||
let (old, new, new1) = (data[0], data[1], data[2]);
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(old);
|
||||
text.update(new);
|
||||
text.update(old, Default::default()).unwrap();
|
||||
text.update(new, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new);
|
||||
text.update_by_line(new1);
|
||||
text.update_by_line(new1, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new1);
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ use loro::LoroDoc;
|
|||
fn update_text() {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update("ϼCCC");
|
||||
text.update("2");
|
||||
text.update("ϼCCC", Default::default()).unwrap();
|
||||
text.update("2", Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), "2");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{fmt::Display, sync::Arc};
|
||||
|
||||
use loro::{cursor::Side, LoroResult, TextDelta};
|
||||
use loro::{cursor::Side, LoroResult, TextDelta, UpdateOptions, UpdateTimeoutError};
|
||||
|
||||
use crate::{ContainerID, LoroValue, LoroValueLike};
|
||||
|
||||
|
@ -101,8 +101,8 @@ impl LoroText {
|
|||
}
|
||||
|
||||
/// Update the current text based on the provided text.
|
||||
pub fn update(&self, text: &str) {
|
||||
self.text.update(text);
|
||||
pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
|
||||
self.text.update(text, options)
|
||||
}
|
||||
|
||||
/// Apply a [delta](https://quilljs.com/docs/delta/) to the text container.
|
||||
|
|
|
@ -2,7 +2,7 @@ use fxhash::FxHashMap;
|
|||
use loro_common::{LoroValue, PeerID};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::change::get_sys_timestamp;
|
||||
use crate::change::{get_sys_timestamp, Timestamp};
|
||||
|
||||
/// `Awareness` is a structure that tracks the ephemeral state of peers.
|
||||
///
|
||||
|
@ -43,7 +43,7 @@ impl Awareness {
|
|||
|
||||
pub fn encode(&self, peers: &[PeerID]) -> Vec<u8> {
|
||||
let mut peers_info = Vec::new();
|
||||
let now = get_sys_timestamp();
|
||||
let now = get_sys_timestamp() as Timestamp;
|
||||
for peer in peers {
|
||||
if let Some(peer_info) = self.peers.get(peer) {
|
||||
if now - peer_info.timestamp > self.timeout {
|
||||
|
@ -64,7 +64,7 @@ impl Awareness {
|
|||
|
||||
pub fn encode_all(&self) -> Vec<u8> {
|
||||
let mut peers_info = Vec::new();
|
||||
let now = get_sys_timestamp();
|
||||
let now = get_sys_timestamp() as Timestamp;
|
||||
for (peer, peer_info) in self.peers.iter() {
|
||||
if now - peer_info.timestamp > self.timeout {
|
||||
continue;
|
||||
|
@ -86,7 +86,7 @@ impl Awareness {
|
|||
let peers_info: Vec<EncodedPeerInfo> = postcard::from_bytes(encoded_peers_info).unwrap();
|
||||
let mut changed_peers = Vec::new();
|
||||
let mut added_peers = Vec::new();
|
||||
let now = get_sys_timestamp();
|
||||
let now = get_sys_timestamp() as Timestamp;
|
||||
for peer_info in peers_info {
|
||||
match self.peers.get(&peer_info.peer) {
|
||||
Some(x) if x.counter >= peer_info.counter || peer_info.peer == self.peer => {
|
||||
|
@ -126,7 +126,7 @@ impl Awareness {
|
|||
|
||||
peer.state = value;
|
||||
peer.counter += 1;
|
||||
peer.timestamp = get_sys_timestamp();
|
||||
peer.timestamp = get_sys_timestamp() as Timestamp;
|
||||
}
|
||||
|
||||
pub fn get_local_state(&self) -> Option<LoroValue> {
|
||||
|
@ -134,7 +134,7 @@ impl Awareness {
|
|||
}
|
||||
|
||||
pub fn remove_outdated(&mut self) -> Vec<PeerID> {
|
||||
let now = get_sys_timestamp();
|
||||
let now = get_sys_timestamp() as Timestamp;
|
||||
let mut removed = Vec::new();
|
||||
self.peers.retain(|id, v| {
|
||||
if now - v.timestamp > self.timeout {
|
||||
|
|
|
@ -263,7 +263,7 @@ impl Change {
|
|||
/// [Unix time](https://en.wikipedia.org/wiki/Unix_time)
|
||||
/// It is the number of milliseconds that have elapsed since 00:00:00 UTC on 1 January 1970.
|
||||
#[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
|
||||
pub(crate) fn get_sys_timestamp() -> Timestamp {
|
||||
pub(crate) fn get_sys_timestamp() -> f64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
@ -275,7 +275,7 @@ pub(crate) fn get_sys_timestamp() -> Timestamp {
|
|||
/// [Unix time](https://en.wikipedia.org/wiki/Unix_time)
|
||||
/// It is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970.
|
||||
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
|
||||
pub fn get_sys_timestamp() -> Timestamp {
|
||||
pub fn get_sys_timestamp() -> f64 {
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
|
@ -285,7 +285,7 @@ pub fn get_sys_timestamp() -> Timestamp {
|
|||
pub fn now() -> f64;
|
||||
}
|
||||
|
||||
now() as Timestamp
|
||||
now()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -14,9 +14,38 @@
|
|||
//!
|
||||
//! The implementation of this algorithm is based on the implementation by
|
||||
//! Brandon Williams.
|
||||
use crate::change::get_sys_timestamp;
|
||||
use fxhash::FxHashMap;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::iter::zip;
|
||||
use std::ops::{Index, IndexMut};
|
||||
|
||||
/// Options for controlling the text update behavior.
|
||||
///
|
||||
/// - `timeout_ms`: Optional timeout in milliseconds for the diff computation
|
||||
/// - `use_refined_diff`: Whether to use a more refined but slower diff algorithm. Defaults to true.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct UpdateOptions {
|
||||
pub timeout_ms: Option<f64>,
|
||||
pub use_refined_diff: bool,
|
||||
}
|
||||
|
||||
impl Default for UpdateOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout_ms: None,
|
||||
use_refined_diff: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, PartialEq)]
|
||||
pub enum UpdateTimeoutError {
|
||||
#[error("Timeout")]
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Utility function to check if a range is empty that works on older rust versions
|
||||
#[inline(always)]
|
||||
fn is_empty_range(start: usize, end: usize) -> bool {
|
||||
|
@ -79,14 +108,33 @@ impl<D: DiffHandler> OperateProxy<D> {
|
|||
pub fn insert(&mut self, old_index: usize, new_index: usize, new_len: usize) {
|
||||
self.handler.insert(old_index, new_index, new_len);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn unwrap(self) -> D {
|
||||
self.handler
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn myers_diff<D: DiffHandler>(proxy: &mut OperateProxy<D>, old: &[u32], new: &[u32]) {
|
||||
pub(crate) fn diff<D: DiffHandler>(
|
||||
proxy: &mut OperateProxy<D>,
|
||||
options: UpdateOptions,
|
||||
old: &[u32],
|
||||
new: &[u32],
|
||||
) -> Result<(), UpdateTimeoutError> {
|
||||
let max_d = (old.len() + new.len() + 1) / 2 + 1;
|
||||
let mut vb = OffsetVec::new(max_d);
|
||||
let mut vf = OffsetVec::new(max_d);
|
||||
let start_time = if options.timeout_ms.is_some() {
|
||||
get_sys_timestamp()
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
conquer(
|
||||
proxy,
|
||||
options.use_refined_diff,
|
||||
options.timeout_ms,
|
||||
start_time,
|
||||
old,
|
||||
0,
|
||||
old.len(),
|
||||
|
@ -95,7 +143,7 @@ pub(crate) fn myers_diff<D: DiffHandler>(proxy: &mut OperateProxy<D>, old: &[u32
|
|||
new.len(),
|
||||
&mut vf,
|
||||
&mut vb,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
struct OffsetVec(isize, Vec<usize>);
|
||||
|
@ -123,8 +171,17 @@ impl IndexMut<isize> for OffsetVec {
|
|||
}
|
||||
}
|
||||
|
||||
struct MiddleSnakeResult {
|
||||
#[allow(unused)]
|
||||
d: usize,
|
||||
x_start: usize,
|
||||
y_start: usize,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn find_middle_snake(
|
||||
timeout_ms: Option<f64>,
|
||||
start_time: f64,
|
||||
old: &[u32],
|
||||
old_start: usize,
|
||||
old_end: usize,
|
||||
|
@ -133,7 +190,7 @@ fn find_middle_snake(
|
|||
new_end: usize,
|
||||
vf: &mut OffsetVec,
|
||||
vb: &mut OffsetVec,
|
||||
) -> Option<(usize, usize)> {
|
||||
) -> Result<Option<MiddleSnakeResult>, UpdateTimeoutError> {
|
||||
let n = old_end - old_start;
|
||||
let m = new_end - new_start;
|
||||
let delta = n as isize - m as isize;
|
||||
|
@ -160,9 +217,20 @@ fn find_middle_snake(
|
|||
}
|
||||
vf[k] = x;
|
||||
if odd && (k - delta).abs() <= (d - 1) && vf[k] + vb[delta - k] >= n {
|
||||
return Some((x0 + old_start, y0 + new_start));
|
||||
return Ok(Some(MiddleSnakeResult {
|
||||
d: d as usize,
|
||||
x_start: x0 + old_start,
|
||||
y_start: y0 + new_start,
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(timeout_ms) = timeout_ms {
|
||||
if get_sys_timestamp() - start_time > timeout_ms {
|
||||
return Err(UpdateTimeoutError::Timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k in (-d..=d).rev().step_by(2) {
|
||||
let mut x = if k == -d || (k != d && vb[k - 1] < vb[k + 1]) {
|
||||
vb[k + 1]
|
||||
|
@ -180,16 +248,30 @@ fn find_middle_snake(
|
|||
}
|
||||
vb[k] = x;
|
||||
if !odd && (k - delta).abs() <= d && vb[k] + vf[delta - k] >= n {
|
||||
return Some((n - x + old_start, m - y + new_start));
|
||||
return Ok(Some(MiddleSnakeResult {
|
||||
d: d as usize,
|
||||
x_start: n - x + old_start,
|
||||
y_start: m - y + new_start,
|
||||
}));
|
||||
}
|
||||
|
||||
if let Some(timeout_ms) = timeout_ms {
|
||||
if get_sys_timestamp() - start_time > timeout_ms {
|
||||
return Err(UpdateTimeoutError::Timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn conquer<D: DiffHandler>(
|
||||
proxy: &mut OperateProxy<D>,
|
||||
should_use_dj: bool,
|
||||
timeout_ms: Option<f64>,
|
||||
start_time: f64,
|
||||
old: &[u32],
|
||||
mut old_start: usize,
|
||||
mut old_end: usize,
|
||||
|
@ -198,7 +280,7 @@ fn conquer<D: DiffHandler>(
|
|||
mut new_end: usize,
|
||||
vf: &mut OffsetVec,
|
||||
vb: &mut OffsetVec,
|
||||
) {
|
||||
) -> Result<(), UpdateTimeoutError> {
|
||||
let common_prefix_len = common_prefix(&old[old_start..old_end], &new[new_start..new_end]);
|
||||
if common_prefix_len > 0 {
|
||||
old_start += common_prefix_len;
|
||||
|
@ -208,21 +290,566 @@ fn conquer<D: DiffHandler>(
|
|||
let common_suffix_len = common_suffix_len(&old[old_start..old_end], &new[new_start..new_end]);
|
||||
old_end -= common_suffix_len;
|
||||
new_end -= common_suffix_len;
|
||||
|
||||
if is_not_empty_range(old_start, old_end) || is_not_empty_range(new_start, new_end) {
|
||||
let len_old = old_end - old_start;
|
||||
let len_new = new_end - new_start;
|
||||
if should_use_dj && (len_old * len_new < 128 * 128) {
|
||||
let ok = dj_diff(
|
||||
proxy,
|
||||
&old[old_start..old_end],
|
||||
&new[new_start..new_end],
|
||||
old_start,
|
||||
new_start,
|
||||
10_000,
|
||||
);
|
||||
if ok {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if is_empty_range(new_start, new_end) {
|
||||
proxy.delete(old_start, old_end - old_start);
|
||||
} else if is_empty_range(old_start, old_end) {
|
||||
proxy.insert(old_start, new_start, new_end - new_start);
|
||||
} else if let Some((x_start, y_start)) =
|
||||
find_middle_snake(old, old_start, old_end, new, new_start, new_end, vf, vb)
|
||||
{
|
||||
} else if let Some(MiddleSnakeResult {
|
||||
d: _,
|
||||
x_start,
|
||||
y_start,
|
||||
}) = find_middle_snake(
|
||||
timeout_ms, start_time, old, old_start, old_end, new, new_start, new_end, vf, vb,
|
||||
)? {
|
||||
conquer(
|
||||
proxy, old, old_start, x_start, new, new_start, y_start, vf, vb,
|
||||
);
|
||||
conquer(proxy, old, x_start, old_end, new, y_start, new_end, vf, vb);
|
||||
proxy,
|
||||
should_use_dj,
|
||||
timeout_ms,
|
||||
start_time,
|
||||
old,
|
||||
old_start,
|
||||
x_start,
|
||||
new,
|
||||
new_start,
|
||||
y_start,
|
||||
vf,
|
||||
vb,
|
||||
)?;
|
||||
conquer(
|
||||
proxy,
|
||||
should_use_dj,
|
||||
timeout_ms,
|
||||
start_time,
|
||||
old,
|
||||
x_start,
|
||||
old_end,
|
||||
new,
|
||||
y_start,
|
||||
new_end,
|
||||
vf,
|
||||
vb,
|
||||
)?;
|
||||
} else {
|
||||
proxy.delete(old_start, old_end - old_start);
|
||||
proxy.insert(old_start, new_start, new_end - new_start);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return false if this method gives up early
|
||||
#[must_use]
|
||||
pub(crate) fn dj_diff<D: DiffHandler>(
|
||||
proxy: &mut OperateProxy<D>,
|
||||
old: &[u32],
|
||||
new: &[u32],
|
||||
old_offset: usize,
|
||||
new_offset: usize,
|
||||
max_try_count: usize,
|
||||
) -> bool {
|
||||
let common_prefix_len = common_prefix(old, new);
|
||||
let common_suffix_len = common_suffix_len(&old[common_prefix_len..], &new[common_prefix_len..]);
|
||||
let old = &old[common_prefix_len..old.len() - common_suffix_len];
|
||||
let new = &new[common_prefix_len..new.len() - common_suffix_len];
|
||||
assert!(old.len() <= u16::MAX as usize);
|
||||
assert!(new.len() <= u16::MAX as usize);
|
||||
if old.is_empty() {
|
||||
if new.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
proxy.insert(
|
||||
old_offset + common_prefix_len,
|
||||
new_offset + common_prefix_len,
|
||||
new.len(),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if new.is_empty() {
|
||||
proxy.delete(old_offset + common_prefix_len, old.len());
|
||||
return true;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
struct Point {
|
||||
x: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Direction {
|
||||
Up,
|
||||
Left,
|
||||
UpLeft,
|
||||
}
|
||||
|
||||
struct QueueItem {
|
||||
point: Point,
|
||||
cost: u32,
|
||||
from: Direction,
|
||||
}
|
||||
|
||||
impl PartialEq for QueueItem {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cost == other.cost
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for QueueItem {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for QueueItem {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
other.cost.cmp(&self.cost)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for QueueItem {}
|
||||
|
||||
let mut visited: FxHashMap<Point, Direction> = FxHashMap::default();
|
||||
let mut q: BinaryHeap<QueueItem> = BinaryHeap::new();
|
||||
q.push(QueueItem {
|
||||
point: Point { x: 0, y: 0 },
|
||||
cost: 0,
|
||||
from: Direction::UpLeft,
|
||||
});
|
||||
|
||||
while let Some(QueueItem { point, cost, from }) = q.pop() {
|
||||
if visited.contains_key(&point) {
|
||||
continue;
|
||||
}
|
||||
|
||||
visited.insert(point, from);
|
||||
if point.x == old.len() as u16 && point.y == new.len() as u16 {
|
||||
break;
|
||||
}
|
||||
|
||||
if visited.len() + q.len() > max_try_count {
|
||||
// println!("give up on: visited len: {}", visited.len());
|
||||
// println!("queue len: {}", q.len());
|
||||
return false;
|
||||
}
|
||||
|
||||
if point.x < old.len() as u16 {
|
||||
let next_point = Point {
|
||||
x: point.x + 1,
|
||||
y: point.y,
|
||||
};
|
||||
|
||||
if !visited.contains_key(&next_point) {
|
||||
let mut next_cost = cost + 1;
|
||||
if from != Direction::Left {
|
||||
next_cost += 8;
|
||||
}
|
||||
|
||||
q.push(QueueItem {
|
||||
point: next_point,
|
||||
cost: next_cost,
|
||||
from: Direction::Left,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if point.y < new.len() as u16 {
|
||||
let next_point = Point {
|
||||
x: point.x,
|
||||
y: point.y + 1,
|
||||
};
|
||||
|
||||
if !visited.contains_key(&next_point) {
|
||||
let direction = Direction::Up;
|
||||
let mut next_cost = cost + 1;
|
||||
if from != direction {
|
||||
next_cost += 8;
|
||||
}
|
||||
|
||||
q.push(QueueItem {
|
||||
point: next_point,
|
||||
cost: next_cost,
|
||||
from: Direction::Up,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if point.x < old.len() as u16
|
||||
&& point.y < new.len() as u16
|
||||
&& old[point.x as usize] == new[point.y as usize]
|
||||
{
|
||||
let next_point = Point {
|
||||
x: point.x + 1,
|
||||
y: point.y + 1,
|
||||
};
|
||||
|
||||
if !visited.contains_key(&next_point) {
|
||||
let next_cost = if from == Direction::UpLeft {
|
||||
cost
|
||||
} else {
|
||||
cost + 1
|
||||
};
|
||||
|
||||
q.push(QueueItem {
|
||||
point: next_point,
|
||||
cost: next_cost,
|
||||
from: Direction::UpLeft,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// println!("visited len: {}", visited.len());
|
||||
// println!("queue len: {}", q.len());
|
||||
|
||||
// Backtrack from end point to construct diff operations
|
||||
let mut current = Point {
|
||||
x: old.len() as u16,
|
||||
y: new.len() as u16,
|
||||
};
|
||||
|
||||
let mut path: Vec<(Direction, usize)> = Vec::new();
|
||||
while current.x > 0 || current.y > 0 {
|
||||
let direction = visited.get(¤t).unwrap();
|
||||
if let Some((last_dir, count)) = path.last_mut() {
|
||||
if last_dir == direction {
|
||||
*count += 1;
|
||||
} else {
|
||||
path.push((*direction, 1));
|
||||
}
|
||||
} else {
|
||||
path.push((*direction, 1));
|
||||
}
|
||||
match direction {
|
||||
Direction::Left => {
|
||||
current.x -= 1;
|
||||
}
|
||||
Direction::Up => {
|
||||
current.y -= 1;
|
||||
}
|
||||
Direction::UpLeft => {
|
||||
current.x -= 1;
|
||||
current.y -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path.reverse();
|
||||
|
||||
let mut old_index = common_prefix_len;
|
||||
let mut new_index = common_prefix_len;
|
||||
|
||||
for (direction, count) in path {
|
||||
match direction {
|
||||
Direction::Left => {
|
||||
proxy.delete(old_offset + old_index, count);
|
||||
old_index += count;
|
||||
}
|
||||
Direction::Up => {
|
||||
proxy.insert(old_offset + old_index, new_index + new_offset, count);
|
||||
new_index += count;
|
||||
}
|
||||
Direction::UpLeft => {
|
||||
old_index += count;
|
||||
new_index += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RecordingDiffHandler {
|
||||
ops: Vec<DiffOperation>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DiffOperation {
|
||||
Insert {
|
||||
old_index: usize,
|
||||
new_index: usize,
|
||||
length: usize,
|
||||
},
|
||||
Delete {
|
||||
old_index: usize,
|
||||
length: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl DiffHandler for RecordingDiffHandler {
|
||||
fn insert(&mut self, pos: usize, start: usize, len: usize) {
|
||||
self.ops.push(DiffOperation::Insert {
|
||||
old_index: pos,
|
||||
new_index: start,
|
||||
length: len,
|
||||
});
|
||||
}
|
||||
|
||||
fn delete(&mut self, pos: usize, len: usize) {
|
||||
self.ops.push(DiffOperation::Delete {
|
||||
old_index: pos,
|
||||
length: len,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recording_diff_handler() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1, 2, 3];
|
||||
let new = vec![1, 4, 3];
|
||||
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Delete {
|
||||
old_index: 1,
|
||||
length: 1
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 2,
|
||||
new_index: 1,
|
||||
length: 1
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dj_diff_same() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1, 2, 3];
|
||||
let new = vec![1, 2, 3];
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(handler.ops, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dj_diff_1() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1];
|
||||
let new = vec![0, 1, 2];
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Insert {
|
||||
old_index: 0,
|
||||
new_index: 0,
|
||||
length: 1
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 1,
|
||||
new_index: 2,
|
||||
length: 1
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff_may_scatter() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1, 2, 3, 4, 5];
|
||||
let new = vec![99, 1, 2, 3, 4, 5, 98, 97, 96, 3, 95, 4, 93, 92, 5, 91];
|
||||
diff(&mut proxy, UpdateOptions::default(), &old, &new).unwrap();
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Insert {
|
||||
old_index: 0,
|
||||
new_index: 0,
|
||||
length: 1
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 5,
|
||||
new_index: 6,
|
||||
length: 10
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff_may_scatter_1() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1, 2, 3, 4, 5];
|
||||
let new = vec![99, 1, 2, 98, 97, 96, 3, 95, 4, 93, 92, 5, 1, 2, 3, 4, 5, 91];
|
||||
diff(&mut proxy, UpdateOptions::default(), &old, &new).unwrap();
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Insert {
|
||||
old_index: 0,
|
||||
new_index: 0,
|
||||
length: 12
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 5,
|
||||
new_index: 17,
|
||||
length: 1
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dj_diff_may_scatter() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1, 2, 3, 4, 5];
|
||||
let new = vec![99, 1, 2, 3, 4, 5, 98, 97, 96, 3, 95, 4, 93, 92, 5, 91];
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Insert {
|
||||
old_index: 0,
|
||||
new_index: 0,
|
||||
length: 1
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 5,
|
||||
new_index: 6,
|
||||
length: 10
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dj_diff_may_scatter_1() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1, 2, 3, 4, 5];
|
||||
let new = vec![99, 1, 2, 98, 97, 96, 3, 95, 4, 93, 92, 5, 1, 2, 3, 4, 5, 91];
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Insert {
|
||||
old_index: 0,
|
||||
new_index: 0,
|
||||
length: 12
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 5,
|
||||
new_index: 17,
|
||||
length: 1
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dj_diff_100_differences() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1; 100];
|
||||
let new = vec![2; 100];
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Delete {
|
||||
old_index: 0,
|
||||
length: 100
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 100,
|
||||
new_index: 0,
|
||||
length: 100
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dj_diff_insert() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1; 100];
|
||||
let mut new = old.clone();
|
||||
new.splice(50..50, [1, 2, 3, 4, 1, 2, 3, 4]);
|
||||
new.splice(0..0, [0, 1, 2, 3, 4, 5, 6, 7]);
|
||||
let _ = dj_diff(&mut proxy, &old, &new, 0, 0, 100_000);
|
||||
let handler = proxy.unwrap();
|
||||
assert_eq!(
|
||||
handler.ops,
|
||||
vec![
|
||||
DiffOperation::Insert {
|
||||
old_index: 0,
|
||||
new_index: 0,
|
||||
length: 9
|
||||
},
|
||||
DiffOperation::Insert {
|
||||
old_index: 50,
|
||||
new_index: 59,
|
||||
length: 7
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1; 100];
|
||||
let new = vec![2; 100];
|
||||
diff(&mut proxy, Default::default(), &old, &new).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeout() {
|
||||
let handler = RecordingDiffHandler::default();
|
||||
let mut proxy = OperateProxy::new(handler);
|
||||
let old = vec![1; 10000];
|
||||
let new = vec![2; 10000];
|
||||
let options = UpdateOptions {
|
||||
timeout_ms: Some(0.1),
|
||||
..Default::default()
|
||||
};
|
||||
let result = diff(&mut proxy, options, &old, &new);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub mod diff_impl;
|
||||
pub(crate) use diff_impl::myers_diff;
|
||||
pub(crate) use diff_impl::diff;
|
||||
pub(crate) use diff_impl::DiffHandler;
|
||||
pub(crate) use diff_impl::OperateProxy;
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
},
|
||||
cursor::{Cursor, Side},
|
||||
delta::{DeltaItem, Meta, StyleMeta, TreeExternalDiff},
|
||||
diff::{myers_diff, OperateProxy},
|
||||
diff::{diff, diff_impl::UpdateTimeoutError, OperateProxy},
|
||||
event::{Diff, TextDiffItem},
|
||||
op::ListSlice,
|
||||
state::{IndexType, State, TreeParentId},
|
||||
|
@ -34,6 +34,7 @@ use std::{
|
|||
};
|
||||
use tracing::{error, info, instrument, trace};
|
||||
|
||||
pub use crate::diff::diff_impl::UpdateOptions;
|
||||
pub use tree::TreeHandler;
|
||||
mod movable_list_apply_delta;
|
||||
mod tree;
|
||||
|
@ -2145,25 +2146,33 @@ impl TextHandler {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip(self))]
|
||||
pub fn update(&self, text: &str) {
|
||||
pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
|
||||
let old_str = self.to_string();
|
||||
let new = text.chars().map(|x| x as u32).collect::<Vec<u32>>();
|
||||
myers_diff(
|
||||
let old = old_str.chars().map(|x| x as u32).collect::<Vec<u32>>();
|
||||
diff(
|
||||
&mut OperateProxy::new(text_update::DiffHook::new(self, &new)),
|
||||
&old_str.chars().map(|x| x as u32).collect::<Vec<u32>>(),
|
||||
options,
|
||||
&old,
|
||||
&new,
|
||||
);
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip(self))]
|
||||
pub fn update_by_line(&self, text: &str) {
|
||||
pub fn update_by_line(
|
||||
&self,
|
||||
text: &str,
|
||||
options: UpdateOptions,
|
||||
) -> Result<(), UpdateTimeoutError> {
|
||||
let hook = text_update::DiffHookForLine::new(self, text);
|
||||
let old_lines = hook.get_old_arr().to_vec();
|
||||
let new_lines = hook.get_new_arr().to_vec();
|
||||
trace!("old_lines: {:?}", old_lines);
|
||||
trace!("new_lines: {:?}", new_lines);
|
||||
myers_diff(&mut OperateProxy::new(hook), &old_lines, &new_lines);
|
||||
diff(
|
||||
&mut OperateProxy::new(hook),
|
||||
options,
|
||||
&old_lines,
|
||||
&new_lines,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
|
|
|
@ -499,7 +499,7 @@ impl OpLog {
|
|||
|
||||
pub fn get_timestamp_for_next_txn(&self) -> Timestamp {
|
||||
if self.configure.record_timestamp() {
|
||||
(get_sys_timestamp() + 500) / 1000
|
||||
(get_sys_timestamp() as Timestamp + 500) / 1000
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use loro_common::{
|
|||
use tracing::{debug_span, info_span, instrument};
|
||||
|
||||
use crate::{
|
||||
change::get_sys_timestamp,
|
||||
change::{get_sys_timestamp, Timestamp},
|
||||
cursor::{AbsolutePosition, Cursor},
|
||||
delta::TreeExternalDiff,
|
||||
event::{Diff, EventTriggerKind},
|
||||
|
@ -397,7 +397,7 @@ impl UndoManagerInner {
|
|||
}
|
||||
|
||||
assert!(self.next_counter.unwrap() < latest_counter);
|
||||
let now = get_sys_timestamp();
|
||||
let now = get_sys_timestamp() as Timestamp;
|
||||
let span = CounterSpan::new(self.next_counter.unwrap(), latest_counter);
|
||||
let meta = self
|
||||
.on_push
|
||||
|
|
|
@ -1238,7 +1238,8 @@ fn test_text_update() {
|
|||
let doc = LoroDoc::new_auto_commit();
|
||||
let text = doc.get_text("text");
|
||||
text.insert(0, "Hello 😊Bro").unwrap();
|
||||
text.update("Hello World Bro😊");
|
||||
text.update("Hello World Bro😊", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(text.to_string(), "Hello World Bro😊");
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ use loro_internal::{
|
|||
cursor::{self, Side},
|
||||
encoding::ImportBlobMetadata,
|
||||
event::Index,
|
||||
handler::UpdateOptions,
|
||||
handler::{
|
||||
Handler, ListHandler, MapHandler, TextDelta, TextHandler, TreeHandler, ValueOrHandler,
|
||||
},
|
||||
|
@ -1824,16 +1825,62 @@ impl LoroText {
|
|||
/// text.update("Hello World");
|
||||
/// console.log(text.toString()); // "Hello World"
|
||||
/// ```
|
||||
pub fn update(&self, text: &str) {
|
||||
self.handler.update(text);
|
||||
///
|
||||
#[wasm_bindgen(skip_typescript)]
|
||||
pub fn update(&self, text: &str, options: JsValue) -> JsResult<()> {
|
||||
let options = if options.is_null() || options.is_undefined() {
|
||||
UpdateOptions {
|
||||
timeout_ms: None,
|
||||
use_refined_diff: true,
|
||||
}
|
||||
} else {
|
||||
let opts = match js_sys::Object::try_from(&options) {
|
||||
Some(o) => o,
|
||||
None => return Err(JsError::new("Invalid options").into()),
|
||||
};
|
||||
UpdateOptions {
|
||||
timeout_ms: js_sys::Reflect::get(opts, &"timeoutMs".into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64()),
|
||||
use_refined_diff: js_sys::Reflect::get(opts, &"useRefinedDiff".into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true),
|
||||
}
|
||||
};
|
||||
self.handler
|
||||
.update(text, options)
|
||||
.map_err(|_| JsError::new("Update timeout").into())
|
||||
}
|
||||
|
||||
/// Update the current text to the target text, the difference is calculated line by line.
|
||||
///
|
||||
/// It uses Myers' diff algorithm to compute the optimal difference.
|
||||
#[wasm_bindgen(js_name = "updateByLine")]
|
||||
pub fn update_by_line(&self, text: &str) {
|
||||
self.handler.update_by_line(text);
|
||||
#[wasm_bindgen(js_name = "updateByLine", skip_typescript)]
|
||||
pub fn update_by_line(&self, text: &str, options: JsValue) -> JsResult<()> {
|
||||
let options = if options.is_null() || options.is_undefined() {
|
||||
UpdateOptions {
|
||||
timeout_ms: None,
|
||||
use_refined_diff: true,
|
||||
}
|
||||
} else {
|
||||
let opts = match js_sys::Object::try_from(&options) {
|
||||
Some(o) => o,
|
||||
None => return Err(JsError::new("Invalid options").into()),
|
||||
};
|
||||
UpdateOptions {
|
||||
timeout_ms: js_sys::Reflect::get(opts, &"timeoutMs".into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_f64()),
|
||||
use_refined_diff: js_sys::Reflect::get(opts, &"useRefinedDiff".into())
|
||||
.ok()
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(true),
|
||||
}
|
||||
};
|
||||
self.handler
|
||||
.update_by_line(text, options)
|
||||
.map_err(|_| JsError::new("Update timeout").into())
|
||||
}
|
||||
|
||||
/// Insert the string at the given index (utf-16 index).
|
||||
|
@ -4849,6 +4896,11 @@ export type JsonChange = {
|
|||
ops: JsonOp[]
|
||||
}
|
||||
|
||||
export interface TextUpdateOptions {
|
||||
timeoutMs?: number,
|
||||
useRefinedDiff?: boolean,
|
||||
}
|
||||
|
||||
export type ExportMode = {
|
||||
mode: "update",
|
||||
from?: VersionVector,
|
||||
|
@ -5429,6 +5481,32 @@ interface LoroText {
|
|||
insert(pos: number, text: string): void;
|
||||
delete(pos: number, len: number): void;
|
||||
subscribe(listener: Listener): Subscription;
|
||||
/**
|
||||
* Update the current text to the target text.
|
||||
*
|
||||
* It will calculate the minimal difference and apply it to the current text.
|
||||
* It uses Myers' diff algorithm to compute the optimal difference.
|
||||
*
|
||||
* This could take a long time for large texts (e.g. > 50_000 characters).
|
||||
* In that case, you should use `updateByLine` instead.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { LoroDoc } from "loro-crdt";
|
||||
*
|
||||
* const doc = new LoroDoc();
|
||||
* const text = doc.getText("text");
|
||||
* text.insert(0, "Hello");
|
||||
* text.update("Hello World");
|
||||
* console.log(text.toString()); // "Hello World"
|
||||
* ```
|
||||
*/
|
||||
update(text: string, options?: TextUpdateOptions): void;
|
||||
/**
|
||||
* Update the current text based on the provided text.
|
||||
* This update calculation is line-based, which will be more efficient but less precise.
|
||||
*/
|
||||
updateByLine(text: string, options?: TextUpdateOptions): void;
|
||||
}
|
||||
interface LoroTree<T extends Record<string, unknown> = Record<string, unknown>> {
|
||||
new(): LoroTree<T>;
|
||||
|
|
|
@ -29,6 +29,8 @@ use std::ops::Range;
|
|||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
|
||||
pub use loro_internal::diff::diff_impl::UpdateOptions;
|
||||
pub use loro_internal::diff::diff_impl::UpdateTimeoutError;
|
||||
pub use loro_internal::subscription::LocalUpdateCallback;
|
||||
pub use loro_internal::subscription::PeerIdUpdateCallback;
|
||||
pub use loro_internal::ChangeMeta;
|
||||
|
@ -1445,13 +1447,37 @@ impl LoroText {
|
|||
}
|
||||
|
||||
/// Update the current text based on the provided text.
|
||||
pub fn update(&self, text: &str) {
|
||||
self.handler.update(text);
|
||||
///
|
||||
/// It will calculate the minimal difference and apply it to the current text.
|
||||
/// It uses Myers' diff algorithm to compute the optimal difference.
|
||||
///
|
||||
/// This could take a long time for large texts (e.g. > 50_000 characters).
|
||||
/// In that case, you should use `updateByLine` instead.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use loro::LoroDoc;
|
||||
///
|
||||
/// let doc = LoroDoc::new();
|
||||
/// let text = doc.get_text("text");
|
||||
/// text.insert(0, "Hello").unwrap();
|
||||
/// text.update("Hello World", Default::default()).unwrap();
|
||||
/// assert_eq!(text.to_string(), "Hello World");
|
||||
/// ```
|
||||
///
|
||||
pub fn update(&self, text: &str, options: UpdateOptions) -> Result<(), UpdateTimeoutError> {
|
||||
self.handler.update(text, options)
|
||||
}
|
||||
|
||||
/// Update the current text based on the provided text by line.
|
||||
pub fn update_by_line(&self, text: &str) {
|
||||
self.handler.update_by_line(text);
|
||||
/// Update the current text based on the provided text.
|
||||
///
|
||||
/// This update calculation is line-based, which will be more efficient but less precise.
|
||||
pub fn update_by_line(
|
||||
&self,
|
||||
text: &str,
|
||||
options: UpdateOptions,
|
||||
) -> Result<(), UpdateTimeoutError> {
|
||||
self.handler.update_by_line(text, options)
|
||||
}
|
||||
|
||||
/// Apply a [delta](https://quilljs.com/docs/delta/) to the text container.
|
||||
|
|
|
@ -5,11 +5,11 @@ fn test_text_update() -> anyhow::Result<()> {
|
|||
let (old, new, new1) = (")+++", "%", "");
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(old);
|
||||
text.update(old, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), old);
|
||||
text.update(new);
|
||||
text.update(new, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new);
|
||||
text.update(new1);
|
||||
text.update(new1, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new1);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -23,11 +23,11 @@ fn test_text_update_by_line() -> anyhow::Result<()> {
|
|||
);
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update_by_line(old);
|
||||
text.update_by_line(old, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), old);
|
||||
text.update_by_line(new);
|
||||
text.update_by_line(new, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new);
|
||||
text.update_by_line(new1);
|
||||
text.update_by_line(new1, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new1);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -36,9 +36,9 @@ fn test_text_update_by_line() -> anyhow::Result<()> {
|
|||
fn test_text_update_empty_to_nonempty() -> anyhow::Result<()> {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update("");
|
||||
text.update("", Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), "");
|
||||
text.update("Hello, Loro!");
|
||||
text.update("Hello, Loro!", Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), "Hello, Loro!");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -47,9 +47,9 @@ fn test_text_update_empty_to_nonempty() -> anyhow::Result<()> {
|
|||
fn test_text_update_nonempty_to_empty() -> anyhow::Result<()> {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update("Initial content");
|
||||
text.update("Initial content", Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), "Initial content");
|
||||
text.update("");
|
||||
text.update("", Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), "");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -58,9 +58,11 @@ fn test_text_update_nonempty_to_empty() -> anyhow::Result<()> {
|
|||
fn test_text_update_with_special_characters() -> anyhow::Result<()> {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update("Special chars: !@#$%^&*()");
|
||||
text.update("Special chars: !@#$%^&*()", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Special chars: !@#$%^&*()");
|
||||
text.update("New special chars: ñáéíóú");
|
||||
text.update("New special chars: ñáéíóú", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "New special chars: ñáéíóú");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -69,9 +71,11 @@ fn test_text_update_with_special_characters() -> anyhow::Result<()> {
|
|||
fn test_text_update_by_line_with_empty_lines() -> anyhow::Result<()> {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update_by_line("Line 1\n\nLine 3\n");
|
||||
text.update_by_line("Line 1\n\nLine 3\n", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Line 1\n\nLine 3\n");
|
||||
text.update_by_line("Line 1\nLine 2\n\nLine 4\n");
|
||||
text.update_by_line("Line 1\nLine 2\n\nLine 4\n", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Line 1\nLine 2\n\nLine 4\n");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -80,9 +84,11 @@ fn test_text_update_by_line_with_empty_lines() -> anyhow::Result<()> {
|
|||
fn test_text_update_by_line_with_different_line_endings() -> anyhow::Result<()> {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update_by_line("Line 1\nLine 2\r\nLine 3\n");
|
||||
text.update_by_line("Line 1\nLine 2\r\nLine 3\n", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Line 1\nLine 2\r\nLine 3\n");
|
||||
text.update_by_line("Line 1\r\nLine 2\nLine 3\r\n");
|
||||
text.update_by_line("Line 1\r\nLine 2\nLine 3\r\n", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Line 1\r\nLine 2\nLine 3\r\n");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -91,9 +97,11 @@ fn test_text_update_by_line_with_different_line_endings() -> anyhow::Result<()>
|
|||
fn test_text_update_by_line_with_no_trailing_newline() -> anyhow::Result<()> {
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update_by_line("Line 1\nLine 2\nLine 3");
|
||||
text.update_by_line("Line 1\nLine 2\nLine 3", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Line 1\nLine 2\nLine 3");
|
||||
text.update_by_line("Line 1\nLine 2\nLine 3\nLine 4");
|
||||
text.update_by_line("Line 1\nLine 2\nLine 3\nLine 4", Default::default())
|
||||
.unwrap();
|
||||
assert_eq!(&text.to_string(), "Line 1\nLine 2\nLine 3\nLine 4");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -114,10 +122,10 @@ fn test_failed_case_0() -> anyhow::Result<()> {
|
|||
let (old, new, new1) = (input[0], input[1], input[2]);
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(old);
|
||||
text.update(new);
|
||||
text.update(old, Default::default()).unwrap();
|
||||
text.update(new, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new);
|
||||
text.update_by_line(new1);
|
||||
text.update_by_line(new1, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new1);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -128,10 +136,10 @@ fn test_failed_case_1() -> anyhow::Result<()> {
|
|||
let (old, new, new1) = (input[0], input[1], input[2]);
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(old);
|
||||
text.update(new);
|
||||
text.update(old, Default::default()).unwrap();
|
||||
text.update(new, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new);
|
||||
text.update_by_line(new1);
|
||||
text.update_by_line(new1, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new1);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -142,10 +150,10 @@ fn test_failed_case_2() -> anyhow::Result<()> {
|
|||
let (old, new, new1) = (input[0], input[1], input[2]);
|
||||
let doc = LoroDoc::new();
|
||||
let text = doc.get_text("text");
|
||||
text.update(old);
|
||||
text.update(new);
|
||||
text.update(old, Default::default()).unwrap();
|
||||
text.update(new, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new);
|
||||
text.update_by_line(new1);
|
||||
text.update_by_line(new1, Default::default()).unwrap();
|
||||
assert_eq!(&text.to_string(), new1);
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue