Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/restore-state-vv-signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"loro-crdt": patch
---

Restore the Rust `LoroDoc::state_vv()` return type to `VersionVector`, preserving
source compatibility with versions before 1.17.0. Keep `try_state_vv()` as the
fallible API for callers that need to handle unreadable history. The legacy
method panics on resolution failure after releasing the document locks. WASM
`version()` continues to return readable errors through the fallible API.
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions context/arena-parent-links.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Arena Parent Links

Verified against code 2026-10-09.
Verified against code 2026-10-22.

`SharedArena` (`crates/loro-internal/src/arena.rs`) stores each container's
parent. Liveness (`DocState::is_deleted`), paths (`DocState::get_path`,
Expand Down Expand Up @@ -161,9 +161,12 @@ already recorded failures, and their history reads also propagate failures
`try_state_vv`, `try_export_json_updates`, and `try_get_cursor_pos` to
receive decode errors. `state_vv` clones `OpLog::vv` when the state and op
log frontiers are equal; that vector is cached and does not parse block
bodies. Otherwise it uses `try_frontiers_to_vv`, and returns
bodies. Otherwise it uses `try_frontiers_to_vv`. `try_state_vv` returns
`FrontiersNotFound` when a frontier id is absent rather than an empty
version vector. `cmp_frontiers` keeps its `FrontiersNotIncluded` error type,
version vector. `state_vv` preserves its pre-1.17 infallible signature and
panics on resolution failure, after releasing the document locks. Rust
callers handling untrusted history should use `try_state_vv`; WASM `version()`
continues using that fallible path. `cmp_frontiers` keeps its `FrontiersNotIncluded` error type,
which can now carry the decode-error message; `travel_change_ancestors`
returns `ChangeTravelError::HistoryUnreadable(LoroError::DecodeError(..))`.
`ChangeTravelError` is now non-exhaustive, so downstream matches need a
Expand Down
2 changes: 1 addition & 1 deletion crates/loro-internal/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "loro-internal"
version = "1.17.0"
version = "1.17.1"
edition = "2021"
license = "MIT"
description = "Loro internal library. Do not use it directly as it's not stable."
Expand Down
2 changes: 1 addition & 1 deletion crates/loro-internal/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.17.0
1.17.1
11 changes: 9 additions & 2 deletions crates/loro-internal/src/loro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1187,12 +1187,19 @@ impl LoroDoc {
/// When the state frontiers are the op-log frontiers, this clones the cached
/// op-log version vector and does not parse change blocks. Otherwise it
/// walks the frontiers.
///
/// # Panics
///
/// Panics if the state's version cannot be resolved from the document history.
/// Use [`Self::try_state_vv`] to handle unreadable history as an error.
#[inline]
pub fn state_vv(&self) -> LoroResult<VersionVector> {
pub fn state_vv(&self) -> VersionVector {
self.try_state_vv()
.expect("Cannot resolve the state version vector")
}

/// [`state_vv`](Self::state_vv), including a decode error from unreadable history.
/// Get the version vector of the current state, returning a decode error
/// when required history cannot be read.
///
/// `Ok(None)` from the frontier walk (the frontiers are not in the dag) is
/// [`LoroError::FrontiersNotFound`] for an id that can be named, not an empty
Expand Down
4 changes: 2 additions & 2 deletions crates/loro-internal/tests/undo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ fn test_simulate_intersecting_remote_undo() {
let snapshot = doc.export(ExportMode::Snapshot).unwrap();
let doc2 = LoroDoc::from_snapshot(&snapshot).unwrap();
let text2 = doc2.get_text("text");
let vv = doc2.state_vv().unwrap();
let vv = doc2.state_vv();
text2.update("123", UpdateOptions::default()).unwrap();
doc2.commit_then_renew();

Expand Down Expand Up @@ -164,7 +164,7 @@ fn test_simulate_non_intersecting_remote_undo() {
// The doc 2 has state of "123" in the "text2" container
let doc2 = LoroDoc::from_snapshot(&snapshot).unwrap();
let text2 = doc2.get_text("text2");
let vv = doc2.state_vv().unwrap();
let vv = doc2.state_vv();
text2.update("123", UpdateOptions::default()).unwrap();
doc2.commit_then_renew();
let update = doc2
Expand Down
4 changes: 2 additions & 2 deletions crates/loro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "loro"
version = "1.17.0"
version = "1.17.1"
edition = "2021"
license = "MIT"
documentation = "https://docs.rs/loro/"
Expand All @@ -14,7 +14,7 @@ keywords = ["crdt", "local-first"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
loro-internal = { path = "../loro-internal", version = "1.17.0" }
loro-internal = { path = "../loro-internal", version = "1.17.1" }
loro-common = { path = "../loro-common", version = "1.17.0", features = [
"serde_json",
] }
Expand Down
17 changes: 11 additions & 6 deletions crates/loro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,15 +998,20 @@ impl LoroDoc {
///
/// When the state is at the op-log frontiers this clones the cached op-log
/// version vector and does not parse change blocks. Otherwise it walks the
/// frontiers. A change block that cannot be parsed is [`LoroError::DecodeError`].
/// Frontiers that are not in the dag are [`LoroError::FrontiersNotFound`] when
/// an id can be named, not an empty version vector.
/// frontiers.
///
/// # Panics
///
/// Panics if the state's version cannot be resolved from the document history.
/// Use [`Self::try_state_vv`] to handle unreadable history as an error.
#[inline]
pub fn state_vv(&self) -> LoroResult<VersionVector> {
self.try_state_vv()
pub fn state_vv(&self) -> VersionVector {
self.doc.state_vv()
}

/// [`state_vv`](Self::state_vv).
/// Get the `VersionVector` version of `DocState`, returning an error if the
/// required history cannot be read or a state frontier is missing.
/// Unreadable history produces [`LoroError::DecodeError`].
#[inline]
pub fn try_state_vv(&self) -> LoroResult<VersionVector> {
self.doc.try_state_vv()
Expand Down
14 changes: 7 additions & 7 deletions crates/loro/tests/integration_test/detached_editing_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,13 @@ fn allow_editing_on_detached_mode_when_detached_editing_is_enabled() {
assert_eq!(doc.get_text("text").to_string(), "Hello alice!");
assert_eq!(&**string.lock().unwrap(), "Hello alice!");
assert_ne!(doc.state_frontiers(), doc.oplog_frontiers());
assert_ne!(doc.oplog_vv(), doc.state_vv().unwrap());
assert_ne!(doc.oplog_vv(), doc.state_vv());
assert_eq!(doc.state_frontiers(), Frontiers::from(ID::new(2, 6)));
assert_eq!(
doc.oplog_frontiers(),
Frontiers::from([ID::new(1, 11), ID::new(2, 6)])
);
assert_eq!(doc.state_vv().unwrap(), vv!(1 => 5, 2 => 7));
assert_eq!(doc.state_vv(), vv!(1 => 5, 2 => 7));
assert_eq!(doc.oplog_vv(), vv!(1 => 12, 2 => 7));

doc.checkout_to_latest();
Expand All @@ -82,7 +82,7 @@ fn allow_editing_on_detached_mode_when_detached_editing_is_enabled() {
assert_eq!(doc.get_text("text").to_string(), "Hello world! alice!");
assert_eq!(&**string.lock().unwrap(), "Hello world! alice!");
assert_eq!(doc.state_frontiers(), doc.oplog_frontiers());
assert_eq!(doc.oplog_vv(), doc.state_vv().unwrap());
assert_eq!(doc.oplog_vv(), doc.state_vv());

// New op on peer id 1
doc.set_peer_id(1).unwrap();
Expand All @@ -91,7 +91,7 @@ fn allow_editing_on_detached_mode_when_detached_editing_is_enabled() {
assert_eq!(&**string.lock().unwrap(), "Hi Hello world! alice!");
assert_eq!(doc.get_text("text").to_string(), "Hi Hello world! alice!");
assert_eq!(doc.state_frontiers(), doc.oplog_frontiers());
assert_eq!(doc.oplog_vv(), doc.state_vv().unwrap());
assert_eq!(doc.oplog_vv(), doc.state_vv());
assert_eq!(doc.state_frontiers(), Frontiers::from_id(ID::new(1, 14)));
let json = doc
.export_json_updates(&Default::default(), &doc.oplog_vv())
Expand Down Expand Up @@ -222,13 +222,13 @@ fn allow_editing_on_detached_mode_when_detached_editing_is_enabled_2() {
assert_eq!(doc.get_text("text").to_string(), "Hello alice!");
assert_eq!(&**string.lock().unwrap(), "Hello alice!");
assert_ne!(doc.state_frontiers(), doc.oplog_frontiers());
assert_ne!(doc.oplog_vv(), doc.state_vv().unwrap());
assert_ne!(doc.oplog_vv(), doc.state_vv());
assert_eq!(doc.state_frontiers(), Frontiers::from(ID::new(0, 6)));
assert_eq!(
doc.oplog_frontiers(),
Frontiers::from([ID::new(1, 11), ID::new(0, 6)])
);
assert_eq!(doc.state_vv().unwrap(), vv!(1 => 5, 0 => 7));
assert_eq!(doc.state_vv(), vv!(1 => 5, 0 => 7));
assert_eq!(doc.oplog_vv(), vv!(1 => 12, 0 => 7));

doc.checkout_to_latest();
Expand All @@ -240,7 +240,7 @@ fn allow_editing_on_detached_mode_when_detached_editing_is_enabled_2() {
assert_eq!(doc.get_text("text").to_string(), "Hello alice! world!");
assert_eq!(&**string.lock().unwrap(), "Hello alice! world!");
assert_eq!(doc.state_frontiers(), doc.oplog_frontiers());
assert_eq!(doc.oplog_vv(), doc.state_vv().unwrap());
assert_eq!(doc.oplog_vv(), doc.state_vv());
}

#[test]
Expand Down
8 changes: 4 additions & 4 deletions crates/loro/tests/integration_test/shallow_snapshot_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ fn the_vv_on_shallow_doc() -> anyhow::Result<()> {
let new_doc = LoroDoc::new();
new_doc.import(&snapshot.unwrap()).unwrap();
assert!(!new_doc.shallow_since_vv().is_empty());
assert_eq!(new_doc.oplog_vv(), new_doc.state_vv().unwrap());
assert_eq!(new_doc.oplog_vv(), doc.state_vv().unwrap());
assert_eq!(new_doc.oplog_vv(), new_doc.state_vv());
assert_eq!(new_doc.oplog_vv(), doc.state_vv());
assert_eq!(new_doc.oplog_frontiers(), doc.oplog_frontiers());
assert_eq!(new_doc.oplog_frontiers(), new_doc.state_frontiers());
assert_eq!(new_doc.get_deep_value(), doc.get_deep_value());
Expand All @@ -351,8 +351,8 @@ fn the_vv_on_shallow_doc() -> anyhow::Result<()> {
doc.commit();
let bytes = doc.export(ExportMode::all_updates());
new_doc.import(&bytes.unwrap()).unwrap();
assert_eq!(new_doc.oplog_vv(), new_doc.state_vv().unwrap());
assert_eq!(new_doc.oplog_vv(), doc.state_vv().unwrap());
assert_eq!(new_doc.oplog_vv(), new_doc.state_vv());
assert_eq!(new_doc.oplog_vv(), doc.state_vv());
assert_eq!(new_doc.oplog_frontiers(), doc.oplog_frontiers());
assert_eq!(new_doc.oplog_frontiers(), new_doc.state_frontiers());
assert_eq!(new_doc.get_deep_value(), doc.get_deep_value());
Expand Down
4 changes: 2 additions & 2 deletions crates/loro/tests/loro_rust_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1278,7 +1278,7 @@ fn sync_two_shallow_docs() {
assert_eq!(doc_a.get_deep_value(), doc_b.get_deep_value());
assert_eq!(doc_a.oplog_vv(), doc_b.oplog_vv());
assert_eq!(doc_a.oplog_frontiers(), doc_b.oplog_frontiers());
assert_eq!(doc_a.state_vv().unwrap(), doc_b.state_vv().unwrap());
assert_eq!(doc_a.state_vv(), doc_b.state_vv());
assert_eq!(doc_a.shallow_since_vv(), doc_b.shallow_since_vv());
}

Expand Down Expand Up @@ -2092,7 +2092,7 @@ fn issue_490() -> anyhow::Result<()> {
fn test_loro_doc() {
let doc = LoroDoc::new();
doc.get_text("text").insert(0, "Hello").unwrap();
doc.state_vv().unwrap();
doc.state_vv();
}

#[test]
Expand Down
32 changes: 32 additions & 0 deletions crates/loro/tests/state_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use loro::{ExportMode, LoroDoc, VersionVector};

#[test]
fn state_vv_keeps_its_infallible_signature_and_tracks_checked_out_state() {
// Downstream code written before 1.17.0 must continue compiling.
let state_vv: fn(&LoroDoc) -> VersionVector = LoroDoc::state_vv;
let source = LoroDoc::new();
source.set_peer_id(1).unwrap();
source.get_text("text").insert(0, "a").unwrap();
source.commit();
let earlier_frontiers = source.state_frontiers();
let earlier_vv = state_vv(&source);
source.get_text("text").insert(1, "b").unwrap();
let snapshot = source.export(ExportMode::Snapshot).unwrap();

let doc = LoroDoc::new();
doc.import(&snapshot).unwrap();
let latest_vv = doc.oplog_vv();
assert_ne!(earlier_vv, latest_vv);
assert_eq!(state_vv(&doc), latest_vv);
assert_eq!(doc.try_state_vv().unwrap(), latest_vv);

doc.checkout(&earlier_frontiers).unwrap();
assert_eq!(doc.get_text("text").to_string(), "a");
assert_eq!(state_vv(&doc), earlier_vv);
assert_eq!(doc.try_state_vv().unwrap(), earlier_vv);
assert_eq!(doc.oplog_vv(), latest_vv);

doc.checkout_to_latest();
assert_eq!(state_vv(&doc), latest_vv);
assert_eq!(doc.try_state_vv().unwrap(), latest_vv);
}
2 changes: 1 addition & 1 deletion crates/loro/tests/unparsable_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ fn state_vv_of_the_fixture_uses_the_cached_oplog_vv() {
let doc = LoroDoc::new();
doc.import(FIXTURE).unwrap();
assert_eq!(doc.state_frontiers(), doc.oplog_frontiers());
let vv = doc.state_vv().unwrap();
let vv = doc.state_vv();
assert_eq!(vv, doc.oplog_vv());
assert_eq!(doc.try_state_vv().unwrap(), doc.oplog_vv());

Expand Down
Loading