From 98f8144849969e00f626e8b8dc0bcc4704a9a61e Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Thu, 22 Oct 2026 15:43:31 +0000 Subject: [PATCH] chore: release Python bindings 1.17.1 --- Cargo.lock | 18 ++++++------ Cargo.toml | 4 +-- README.md | 9 ++++++ loro.pyi | 8 ++++++ src/doc.rs | 19 +++++++++++-- tests/test_pending_updates.py | 52 +++++++++++++++++++++++++++++++++++ 6 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 tests/test_pending_updates.py diff --git a/Cargo.lock b/Cargo.lock index 24c4376..8feba3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "loro" -version = "1.16.2" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e86dea63b66576db244cbc3a40bdb17517263c93f318e2fbe08aca3be1192cb" +checksum = "410c939fe3bee34b8ed61f7aea93201fb136629f685ffa557039c4d9952f9988" dependencies = [ "enum-as-inner 0.6.1", "generic-btree", @@ -526,9 +526,9 @@ dependencies = [ [[package]] name = "loro-common" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2146b353a56b0a743f8643725cb9f42dfe209665939b59db901ecea732a3ab29" +checksum = "bd3f7afbafc4db85aefe9620e7e99fa49c974477563904f226ae96fc9674eade" dependencies = [ "arbitrary", "enum-as-inner 0.6.1", @@ -556,9 +556,9 @@ dependencies = [ [[package]] name = "loro-internal" -version = "1.16.2" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d5806b65ee68c3ae7891959dd968898860dab74fbf6498b312de9b548dce5ec" +checksum = "7e533c1688430aa4fcdd933f00861e331c169a4158ee6d27d054e0558892d447" dependencies = [ "append-only-bytes", "arref", @@ -603,9 +603,9 @@ dependencies = [ [[package]] name = "loro-kv-store" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841e206d3749fd14f23e64985be230dad2b2199da1d381c44c3dd0d2b9583fcc" +checksum = "c7331ebe6902effeb0c3d52aba9e30fb0118bc926faed6f3848487aab40c1a4e" dependencies = [ "bytes", "ensure-cov", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "loro_py" -version = "1.16.2" +version = "1.17.1" dependencies = [ "loro", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 3b3b8f6..d9533a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loro_py" -version = "1.16.2" +version = "1.17.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,7 +9,7 @@ name = "loro" crate-type = ["cdylib"] [dependencies] -loro = { version = "1.16.2", features = ["counter", "jsonpath"] } +loro = { version = "1.17.1", features = ["counter", "jsonpath"] } rustc-hash = "2.1.1" pyo3 = { version = "0.26.0" } serde_json = "1" diff --git a/README.md b/README.md index 96f3f6a..e34a20f 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,12 @@ cd loro-py pip install maturin maturin develop ``` + +## 1.17.1 + +Uses Loro core 1.17.1. Adds `LoroDoc.export_pending_updates()` to persist received +updates waiting for missing dependencies alongside a consistent snapshot. Restore +the snapshot first, then import the pending bytes. `None` means nothing is pending; +clear any previously saved pending bytes when replacing a checkpoint with `None`. +JSON export keeps returning the existing JSON string and reports history decode +failures as Python exceptions. The `state_vv` property retains its existing type. diff --git a/loro.pyi b/loro.pyi index dea128b..1e58dc7 100644 --- a/loro.pyi +++ b/loro.pyi @@ -788,6 +788,14 @@ class LoroDoc: """ ... + def export_pending_updates(self) -> typing.Optional[bytes]: + """Export remote updates waiting for dependencies without committing local edits. + + Save with a consistent snapshot. Restore the snapshot first, then import + these bytes. None means no received updates are pending. + """ + ... + def get_path_to_container( self, id: ContainerID ) -> typing.Optional[list[tuple[ContainerID, Index]]]: diff --git a/src/doc.rs b/src/doc.rs index 23cb0e6..7927ae6 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -515,11 +515,15 @@ impl LoroDoc { // TODO: return an object /// Export the current state with json-string format of the document. #[inline] - pub fn export_json_updates(&self, start_vv: VersionVector, end_vv: VersionVector) -> String { + pub fn export_json_updates( + &self, + start_vv: VersionVector, + end_vv: VersionVector, + ) -> PyLoroResult { let json = self .doc - .export_json_updates(&start_vv.into(), &end_vv.into()); - serde_json::to_string(&json).unwrap() + .export_json_updates(&start_vv.into(), &end_vv.into())?; + serde_json::to_string(&json).map_err(|error| PyLoroError::Error(error.to_string())) } /// Exports changes within the specified ID span to JSON schema format. @@ -873,6 +877,15 @@ impl LoroDoc { Ok(Cow::Owned(ans)) } + /// Export received remote updates that are waiting for missing dependencies. + /// + /// Returns None when nothing is pending. Save these bytes together with a + /// consistent snapshot, then restore the snapshot before importing these bytes. + /// This does not commit local edits or consume pending updates. + pub fn export_pending_updates(&self) -> PyLoroResult>> { + Ok(self.doc.export_pending_updates()?.map(Cow::Owned)) + } + // /// Analyze the container info of the doc // /// // /// This is used for development and debugging. It can be slow. diff --git a/tests/test_pending_updates.py b/tests/test_pending_updates.py new file mode 100644 index 0000000..85ac117 --- /dev/null +++ b/tests/test_pending_updates.py @@ -0,0 +1,52 @@ +import json + +from loro import ExportMode, LoroDoc, VersionVector + + +def test_pending_updates_survive_a_snapshot_checkpoint(): + source = LoroDoc() + source.get_text("text").insert(0, "base") + base = source.export(ExportMode.Snapshot()) + before_a = source.oplog_vv + source.get_text("text").insert(4, "A") + a = source.export(ExportMode.Updates(before_a)) + after_a = source.oplog_vv + source.get_text("text").insert(5, "B") + b = source.export(ExportMode.Updates(after_a)) + + receiver = LoroDoc() + receiver.import_(base) + receiver.import_(b) + assert receiver.get_text("text").to_string() == "base" + snapshot = receiver.export(ExportMode.Snapshot()) + pending = receiver.export_pending_updates() + assert isinstance(pending, bytes) + assert receiver.export_pending_updates() == pending + + restored = LoroDoc() + restored.import_(snapshot) + restored.import_(pending) + assert restored.get_text("text").to_string() == "base" + restored.import_(a) + assert restored.get_text("text").to_string() == "baseAB" + assert restored.export_pending_updates() is None + + +def test_pending_export_does_not_commit_local_edits(): + doc = LoroDoc() + doc.get_text("text").insert(0, "local") + pending_count = doc.get_pending_txn_len() + assert pending_count > 0 + assert doc.export_pending_updates() is None + assert doc.get_pending_txn_len() == pending_count + + +def test_json_export_keeps_the_existing_schema(): + doc = LoroDoc() + doc.get_text("text").insert(0, "hello") + doc.commit() + updates = doc.export_json_updates(VersionVector(), doc.oplog_vv) + assert "schema_version" in json.loads(updates) + restored = LoroDoc() + restored.import_json_updates(updates) + assert restored.get_text("text").to_string() == "hello"