Skip to main content

loro_internal/
loro.rs

1use crate::encoding::json_schema::{encode_change, export_json_in_id_span};
2pub use crate::encoding::ExportMode;
3use crate::pre_commit::{FirstCommitFromPeerCallback, FirstCommitFromPeerPayload};
4pub use crate::state::analyzer::{ContainerAnalysisInfo, DocAnalysis};
5use crate::sync::{AtomicBool, AtomicUsize};
6pub(crate) use crate::LoroDocInner;
7use crate::{
8    arena::SharedArena,
9    change::{Change, Timestamp},
10    configure::{Configure, DefaultRandom, SecureRandomGenerator, StyleConfig},
11    container::{
12        idx::ContainerIdx, list::list_op::InnerListOp, richtext::config::StyleConfigMap,
13        IntoContainerId,
14    },
15    cursor::{AbsolutePosition, CannotFindRelativePosition, Cursor, PosQueryResult},
16    dag::{Dag, DagUtils},
17    diff_calc::{DiffCalculator, DiffMode},
18    encoding::{
19        self, decode_snapshot, export_fast_snapshot, export_fast_updates,
20        export_fast_updates_in_range, export_shallow_snapshot, export_snapshot_at,
21        export_state_only_snapshot,
22        json_schema::{encode_change_to_json, json::JsonSchema},
23        parse_header_and_body, EncodeMode, ImportBlobMetadata, ImportStatus, ParsedHeaderAndBody,
24    },
25    event::{str_to_path, EventTriggerKind, Index, InternalDocDiff},
26    handler::{Handler, MovableListHandler, TextHandler, TreeHandler, ValueOrHandler},
27    id::PeerID,
28    json::JsonChange,
29    op::InnerContent,
30    oplog::{loro_dag::FrontiersNotIncluded, OpLog},
31    state::DocState,
32    subscription::{LocalUpdateCallback, Observer, Subscriber},
33    undo::DiffBatch,
34    utils::subscription::{SubscriberSetWithQueue, Subscription},
35    version::{shrink_frontiers, Frontiers, ImVersionVector, VersionRange, VersionVectorDiff},
36    ChangeMeta, DocDiff, HandlerTrait, InternalString, ListHandler, LoroDoc, LoroError, MapHandler,
37    VersionVector,
38};
39use crate::{change::ChangeRef, lock::LockKind};
40use crate::{lock::LoroMutexGuard, pre_commit::PreCommitCallback};
41use crate::{
42    lock::{LoroLockGroup, LoroMutex},
43    txn::Transaction,
44};
45use either::Either;
46use loro_common::{
47    ContainerID, ContainerType, HasCounterSpan, HasIdSpan, HasLamportSpan, IdSpan, LoroEncodeError,
48    LoroResult, LoroValue, ID,
49};
50use rle::HasLength;
51use rustc_hash::{FxHashMap, FxHashSet};
52use std::{
53    borrow::Cow,
54    cmp::Ordering,
55    collections::{hash_map::Entry, BinaryHeap},
56    ops::ControlFlow,
57    sync::{
58        atomic::Ordering::{Acquire, Release},
59        Arc,
60    },
61};
62use tracing::{debug_span, info_span, instrument, warn};
63
64impl Default for LoroDoc {
65    fn default() -> Self {
66        Self::new()
67    }
68}
69
70impl std::fmt::Debug for LoroDocInner {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("LoroDoc")
73            .field("config", &self.config)
74            .field("auto_commit", &self.auto_commit)
75            .field("detached", &self.detached)
76            .finish()
77    }
78}
79
80impl LoroDoc {
81    /// Run the provided closure within a commit barrier.
82    ///
83    /// This finalizes any pending auto-commit transaction first (preserving
84    /// options across an empty txn), executes `f`, then renews the transaction
85    /// (carrying preserved options) if auto-commit is enabled. This is the
86    /// common implicit-commit pattern used by internal operations such as
87    /// import/export/checkouts.
88    #[inline]
89    pub fn with_barrier<F, R>(&self, f: F) -> R
90    where
91        F: FnOnce() -> R,
92    {
93        let (options, guard) = self.implicit_commit_then_stop();
94        let result = f();
95        drop(guard);
96        self.renew_txn_if_auto_commit(options);
97        result
98    }
99
100    pub fn new() -> Self {
101        let visible_op_count = Arc::new(AtomicUsize::new(0));
102        let oplog = OpLog::new(visible_op_count.clone());
103        let arena = oplog.arena.clone();
104        let config: Configure = oplog.configure.clone();
105        let lock_group = LoroLockGroup::new();
106        let global_txn = Arc::new(lock_group.new_lock(None, LockKind::Txn));
107        let inner = Arc::new_cyclic(|w| {
108            let state = DocState::new_arc(w.clone(), arena.clone(), config.clone(), &lock_group);
109            LoroDocInner {
110                oplog: Arc::new(lock_group.new_lock(oplog, LockKind::OpLog)),
111                state,
112                config,
113                visible_op_count,
114                detached: AtomicBool::new(false),
115                auto_commit: AtomicBool::new(false),
116                observer: Arc::new(Observer::new(arena.clone())),
117                diff_calculator: Arc::new(
118                    lock_group.new_lock(DiffCalculator::new(true), LockKind::DiffCalculator),
119                ),
120                txn: global_txn,
121                arena,
122                local_update_subs: SubscriberSetWithQueue::new(),
123                peer_id_change_subs: SubscriberSetWithQueue::new(),
124                pre_commit_subs: SubscriberSetWithQueue::new(),
125                first_commit_from_peer_subs: SubscriberSetWithQueue::new(),
126            }
127        });
128        LoroDoc { inner }
129    }
130
131    pub fn fork(&self) -> Self {
132        if self.is_detached() {
133            return self
134                .fork_at(&self.state_frontiers())
135                .expect("fork_at on detached doc should not fail");
136        }
137
138        let snapshot = self
139            .with_barrier(|| encoding::fast_snapshot::encode_snapshot_inner(self))
140            .expect("forking a valid document should encode a snapshot");
141        let doc = Self::new();
142        doc.with_barrier(|| {
143            encoding::fast_snapshot::decode_snapshot_inner(snapshot, &doc, Default::default())
144        })
145        .unwrap();
146        doc.set_config(&self.config);
147        if self.auto_commit.load(std::sync::atomic::Ordering::Relaxed) {
148            doc.start_auto_commit();
149        }
150        doc
151    }
152    /// Enables editing of the document in detached mode.
153    ///
154    /// By default, the document cannot be edited in detached mode (after calling
155    /// `detach` or checking out a version other than the latest). This method
156    /// allows editing in detached mode.
157    ///
158    /// # Important Notes:
159    ///
160    /// - After enabling this mode, the document will use a different PeerID. Each
161    ///   time you call checkout, a new PeerID will be used.
162    /// - If you set a custom PeerID while this mode is enabled, ensure that
163    ///   concurrent operations with the same PeerID are not possible.
164    /// - On detached mode, importing will not change the state of the document.
165    ///   It also doesn't change the version of the [DocState]. The changes will be
166    ///   recorded into [OpLog] only. You need to call `checkout` to make it take effect.
167    pub fn set_detached_editing(&self, enable: bool) {
168        self.config.set_detached_editing(enable);
169        if enable && self.is_detached() {
170            self.with_barrier(|| {
171                self.renew_peer_id();
172            });
173        }
174    }
175
176    /// Create a doc with auto commit enabled.
177    #[inline]
178    pub fn new_auto_commit() -> Self {
179        let doc = Self::new();
180        doc.start_auto_commit();
181        doc
182    }
183
184    #[inline(always)]
185    pub fn set_peer_id(&self, peer: PeerID) -> LoroResult<()> {
186        if peer == PeerID::MAX {
187            return Err(LoroError::InvalidPeerID);
188        }
189        let next_id = self.oplog.lock().next_id(peer);
190        if self.auto_commit.load(Acquire) {
191            let doc_state = self.state.lock();
192            doc_state
193                .peer
194                .store(peer, std::sync::atomic::Ordering::Relaxed);
195
196            if doc_state.is_in_txn() {
197                drop(doc_state);
198                // Use implicit-style barrier to avoid swallowing next-commit options
199                self.with_barrier(|| {});
200            }
201            self.peer_id_change_subs.emit(&(), next_id);
202            return Ok(());
203        }
204
205        let doc_state = self.state.lock();
206        if doc_state.is_in_txn() {
207            return Err(LoroError::TransactionError(
208                "Cannot change peer id during transaction"
209                    .to_string()
210                    .into_boxed_str(),
211            ));
212        }
213
214        doc_state
215            .peer
216            .store(peer, std::sync::atomic::Ordering::Relaxed);
217        drop(doc_state);
218        self.peer_id_change_subs.emit(&(), next_id);
219        Ok(())
220    }
221
222    /// Renews the PeerID for the document.
223    pub(crate) fn renew_peer_id(&self) {
224        let mut peer_id = DefaultRandom.next_u64();
225        while peer_id == PeerID::MAX {
226            peer_id = DefaultRandom.next_u64();
227        }
228        self.set_peer_id(peer_id).unwrap();
229    }
230
231    /// Implicitly commit the cumulative auto-commit transaction.
232    /// This method only has effect when `auto_commit` is true.
233    ///
234    /// Follow-ups: the caller is responsible for renewing the transaction
235    /// as needed (e.g., via `renew_txn_if_auto_commit`). Prefer using
236    /// `with_barrier(...)` for most internal flows to handle this safely.
237    ///
238    /// Empty-commit behavior: if the pending transaction is empty, the returned
239    /// `Some(CommitOptions)` preserves next-commit options such as message and
240    /// timestamp so they can carry into the renewed transaction. Transient
241    /// labels like `origin` do not carry across an empty commit.
242    #[inline]
243    #[must_use]
244    pub fn implicit_commit_then_stop(
245        &self,
246    ) -> (
247        Option<CommitOptions>,
248        LoroMutexGuard<'_, Option<Transaction>>,
249    ) {
250        // Implicit commit: preserve options on empty commit
251        let (a, b) = self.commit_internal(CommitOptions::new().immediate_renew(false), true);
252        (a, b.unwrap())
253    }
254
255    /// Commit the cumulative auto commit transaction.
256    /// It will start the next one immediately
257    ///
258    /// It only returns Some(options_of_the_empty_txn) when the txn is empty
259    #[inline]
260    pub fn commit_then_renew(&self) -> Option<CommitOptions> {
261        // Explicit commit: swallow options on empty commit
262        self.commit_internal(CommitOptions::new().immediate_renew(true), false)
263            .0
264    }
265
266    /// This method is called before the commit.
267    /// It can be used to modify the change before it is committed.
268    ///
269    /// It return Some(txn) if the txn is None
270    fn before_commit(&self) -> Option<LoroMutexGuard<'_, Option<Transaction>>> {
271        let mut txn_guard = self.txn.lock();
272        let Some(txn) = txn_guard.as_mut() else {
273            return Some(txn_guard);
274        };
275
276        if txn.is_peer_first_appearance {
277            txn.is_peer_first_appearance = false;
278            drop(txn_guard);
279            // First commit from a peer
280            self.first_commit_from_peer_subs.emit(
281                &(),
282                FirstCommitFromPeerPayload {
283                    peer: self.peer_id(),
284                },
285            );
286        }
287
288        None
289    }
290
291    /// Core implementation for committing the cumulative auto-commit transaction.
292    ///
293    /// - When `preserve_on_empty` is true (implicit commits like export/checkout),
294    ///   commit options from an empty transaction are carried over to the next transaction
295    ///   (except `origin`, which never carries across an empty commit).
296    /// - When `preserve_on_empty` is false (explicit commits), commit options from an
297    ///   empty transaction are swallowed and NOT carried over.
298    #[instrument(skip_all)]
299    fn commit_internal(
300        &self,
301        config: CommitOptions,
302        preserve_on_empty: bool,
303    ) -> (
304        Option<CommitOptions>,
305        Option<LoroMutexGuard<'_, Option<Transaction>>>,
306    ) {
307        if !self.auto_commit.load(Acquire) {
308            let txn_guard = self.txn.lock();
309            // if not auto_commit, nothing should happen
310            // because the global txn is not used
311            return (None, Some(txn_guard));
312        }
313
314        loop {
315            if let Some(txn_guard) = self.before_commit() {
316                return (None, Some(txn_guard));
317            }
318
319            let mut txn_guard = self.txn.lock();
320            let txn = txn_guard.take();
321            let Some(mut txn) = txn else {
322                return (None, Some(txn_guard));
323            };
324            let on_commit = txn.take_on_commit();
325            if let Some(origin) = config.origin.clone() {
326                txn.set_origin(origin);
327            }
328
329            if let Some(timestamp) = config.timestamp {
330                txn.set_timestamp(timestamp);
331            }
332
333            if let Some(msg) = config.commit_msg.as_ref() {
334                txn.set_msg(Some(msg.clone()));
335            }
336
337            let id_span = txn.id_span();
338            let mut options = txn.commit().unwrap();
339            // Empty commit returns Some(options). We may preserve parts of it for implicit commits.
340            if let Some(opts) = options.as_mut() {
341                // `origin` is an event-only label and never carries across an empty commit
342                if config.origin.is_some() {
343                    opts.set_origin(None);
344                }
345                // For explicit commits, swallow options from empty commit entirely
346                if !preserve_on_empty {
347                    options = None;
348                }
349            }
350            if config.immediate_renew && self.can_edit() {
351                let mut t = self.txn().unwrap();
352                if let Some(options) = options.as_ref() {
353                    t.set_options(options.clone());
354                }
355                *txn_guard = Some(t);
356            }
357
358            if let Some(on_commit) = on_commit {
359                drop(txn_guard);
360                on_commit(&self.state, &self.oplog, id_span);
361                txn_guard = self.txn.lock();
362                if !config.immediate_renew && txn_guard.is_some() {
363                    // make sure that txn_guard is None when config.immediate_renew is false
364                    continue;
365                }
366            }
367
368            return (
369                options,
370                if !config.immediate_renew {
371                    Some(txn_guard)
372                } else {
373                    None
374                },
375            );
376        }
377    }
378
379    /// Commit the cumulative auto commit transaction (explicit API).
380    ///
381    /// This is used by user-facing explicit commits. If the transaction is empty,
382    /// any provided commit options are swallowed and will NOT carry over.
383    #[instrument(skip_all)]
384    pub fn commit_with(
385        &self,
386        config: CommitOptions,
387    ) -> (
388        Option<CommitOptions>,
389        Option<LoroMutexGuard<'_, Option<Transaction>>>,
390    ) {
391        self.commit_internal(config, false)
392    }
393
394    /// Set the commit message of the next commit
395    pub fn set_next_commit_message(&self, message: &str) {
396        let mut binding = self.txn.lock();
397        let Some(txn) = binding.as_mut() else {
398            return;
399        };
400
401        if message.is_empty() {
402            txn.set_msg(None)
403        } else {
404            txn.set_msg(Some(message.into()))
405        }
406    }
407
408    /// Set the origin of the next commit
409    pub fn set_next_commit_origin(&self, origin: &str) {
410        let mut txn = self.txn.lock();
411        if let Some(txn) = txn.as_mut() {
412            txn.set_origin(origin.into());
413        }
414    }
415
416    /// Set the timestamp of the next commit
417    pub fn set_next_commit_timestamp(&self, timestamp: Timestamp) {
418        let mut txn = self.txn.lock();
419        if let Some(txn) = txn.as_mut() {
420            txn.set_timestamp(timestamp);
421        }
422    }
423
424    /// Set the options of the next commit
425    pub fn set_next_commit_options(&self, options: CommitOptions) {
426        let mut txn = self.txn.lock();
427        if let Some(txn) = txn.as_mut() {
428            txn.set_options(options);
429        }
430    }
431
432    /// Clear the options of the next commit
433    pub fn clear_next_commit_options(&self) {
434        let mut txn = self.txn.lock();
435        if let Some(txn) = txn.as_mut() {
436            txn.set_options(CommitOptions::new());
437        }
438    }
439
440    /// Set whether to record the timestamp of each change. Default is `false`.
441    ///
442    /// If enabled, the Unix timestamp will be recorded for each change automatically.
443    ///
444    /// You can also set each timestamp manually when you commit a change.
445    /// The timestamp manually set will override the automatic one.
446    ///
447    /// NOTE: Timestamps are forced to be in ascending order.
448    /// If you commit a new change with a timestamp that is less than the existing one,
449    /// the largest existing timestamp will be used instead.
450    #[inline]
451    pub fn set_record_timestamp(&self, record: bool) {
452        self.config.set_record_timestamp(record);
453    }
454
455    /// Set the interval of mergeable changes, in seconds.
456    ///
457    /// If two continuous local changes are within the interval, they will be merged into one change.
458    /// The default value is 1000 seconds.
459    #[inline]
460    pub fn set_change_merge_interval(&self, interval: i64) {
461        self.config.set_merge_interval(interval);
462    }
463
464    pub fn can_edit(&self) -> bool {
465        !self.is_detached() || self.config.detached_editing()
466    }
467
468    pub fn is_detached_editing_enabled(&self) -> bool {
469        self.config.detached_editing()
470    }
471
472    #[inline]
473    pub fn config_text_style(&self, text_style: StyleConfigMap) {
474        self.config.text_style_config.write().map = text_style.map;
475    }
476
477    #[inline]
478    pub fn config_default_text_style(&self, text_style: Option<StyleConfig>) {
479        self.config.text_style_config.write().default_style = text_style;
480    }
481    pub fn from_snapshot(bytes: &[u8]) -> LoroResult<Self> {
482        let doc = Self::new();
483        let ParsedHeaderAndBody { mode, body, .. } = parse_header_and_body(bytes, true)?;
484        if mode.is_snapshot() {
485            doc.with_barrier(|| -> Result<(), LoroError> {
486                decode_snapshot(&doc, mode, body, Default::default())?;
487                Ok(())
488            })?;
489            Ok(doc)
490        } else {
491            Err(LoroError::DecodeError(
492                "Invalid encode mode".to_string().into(),
493            ))
494        }
495    }
496
497    /// Is the document empty? (no ops)
498    #[inline(always)]
499    pub fn can_reset_with_snapshot(&self) -> bool {
500        let oplog = self.oplog.lock();
501        if oplog.batch_importing {
502            return false;
503        }
504
505        if self.is_detached() {
506            return false;
507        }
508
509        oplog.is_empty() && self.state.lock().can_import_snapshot()
510    }
511
512    /// Whether [OpLog] and [DocState] are detached.
513    ///
514    /// If so, the document is in readonly mode by default and importing will not change the state of the document.
515    /// It also doesn't change the version of the [DocState]. The changes will be recorded into [OpLog] only.
516    /// You need to call `checkout` to make it take effect.
517    #[inline(always)]
518    pub fn is_detached(&self) -> bool {
519        self.detached.load(Acquire)
520    }
521
522    pub(crate) fn set_detached(&self, detached: bool) {
523        self.detached.store(detached, Release);
524    }
525
526    #[inline(always)]
527    pub fn peer_id(&self) -> PeerID {
528        self.state
529            .lock()
530            .peer
531            .load(std::sync::atomic::Ordering::Relaxed)
532    }
533
534    #[inline(always)]
535    pub fn detach(&self) {
536        self.with_barrier(|| self.set_detached(true));
537    }
538
539    #[inline(always)]
540    pub fn attach(&self) {
541        self.checkout_to_latest()
542    }
543
544    /// Get the timestamp of the current state.
545    /// It's the last edit time of the [DocState].
546    pub fn state_timestamp(&self) -> Timestamp {
547        // Acquire locks in correct order: read frontiers first, then query OpLog.
548        let f = { self.state.lock().frontiers.clone() };
549        self.oplog.lock().get_timestamp_of_version(&f)
550    }
551
552    #[inline(always)]
553    pub fn app_state(&self) -> &Arc<LoroMutex<DocState>> {
554        &self.state
555    }
556
557    #[inline]
558    pub fn get_state_deep_value(&self) -> LoroValue {
559        self.state.lock().get_deep_value()
560    }
561
562    #[inline(always)]
563    pub fn oplog(&self) -> &Arc<LoroMutex<OpLog>> {
564        &self.oplog
565    }
566
567    #[inline(always)]
568    pub fn import(&self, bytes: &[u8]) -> Result<ImportStatus, LoroError> {
569        let s = debug_span!("import", peer = self.peer_id());
570        let _e = s.enter();
571        self.import_with(bytes, Default::default())
572    }
573
574    #[inline]
575    pub fn import_with(
576        &self,
577        bytes: &[u8],
578        origin: InternalString,
579    ) -> Result<ImportStatus, LoroError> {
580        self.with_barrier(|| self._import_with(bytes, origin))
581    }
582
583    #[tracing::instrument(skip_all)]
584    fn _import_with(
585        &self,
586        bytes: &[u8],
587        origin: InternalString,
588    ) -> Result<ImportStatus, LoroError> {
589        ensure_cov::notify_cov("loro_internal::import");
590        let parsed = parse_header_and_body(bytes, true)?;
591        loro_common::info!("Importing with mode={:?}", &parsed.mode);
592        let result = match parsed.mode {
593            EncodeMode::OutdatedRle => {
594                if self.state.lock().is_in_txn() {
595                    return Err(LoroError::ImportWhenInTxn);
596                }
597
598                let s = tracing::span!(
599                    tracing::Level::INFO,
600                    "Import updates ",
601                    peer = self.peer_id()
602                );
603                let _e = s.enter();
604                self.update_oplog_and_apply_delta_to_state_if_needed(
605                    |oplog| oplog.decode(parsed),
606                    origin,
607                )
608            }
609            EncodeMode::OutdatedSnapshot => {
610                if self.can_reset_with_snapshot() {
611                    loro_common::info!("Init by snapshot {}", self.peer_id());
612                    decode_snapshot(self, parsed.mode, parsed.body, origin)
613                } else {
614                    self.update_oplog_and_apply_delta_to_state_if_needed(
615                        |oplog| oplog.decode(parsed),
616                        origin,
617                    )
618                }
619            }
620            EncodeMode::FastSnapshot => {
621                if self.can_reset_with_snapshot() {
622                    ensure_cov::notify_cov("loro_internal::import::snapshot");
623                    loro_common::info!("Init by fast snapshot {}", self.peer_id());
624                    decode_snapshot(self, parsed.mode, parsed.body, origin)
625                } else {
626                    self.import_changes_and_apply_delta_to_state_if_needed(
627                        |oplog| encoding::decode_oplog_changes(oplog, parsed),
628                        origin,
629                    )
630
631                    // let new_doc = LoroDoc::new();
632                    // new_doc.import(bytes)?;
633                    // let updates = new_doc.export(ExportMode::updates(&self.oplog_vv())).unwrap();
634                    // return self.import_with(updates.as_slice(), origin);
635                }
636            }
637            EncodeMode::FastUpdates => self.import_changes_and_apply_delta_to_state_if_needed(
638                |oplog| encoding::decode_oplog_changes(oplog, parsed),
639                origin,
640            ),
641            EncodeMode::Auto => {
642                unreachable!()
643            }
644        };
645
646        self.emit_events();
647
648        result
649    }
650
651    #[tracing::instrument(skip_all)]
652    pub(crate) fn update_oplog_and_apply_delta_to_state_if_needed(
653        &self,
654        f: impl FnOnce(&mut OpLog) -> Result<ImportStatus, LoroError>,
655        origin: InternalString,
656    ) -> Result<ImportStatus, LoroError> {
657        let mut oplog = self.oplog.lock();
658        oplog.begin_import_rollback();
659        if !self.is_detached() {
660            let old_vv = oplog.vv().clone();
661            let old_frontiers = oplog.frontiers().clone();
662            let result = f(&mut oplog);
663            if &old_vv != oplog.vv() {
664                let mut diff = DiffCalculator::new(false);
665                let (diff, diff_mode) = diff.calc_diff_internal(
666                    &oplog,
667                    &old_vv,
668                    &old_frontiers,
669                    oplog.vv(),
670                    oplog.dag.get_frontiers(),
671                    None,
672                );
673                let mut state = self.state.lock();
674                if let Err(e) = state.apply_diff(
675                    InternalDocDiff {
676                        origin,
677                        diff: (diff).into(),
678                        by: EventTriggerKind::Import,
679                        new_version: Cow::Owned(oplog.frontiers().clone()),
680                    },
681                    diff_mode,
682                ) {
683                    oplog.rollback_import();
684                    return Err(e);
685                }
686            }
687            match result {
688                Ok(result) => {
689                    oplog.commit_import_rollback();
690                    Ok(result)
691                }
692                Err(e) => {
693                    // Some import errors are reported after a valid prefix has
694                    // been inserted into the oplog. Keep that prefix if it was
695                    // also applied to state; rollback is for failed state apply
696                    // or decode errors that made no visible oplog progress.
697                    if &old_vv == oplog.vv() {
698                        oplog.rollback_import();
699                    } else {
700                        oplog.commit_import_rollback();
701                    }
702                    Err(e)
703                }
704            }
705        } else {
706            match f(&mut oplog) {
707                Ok(result) => {
708                    oplog.commit_import_rollback();
709                    Ok(result)
710                }
711                Err(e) => {
712                    oplog.rollback_import();
713                    Err(e)
714                }
715            }
716        }
717    }
718
719    #[tracing::instrument(skip_all)]
720    pub(crate) fn import_changes_and_apply_delta_to_state_if_needed(
721        &self,
722        decode_changes: impl FnOnce(&mut OpLog) -> Result<Vec<Change>, LoroError>,
723        origin: InternalString,
724    ) -> Result<ImportStatus, LoroError> {
725        let mut oplog = self.oplog.lock();
726        let arena_checkpoint = oplog.arena.checkpoint_for_rollback();
727        let changes = match decode_changes(&mut oplog) {
728            Ok(changes) => changes,
729            Err(e) => {
730                oplog.arena.rollback(arena_checkpoint);
731                return Err(e);
732            }
733        };
734
735        let preflight = oplog.preflight_import_changes(&changes);
736        if preflight.has_deps_before_shallow_root
737            && (self.is_detached() || !preflight.applies_to_dag)
738        {
739            oplog.arena.rollback(arena_checkpoint);
740            return Err(LoroError::ImportUpdatesThatDependsOnOutdatedVersion);
741        }
742
743        if self.is_detached() {
744            let result = encoding::apply_decoded_changes_to_oplog(&mut oplog, changes);
745            if result.has_deps_before_shallow_root {
746                return Err(LoroError::ImportUpdatesThatDependsOnOutdatedVersion);
747            }
748
749            return Ok(result.status);
750        }
751
752        if !preflight.applies_to_dag {
753            let pending_root_containers = pending_root_containers_to_materialize(&oplog, &changes);
754            let result = encoding::apply_decoded_changes_to_oplog(&mut oplog, changes);
755            if result.has_deps_before_shallow_root {
756                oplog.arena.rollback(arena_checkpoint);
757                return Err(LoroError::ImportUpdatesThatDependsOnOutdatedVersion);
758            }
759
760            if !pending_root_containers.is_empty() {
761                let mut state = self.state.lock();
762                for id in pending_root_containers {
763                    state.ensure_container(&id);
764                }
765            }
766
767            return Ok(result.status);
768        }
769
770        let old_vv = oplog.vv().clone();
771        let old_frontiers = oplog.frontiers().clone();
772        // Checked before the changes are applied, while the store still holds only old history.
773        let isolated_batch = isolated_scalar_root_batch(&oplog, &changes).filter(|(_, names)| {
774            let mut state = self.state.lock();
775            names.iter().all(|name| {
776                let id = ContainerID::new_root(name, ContainerType::Map);
777                !state.store.contains_id(&id)
778            })
779        });
780        let rollback_enabled = preflight.needs_state_apply_rollback;
781        if rollback_enabled {
782            oplog.begin_import_rollback_with_arena(arena_checkpoint);
783        }
784
785        let result = encoding::apply_decoded_changes_to_oplog(&mut oplog, changes);
786        if &old_vv != oplog.vv() {
787            let mut diff = DiffCalculator::new(false);
788            // Applying may have unlocked pending changes; the isolated fast path is only valid
789            // when exactly the candidate batch was appended on top of the old version.
790            let isolated_batch = isolated_batch.filter(|(component_vv, _)| {
791                component_vv
792                    .iter()
793                    .all(|(peer, end)| oplog.vv().get(peer) == Some(end))
794                    && oplog.vv().iter().all(|(peer, end)| {
795                        let old_end = old_vv.get(peer).copied().unwrap_or(0);
796                        let component_end = component_vv.get(peer).copied().unwrap_or(0);
797                        *end == old_end.max(component_end)
798                    })
799            });
800            let (diff, diff_mode) = if let Some((component_vv, _)) = isolated_batch {
801                let component_frontiers = oplog.dag.vv_to_frontiers(&component_vv);
802                let (diff, _) = diff.calc_diff_internal(
803                    &oplog,
804                    &VersionVector::default(),
805                    &Frontiers::default(),
806                    &component_vv,
807                    &component_frontiers,
808                    None,
809                );
810                (diff, DiffMode::Import)
811            } else {
812                diff.calc_diff_internal(
813                    &oplog,
814                    &old_vv,
815                    &old_frontiers,
816                    oplog.vv(),
817                    oplog.dag.get_frontiers(),
818                    None,
819                )
820            };
821            let mut state = self.state.lock();
822            if let Err(e) = state.apply_diff(
823                InternalDocDiff {
824                    origin,
825                    diff: (diff).into(),
826                    by: EventTriggerKind::Import,
827                    new_version: Cow::Owned(oplog.frontiers().clone()),
828                },
829                diff_mode,
830            ) {
831                if rollback_enabled {
832                    oplog.rollback_import();
833                    return Err(e);
834                }
835
836                panic!("state apply returned Err for import without rollback guard: {e}");
837            }
838        }
839
840        if result.has_deps_before_shallow_root {
841            if rollback_enabled {
842                oplog.commit_import_rollback();
843            }
844            return Err(LoroError::ImportUpdatesThatDependsOnOutdatedVersion);
845        }
846
847        if rollback_enabled {
848            oplog.commit_import_rollback();
849        }
850        Ok(result.status)
851    }
852
853    fn emit_events(&self) {
854        // we should not hold the lock when emitting events
855        let events = {
856            let mut state = self.state.lock();
857            state.take_events()
858        };
859        for event in events {
860            self.observer.emit(event);
861        }
862    }
863
864    pub(crate) fn drop_pending_events(&self) -> Vec<DocDiff> {
865        let mut state = self.state.lock();
866        state.take_events()
867    }
868
869    /// Import the json schema updates.
870    ///
871    /// only supports backward compatibility but not forward compatibility.
872    #[tracing::instrument(skip_all)]
873    pub fn import_json_updates<T: TryInto<JsonSchema>>(&self, json: T) -> LoroResult<ImportStatus> {
874        let json = json.try_into().map_err(|_| LoroError::InvalidJsonSchema)?;
875        self.with_barrier(|| {
876            let result = self.import_changes_and_apply_delta_to_state_if_needed(
877                |oplog| crate::encoding::json_schema::decode_json_changes(json, &oplog.arena),
878                Default::default(),
879            );
880            self.emit_events();
881            result
882        })
883    }
884
885    pub fn export_json_updates(
886        &self,
887        start_vv: &VersionVector,
888        end_vv: &VersionVector,
889        with_peer_compression: bool,
890    ) -> JsonSchema {
891        self.with_barrier(|| {
892            let oplog = self.oplog.lock();
893            let mut start_vv = start_vv;
894            let _temp: Option<VersionVector>;
895            if !oplog.dag.shallow_since_vv().is_empty() {
896                // Make sure that start_vv >= shallow_since_vv
897                let mut include_all = true;
898                for (peer, counter) in oplog.dag.shallow_since_vv().iter() {
899                    if start_vv.get(peer).unwrap_or(&0) < counter {
900                        include_all = false;
901                        break;
902                    }
903                }
904                if !include_all {
905                    let mut vv = start_vv.clone();
906                    for (&peer, &counter) in oplog.dag.shallow_since_vv().iter() {
907                        vv.extend_to_include_end_id(ID::new(peer, counter));
908                    }
909                    _temp = Some(vv);
910                    start_vv = _temp.as_ref().unwrap();
911                }
912            }
913
914            crate::encoding::json_schema::export_json(
915                &oplog,
916                start_vv,
917                end_vv,
918                with_peer_compression,
919            )
920        })
921    }
922
923    pub fn export_json_in_id_span(&self, id_span: IdSpan) -> Vec<JsonChange> {
924        let oplog = self.oplog.lock();
925        let mut changes = export_json_in_id_span(&oplog, id_span);
926        if let Some(uncommit) = oplog.get_uncommitted_change_in_span(id_span) {
927            let change_json = encode_change(ChangeRef::from_change(&uncommit), &self.arena, None);
928            changes.push(change_json);
929        }
930        changes
931    }
932
933    /// Get the version vector of the current OpLog
934    #[inline]
935    pub fn oplog_vv(&self) -> VersionVector {
936        self.oplog.lock().vv().clone()
937    }
938
939    /// Get the version vector of the current [DocState]
940    #[inline]
941    pub fn state_vv(&self) -> VersionVector {
942        let oplog = self.oplog.lock();
943        let f = &self.state.lock().frontiers;
944        oplog.dag.frontiers_to_vv(f).unwrap()
945    }
946
947    pub fn get_by_path(&self, path: &[Index]) -> Option<ValueOrHandler> {
948        let value: LoroValue = self.state.lock().get_value_by_path(path)?;
949        if let LoroValue::Container(c) = value {
950            Some(ValueOrHandler::Handler(Handler::new_attached(
951                c.clone(),
952                self.clone(),
953            )))
954        } else {
955            Some(ValueOrHandler::Value(value))
956        }
957    }
958
959    /// Get the handler by the string path.
960    pub fn get_by_str_path(&self, path: &str) -> Option<ValueOrHandler> {
961        let path = str_to_path(path)?;
962        self.get_by_path(&path)
963    }
964
965    pub fn get_uncommitted_ops_as_json(&self) -> Option<JsonSchema> {
966        let arena = &self.arena;
967        let txn = self.txn.lock();
968        let txn = txn.as_ref()?;
969        let ops_ = txn.local_ops();
970        let new_id = ID {
971            peer: *txn.peer(),
972            counter: ops_.first()?.counter,
973        };
974        let change = ChangeRef {
975            id: &new_id,
976            deps: txn.frontiers(),
977            timestamp: &txn
978                .timestamp()
979                .as_ref()
980                .copied()
981                .unwrap_or_else(|| self.oplog.lock().get_timestamp_for_next_txn()),
982            commit_msg: txn.msg(),
983            ops: ops_,
984            lamport: txn.lamport(),
985        };
986        let json = encode_change_to_json(change, arena);
987        Some(json)
988    }
989
990    #[inline]
991    pub fn get_handler(&self, id: ContainerID) -> Option<Handler> {
992        if self.has_container(&id) {
993            self.ensure_root_container(&id);
994            Some(Handler::new_attached(id, self.clone()))
995        } else {
996            None
997        }
998    }
999
1000    #[inline]
1001    fn ensure_root_container(&self, id: &ContainerID) {
1002        // Mergeable roots stay lazily materialized: their existence is derived from the
1003        // parent map's child ref, and a store entry only appears once the child gets its
1004        // own ops (or via the shallow-export alive walk).
1005        if id.is_root() && !id.is_mergeable() {
1006            self.state.lock().ensure_container(id);
1007        }
1008    }
1009
1010    /// id can be a str, ContainerID, or ContainerIdRaw.
1011    /// if it's str it will use Root container, which will not be None
1012    #[inline]
1013    pub fn try_get_text<I: IntoContainerId>(&self, id: I) -> Option<TextHandler> {
1014        let id = id.into_container_id(&self.arena, ContainerType::Text);
1015        if !self.has_container(&id) {
1016            return None;
1017        }
1018        self.ensure_root_container(&id);
1019        Handler::new_attached(id, self.clone()).into_text().ok()
1020    }
1021
1022    /// id can be a str, ContainerID, or ContainerIdRaw.
1023    /// if it's str it will use Root container, which will not be None
1024    #[inline]
1025    pub fn get_text<I: IntoContainerId>(&self, id: I) -> TextHandler {
1026        self.try_get_text(id)
1027            .expect("The container does not exist in the document. Use `try_get_text` or `get_container` to check for existence.")
1028    }
1029
1030    /// id can be a str, ContainerID, or ContainerIdRaw.
1031    /// if it's str it will use Root container, which will not be None
1032    #[inline]
1033    pub fn try_get_list<I: IntoContainerId>(&self, id: I) -> Option<ListHandler> {
1034        let id = id.into_container_id(&self.arena, ContainerType::List);
1035        if !self.has_container(&id) {
1036            return None;
1037        }
1038        self.ensure_root_container(&id);
1039        Handler::new_attached(id, self.clone()).into_list().ok()
1040    }
1041
1042    /// id can be a str, ContainerID, or ContainerIdRaw.
1043    /// if it's str it will use Root container, which will not be None
1044    #[inline]
1045    pub fn get_list<I: IntoContainerId>(&self, id: I) -> ListHandler {
1046        self.try_get_list(id)
1047            .expect("The container does not exist in the document. Use `try_get_list` or `get_container` to check for existence.")
1048    }
1049
1050    /// id can be a str, ContainerID, or ContainerIdRaw.
1051    /// if it's str it will use Root container, which will not be None
1052    #[inline]
1053    pub fn try_get_movable_list<I: IntoContainerId>(&self, id: I) -> Option<MovableListHandler> {
1054        let id = id.into_container_id(&self.arena, ContainerType::MovableList);
1055        if !self.has_container(&id) {
1056            return None;
1057        }
1058        self.ensure_root_container(&id);
1059        Handler::new_attached(id, self.clone())
1060            .into_movable_list()
1061            .ok()
1062    }
1063
1064    /// id can be a str, ContainerID, or ContainerIdRaw.
1065    /// if it's str it will use Root container, which will not be None
1066    #[inline]
1067    pub fn get_movable_list<I: IntoContainerId>(&self, id: I) -> MovableListHandler {
1068        self.try_get_movable_list(id)
1069            .expect("The container does not exist in the document. Use `try_get_movable_list` or `get_container` to check for existence.")
1070    }
1071
1072    /// id can be a str, ContainerID, or ContainerIdRaw.
1073    /// if it's str it will use Root container, which will not be None
1074    #[inline]
1075    pub fn try_get_map<I: IntoContainerId>(&self, id: I) -> Option<MapHandler> {
1076        let id = id.into_container_id(&self.arena, ContainerType::Map);
1077        if !self.has_container(&id) {
1078            return None;
1079        }
1080        self.ensure_root_container(&id);
1081        Handler::new_attached(id, self.clone()).into_map().ok()
1082    }
1083
1084    /// id can be a str, ContainerID, or ContainerIdRaw.
1085    /// if it's str it will use Root container, which will not be None
1086    #[inline]
1087    pub fn get_map<I: IntoContainerId>(&self, id: I) -> MapHandler {
1088        self.try_get_map(id)
1089            .expect("The container does not exist in the document. Use `try_get_map` or `get_container` to check for existence.")
1090    }
1091
1092    /// id can be a str, ContainerID, or ContainerIdRaw.
1093    /// if it's str it will use Root container, which will not be None
1094    #[inline]
1095    pub fn try_get_tree<I: IntoContainerId>(&self, id: I) -> Option<TreeHandler> {
1096        let id = id.into_container_id(&self.arena, ContainerType::Tree);
1097        if !self.has_container(&id) {
1098            return None;
1099        }
1100        self.ensure_root_container(&id);
1101        Handler::new_attached(id, self.clone()).into_tree().ok()
1102    }
1103
1104    /// id can be a str, ContainerID, or ContainerIdRaw.
1105    /// if it's str it will use Root container, which will not be None
1106    #[inline]
1107    pub fn get_tree<I: IntoContainerId>(&self, id: I) -> TreeHandler {
1108        self.try_get_tree(id)
1109            .expect("The container does not exist in the document. Use `try_get_tree` or `get_container` to check for existence.")
1110    }
1111
1112    #[cfg(feature = "counter")]
1113    pub fn try_get_counter<I: IntoContainerId>(
1114        &self,
1115        id: I,
1116    ) -> Option<crate::handler::counter::CounterHandler> {
1117        let id = id.into_container_id(&self.arena, ContainerType::Counter);
1118        if !self.has_container(&id) {
1119            return None;
1120        }
1121        self.ensure_root_container(&id);
1122        Handler::new_attached(id, self.clone()).into_counter().ok()
1123    }
1124
1125    #[cfg(feature = "counter")]
1126    pub fn get_counter<I: IntoContainerId>(
1127        &self,
1128        id: I,
1129    ) -> crate::handler::counter::CounterHandler {
1130        self.try_get_counter(id)
1131            .expect("The container does not exist in the document. Use `try_get_counter` or `get_container` to check for existence.")
1132    }
1133
1134    #[must_use]
1135    pub fn has_container(&self, id: &ContainerID) -> bool {
1136        if id.is_root() && !id.is_mergeable() {
1137            return true;
1138        }
1139
1140        let exist = self.state.lock().does_container_exist(id);
1141        exist
1142    }
1143
1144    /// Undo the operations between the given id_span. It can be used even in a collaborative environment.
1145    ///
1146    /// This is an internal API. You should NOT use it directly.
1147    ///
1148    /// # Internal
1149    ///
1150    /// This method will use the diff calculator to calculate the diff required to time travel
1151    /// from the end of id_span to the beginning of the id_span. Then it will convert the diff to
1152    /// operations and apply them to the OpLog with a dep on the last id of the given id_span.
1153    ///
1154    /// This implementation is kinda slow, but it's simple and maintainable. We can optimize it
1155    /// further when it's needed. The time complexity is O(n + m), n is the ops in the id_span, m is the
1156    /// distance from id_span to the current latest version.
1157    #[instrument(level = "info", skip_all)]
1158    pub fn undo_internal(
1159        &self,
1160        id_span: IdSpan,
1161        container_remap: &mut FxHashMap<ContainerID, ContainerID>,
1162        post_transform_base: Option<&DiffBatch>,
1163        before_diff: &mut dyn FnMut(&DiffBatch),
1164    ) -> LoroResult<CommitWhenDrop<'_>> {
1165        if !self.can_edit() {
1166            return Err(LoroError::EditWhenDetached);
1167        }
1168
1169        let (options, txn) = self.implicit_commit_then_stop();
1170        if !self.oplog().lock().vv().includes_id(id_span.id_last()) {
1171            self.renew_txn_if_auto_commit(options);
1172            return Err(LoroError::UndoInvalidIdSpan(id_span.id_last()));
1173        }
1174
1175        let (was_recording, latest_frontiers) = {
1176            let mut state = self.state.lock();
1177            let was_recording = state.is_recording();
1178            state.stop_and_clear_recording();
1179            (was_recording, state.frontiers.clone())
1180        };
1181
1182        let spans = self.oplog.lock().split_span_based_on_deps(id_span);
1183        let diff = crate::undo::undo(
1184            spans,
1185            match post_transform_base {
1186                Some(d) => Either::Right(d),
1187                None => Either::Left(&latest_frontiers),
1188            },
1189            |from, to| {
1190                self._checkout_without_emitting(from, false, false).unwrap();
1191                self.state.lock().start_recording();
1192                self._checkout_without_emitting(to, false, false).unwrap();
1193                let mut state = self.state.lock();
1194                let e = state.take_events();
1195                state.stop_and_clear_recording();
1196                DiffBatch::new(e)
1197            },
1198            before_diff,
1199        );
1200
1201        // println!("\nundo_internal: diff: {:?}", diff);
1202        // println!("container remap: {:?}", container_remap);
1203
1204        self._checkout_without_emitting(&latest_frontiers, false, false)?;
1205        self.set_detached(false);
1206        if was_recording {
1207            self.state.lock().start_recording();
1208        }
1209        drop(txn);
1210        self.start_auto_commit();
1211        // Try applying the diff, but ignore the error if it happens.
1212        // MovableList's undo behavior is too tricky to handle in a collaborative env
1213        // so in edge cases this may be an Error
1214        if let Err(e) = self._apply_diff(diff, container_remap, true) {
1215            warn!("Undo Failed {:?}", e);
1216        }
1217
1218        if let Some(options) = options {
1219            self.set_next_commit_options(options);
1220        }
1221        Ok(CommitWhenDrop {
1222            doc: self,
1223            default_options: CommitOptions::new().origin("undo"),
1224        })
1225    }
1226
1227    /// Generate a series of local operations that can revert the current doc to the target
1228    /// version.
1229    ///
1230    /// Internally, it will calculate the diff between the current state and the target state,
1231    /// and apply the diff to the current state.
1232    pub fn revert_to(&self, target: &Frontiers) -> LoroResult<()> {
1233        // TODO: test when the doc is readonly
1234        // TODO: test when the doc is detached but enabled editing
1235        let f = self.state_frontiers();
1236        let diff = self.diff(&f, target)?;
1237        self._apply_diff(diff, &mut Default::default(), false)
1238    }
1239
1240    /// Calculate the diff between two versions so that apply diff on a will make the state same as b.
1241    ///
1242    /// NOTE: This method will make the doc enter the **detached mode**.
1243    // FIXME: This method needs testing (no event should be emitted during processing this)
1244    pub fn diff(&self, a: &Frontiers, b: &Frontiers) -> LoroResult<DiffBatch> {
1245        {
1246            // Check whether a and b are valid before checkout so this returns a normal error
1247            // instead of panicking on shallow docs.
1248            let oplog = self.oplog.lock();
1249            let validate_frontiers = |frontiers: &Frontiers| -> LoroResult<()> {
1250                for id in frontiers.iter() {
1251                    if !oplog.dag.contains(id) {
1252                        return Err(LoroError::FrontiersNotFound(id));
1253                    }
1254                }
1255
1256                if oplog.dag.is_before_shallow_root(frontiers) {
1257                    return Err(LoroError::SwitchToVersionBeforeShallowRoot);
1258                }
1259
1260                Ok(())
1261            };
1262
1263            validate_frontiers(a)?;
1264            validate_frontiers(b)?;
1265        }
1266
1267        let (options, txn) = self.implicit_commit_then_stop();
1268        let was_detached = self.is_detached();
1269        let old_frontiers = self.state_frontiers();
1270        let was_recording = {
1271            let mut state = self.state.lock();
1272            let is_recording = state.is_recording();
1273            state.stop_and_clear_recording();
1274            is_recording
1275        };
1276        let result = (|| {
1277            self._checkout_without_emitting(a, true, false)?;
1278            self.state.lock().start_recording();
1279            self._checkout_without_emitting(b, true, false)?;
1280            let mut state = self.state.lock();
1281            let e = state.take_events();
1282            state.stop_and_clear_recording();
1283            Ok::<_, LoroError>(e)
1284        })();
1285
1286        // Always restore state regardless of whether diff calculation succeeded
1287        self._checkout_without_emitting(&old_frontiers, false, false)
1288            .unwrap();
1289        drop(txn);
1290        if !was_detached {
1291            self.set_detached(false);
1292            self.renew_txn_if_auto_commit(options);
1293        }
1294        if was_recording {
1295            self.state.lock().start_recording();
1296        }
1297        result.map(DiffBatch::new)
1298    }
1299
1300    /// Apply a diff to the current state.
1301    #[inline(always)]
1302    pub fn apply_diff(&self, diff: DiffBatch) -> LoroResult<()> {
1303        self._apply_diff(diff, &mut Default::default(), true)
1304    }
1305
1306    /// Apply a diff to the current state.
1307    ///
1308    /// This method will not recreate containers with the same [ContainerID]s.
1309    /// While this can be convenient in certain cases, it can break several internal invariants:
1310    ///
1311    /// 1. Each container should appear only once in the document. Allowing containers with the same ID
1312    ///    would result in multiple instances of the same container in the document.
1313    /// 2. Unreachable containers should be removable from the state when necessary.
1314    ///
1315    /// However, the diff may contain operations that depend on container IDs.
1316    /// Therefore, users need to provide a `container_remap` to record and retrieve the container ID remapping.
1317    pub(crate) fn _apply_diff(
1318        &self,
1319        diff: DiffBatch,
1320        container_remap: &mut FxHashMap<ContainerID, ContainerID>,
1321        skip_unreachable: bool,
1322    ) -> LoroResult<()> {
1323        if !self.can_edit() {
1324            return Err(LoroError::EditWhenDetached);
1325        }
1326
1327        let mut ans: LoroResult<()> = Ok(());
1328        let mut missing_containers: Vec<ContainerID> = Vec::new();
1329        for (mut id, diff) in diff.into_iter() {
1330            let mut remapped = false;
1331            while let Some(rid) = container_remap.get(&id) {
1332                remapped = true;
1333                id = rid.clone();
1334            }
1335
1336            if matches!(&id, ContainerID::Normal { .. }) && self.arena.id_to_idx(&id).is_none() {
1337                // Not in arena does not imply non-existent; consult state/kv and register lazily
1338                let exists = self.state.lock().does_container_exist(&id);
1339                if !exists {
1340                    missing_containers.push(id);
1341                    continue;
1342                }
1343                // Ensure registration so handlers can be created
1344                self.state.lock().ensure_container(&id);
1345            }
1346
1347            if skip_unreachable && !remapped && !self.state.lock().get_reachable(&id) {
1348                continue;
1349            }
1350
1351            let Some(h) = self.get_handler(id.clone()) else {
1352                return Err(LoroError::ContainersNotFound {
1353                    containers: Box::new(vec![id]),
1354                });
1355            };
1356            if let Err(e) = h.apply_diff(diff, container_remap) {
1357                ans = Err(e);
1358            }
1359        }
1360
1361        if !missing_containers.is_empty() {
1362            return Err(LoroError::ContainersNotFound {
1363                containers: Box::new(missing_containers),
1364            });
1365        }
1366
1367        ans
1368    }
1369
1370    /// This is for debugging purpose. It will travel the whole oplog
1371    #[inline]
1372    pub fn diagnose_size(&self) {
1373        self.oplog().lock().diagnose_size();
1374    }
1375
1376    #[inline]
1377    pub fn oplog_frontiers(&self) -> Frontiers {
1378        self.oplog().lock().frontiers().clone()
1379    }
1380
1381    #[inline]
1382    pub fn state_frontiers(&self) -> Frontiers {
1383        self.state.lock().frontiers.clone()
1384    }
1385
1386    /// - Ordering::Less means self is less than target or parallel
1387    /// - Ordering::Equal means versions equal
1388    /// - Ordering::Greater means self's version is greater than target
1389    #[inline]
1390    pub fn cmp_with_frontiers(&self, other: &Frontiers) -> Ordering {
1391        self.oplog().lock().cmp_with_frontiers(other)
1392    }
1393
1394    /// Compare two [Frontiers] causally.
1395    ///
1396    /// If one of the [Frontiers] are not included, it will return [FrontiersNotIncluded].
1397    #[inline]
1398    pub fn cmp_frontiers(
1399        &self,
1400        a: &Frontiers,
1401        b: &Frontiers,
1402    ) -> Result<Option<Ordering>, FrontiersNotIncluded> {
1403        self.oplog().lock().cmp_frontiers(a, b)
1404    }
1405
1406    pub fn subscribe_root(&self, callback: Subscriber) -> Subscription {
1407        let mut state = self.state.lock();
1408        if !state.is_recording() {
1409            state.start_recording();
1410        }
1411
1412        self.observer.subscribe_root(callback)
1413    }
1414
1415    pub fn subscribe(&self, container_id: &ContainerID, callback: Subscriber) -> Subscription {
1416        let mut state = self.state.lock();
1417        if !state.is_recording() {
1418            state.start_recording();
1419        }
1420
1421        self.observer.subscribe(container_id, callback)
1422    }
1423
1424    pub fn subscribe_local_update(&self, callback: LocalUpdateCallback) -> Subscription {
1425        let (sub, activate) = self.local_update_subs.inner().insert((), callback);
1426        activate();
1427        sub
1428    }
1429
1430    // PERF: opt
1431    #[tracing::instrument(skip_all)]
1432    pub fn import_batch(&self, bytes: &[Vec<u8>]) -> LoroResult<ImportStatus> {
1433        if bytes.is_empty() {
1434            return Ok(ImportStatus::default());
1435        }
1436
1437        if bytes.len() == 1 {
1438            return self.import(&bytes[0]);
1439        }
1440
1441        let mut success = VersionRange::default();
1442        let mut meta_arr = bytes
1443            .iter()
1444            .map(|b| Ok((LoroDoc::decode_import_blob_meta(b, false)?, b)))
1445            .collect::<LoroResult<Vec<(ImportBlobMetadata, &Vec<u8>)>>>()?;
1446        meta_arr.sort_by(|a, b| {
1447            a.0.mode
1448                .cmp(&b.0.mode)
1449                .then(b.0.change_num.cmp(&a.0.change_num))
1450        });
1451
1452        let (options, txn) = self.implicit_commit_then_stop();
1453        // Why we should keep locking `txn` here
1454        //
1455        // In a multi-threaded environment, `import_batch` used to drop the txn lock
1456        // (via `commit_then_stop` + `drop(txn)`) and call `detach()`/`checkout_to_latest()`
1457        // around the batch import. That created a race where another thread could
1458        // start or renew the auto-commit txn and perform local edits while we were
1459        // importing and temporarily detached. Those interleaved local edits could
1460        // violate invariants between `OpLog` and `DocState` (e.g., state being
1461        // updated when we expect it not to, missed events, or inconsistent
1462        // frontiers), as exposed by the loom test `local_edits_during_batch_import`.
1463        //
1464        // The fix is to hold the txn mutex for the entire critical section:
1465        // - Stop the current txn and keep the mutex guard.
1466        // - Force-detach with `set_detached(true)` (avoids `detach()` side effects),
1467        //   then run each `_import_with(...)` while detached so imports only touch
1468        //   the `OpLog`.
1469        // - After importing, reattach by checking out to latest and renew the txn
1470        //   using `_checkout_to_latest_with_guard`, which keeps the mutex held while
1471        //   (re)starting the auto-commit txn.
1472        //
1473        // Holding the lock ensures no concurrent thread can create/renew a txn and
1474        // do local edits in the middle of the batch import, making the whole
1475        // operation atomic with respect to local edits.
1476        let is_detached = self.is_detached();
1477        self.set_detached(true);
1478        self.oplog.lock().batch_importing = true;
1479        let mut err = None;
1480        for (_meta, data) in meta_arr {
1481            match self._import_with(data, Default::default()) {
1482                Ok(s) => {
1483                    for (peer, (start, end)) in s.success.iter() {
1484                        match success.0.entry(*peer) {
1485                            Entry::Occupied(mut e) => {
1486                                e.get_mut().1 = *end.max(&e.get().1);
1487                            }
1488                            Entry::Vacant(e) => {
1489                                e.insert((*start, *end));
1490                            }
1491                        }
1492                    }
1493                }
1494                Err(e) => {
1495                    err = Some(e);
1496                }
1497            }
1498        }
1499
1500        let mut oplog = self.oplog.lock();
1501        oplog.batch_importing = false;
1502        let pending = oplog.pending_changes.version_range();
1503        drop(oplog);
1504        if !is_detached {
1505            self._checkout_to_latest_with_guard(txn);
1506        } else {
1507            drop(txn);
1508        }
1509
1510        self.renew_txn_if_auto_commit(options);
1511        if let Some(err) = err {
1512            return Err(err);
1513        }
1514
1515        Ok(ImportStatus {
1516            success,
1517            pending: if pending.is_empty() {
1518                None
1519            } else {
1520                Some(pending)
1521            },
1522        })
1523    }
1524
1525    /// Get shallow value of the document.
1526    #[inline]
1527    pub fn get_value(&self) -> LoroValue {
1528        self.state.lock().get_value()
1529    }
1530
1531    /// Get deep value of the document.
1532    #[inline]
1533    pub fn get_deep_value(&self) -> LoroValue {
1534        self.state.lock().get_deep_value()
1535    }
1536
1537    /// Get deep value of the document with container id
1538    #[inline]
1539    pub fn get_deep_value_with_id(&self) -> LoroValue {
1540        self.state.lock().get_deep_value_with_id()
1541    }
1542
1543    pub fn checkout_to_latest(&self) {
1544        let (options, _guard) = self.implicit_commit_then_stop();
1545        if !self.is_detached() {
1546            drop(_guard);
1547            self.renew_txn_if_auto_commit(options);
1548            return;
1549        }
1550
1551        self._checkout_to_latest_without_commit(true)
1552            .expect("checkout to oplog frontiers should succeed");
1553        self.emit_events();
1554        drop(_guard);
1555        self.renew_txn_if_auto_commit(options);
1556    }
1557
1558    fn _checkout_to_latest_with_guard(&self, guard: LoroMutexGuard<Option<Transaction>>) {
1559        if !self.is_detached() {
1560            self._renew_txn_if_auto_commit_with_guard(None, guard);
1561            return;
1562        }
1563
1564        self._checkout_to_latest_without_commit(true)
1565            .expect("checkout to oplog frontiers should succeed");
1566        self._renew_txn_if_auto_commit_with_guard(None, guard);
1567    }
1568
1569    /// NOTE: The caller of this method should ensure the txn is locked and set to None
1570    pub(crate) fn _checkout_to_latest_without_commit(
1571        &self,
1572        to_commit_then_renew: bool,
1573    ) -> LoroResult<()> {
1574        self._checkout_to_latest_without_commit_with_event(
1575            to_commit_then_renew,
1576            "checkout".into(),
1577            EventTriggerKind::Checkout,
1578        )
1579    }
1580
1581    pub(crate) fn _checkout_to_latest_without_commit_as_import(
1582        &self,
1583        to_commit_then_renew: bool,
1584        origin: InternalString,
1585    ) -> LoroResult<()> {
1586        self._checkout_to_latest_without_commit_with_event(
1587            to_commit_then_renew,
1588            origin,
1589            EventTriggerKind::Import,
1590        )
1591    }
1592
1593    fn _checkout_to_latest_without_commit_with_event(
1594        &self,
1595        to_commit_then_renew: bool,
1596        origin: InternalString,
1597        triggered_by: EventTriggerKind,
1598    ) -> LoroResult<()> {
1599        tracing::info_span!("CheckoutToLatest", peer = self.peer_id()).in_scope(|| {
1600            let f = self.oplog_frontiers();
1601            let this = &self;
1602            let frontiers = &f;
1603            this._checkout_without_emitting_with_event(
1604                frontiers,
1605                false,
1606                to_commit_then_renew,
1607                origin,
1608                triggered_by,
1609            )?;
1610            // We don't need to shrink frontiers because oplog's frontiers are already shrinked.
1611            this.emit_events();
1612            if this.config.detached_editing() {
1613                this.renew_peer_id();
1614            }
1615
1616            self.set_detached(false);
1617            Ok(())
1618        })
1619    }
1620
1621    /// Checkout [DocState] to a specific version.
1622    ///
1623    /// This will make the current [DocState] detached from the latest version of [OpLog].
1624    /// Any further import will not be reflected on the [DocState], until user call [LoroDoc::attach()]
1625    pub fn checkout(&self, frontiers: &Frontiers) -> LoroResult<()> {
1626        let was_detached = self.is_detached();
1627        let (options, guard) = self.implicit_commit_then_stop();
1628        let result = self._checkout_without_emitting(frontiers, true, true);
1629        if result.is_ok() {
1630            self.emit_events();
1631        }
1632        drop(guard);
1633        if self.config.detached_editing() {
1634            if result.is_ok() {
1635                self.renew_peer_id();
1636            }
1637            self.renew_txn_if_auto_commit(options);
1638        } else if result.is_err() {
1639            if !was_detached {
1640                self.renew_txn_if_auto_commit(options);
1641            }
1642        } else if !self.is_detached() {
1643            self.renew_txn_if_auto_commit(options);
1644        }
1645
1646        result
1647    }
1648
1649    /// NOTE: The caller of this method should ensure the txn is locked and set to None
1650    #[instrument(level = "info", skip(self))]
1651    pub(crate) fn _checkout_without_emitting(
1652        &self,
1653        frontiers: &Frontiers,
1654        to_shrink_frontiers: bool,
1655        to_commit_then_renew: bool,
1656    ) -> Result<(), LoroError> {
1657        self._checkout_without_emitting_with_event(
1658            frontiers,
1659            to_shrink_frontiers,
1660            to_commit_then_renew,
1661            "checkout".into(),
1662            EventTriggerKind::Checkout,
1663        )
1664    }
1665
1666    fn _checkout_without_emitting_with_event(
1667        &self,
1668        frontiers: &Frontiers,
1669        to_shrink_frontiers: bool,
1670        _to_commit_then_renew: bool,
1671        origin: InternalString,
1672        triggered_by: EventTriggerKind,
1673    ) -> Result<(), LoroError> {
1674        if !self.txn.is_locked() {
1675            return Err(LoroError::TransactionError(
1676                "checkout requires the transaction mutex to be held"
1677                    .to_string()
1678                    .into_boxed_str(),
1679            ));
1680        }
1681        let from_frontiers = self.state_frontiers();
1682        loro_common::info!(
1683            "checkout from={:?} to={:?} cur_vv={:?}",
1684            from_frontiers,
1685            frontiers,
1686            self.oplog_vv()
1687        );
1688
1689        if &from_frontiers == frontiers {
1690            self.set_detached(frontiers != &self.oplog_frontiers());
1691            return Ok(());
1692        }
1693
1694        let oplog = self.oplog.lock();
1695        if oplog.dag.is_before_shallow_root(frontiers) {
1696            return Err(LoroError::SwitchToVersionBeforeShallowRoot);
1697        }
1698
1699        let frontiers = if to_shrink_frontiers {
1700            shrink_frontiers(frontiers, &oplog.dag).map_err(LoroError::FrontiersNotFound)?
1701        } else {
1702            frontiers.clone()
1703        };
1704
1705        if from_frontiers == frontiers {
1706            return Ok(());
1707        }
1708
1709        let mut state = self.state.lock();
1710        let mut calc = self.diff_calculator.lock();
1711        for i in frontiers.iter() {
1712            if !oplog.dag.contains(i) {
1713                return Err(LoroError::FrontiersNotFound(i));
1714            }
1715        }
1716
1717        let before = oplog.dag.frontiers_to_vv(&state.frontiers).ok_or_else(|| {
1718            LoroError::NotFoundError(
1719                format!(
1720                    "Cannot find the current state version {:?}",
1721                    state.frontiers
1722                )
1723                .into_boxed_str(),
1724            )
1725        })?;
1726        let Some(after) = &oplog.dag.frontiers_to_vv(&frontiers) else {
1727            return Err(LoroError::NotFoundError(
1728                format!("Cannot find the specified version {:?}", frontiers).into_boxed_str(),
1729            ));
1730        };
1731
1732        self.set_detached(true);
1733        let (diff, diff_mode) =
1734            calc.calc_diff_internal(&oplog, &before, &state.frontiers, after, &frontiers, None);
1735        state.apply_diff(
1736            InternalDocDiff {
1737                origin,
1738                diff: Cow::Owned(diff),
1739                by: triggered_by,
1740                new_version: Cow::Owned(frontiers.clone()),
1741            },
1742            diff_mode,
1743        )?;
1744
1745        Ok(())
1746    }
1747
1748    #[inline]
1749    pub fn vv_to_frontiers(&self, vv: &VersionVector) -> Frontiers {
1750        self.oplog.lock().dag.vv_to_frontiers(vv)
1751    }
1752
1753    #[inline]
1754    pub fn frontiers_to_vv(&self, frontiers: &Frontiers) -> Option<VersionVector> {
1755        self.oplog.lock().dag.frontiers_to_vv(frontiers)
1756    }
1757
1758    /// Import ops from other doc.
1759    ///
1760    /// After `a.merge(b)` and `b.merge(a)`, `a` and `b` will have the same content if they are in attached mode.
1761    pub fn merge(&self, other: &Self) -> LoroResult<ImportStatus> {
1762        let updates = other.export(ExportMode::updates(&self.oplog_vv())).unwrap();
1763        self.import(&updates)
1764    }
1765
1766    pub(crate) fn arena(&self) -> &SharedArena {
1767        &self.arena
1768    }
1769
1770    #[inline]
1771    pub fn len_ops(&self) -> usize {
1772        if self.oplog.can_lock_in_this_thread() {
1773            return self.oplog.lock().visible_op_count_exact();
1774        }
1775
1776        self.visible_op_count.load(Acquire)
1777    }
1778
1779    #[inline]
1780    pub fn len_changes(&self) -> usize {
1781        let oplog = self.oplog.lock();
1782        oplog.len_changes()
1783    }
1784
1785    pub fn config(&self) -> &Configure {
1786        &self.config
1787    }
1788
1789    /// This method compare the consistency between the current doc state
1790    /// and the state calculated by diff calculator from beginning.
1791    ///
1792    /// Panic when it's not consistent
1793    pub fn check_state_diff_calc_consistency_slow(&self) {
1794        // #[cfg(any(test, debug_assertions, feature = "test_utils"))]
1795        {
1796            static IS_CHECKING: std::sync::atomic::AtomicBool =
1797                std::sync::atomic::AtomicBool::new(false);
1798            if IS_CHECKING.load(std::sync::atomic::Ordering::Acquire) {
1799                return;
1800            }
1801
1802            IS_CHECKING.store(true, std::sync::atomic::Ordering::Release);
1803            let peer_id = self.peer_id();
1804            let s = info_span!("CheckStateDiffCalcConsistencySlow", ?peer_id);
1805            let _g = s.enter();
1806            let options = self.implicit_commit_then_stop().0;
1807            self.oplog.lock().check_dag_correctness();
1808            if self.is_shallow() {
1809                // For shallow documents, we cannot replay from the beginning as the history is not complete.
1810                //
1811                // Instead, we:
1812                // 1. Export the initial state from the GC snapshot.
1813                // 2. Create a new document and import the initial snapshot.
1814                // 3. Export updates from the shallow start version vector to the current version.
1815                // 4. Import these updates into the new document.
1816                // 5. Compare the states of the new document and the current document.
1817
1818                // Step 1: Export the initial state from the GC snapshot.
1819                let initial_snapshot = self
1820                    .export(ExportMode::state_only(Some(
1821                        &self.shallow_since_frontiers(),
1822                    )))
1823                    .unwrap();
1824
1825                // Step 2: Create a new document and import the initial snapshot.
1826                let doc = LoroDoc::new();
1827                doc.import(&initial_snapshot).unwrap();
1828                self.checkout(&self.shallow_since_frontiers()).unwrap();
1829                assert_eq!(self.get_deep_value(), doc.get_deep_value());
1830
1831                // Step 3: Export updates since the shallow start version vector to the current version.
1832                let updates = self.export(ExportMode::all_updates()).unwrap();
1833
1834                // Step 4: Import these updates into the new document.
1835                doc.import(&updates).unwrap();
1836                self.checkout_to_latest();
1837
1838                // Step 5: Checkout to the current state's frontiers and compare the states.
1839                // doc.checkout(&self.state_frontiers()).unwrap();
1840                assert_eq!(doc.get_deep_value(), self.get_deep_value());
1841                let mut calculated_state = doc.app_state().lock();
1842                let mut current_state = self.app_state().lock();
1843                current_state.check_is_the_same(&mut calculated_state);
1844            } else {
1845                let f = self.state_frontiers();
1846                let vv = self.oplog().lock().dag.frontiers_to_vv(&f).unwrap();
1847                let bytes = self.export(ExportMode::updates_till(&vv)).unwrap();
1848                let doc = Self::new();
1849                doc.import(&bytes).unwrap();
1850                let mut calculated_state = doc.app_state().lock();
1851                let mut current_state = self.app_state().lock();
1852                current_state.check_is_the_same(&mut calculated_state);
1853            }
1854
1855            self.renew_txn_if_auto_commit(options);
1856            IS_CHECKING.store(false, std::sync::atomic::Ordering::Release);
1857        }
1858    }
1859
1860    pub fn query_pos(&self, pos: &Cursor) -> Result<PosQueryResult, CannotFindRelativePosition> {
1861        self.query_pos_internal(pos, true)
1862    }
1863
1864    /// Get position in a seq container
1865    pub(crate) fn query_pos_internal(
1866        &self,
1867        pos: &Cursor,
1868        ret_event_index: bool,
1869    ) -> Result<PosQueryResult, CannotFindRelativePosition> {
1870        if !self.has_container(&pos.container) {
1871            return Err(CannotFindRelativePosition::IdNotFound);
1872        }
1873
1874        let mut state = self.state.lock();
1875        if let Some(ans) = state.get_relative_position(pos, ret_event_index) {
1876            Ok(PosQueryResult {
1877                update: None,
1878                current: AbsolutePosition {
1879                    pos: ans,
1880                    side: pos.side,
1881                },
1882            })
1883        } else {
1884            // We need to trace back to the version where the relative position is valid.
1885            // The optimal way to find that version is to have succ info like Automerge.
1886            //
1887            // But we don't have that info now, so an alternative way is to trace back
1888            // to version with frontiers of `[pos.id]`. But this may be very slow even if
1889            // the target is just deleted a few versions ago.
1890            //
1891            // What we need is to trace back to the latest version that deletes the target
1892            // id.
1893
1894            // commit the txn to make sure we can query the history correctly, preserving options
1895            drop(state);
1896            let result = self.with_barrier(|| {
1897                let oplog = self.oplog().lock();
1898                // TODO: assert pos.id is not unknown
1899                if let Some(id) = pos.id {
1900                    // Ensure the container is registered if it exists lazily
1901                    if oplog.arena.id_to_idx(&pos.container).is_none() {
1902                        let mut s = self.state.lock();
1903                        if !s.does_container_exist(&pos.container) {
1904                            return Err(CannotFindRelativePosition::ContainerDeleted);
1905                        }
1906                        s.ensure_container(&pos.container);
1907                        drop(s);
1908                    }
1909                    let idx = oplog.arena.id_to_idx(&pos.container).unwrap();
1910                    // We know where the target id is when we trace back to the delete_op_id.
1911                    let Some(delete_op_id) = find_last_delete_op(&oplog, id, idx) else {
1912                        if oplog.shallow_since_vv().includes_id(id) {
1913                            return Err(CannotFindRelativePosition::HistoryCleared);
1914                        }
1915
1916                        tracing::error!("Cannot find id {}", id);
1917                        return Err(CannotFindRelativePosition::IdNotFound);
1918                    };
1919                    // Should use persist mode so that it will force all the diff calculators to use the `checkout` mode
1920                    let mut diff_calc = DiffCalculator::new(true);
1921                    let before_frontiers: Frontiers = oplog.dag.find_deps_of_id(delete_op_id);
1922                    let before = &oplog.dag.frontiers_to_vv(&before_frontiers).unwrap();
1923                    // TODO: PERF: it doesn't need to calc the effects here
1924                    diff_calc.calc_diff_internal(
1925                        &oplog,
1926                        before,
1927                        &before_frontiers,
1928                        oplog.vv(),
1929                        oplog.frontiers(),
1930                        Some(&|target| idx == target),
1931                    );
1932                    // TODO: remove depth info
1933                    let depth = self.arena.get_depth(idx);
1934                    let (_, diff_calc) = &mut diff_calc.get_or_create_calc(idx, depth);
1935                    match diff_calc {
1936                        crate::diff_calc::ContainerDiffCalculator::Richtext(text) => {
1937                            let c = text.get_id_latest_pos(id).unwrap();
1938                            let new_pos = c.pos;
1939                            let handler = self.get_text(&pos.container);
1940                            let current_pos = handler.convert_entity_index_to_event_index(new_pos);
1941                            Ok(PosQueryResult {
1942                                update: handler.get_cursor(current_pos, c.side),
1943                                current: AbsolutePosition {
1944                                    pos: current_pos,
1945                                    side: c.side,
1946                                },
1947                            })
1948                        }
1949                        crate::diff_calc::ContainerDiffCalculator::List(list) => {
1950                            let c = list.get_id_latest_pos(id).unwrap();
1951                            let new_pos = c.pos;
1952                            let handler = self.get_list(&pos.container);
1953                            Ok(PosQueryResult {
1954                                update: handler.get_cursor(new_pos, c.side),
1955                                current: AbsolutePosition {
1956                                    pos: new_pos,
1957                                    side: c.side,
1958                                },
1959                            })
1960                        }
1961                        crate::diff_calc::ContainerDiffCalculator::MovableList(list) => {
1962                            let c = list.get_id_latest_pos(id).unwrap();
1963                            let new_pos = c.pos;
1964                            let handler = self.get_movable_list(&pos.container);
1965                            let new_pos = handler.op_pos_to_user_pos(new_pos);
1966                            Ok(PosQueryResult {
1967                                update: handler.get_cursor(new_pos, c.side),
1968                                current: AbsolutePosition {
1969                                    pos: new_pos,
1970                                    side: c.side,
1971                                },
1972                            })
1973                        }
1974                        crate::diff_calc::ContainerDiffCalculator::Tree(_) => unreachable!(),
1975                        crate::diff_calc::ContainerDiffCalculator::Map(_) => unreachable!(),
1976                        #[cfg(feature = "counter")]
1977                        crate::diff_calc::ContainerDiffCalculator::Counter(_) => unreachable!(),
1978                        crate::diff_calc::ContainerDiffCalculator::Unknown(_) => unreachable!(),
1979                    }
1980                } else {
1981                    match pos.container.container_type() {
1982                        ContainerType::Text => {
1983                            let text = self.get_text(&pos.container);
1984                            Ok(PosQueryResult {
1985                                update: Some(Cursor {
1986                                    id: None,
1987                                    container: text.id(),
1988                                    side: pos.side,
1989                                    origin_pos: text.len_unicode(),
1990                                }),
1991                                current: AbsolutePosition {
1992                                    pos: text.len_event(),
1993                                    side: pos.side,
1994                                },
1995                            })
1996                        }
1997                        ContainerType::List => {
1998                            let list = self.get_list(&pos.container);
1999                            Ok(PosQueryResult {
2000                                update: Some(Cursor {
2001                                    id: None,
2002                                    container: list.id(),
2003                                    side: pos.side,
2004                                    origin_pos: list.len(),
2005                                }),
2006                                current: AbsolutePosition {
2007                                    pos: list.len(),
2008                                    side: pos.side,
2009                                },
2010                            })
2011                        }
2012                        ContainerType::MovableList => {
2013                            let list = self.get_movable_list(&pos.container);
2014                            Ok(PosQueryResult {
2015                                update: Some(Cursor {
2016                                    id: None,
2017                                    container: list.id(),
2018                                    side: pos.side,
2019                                    origin_pos: list.len(),
2020                                }),
2021                                current: AbsolutePosition {
2022                                    pos: list.len(),
2023                                    side: pos.side,
2024                                },
2025                            })
2026                        }
2027                        ContainerType::Map | ContainerType::Tree | ContainerType::Unknown(_) => {
2028                            unreachable!()
2029                        }
2030                        #[cfg(feature = "counter")]
2031                        ContainerType::Counter => unreachable!(),
2032                    }
2033                }
2034            });
2035            result
2036        }
2037    }
2038
2039    /// Free the history cache that is used for making checkout faster.
2040    ///
2041    /// If you use checkout that switching to an old/concurrent version, the history cache will be built.
2042    /// You can free it by calling this method.
2043    pub fn free_history_cache(&self) {
2044        self.oplog.lock().free_history_cache();
2045    }
2046
2047    /// Free the cached diff calculator that is used for checkout.
2048    pub fn free_diff_calculator(&self) {
2049        *self.diff_calculator.lock() = DiffCalculator::new(true);
2050    }
2051
2052    /// If you use checkout that switching to an old/concurrent version, the history cache will be built.
2053    /// You can free it by calling `free_history_cache`.
2054    pub fn has_history_cache(&self) -> bool {
2055        self.oplog.lock().has_history_cache()
2056    }
2057
2058    /// Encoded all ops and history cache to bytes and store them in the kv store.
2059    ///
2060    /// The parsed ops will be dropped
2061    #[inline]
2062    pub fn compact_change_store(&self) {
2063        self.with_barrier(|| {
2064            self.oplog.lock().compact_change_store();
2065        });
2066    }
2067
2068    /// Analyze the container info of the doc
2069    ///
2070    /// This is used for development and debugging
2071    #[inline]
2072    pub fn analyze(&self) -> DocAnalysis {
2073        DocAnalysis::analyze(self)
2074    }
2075
2076    /// Get the path from the root to the container
2077    pub fn get_path_to_container(&self, id: &ContainerID) -> Option<Vec<(ContainerID, Index)>> {
2078        let mut state = self.state.lock();
2079        if state.arena.id_to_idx(id).is_none() {
2080            if id.is_mergeable() {
2081                // Mergeable children can be logically active via the parent map marker
2082                // before they have their own encoded state. Register only the arena edge; do not
2083                // create container state or change `has_container` semantics.
2084                state.arena.register_container(id);
2085            } else if !state.does_container_exist(id) {
2086                return None;
2087            } else {
2088                state.ensure_container(id);
2089            }
2090        }
2091        let idx = state.arena.id_to_idx(id).unwrap();
2092        state.get_path(idx)
2093    }
2094
2095    #[instrument(skip(self))]
2096    pub fn export(&self, mode: ExportMode) -> Result<Vec<u8>, LoroEncodeError> {
2097        self.with_barrier(|| {
2098            let ans = match mode {
2099                ExportMode::Snapshot => export_fast_snapshot(self)?,
2100                ExportMode::Updates { from } => export_fast_updates(self, &from),
2101                ExportMode::UpdatesInRange { spans } => {
2102                    export_fast_updates_in_range(&self.oplog.lock(), spans.as_ref())
2103                }
2104                ExportMode::ShallowSnapshot(f) => export_shallow_snapshot(self, &f)?,
2105                ExportMode::StateOnly(f) => match f {
2106                    Some(f) => export_state_only_snapshot(self, &f)?,
2107                    None => export_state_only_snapshot(self, &self.oplog_frontiers())?,
2108                },
2109                ExportMode::SnapshotAt { version } => export_snapshot_at(self, &version)?,
2110            };
2111            Ok(ans)
2112        })
2113    }
2114
2115    /// The doc only contains the history since the shallow history start version vector.
2116    ///
2117    /// This is empty if the doc is not shallow.
2118    ///
2119    /// The ops included by the shallow history start version vector are not in the doc.
2120    pub fn shallow_since_vv(&self) -> ImVersionVector {
2121        self.oplog().lock().shallow_since_vv().clone()
2122    }
2123
2124    pub fn shallow_since_frontiers(&self) -> Frontiers {
2125        self.oplog().lock().shallow_since_frontiers().clone()
2126    }
2127
2128    /// Check if the doc contains the full history.
2129    pub fn is_shallow(&self) -> bool {
2130        !self.oplog().lock().shallow_since_vv().is_empty()
2131    }
2132
2133    /// Get the number of operations in the pending transaction.
2134    ///
2135    /// The pending transaction is the one that is not committed yet. It will be committed
2136    /// after calling `doc.commit()`, `doc.export(mode)` or `doc.checkout(version)`.
2137    pub fn get_pending_txn_len(&self) -> usize {
2138        if let Some(txn) = self.txn.lock().as_ref() {
2139            txn.len()
2140        } else {
2141            0
2142        }
2143    }
2144
2145    #[inline]
2146    pub fn find_id_spans_between(&self, from: &Frontiers, to: &Frontiers) -> VersionVectorDiff {
2147        self.oplog().lock().dag.find_path(from, to)
2148    }
2149
2150    /// Subscribe to the first commit from a peer. Operations performed on the `LoroDoc` within this callback
2151    /// will be merged into the current commit.
2152    ///
2153    /// This is useful for managing the relationship between `PeerID` and user information.
2154    /// For example, you could store user names in a `LoroMap` using `PeerID` as the key and the `UserID` as the value.
2155    pub fn subscribe_first_commit_from_peer(
2156        &self,
2157        callback: FirstCommitFromPeerCallback,
2158    ) -> Subscription {
2159        let (s, enable) = self
2160            .first_commit_from_peer_subs
2161            .inner()
2162            .insert((), callback);
2163        enable();
2164        s
2165    }
2166
2167    /// Subscribe to the pre-commit event.
2168    ///
2169    /// The callback will be called when the changes are committed but not yet applied to the OpLog.
2170    /// You can modify the commit message and timestamp in the callback by [`ChangeModifier`].
2171    pub fn subscribe_pre_commit(&self, callback: PreCommitCallback) -> Subscription {
2172        let (s, enable) = self.pre_commit_subs.inner().insert((), callback);
2173        enable();
2174        s
2175    }
2176}
2177
2178fn pending_root_containers_to_materialize(oplog: &OpLog, changes: &[Change]) -> Vec<ContainerID> {
2179    let mut roots = FxHashSet::default();
2180    for change in changes {
2181        if change.ctr_end() <= oplog.vv().get(&change.id.peer).copied().unwrap_or(0) {
2182            continue;
2183        }
2184
2185        if oplog.dag.is_before_shallow_root(&change.deps)
2186            || oplog
2187                .dag
2188                .get_change_lamport_from_deps(&change.deps)
2189                .is_some()
2190        {
2191            continue;
2192        }
2193
2194        for op in change.ops.iter() {
2195            let id = oplog
2196                .arena
2197                .get_container_id(op.container)
2198                .expect("decoded op container should be registered");
2199            // Mergeable containers share the `ContainerID::Root` namespace but are logical
2200            // *children*: their existence is governed by their parent map's marker, and their
2201            // parent can be any (possibly not-yet-imported) map. Eagerly materializing one
2202            // while its causal dependencies are still pending has no valid parent edge to
2203            // resolve its depth against, which used to panic in `ContainerWrapper::new`.
2204            // They get materialized correctly through the normal diff path once the creating
2205            // change applies, so skip them here (mirrors the `!is_mergeable()` guard in
2206            // `ensure_root_container`). See the `mergeable_container::pending` regression test.
2207            if id.is_root() && !id.is_mergeable() {
2208                roots.insert(id);
2209            }
2210        }
2211    }
2212
2213    roots.into_iter().collect()
2214}
2215
2216/// Identify a causally closed update component that can be applied without replaying unrelated
2217/// history. The first version is deliberately narrow: it accepts only scalar Map operations on
2218/// top-level roots that never appeared in the existing history.
2219fn isolated_scalar_root_batch(
2220    oplog: &OpLog,
2221    changes: &[Change],
2222) -> Option<(VersionVector, FxHashSet<InternalString>)> {
2223    if changes.is_empty() || !oplog.shallow_since_vv().is_empty() {
2224        return None;
2225    }
2226
2227    // Every peer in the batch must be brand new and contiguously covered from counter 0.
2228    // (`changes` is sorted by lamport, so one peer's changes appear in counter order.)
2229    let mut component_vv = VersionVector::new();
2230    let mut root_names = FxHashSet::default();
2231    for change in changes {
2232        let peer = change.id.peer;
2233        if oplog.vv().get(&peer).copied().unwrap_or(0) != 0 {
2234            return None;
2235        }
2236        let end = component_vv.entry(peer).or_insert(0);
2237        if change.id.counter != *end {
2238            return None;
2239        }
2240        *end = change.ctr_end();
2241
2242        for op in change.ops.iter() {
2243            let cid = oplog.arena.idx_to_id(op.container)?;
2244            if cid.is_mergeable() {
2245                return None;
2246            }
2247            let ContainerID::Root {
2248                name,
2249                container_type: ContainerType::Map,
2250            } = cid
2251            else {
2252                return None;
2253            };
2254            let InnerContent::Map(map) = &op.content else {
2255                return None;
2256            };
2257            if let Some(value) = &map.value {
2258                if value.is_container()
2259                    || loro_common::parse_mergeable_marker(
2260                        &ContainerID::new_root(&name, ContainerType::Map),
2261                        &map.key,
2262                        value,
2263                    )
2264                    .is_some()
2265                {
2266                    return None;
2267                }
2268            }
2269            root_names.insert(name);
2270        }
2271    }
2272
2273    // The batch must be causally closed: every dependency points inside the batch itself.
2274    for change in changes {
2275        for dep in change.deps.iter() {
2276            if dep.counter >= component_vv.get(&dep.peer).copied().unwrap_or(0) {
2277                return None;
2278            }
2279        }
2280    }
2281
2282    if oplog
2283        .change_store()
2284        .old_history_may_touch_root_names(&root_names)
2285    {
2286        return None;
2287    }
2288
2289    Some((component_vv, root_names))
2290}
2291
2292#[derive(Debug, thiserror::Error)]
2293pub enum ChangeTravelError {
2294    #[error("Target id not found {0:?}")]
2295    TargetIdNotFound(ID),
2296    #[error("The shallow history of the doc doesn't include the target version")]
2297    TargetVersionNotIncluded,
2298}
2299
2300impl LoroDoc {
2301    pub fn travel_change_ancestors(
2302        &self,
2303        ids: &[ID],
2304        f: &mut dyn FnMut(ChangeMeta) -> ControlFlow<()>,
2305    ) -> Result<(), ChangeTravelError> {
2306        let (options, guard) = self.implicit_commit_then_stop();
2307        drop(guard);
2308        struct PendingNode(ChangeMeta);
2309        impl PartialEq for PendingNode {
2310            fn eq(&self, other: &Self) -> bool {
2311                self.0.lamport_last() == other.0.lamport_last() && self.0.id.peer == other.0.id.peer
2312            }
2313        }
2314
2315        impl Eq for PendingNode {}
2316        impl PartialOrd for PendingNode {
2317            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2318                Some(self.cmp(other))
2319            }
2320        }
2321
2322        impl Ord for PendingNode {
2323            fn cmp(&self, other: &Self) -> Ordering {
2324                self.0
2325                    .lamport_last()
2326                    .cmp(&other.0.lamport_last())
2327                    .then_with(|| self.0.id.peer.cmp(&other.0.id.peer))
2328            }
2329        }
2330
2331        for id in ids {
2332            let op_log = &self.oplog().lock();
2333            if !op_log.vv().includes_id(*id) {
2334                return Err(ChangeTravelError::TargetIdNotFound(*id));
2335            }
2336            if op_log.dag.shallow_since_vv().includes_id(*id) {
2337                return Err(ChangeTravelError::TargetVersionNotIncluded);
2338            }
2339        }
2340
2341        let mut visited = FxHashSet::default();
2342        let mut pending: BinaryHeap<PendingNode> = BinaryHeap::new();
2343        for id in ids {
2344            pending.push(PendingNode(ChangeMeta::from_change(
2345                &self.oplog().lock().get_change_at(*id).unwrap(),
2346            )));
2347        }
2348        while let Some(PendingNode(node)) = pending.pop() {
2349            let deps = node.deps.clone();
2350            if f(node).is_break() {
2351                break;
2352            }
2353
2354            for dep in deps.iter() {
2355                let Some(dep_node) = self.oplog().lock().get_change_at(dep) else {
2356                    continue;
2357                };
2358                if visited.contains(&dep_node.id) {
2359                    continue;
2360                }
2361
2362                visited.insert(dep_node.id);
2363                pending.push(PendingNode(ChangeMeta::from_change(&dep_node)));
2364            }
2365        }
2366
2367        let ans = Ok(());
2368        self.renew_txn_if_auto_commit(options);
2369        ans
2370    }
2371
2372    pub fn get_changed_containers_in(&self, id: ID, len: usize) -> FxHashSet<ContainerID> {
2373        self.with_barrier(|| {
2374            let mut set = FxHashSet::default();
2375            let len = i64::try_from(len).unwrap_or(i64::MAX);
2376            let start = i64::from(id.counter);
2377            let end = start.saturating_add(len);
2378            if end <= 0 {
2379                return set;
2380            }
2381
2382            let start = start.max(0).min(i64::from(i32::MAX));
2383            let end = end.max(0).min(i64::from(i32::MAX));
2384            if start >= end {
2385                return set;
2386            }
2387
2388            {
2389                let oplog = self.oplog().lock();
2390                let span = IdSpan::new(id.peer, start as i32, end as i32);
2391                for op in oplog.iter_ops(span) {
2392                    let id = oplog.arena.get_container_id(op.container()).unwrap();
2393                    set.insert(id);
2394                }
2395            }
2396            set
2397        })
2398    }
2399
2400    pub fn delete_root_container(&self, cid: ContainerID) {
2401        if !cid.is_root() {
2402            return;
2403        }
2404
2405        // Do not treat "not in arena" as non-existence; consult state/kv
2406        if !self.has_container(&cid) {
2407            return;
2408        }
2409
2410        let Some(h) = self.get_handler(cid.clone()) else {
2411            return;
2412        };
2413
2414        self.config
2415            .deleted_root_containers
2416            .lock()
2417            .insert(cid.clone());
2418        if let Err(e) = h.clear() {
2419            self.config.deleted_root_containers.lock().remove(&cid);
2420            eprintln!("Failed to clear handler: {:?}", e);
2421        }
2422    }
2423
2424    pub fn set_hide_empty_root_containers(&self, hide: bool) {
2425        self.config
2426            .hide_empty_root_containers
2427            .store(hide, std::sync::atomic::Ordering::Relaxed);
2428    }
2429}
2430
2431fn find_last_delete_op(oplog: &OpLog, id: ID, idx: ContainerIdx) -> Option<ID> {
2432    // Any delete op that covers `id` must have observed it, so its peer's counter
2433    // at delete time was > id.counter. start_vv (the vv at `id`) is therefore a
2434    // valid lower bound: changes at or before start_vv[peer] predate `id` and can
2435    // be skipped. We scan peer-by-peer rather than using the DAG-ordered
2436    // iter_changes_causally_rev, which is O(total changes).
2437    //
2438    // We choose the matching delete op with the greatest (op_lamport, peer, counter)
2439    // ordering. op_lamport is the Lamport of the specific op within the change
2440    // (change.lamport + op offset), not just the change's starting Lamport, so
2441    // concurrent deletes with equal change Lamports are broken deterministically.
2442    let start_vv = oplog
2443        .dag
2444        .frontiers_to_vv(&id.into())
2445        .unwrap_or_else(|| oplog.shallow_since_vv().to_vv());
2446
2447    // (op_lamport, peer) gives a deterministic total order for concurrent deletes.
2448    // A single peer cannot produce two ops with the same lamport, so peer suffices
2449    // as a tie-breaker.
2450    let mut best: Option<((loro_common::Lamport, loro_common::PeerID), ID)> = None;
2451
2452    for change in oplog.iter_changes_peer_by_peer(&start_vv, oplog.vv()) {
2453        let peer = change.peer();
2454        for op in change.ops.iter() {
2455            if op.container != idx {
2456                continue;
2457            }
2458            if let InnerContent::List(InnerListOp::Delete(d)) = &op.content {
2459                if d.id_start.to_span(d.atom_len()).contains(id) {
2460                    debug_assert!(op.counter >= change.id().counter);
2461                    let op_lamport =
2462                        change.lamport + (op.counter - change.id().counter) as loro_common::Lamport;
2463                    let key = (op_lamport, peer);
2464                    if best.is_none_or(|(bk, _)| key > bk) {
2465                        best = Some((key, ID::new(peer, op.counter)));
2466                    }
2467                }
2468            }
2469        }
2470    }
2471
2472    best.map(|(_, op_id)| op_id)
2473}
2474
2475#[derive(Debug)]
2476pub struct CommitWhenDrop<'a> {
2477    doc: &'a LoroDoc,
2478    default_options: CommitOptions,
2479}
2480
2481impl Drop for CommitWhenDrop<'_> {
2482    fn drop(&mut self) {
2483        {
2484            let mut guard = self.doc.txn.lock();
2485            if let Some(txn) = guard.as_mut() {
2486                txn.set_default_options(std::mem::take(&mut self.default_options));
2487            };
2488        }
2489
2490        self.doc.commit_then_renew();
2491    }
2492}
2493
2494/// Options for configuring a commit operation.
2495#[derive(Debug, Clone)]
2496pub struct CommitOptions {
2497    /// Origin identifier for the commit event, used to track the source of changes.
2498    /// It doesn't persist.
2499    pub origin: Option<InternalString>,
2500
2501    /// Whether to immediately start a new transaction after committing.
2502    /// Defaults to true.
2503    pub immediate_renew: bool,
2504
2505    /// Custom timestamp for the commit in seconds since Unix epoch.
2506    /// If None, the current time will be used.
2507    pub timestamp: Option<Timestamp>,
2508
2509    /// Optional commit message to attach to the changes. It will be persisted.
2510    pub commit_msg: Option<Arc<str>>,
2511}
2512
2513impl CommitOptions {
2514    /// Creates a new CommitOptions with default values.
2515    pub fn new() -> Self {
2516        Self {
2517            origin: None,
2518            immediate_renew: true,
2519            timestamp: None,
2520            commit_msg: None,
2521        }
2522    }
2523
2524    /// Sets the origin identifier for this commit.
2525    pub fn origin(mut self, origin: &str) -> Self {
2526        self.origin = Some(origin.into());
2527        self
2528    }
2529
2530    /// Sets whether to immediately start a new transaction after committing.
2531    pub fn immediate_renew(mut self, immediate_renew: bool) -> Self {
2532        self.immediate_renew = immediate_renew;
2533        self
2534    }
2535
2536    /// Set the timestamp of the commit.
2537    ///
2538    /// The timestamp is the number of **seconds** that have elapsed since 00:00:00 UTC on January 1, 1970.
2539    pub fn timestamp(mut self, timestamp: Timestamp) -> Self {
2540        self.timestamp = Some(timestamp);
2541        self
2542    }
2543
2544    /// Sets a commit message to be attached to the changes.
2545    pub fn commit_msg(mut self, commit_msg: &str) -> Self {
2546        self.commit_msg = Some(commit_msg.into());
2547        self
2548    }
2549
2550    /// Sets the origin identifier for this commit.
2551    pub fn set_origin(&mut self, origin: Option<&str>) {
2552        self.origin = origin.map(|x| x.into())
2553    }
2554
2555    /// Sets the timestamp for this commit.
2556    pub fn set_timestamp(&mut self, timestamp: Option<Timestamp>) {
2557        self.timestamp = timestamp;
2558    }
2559}
2560
2561impl Default for CommitOptions {
2562    fn default() -> Self {
2563        Self::new()
2564    }
2565}
2566
2567#[cfg(test)]
2568mod test {
2569    use std::{
2570        panic::AssertUnwindSafe,
2571        sync::{
2572            atomic::{AtomicUsize, Ordering},
2573            Arc,
2574        },
2575    };
2576
2577    use crate::{
2578        cursor::PosType,
2579        encoding::json_schema::json::{JsonOpContent, JsonSchema, ListOp},
2580        encoding::{fast_snapshot::EMPTY_MARK, EncodeMode},
2581        loro::ExportMode,
2582        version::{Frontiers, VersionVector},
2583        LoroDoc, ToJson, TreeParentId,
2584    };
2585    use bytes::{BufMut, Bytes};
2586    use loro_common::{ContainerID, ContainerType, ID};
2587    use loro_kv_store::{mem_store::MemKvConfig, MemKvStore};
2588
2589    const XXH_SEED: u32 = u32::from_le_bytes(*b"LORO");
2590
2591    fn encode_import_blob(mode: EncodeMode, body: &[u8]) -> Vec<u8> {
2592        let mut ans = Vec::new();
2593        ans.extend_from_slice(b"loro");
2594        ans.extend_from_slice(&[0; 16]);
2595        ans.extend_from_slice(&mode.to_bytes());
2596        ans.extend_from_slice(body);
2597        let checksum = xxhash_rust::xxh32::xxh32(&ans[20..], XXH_SEED);
2598        ans[16..20].copy_from_slice(&checksum.to_le_bytes());
2599        ans
2600    }
2601
2602    fn encode_fast_snapshot_import(oplog_bytes: &[u8]) -> Vec<u8> {
2603        let mut body = Vec::new();
2604        body.put_u32_le(oplog_bytes.len() as u32);
2605        body.extend_from_slice(oplog_bytes);
2606        body.put_u32_le(EMPTY_MARK.len() as u32);
2607        body.extend_from_slice(EMPTY_MARK);
2608        body.put_u32_le(0);
2609        encode_import_blob(EncodeMode::FastSnapshot, &body)
2610    }
2611
2612    fn sstable_with_huge_meta_block_count() -> Vec<u8> {
2613        let mut bytes = Vec::new();
2614        bytes.extend_from_slice(b"LORO");
2615        bytes.push(0);
2616        bytes.put_u32_le(10_000_000);
2617        bytes.put_u32_le(xxhash_rust::xxh32::xxh32(&[], XXH_SEED));
2618        bytes.put_u32_le(5);
2619        bytes
2620    }
2621
2622    fn snapshot_oplog_with_malformed_block() -> Vec<u8> {
2623        let peer = 1;
2624        let id = ID::new(peer, 0);
2625        let vv = VersionVector::from_iter([(peer, 1)]);
2626        let frontiers = Frontiers::from_id(id);
2627        let mut store = MemKvStore::new(MemKvConfig::default());
2628        store.set(b"vv", vv.encode().into());
2629        store.set(b"fr", frontiers.encode().into());
2630        store.set(&id.to_bytes(), Bytes::from_static(&[0]));
2631        store.export_all().to_vec()
2632    }
2633
2634    fn make_json_import_stress_doc(peer: u64) -> LoroDoc {
2635        let doc = LoroDoc::new_auto_commit();
2636        doc.set_peer_id(peer).unwrap();
2637
2638        let text = doc.get_text("text");
2639        let mut text_pos = 0;
2640        for i in 0..32 {
2641            let chunk = format!("segment-{i}-abcdefghijklmnopqrstuvwxyz;");
2642            text.insert_unicode(text_pos, &chunk).unwrap();
2643            text_pos += chunk.chars().count();
2644        }
2645
2646        let list = doc.get_list("list");
2647        for i in 0..32 {
2648            list.insert(i, format!("item-{i}")).unwrap();
2649        }
2650
2651        let map = doc.get_map("map");
2652        for i in 0..32 {
2653            let key = format!("key-{i}");
2654            map.insert(&key, format!("value-{i}")).unwrap();
2655        }
2656
2657        let tree = doc.get_tree("tree");
2658        let mut parent = TreeParentId::Root;
2659        for i in 0..16 {
2660            let node = tree.create(parent).unwrap();
2661            let meta = tree.get_meta(node).unwrap();
2662            meta.insert("name", format!("node-{i}")).unwrap();
2663            meta.insert("payload", format!("payload-{i}-{}", "x".repeat(16)))
2664                .unwrap();
2665            parent = TreeParentId::Node(node);
2666        }
2667
2668        doc
2669    }
2670
2671    fn make_json_list_update_with_four_ops(peer: u64) -> (LoroDoc, JsonSchema) {
2672        let doc = LoroDoc::new();
2673        doc.set_peer_id(peer).unwrap();
2674        let map = doc.get_map("map");
2675        let list = doc.get_list("list");
2676        let text = doc.get_text("text");
2677
2678        let mut txn = doc.txn().unwrap();
2679        map.insert_with_txn(&mut txn, "prefix", "map-value".into())
2680            .unwrap();
2681        list.insert_with_txn(&mut txn, 0, "seed".into()).unwrap();
2682        text.insert_with_txn(&mut txn, 0, "text-value", PosType::Unicode)
2683            .unwrap();
2684        list.insert_with_txn(&mut txn, 1, "tail".into()).unwrap();
2685        txn.commit().unwrap();
2686
2687        let json = doc.export_json_updates(&Default::default(), &doc.oplog_vv(), false);
2688        assert_eq!(json.changes.len(), 1);
2689        assert_eq!(json.changes[0].ops.len(), 4);
2690        (doc, json)
2691    }
2692
2693    fn move_last_list_insert_far_out_of_bounds(json: &mut JsonSchema) {
2694        let last_change = json.changes.last_mut().unwrap();
2695        let last_op = last_change.ops.last_mut().unwrap();
2696        match &mut last_op.content {
2697            JsonOpContent::List(ListOp::Insert { pos, .. }) => {
2698                *pos = 1_000;
2699            }
2700            other => panic!("expected list insert op, got {other:?}"),
2701        }
2702    }
2703
2704    #[test]
2705    fn test_sync() {
2706        fn is_send_sync<T: Send + Sync>(_v: T) {}
2707        let loro = super::LoroDoc::new();
2708        is_send_sync(loro)
2709    }
2710
2711    #[test]
2712    fn import_rejects_huge_sstable_meta_block_count_without_panic() {
2713        let bytes = encode_fast_snapshot_import(&sstable_with_huge_meta_block_count());
2714
2715        let result = std::panic::catch_unwind(AssertUnwindSafe(|| LoroDoc::new().import(&bytes)));
2716        assert!(result.is_ok(), "malformed import should not panic");
2717        assert!(result.unwrap().is_err());
2718    }
2719
2720    #[test]
2721    fn import_rejects_malformed_change_block_without_panic() {
2722        let bytes = encode_fast_snapshot_import(&snapshot_oplog_with_malformed_block());
2723
2724        let result = std::panic::catch_unwind(AssertUnwindSafe(|| LoroDoc::new().import(&bytes)));
2725        assert!(result.is_ok(), "malformed import should not panic");
2726        assert!(result.unwrap().is_err());
2727    }
2728
2729    #[test]
2730    fn failed_import_rolls_back_oplog_and_arena() {
2731        let src = LoroDoc::new();
2732        src.set_peer_id(1).unwrap();
2733        let text = src.get_text("text");
2734        let mut txn = src.txn().unwrap();
2735        text.insert_with_txn(&mut txn, 0, "hello", PosType::Unicode)
2736            .unwrap();
2737        txn.commit().unwrap();
2738        let update = src.export(ExportMode::all_updates()).unwrap();
2739
2740        let dst = LoroDoc::new();
2741        let vv_before_import = dst.oplog_vv();
2742        let state_before_import = dst.get_deep_value();
2743        let err = dst
2744            .import_with(&update, "__loro_fail_import_state_apply".into())
2745            .unwrap_err();
2746        assert!(err.to_string().contains("state apply failpoint"));
2747        assert_eq!(dst.oplog_vv(), vv_before_import);
2748        assert_eq!(dst.get_deep_value(), state_before_import);
2749        assert!(dst.oplog().lock().is_empty());
2750
2751        dst.import(&update).unwrap();
2752        assert_eq!(dst.get_deep_value(), src.get_deep_value());
2753    }
2754
2755    #[test]
2756    fn failed_incremental_import_restores_previous_change_store_block() {
2757        let src = LoroDoc::new();
2758        src.set_peer_id(1).unwrap();
2759        let text = src.get_text("text");
2760        let mut txn = src.txn().unwrap();
2761        text.insert_with_txn(&mut txn, 0, "a", PosType::Unicode)
2762            .unwrap();
2763        txn.commit().unwrap();
2764        let first_update = src.export(ExportMode::all_updates()).unwrap();
2765        let first_vv = src.oplog_vv();
2766
2767        let mut txn = src.txn().unwrap();
2768        text.insert_with_txn(&mut txn, 1, "b", PosType::Unicode)
2769            .unwrap();
2770        txn.commit().unwrap();
2771        let second_update = src.export(ExportMode::updates(&first_vv)).unwrap();
2772
2773        let dst = LoroDoc::new();
2774        dst.import(&first_update).unwrap();
2775        let vv_before_import = dst.oplog_vv();
2776        let state_before_import = dst.get_deep_value();
2777        dst.import_with(&second_update, "__loro_fail_import_state_apply".into())
2778            .unwrap_err();
2779        assert_eq!(dst.oplog_vv(), vv_before_import);
2780        assert_eq!(dst.get_deep_value(), state_before_import);
2781
2782        dst.import(&second_update).unwrap();
2783        assert_eq!(dst.get_deep_value(), src.get_deep_value());
2784    }
2785
2786    #[test]
2787    fn failed_import_json_updates_rolls_back_complex_empty_doc() {
2788        let src = make_json_import_stress_doc(11);
2789        let json = src.export_json_updates(&Default::default(), &src.oplog_vv(), false);
2790
2791        let dst = LoroDoc::new();
2792        let vv_before_import = dst.oplog_vv();
2793        let frontiers_before_import = dst.oplog_frontiers();
2794        let state_before_import = dst.get_deep_value();
2795        for _ in 0..3 {
2796            crate::state::fail_next_import_state_apply_for_test();
2797            let err = dst.import_json_updates(json.clone()).unwrap_err();
2798            assert!(err.to_string().contains("state apply failpoint"));
2799            assert_eq!(dst.oplog_vv(), vv_before_import);
2800            assert_eq!(dst.oplog_frontiers(), frontiers_before_import);
2801            assert_eq!(dst.get_deep_value(), state_before_import);
2802            assert!(dst.oplog().lock().is_empty());
2803        }
2804
2805        dst.import_json_updates(json).unwrap();
2806        assert_eq!(dst.oplog_vv(), src.oplog_vv());
2807        assert_eq!(dst.oplog_frontiers(), src.oplog_frontiers());
2808        assert_eq!(dst.get_deep_value(), src.get_deep_value());
2809    }
2810
2811    #[test]
2812    fn failed_incremental_import_json_updates_restores_previous_change_store_block() {
2813        let src = LoroDoc::new_auto_commit();
2814        src.set_peer_id(12).unwrap();
2815        let text = src.get_text("text");
2816        text.insert_unicode(0, "a").unwrap();
2817        let list = src.get_list("list");
2818        list.push("seed").unwrap();
2819        let map = src.get_map("map");
2820        map.insert("seed", "value").unwrap();
2821        let tree = src.get_tree("tree");
2822        let root = tree.create(TreeParentId::Root).unwrap();
2823        tree.get_meta(root).unwrap().insert("name", "root").unwrap();
2824
2825        let first_vv = src.oplog_vv();
2826        let first_json = src.export_json_updates(&Default::default(), &first_vv, false);
2827
2828        let mut text_pos = text.len_unicode();
2829        for i in 0..64 {
2830            let chunk = format!("chunk-{i};");
2831            text.insert_unicode(text_pos, &chunk).unwrap();
2832            text_pos += chunk.chars().count();
2833        }
2834        for i in 0..32 {
2835            list.push(format!("after-{i}")).unwrap();
2836            let key = format!("after-{i}");
2837            map.insert(&key, format!("value-{i}")).unwrap();
2838        }
2839        let child = tree.create(TreeParentId::Node(root)).unwrap();
2840        tree.get_meta(child)
2841            .unwrap()
2842            .insert("name", "child")
2843            .unwrap();
2844
2845        let second_json = src.export_json_updates(&first_vv, &src.oplog_vv(), false);
2846
2847        let dst = LoroDoc::new();
2848        dst.import_json_updates(first_json).unwrap();
2849        let vv_before_import = dst.oplog_vv();
2850        let frontiers_before_import = dst.oplog_frontiers();
2851        let state_before_import = dst.get_deep_value();
2852
2853        for _ in 0..2 {
2854            crate::state::fail_next_import_state_apply_for_test();
2855            let err = dst.import_json_updates(second_json.clone()).unwrap_err();
2856            assert!(err.to_string().contains("state apply failpoint"));
2857            assert_eq!(dst.oplog_vv(), vv_before_import);
2858            assert_eq!(dst.oplog_frontiers(), frontiers_before_import);
2859            assert_eq!(dst.get_deep_value(), state_before_import);
2860        }
2861
2862        dst.import_json_updates(second_json).unwrap();
2863        assert_eq!(dst.oplog_vv(), src.oplog_vv());
2864        assert_eq!(dst.oplog_frontiers(), src.oplog_frontiers());
2865        assert_eq!(dst.get_deep_value(), src.get_deep_value());
2866    }
2867
2868    #[test]
2869    fn malformed_later_import_json_update_rolls_back_after_valid_prefix_enters_oplog() {
2870        let peer = 13;
2871        let (src, good_json) = make_json_list_update_with_four_ops(peer);
2872        let mut bad_json = good_json.clone();
2873        move_last_list_insert_far_out_of_bounds(&mut bad_json);
2874
2875        let good_dst = LoroDoc::new();
2876        good_dst.import_json_updates(good_json.clone()).unwrap();
2877        assert_eq!(good_dst.get_deep_value(), src.get_deep_value());
2878
2879        let last_op_counter = good_json.changes[0].ops.last().unwrap().counter;
2880        let prefix_vv = VersionVector::from_iter([(peer, last_op_counter)]);
2881        let prefix_json = src.export_json_updates(&Default::default(), &prefix_vv, false);
2882        assert_eq!(
2883            prefix_json.changes[0].ops.len(),
2884            good_json.changes[0].ops.len() - 1
2885        );
2886        let good_suffix_json = src.export_json_updates(&prefix_vv, &src.oplog_vv(), false);
2887        assert_eq!(good_suffix_json.changes[0].ops.len(), 1);
2888        let mut bad_suffix_json = good_suffix_json.clone();
2889        move_last_list_insert_far_out_of_bounds(&mut bad_suffix_json);
2890
2891        let prefix_dst = LoroDoc::new();
2892        prefix_dst.import_json_updates(prefix_json.clone()).unwrap();
2893        let vv_before_bad_suffix = prefix_dst.oplog_vv();
2894        let frontiers_before_bad_suffix = prefix_dst.oplog_frontiers();
2895        let state_before_bad_suffix = prefix_dst.get_deep_value();
2896
2897        let bad_suffix_json = serde_json::to_string(&bad_suffix_json).unwrap();
2898        let err = prefix_dst
2899            .import_json_updates(&bad_suffix_json)
2900            .unwrap_err();
2901        assert!(
2902            err.to_string().contains("list diff"),
2903            "expected state list bounds validation, got {err:?}"
2904        );
2905        assert_eq!(prefix_dst.oplog_vv(), vv_before_bad_suffix);
2906        assert_eq!(prefix_dst.oplog_frontiers(), frontiers_before_bad_suffix);
2907        assert_eq!(prefix_dst.get_deep_value(), state_before_bad_suffix);
2908
2909        prefix_dst.import_json_updates(good_suffix_json).unwrap();
2910        assert_eq!(prefix_dst.get_deep_value(), src.get_deep_value());
2911        assert_eq!(prefix_dst.oplog_vv(), src.oplog_vv());
2912
2913        let dst = LoroDoc::new();
2914        let vv_before_import = dst.oplog_vv();
2915        let frontiers_before_import = dst.oplog_frontiers();
2916        let state_before_import = dst.get_deep_value();
2917        let bad_json = serde_json::to_string(&bad_json).unwrap();
2918        let err = dst.import_json_updates(&bad_json).unwrap_err();
2919        assert!(
2920            err.to_string().contains("list diff"),
2921            "expected state list bounds validation, got {err:?}"
2922        );
2923        assert_eq!(dst.oplog_vv(), vv_before_import);
2924        assert_eq!(dst.oplog_frontiers(), frontiers_before_import);
2925        assert_eq!(dst.get_deep_value(), state_before_import);
2926        assert!(dst.oplog().lock().is_empty());
2927    }
2928
2929    #[test]
2930    fn failed_import_restores_pending_changes_that_were_applied_during_import() {
2931        let src = LoroDoc::new();
2932        src.set_peer_id(14).unwrap();
2933        let text = src.get_text("text");
2934
2935        let mut txn = src.txn().unwrap();
2936        text.insert_with_txn(&mut txn, 0, "a", PosType::Unicode)
2937            .unwrap();
2938        txn.commit().unwrap();
2939        let first_update = src.export(ExportMode::all_updates()).unwrap();
2940        let first_vv = src.oplog_vv();
2941
2942        let mut txn = src.txn().unwrap();
2943        text.insert_with_txn(&mut txn, 1, "b", PosType::Unicode)
2944            .unwrap();
2945        txn.commit().unwrap();
2946        let second_update = src.export(ExportMode::updates(&first_vv)).unwrap();
2947
2948        let dst = LoroDoc::new();
2949        let status = dst.import(&second_update).unwrap();
2950        assert!(status.success.is_empty());
2951        assert!(status.pending.is_some());
2952        let vv_before_dependency = dst.oplog_vv();
2953        let frontiers_before_dependency = dst.oplog_frontiers();
2954        let state_before_dependency = dst.get_deep_value();
2955
2956        crate::state::fail_next_import_state_apply_for_test();
2957        let err = dst.import(&first_update).unwrap_err();
2958        assert!(err.to_string().contains("state apply failpoint"));
2959        assert_eq!(dst.oplog_vv(), vv_before_dependency);
2960        assert_eq!(dst.oplog_frontiers(), frontiers_before_dependency);
2961        assert_eq!(dst.get_deep_value(), state_before_dependency);
2962
2963        dst.import(&first_update).unwrap();
2964        assert_eq!(dst.oplog_vv(), src.oplog_vv());
2965        assert_eq!(dst.oplog_frontiers(), src.oplog_frontiers());
2966        assert_eq!(dst.get_deep_value(), src.get_deep_value());
2967    }
2968
2969    #[test]
2970    fn failed_import_json_updates_does_not_emit_or_leave_events() {
2971        let (src, good_json) = make_json_list_update_with_four_ops(15);
2972        let mut bad_json = good_json.clone();
2973        move_last_list_insert_far_out_of_bounds(&mut bad_json);
2974
2975        let dst = LoroDoc::new();
2976        let event_count = Arc::new(AtomicUsize::new(0));
2977        let event_count_cloned = event_count.clone();
2978        let _sub = dst.subscribe_root(Arc::new(move |_| {
2979            event_count_cloned.fetch_add(1, Ordering::SeqCst);
2980        }));
2981
2982        let bad_json = serde_json::to_string(&bad_json).unwrap();
2983        let err = dst.import_json_updates(&bad_json).unwrap_err();
2984        assert!(
2985            err.to_string().contains("list diff"),
2986            "expected state list bounds validation, got {err:?}"
2987        );
2988        assert_eq!(event_count.load(Ordering::SeqCst), 0);
2989        assert!(dst.drop_pending_events().is_empty());
2990        assert!(dst.oplog().lock().is_empty());
2991
2992        dst.import_json_updates(good_json).unwrap();
2993        assert_eq!(event_count.load(Ordering::SeqCst), 1);
2994        assert_eq!(dst.get_deep_value(), src.get_deep_value());
2995    }
2996
2997    #[test]
2998    fn test_checkout() {
2999        let loro = LoroDoc::new();
3000        loro.set_peer_id(1).unwrap();
3001        let text = loro.get_text("text");
3002        let map = loro.get_map("map");
3003        let list = loro.get_list("list");
3004        let mut txn = loro.txn().unwrap();
3005        for i in 0..10 {
3006            map.insert_with_txn(&mut txn, "key", i.into()).unwrap();
3007            text.insert_with_txn(&mut txn, 0, &i.to_string(), PosType::Unicode)
3008                .unwrap();
3009            list.insert_with_txn(&mut txn, 0, i.into()).unwrap();
3010        }
3011        txn.commit().unwrap();
3012        let b = LoroDoc::new();
3013        b.import(&loro.export(ExportMode::Snapshot).unwrap())
3014            .unwrap();
3015        loro.checkout(&Frontiers::default()).unwrap();
3016        {
3017            let json = &loro.get_deep_value();
3018            assert_eq!(
3019                json.to_json_value(),
3020                serde_json::json!({"text":"","list":[],"map":{}})
3021            );
3022        }
3023
3024        b.checkout(&ID::new(1, 2).into()).unwrap();
3025        {
3026            let json = &b.get_deep_value();
3027            assert_eq!(
3028                json.to_json_value(),
3029                serde_json::json!({"text":"0","list":[0],"map":{"key":0}})
3030            );
3031        }
3032
3033        loro.checkout(&ID::new(1, 3).into()).unwrap();
3034        {
3035            let json = &loro.get_deep_value();
3036            assert_eq!(
3037                json.to_json_value(),
3038                serde_json::json!({"text":"0","list":[0],"map":{"key":1}})
3039            );
3040        }
3041
3042        b.checkout(&ID::new(1, 29).into()).unwrap();
3043        {
3044            let json = &b.get_deep_value();
3045            assert_eq!(
3046                json.to_json_value(),
3047                serde_json::json!({"text":"9876543210","list":[9,8,7,6,5,4,3,2,1,0],"map":{"key":9}})
3048            );
3049        }
3050    }
3051
3052    #[test]
3053    fn import_batch_err_181() {
3054        let a = LoroDoc::new_auto_commit();
3055        let update_a = a.export(ExportMode::Snapshot);
3056        let b = LoroDoc::new_auto_commit();
3057        b.import_batch(&[update_a.unwrap()]).unwrap();
3058        b.get_text("text")
3059            .insert(0, "hello", PosType::Unicode)
3060            .unwrap();
3061        b.commit_then_renew();
3062        let oplog = b.oplog().lock();
3063        drop(oplog);
3064        b.export(ExportMode::all_updates()).unwrap();
3065    }
3066
3067    #[test]
3068    fn poisoned_mutex_keeps_follow_up_operations_failed() {
3069        let doc = LoroDoc::new();
3070        let oplog = doc.oplog.clone();
3071        let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
3072            let _guard = oplog.lock();
3073            panic!("poison oplog");
3074        }));
3075
3076        let err = std::panic::catch_unwind(AssertUnwindSafe(|| doc.oplog_vv()))
3077            .expect_err("poisoned lock should continue to fail fast");
3078        let msg = if let Some(msg) = err.downcast_ref::<&str>() {
3079            (*msg).to_string()
3080        } else if let Some(msg) = err.downcast_ref::<String>() {
3081            msg.clone()
3082        } else {
3083            String::new()
3084        };
3085        assert!(msg.contains("poisoned LoroMutex"), "{msg}");
3086    }
3087
3088    #[test]
3089    fn repeated_independent_scalar_root_imports_scan_history_once() {
3090        let base = LoroDoc::new_auto_commit();
3091        base.set_peer_id(1).unwrap();
3092        for i in 0..64 {
3093            base.get_map("existing")
3094                .insert(&format!("key-{i}"), i)
3095                .unwrap();
3096            base.commit_then_renew();
3097        }
3098
3099        let target = LoroDoc::new();
3100        target
3101            .import(&base.export(ExportMode::Snapshot).unwrap())
3102            .unwrap();
3103        assert!(!target.has_history_cache());
3104
3105        for i in 0_u64..8 {
3106            let remote = LoroDoc::new_auto_commit();
3107            remote.set_peer_id(2 + i).unwrap();
3108            remote
3109                .get_map(format!("isolated-{i}"))
3110                .insert("value", i as i32)
3111                .unwrap();
3112            let update = remote.export(ExportMode::all_updates()).unwrap();
3113            target.import(&update).unwrap();
3114            assert_eq!(
3115                target.get_map(format!("isolated-{i}")).get("value"),
3116                Some((i as i32).into())
3117            );
3118        }
3119
3120        assert!(!target.has_history_cache());
3121        assert_eq!(
3122            target
3123                .oplog
3124                .lock()
3125                .change_store()
3126                .root_history_scan_count_for_test(),
3127            1
3128        );
3129    }
3130
3131    #[test]
3132    fn independent_fast_path_rejects_root_present_only_in_snapshot_state() {
3133        fn snapshot_sections(doc: &LoroDoc) -> crate::encoding::fast_snapshot::Snapshot {
3134            let (_, txn_guard) = doc.implicit_commit_then_stop();
3135            drop(txn_guard);
3136            crate::encoding::fast_snapshot::encode_snapshot_inner(doc).unwrap()
3137        }
3138
3139        let base = LoroDoc::new_auto_commit();
3140        base.set_peer_id(1).unwrap();
3141        base.get_map("existing").insert("value", "base").unwrap();
3142        let base_sections = snapshot_sections(&base);
3143
3144        let state_donor = LoroDoc::new_auto_commit();
3145        state_donor.set_peer_id(999).unwrap();
3146        state_donor
3147            .get_map("isolated")
3148            .insert("value", "stale")
3149            .unwrap();
3150        let donor_sections = snapshot_sections(&state_donor);
3151
3152        let target = LoroDoc::new();
3153        crate::encoding::fast_snapshot::decode_snapshot_inner(
3154            crate::encoding::fast_snapshot::Snapshot {
3155                oplog_bytes: base_sections.oplog_bytes,
3156                state_bytes: donor_sections.state_bytes,
3157                shallow_root_state_bytes: Bytes::new(),
3158            },
3159            &target,
3160            Default::default(),
3161        )
3162        .unwrap();
3163        assert!(!target.has_history_cache());
3164
3165        let left = LoroDoc::new_auto_commit();
3166        left.set_peer_id(2).unwrap();
3167        left.get_map("isolated").insert("value", "left").unwrap();
3168        let right = LoroDoc::new_auto_commit();
3169        right.set_peer_id(3).unwrap();
3170        right.get_map("isolated").insert("value", "winner").unwrap();
3171        let aggregate = LoroDoc::new();
3172        aggregate
3173            .import(&left.export(ExportMode::all_updates()).unwrap())
3174            .unwrap();
3175        aggregate
3176            .import(&right.export(ExportMode::all_updates()).unwrap())
3177            .unwrap();
3178
3179        target
3180            .import(&aggregate.export(ExportMode::all_updates()).unwrap())
3181            .unwrap();
3182
3183        assert_eq!(
3184            target.get_map("isolated").get("value"),
3185            Some("winner".into())
3186        );
3187        assert!(target.has_history_cache());
3188    }
3189
3190    #[test]
3191    fn independent_scalar_root_import_rolls_back_after_state_failure() {
3192        let base = LoroDoc::new_auto_commit();
3193        base.set_peer_id(1).unwrap();
3194        base.get_map("existing").insert("value", "base").unwrap();
3195
3196        let target = LoroDoc::new();
3197        target
3198            .import(&base.export(ExportMode::Snapshot).unwrap())
3199            .unwrap();
3200        let vv_before = target.oplog_vv();
3201        let state_before = target.get_deep_value();
3202
3203        let remote = LoroDoc::new_auto_commit();
3204        remote.set_peer_id(2).unwrap();
3205        remote.get_map("isolated").insert("value", 7).unwrap();
3206        let update = remote.export(ExportMode::all_updates()).unwrap();
3207
3208        crate::state::fail_next_import_state_apply_for_test();
3209        let err = target.import(&update).unwrap_err();
3210        assert!(err.to_string().contains("state apply failpoint"));
3211        assert_eq!(target.oplog_vv(), vv_before);
3212        assert_eq!(target.get_deep_value(), state_before);
3213        assert!(!target.has_history_cache());
3214
3215        target.import(&update).unwrap();
3216        assert_eq!(target.get_map("isolated").get("value"), Some(7.into()));
3217    }
3218
3219    #[test]
3220    fn independent_fast_path_rejects_root_name_seen_in_history() {
3221        let base = LoroDoc::new_auto_commit();
3222        base.set_peer_id(1).unwrap();
3223        base.get_map("shared").insert("value", "base").unwrap();
3224
3225        let target = LoroDoc::new();
3226        target
3227            .import(&base.export(ExportMode::Snapshot).unwrap())
3228            .unwrap();
3229        assert!(!target.has_history_cache());
3230
3231        let remote = LoroDoc::new_auto_commit();
3232        remote.set_peer_id(999).unwrap();
3233        remote.get_map("shared").insert("value", "remote").unwrap();
3234        target
3235            .import(&remote.export(ExportMode::all_updates()).unwrap())
3236            .unwrap();
3237
3238        assert_eq!(target.get_map("shared").get("value"), Some("remote".into()));
3239        assert!(target.has_history_cache());
3240    }
3241
3242    #[test]
3243    fn independent_fast_path_rejects_deleted_root_name() {
3244        let base = LoroDoc::new_auto_commit();
3245        base.set_peer_id(1).unwrap();
3246        let root_id = ContainerID::new_root("deleted", ContainerType::Map);
3247        base.get_map("deleted").insert("value", "base").unwrap();
3248        base.delete_root_container(root_id);
3249
3250        let target = LoroDoc::new();
3251        target
3252            .import(&base.export(ExportMode::Snapshot).unwrap())
3253            .unwrap();
3254        assert!(!target.has_history_cache());
3255
3256        let remote = LoroDoc::new_auto_commit();
3257        remote.set_peer_id(2).unwrap();
3258        remote.get_map("deleted").insert("value", "remote").unwrap();
3259        target
3260            .import(&remote.export(ExportMode::all_updates()).unwrap())
3261            .unwrap();
3262
3263        assert!(target.has_history_cache());
3264    }
3265}