diff --git a/crates/loro-internal/src/encoding/encode_enhanced.rs b/crates/loro-internal/src/encoding/encode_enhanced.rs index f7c89697..bd92ca0f 100644 --- a/crates/loro-internal/src/encoding/encode_enhanced.rs +++ b/crates/loro-internal/src/encoding/encode_enhanced.rs @@ -677,7 +677,7 @@ pub fn decode_oplog_v2(oplog: &mut OpLog, input: &[u8]) -> Result<(), LoroError> // convert change into inner format let mut ops = RleVec::new(); for op in change.ops { - let mut lamport = change.lamport; + let lamport = change.lamport; let content = op.content; let op = converter.convert_single_op( &op.container, @@ -686,7 +686,6 @@ pub fn decode_oplog_v2(oplog: &mut OpLog, input: &[u8]) -> Result<(), LoroError> lamport, content, ); - lamport += op.atom_len() as Lamport; ops.push(op); } diff --git a/crates/loro-internal/src/loro.rs b/crates/loro-internal/src/loro.rs index a182fa24..8f320230 100644 --- a/crates/loro-internal/src/loro.rs +++ b/crates/loro-internal/src/loro.rs @@ -83,6 +83,20 @@ impl LoroDoc { doc } + pub fn from_snapshot(bytes: &[u8]) -> LoroResult { + let doc = Self::new(); + let (input, mode) = parse_encode_header(bytes)?; + match mode { + EncodeMode::Snapshot => { + decode_app_snapshot(&doc, input, true)?; + Ok(doc) + } + _ => Err(LoroError::DecodeError( + "Invalid encode mode".to_string().into(), + )), + } + } + /// Is the document empty? (no ops) #[inline(always)] pub fn can_reset_with_snapshot(&self) -> bool { @@ -340,21 +354,7 @@ impl LoroDoc { bytes: &[u8], origin: string_cache::Atom, ) -> Result<(), LoroError> { - if bytes.len() <= 6 { - return Err(LoroError::DecodeError("Invalid bytes".into())); - } - - let (magic_bytes, input) = bytes.split_at(4); - let magic_bytes: [u8; 4] = magic_bytes.try_into().unwrap(); - if magic_bytes != MAGIC_BYTES { - return Err(LoroError::DecodeError("Invalid header bytes".into())); - } - let (version, input) = input.split_at(1); - if version != [ENCODE_SCHEMA_VERSION] { - return Err(LoroError::DecodeError("Invalid version".into())); - } - - let mode: EncodeMode = input[0].try_into()?; + let (input, mode) = parse_encode_header(bytes)?; match mode { EncodeMode::Updates | EncodeMode::RleUpdates | EncodeMode::CompressedRleUpdates => { // TODO: need to throw error if state is in transaction @@ -386,10 +386,10 @@ impl LoroDoc { } EncodeMode::Snapshot => { if self.can_reset_with_snapshot() { - decode_app_snapshot(self, &input[1..], !self.detached)?; + decode_app_snapshot(self, input, !self.detached)?; } else { let app = LoroDoc::new(); - decode_app_snapshot(&app, &input[1..], false)?; + decode_app_snapshot(&app, input, false)?; let oplog = self.oplog.lock().unwrap(); // TODO: PERF: the ser and de can be optimized out let updates = app.export_from(oplog.vv()); @@ -629,6 +629,23 @@ impl LoroDoc { } } +fn parse_encode_header(bytes: &[u8]) -> Result<(&[u8], EncodeMode), LoroError> { + if bytes.len() <= 6 { + return Err(LoroError::DecodeError("Invalid import data".into())); + } + let (magic_bytes, input) = bytes.split_at(4); + let magic_bytes: [u8; 4] = magic_bytes.try_into().unwrap(); + if magic_bytes != MAGIC_BYTES { + return Err(LoroError::DecodeError("Invalid header bytes".into())); + } + let (version, input) = input.split_at(1); + if version != [ENCODE_SCHEMA_VERSION] { + return Err(LoroError::DecodeError("Invalid version".into())); + } + let mode: EncodeMode = input[0].try_into()?; + Ok((&input[1..], mode)) +} + impl Default for LoroDoc { fn default() -> Self { Self::new() diff --git a/crates/loro-internal/src/oplog/pending_changes.rs b/crates/loro-internal/src/oplog/pending_changes.rs index e8d984dc..5c21c803 100644 --- a/crates/loro-internal/src/oplog/pending_changes.rs +++ b/crates/loro-internal/src/oplog/pending_changes.rs @@ -208,7 +208,7 @@ impl OpLog { pub(super) fn to_local_op(change: Change, converter: &mut OpConverter) -> Change { let mut ops = RleVec::new(); for op in change.ops { - let mut lamport = change.lamport; + let lamport = change.lamport; let content = op.content; let op = converter.convert_single_op( &op.container, @@ -217,7 +217,6 @@ pub(super) fn to_local_op(change: Change, converter: &mut OpConverter) lamport, content, ); - lamport += op.atom_len() as Lamport; ops.push(op); } Change { diff --git a/crates/loro-internal/tests/test.rs b/crates/loro-internal/tests/test.rs index fd196bde..d0c2cd11 100644 --- a/crates/loro-internal/tests/test.rs +++ b/crates/loro-internal/tests/test.rs @@ -43,6 +43,21 @@ fn import_after_init_handlers() { a.import(&b.export_snapshot()).unwrap(); a.commit(); } +fn test_from_snapshot() { + let a = LoroDoc::new_auto_commit(); + a.get_text("text").insert_(0, "0").unwrap(); + let snapshot = a.export_snapshot(); + let c = LoroDoc::from_snapshot(&snapshot).unwrap(); + assert_eq!(a.get_deep_value(), c.get_deep_value()); + assert_eq!(a.oplog_frontiers(), c.oplog_frontiers()); + assert_eq!(a.state_frontiers(), c.state_frontiers()); + let updates = a.export_from(&Default::default()); + let d = match LoroDoc::from_snapshot(&updates) { + Ok(_) => panic!(), + Err(e) => e, + }; + assert!(matches!(d, loro_common::LoroError::DecodeError(..))); +} #[test] fn test_pending() { diff --git a/crates/loro-wasm/src/lib.rs b/crates/loro-wasm/src/lib.rs index 24fc4bb7..ce11c219 100644 --- a/crates/loro-wasm/src/lib.rs +++ b/crates/loro-wasm/src/lib.rs @@ -146,6 +146,12 @@ impl Loro { Self(doc) } + #[wasm_bindgen(js_name = "fromSnapshot")] + pub fn from_snapshot(snapshot: &[u8]) -> JsResult { + let doc = LoroDoc::from_snapshot(snapshot)?; + Ok(Loro(doc)) + } + pub fn attach(&mut self) { self.0.attach(); }