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