diff --git a/Cargo.lock b/Cargo.lock index 76052fe15..40541d4f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1598,6 +1598,7 @@ version = "1.17.1" dependencies = [ "anyhow", "base64", + "bytes", "ctor 0.2.9", "dev-utils", "enum-as-inner 0.6.1", diff --git a/crates/loro-internal/src/encoding.rs b/crates/loro-internal/src/encoding.rs index 9495e3d84..a27deb19c 100644 --- a/crates/loro-internal/src/encoding.rs +++ b/crates/loro-internal/src/encoding.rs @@ -17,6 +17,7 @@ use crate::oplog::ImportedValues; use crate::version::{Frontiers, VersionRange}; use crate::LoroDoc; use crate::{oplog::OpLog, LoroError, VersionVector}; +use bytes::Bytes; use loro_common::{IdSpan, InternalString, LoroEncodeError, LoroResult, ID}; use num_traits::{FromPrimitive, ToPrimitive}; use std::borrow::Cow; @@ -504,14 +505,21 @@ pub(crate) fn decode_snapshot( mode: EncodeMode, body: &[u8], origin: InternalString, +) -> Result { + decode_snapshot_bytes(doc, mode, Bytes::copy_from_slice(body), origin) +} + +pub(crate) fn decode_snapshot_bytes( + doc: &LoroDoc, + mode: EncodeMode, + body: Bytes, + origin: InternalString, ) -> Result { match mode { EncodeMode::OutdatedSnapshot => { return Err(LoroError::ImportUnsupportedEncodingMode); } - EncodeMode::FastSnapshot => { - fast_snapshot::decode_snapshot(doc, body.to_vec().into(), origin)? - } + EncodeMode::FastSnapshot => fast_snapshot::decode_snapshot(doc, body, origin)?, _ => { return Err(LoroError::DecodeError( format!("Invalid snapshot encoding mode: {mode:?}").into_boxed_str(), diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index bfb48173c..425907e6e 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -16,7 +16,7 @@ use crate::{ dag::{Dag, DagUtils}, diff_calc::{DiffCalculator, DiffMode}, encoding::{ - self, decode_snapshot, export_fast_snapshot, export_fast_updates, + self, decode_snapshot, decode_snapshot_bytes, export_fast_snapshot, export_fast_updates, export_fast_updates_in_range, export_shallow_snapshot, export_snapshot_at, export_state_only_snapshot, json_schema::{encode_change_to_json, json::JsonSchema}, @@ -42,6 +42,7 @@ use crate::{ lock::{LoroLockGroup, LoroMutex}, txn::Transaction, }; +use bytes::Bytes; use either::Either; #[cfg(test)] @@ -581,11 +582,18 @@ impl LoroDoc { self.config.text_style_config.write().default_style = text_style; } pub fn from_snapshot(bytes: &[u8]) -> LoroResult { + Self::from_snapshot_bytes(Bytes::copy_from_slice(bytes)) + } + + /// Like [`Self::from_snapshot`], but keeps references into `bytes` instead + /// of copying the snapshot. + pub fn from_snapshot_bytes(bytes: Bytes) -> LoroResult { let doc = Self::new(); - let ParsedHeaderAndBody { mode, body, .. } = parse_header_and_body(bytes, true)?; + let ParsedHeaderAndBody { mode, body, .. } = parse_header_and_body(&bytes, true)?; if mode.is_snapshot() { + let body = bytes.slice_ref(body); doc.with_barrier(|| -> Result<(), LoroError> { - decode_snapshot(&doc, mode, body, Default::default())?; + decode_snapshot_bytes(&doc, mode, body, Default::default())?; Ok(()) })?; Ok(doc) diff --git a/crates/loro/Cargo.toml b/crates/loro/Cargo.toml index 295975201..372f48f07 100644 --- a/crates/loro/Cargo.toml +++ b/crates/loro/Cargo.toml @@ -14,6 +14,7 @@ keywords = ["crdt", "local-first"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bytes = { workspace = true } loro-internal = { path = "../loro-internal", version = "1.17.1" } loro-common = { path = "../loro-common", version = "1.17.0", features = [ "serde_json", diff --git a/crates/loro/src/lib.rs b/crates/loro/src/lib.rs index 2bc5a1a03..9fa682220 100644 --- a/crates/loro/src/lib.rs +++ b/crates/loro/src/lib.rs @@ -742,6 +742,16 @@ impl LoroDoc { Ok(Self::_new(inner)) } + /// Create a new `LoroDoc` from a snapshot without copying it. + /// + /// The document keeps references into `bytes` and decodes containers when + /// they are first read, so a memory-mapped snapshot is only read where used. + pub fn from_snapshot_bytes(bytes: bytes::Bytes) -> LoroResult { + let inner = InnerLoroDoc::from_snapshot_bytes(bytes)?; + inner.start_auto_commit(); + Ok(Self::_new(inner)) + } + /// Import data exported by [`LoroDoc::export`] or [`LoroDoc::export_pending_updates`]. /// /// Use [`ExportMode::Snapshot`] for full-state snapshots, or diff --git a/crates/loro/tests/loro_rust_test.rs b/crates/loro/tests/loro_rust_test.rs index 3df232a9f..e0a6d2cca 100644 --- a/crates/loro/tests/loro_rust_test.rs +++ b/crates/loro/tests/loro_rust_test.rs @@ -3613,6 +3613,17 @@ fn test_from_shallow_snapshot() { assert_eq!(new_doc.get_deep_value(), LoroValue::Map(expected)); } +#[test] +#[parallel] +fn test_from_snapshot_bytes_keeps_the_buffer() { + let doc = LoroDoc::new(); + doc.get_text("text").insert(0, "Hello").unwrap(); + let snapshot = bytes::Bytes::from(doc.export(ExportMode::Snapshot).unwrap()); + let new_doc = LoroDoc::from_snapshot_bytes(snapshot.clone()).unwrap(); + assert!(!snapshot.is_unique()); + assert_eq!(new_doc.get_deep_value(), doc.get_deep_value()); +} + #[test] #[parallel] fn test_checkout_to_unknown_version() {