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