Skip to main content

loro_internal/
oplog.rs

1mod change_store;
2pub(crate) mod loro_dag;
3mod pending_changes;
4
5use crate::sync::{AtomicUsize, Mutex};
6use bytes::Bytes;
7use std::borrow::Cow;
8use std::cell::RefCell;
9use std::cmp::Ordering;
10use std::rc::Rc;
11use std::sync::Arc;
12use tracing::trace_span;
13
14use self::change_store::iter::MergedChangeIter;
15use self::pending_changes::{PendingChanges, PendingChangesRollback};
16use super::arena::{SharedArena, SharedArenaRollback};
17use crate::change::{get_sys_timestamp, Change, Lamport, Timestamp};
18use crate::configure::Configure;
19use crate::container::idx::ContainerIdx;
20use crate::container::list::list_op;
21use crate::dag::{Dag, DagUtils, MeetAsBase};
22use crate::diff_calc::DiffMode;
23use crate::encoding::decode_oplog;
24use crate::encoding::{ImportStatus, ParsedHeaderAndBody};
25use crate::history_cache::ContainerHistoryCache;
26use crate::id::{Counter, PeerID, ID};
27use crate::op::{FutureInnerContent, ListSlice, RawOpContent, RemoteOp, RichOp};
28use crate::span::{HasCounterSpan, HasLamportSpan};
29use crate::version::{Frontiers, ImVersionVector, VersionVector};
30use crate::LoroError;
31use change_store::{BlockOpRef, ChangeStoreRollback};
32use loro_common::{ContainerType, HasIdSpan, IdLp, IdSpan};
33use rle::{HasLength, RleVec, Sliceable};
34use rustc_hash::FxHashSet;
35use smallvec::SmallVec;
36
37pub use self::loro_dag::{AppDag, AppDagNode, FrontiersNotIncluded};
38pub use change_store::{BlockChangeRef, ChangeStore};
39
40/// [OpLog] store all the ops i.e. the history.
41/// It allows multiple [AppState] to attach to it.
42/// So you can derive different versions of the state from the [OpLog].
43/// It allows us to build a version control system.
44///
45/// The causal graph should always be a DAG and complete, so two versions always have a replay base (at worst the empty version).
46/// If deps are missing, we can't import the change. It will be put into the `pending_changes`.
47pub struct OpLog {
48    pub(crate) dag: AppDag,
49    pub(crate) arena: SharedArena,
50    visible_op_count: Arc<AtomicUsize>,
51    change_store: ChangeStore,
52    history_cache: Mutex<ContainerHistoryCache>,
53    /// Pending changes that haven't been applied to the dag.
54    /// A change can be imported only when all its deps are already imported.
55    /// Key is the ID of the missing dep
56    pub(crate) pending_changes: PendingChanges,
57    /// Whether we are importing a batch of changes.
58    /// If so the Dag's frontiers won't be updated until the batch is finished.
59    pub(crate) batch_importing: bool,
60    pub(crate) configure: Configure,
61    /// The uncommitted change, it's a placeholder for the change
62    /// that is being edited in pre-commit callback.
63    pub(crate) uncommitted_change: Option<Change>,
64    pub(crate) import_rollback: Option<ImportRollback>,
65}
66
67pub(crate) struct ImportRollback {
68    old_vv: VersionVector,
69    arena: SharedArenaRollback,
70    change_store: ChangeStoreRollback,
71    pending: PendingChangesRollback,
72}
73
74#[derive(Debug, Default, Clone, Copy)]
75pub(crate) struct ImportChangesPreflight {
76    pub applies_to_dag: bool,
77    pub has_deps_before_shallow_root: bool,
78    pub needs_state_apply_rollback: bool,
79}
80
81impl std::fmt::Debug for OpLog {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        f.debug_struct("OpLog")
84            .field("dag", &self.dag)
85            .field("pending_changes", &self.pending_changes)
86            .finish()
87    }
88}
89
90#[cfg(test)]
91thread_local! {
92    /// Counts falls from the multi-head critical version search back to the
93    /// single-head whole-DAG descent. Test observability only; thread-local
94    /// so concurrently running tests cannot disturb each other's counts
95    /// (diff calc runs on the importing thread).
96    pub(crate) static CRITICAL_BASE_FALLBACK_COUNT: std::cell::Cell<u64> =
97        const { std::cell::Cell::new(0) };
98}
99
100/// Result of [`OpLog::latest_critical_version_below_meet`].
101#[derive(Debug, PartialEq, Eq)]
102pub(crate) enum CriticalVersionSearch {
103    /// The greatest critical version ≤ `meet(from, to)`.
104    Found(VersionVector),
105    /// Provably no non-empty critical version ≤ meet exists: the fixpoint
106    /// bottomed out at ∅, and every critical cut ≤ meet survives every
107    /// lowering step, so the single-head descent cannot find one either —
108    /// callers should skip its whole-DAG walk.
109    NoneBelowMeet,
110    /// A change's deps reach below trimmed history; nothing is known. Fall
111    /// back to the single-head descent.
112    Unknown,
113}
114
115/// The replay base [`OpLog::iter_from_replay_base_causally`] chose.
116#[derive(Debug)]
117pub(crate) struct ReplayBase {
118    pub vv: VersionVector,
119    pub diff_mode: DiffMode,
120    /// The base is a *critical version* of `ancestry(from) ∪ ancestry(to)`:
121    /// no event above it is concurrent with any event inside it. Calculators
122    /// that seed their CRDT state with one opaque "unknown" span for
123    /// everything below the base need exactly this property, because such a
124    /// span can never be retreated.
125    pub is_critical: bool,
126    /// `Some` when the import was proved register-only concurrent with
127    /// `from` and replays from it (see `OpLog::register_only_concurrency`):
128    /// every container with an op in the concurrent old history.
129    pub concurrent_containers: Option<FxHashSet<ContainerIdx>>,
130}
131
132impl OpLog {
133    #[inline]
134    pub(crate) fn new(visible_op_count: Arc<AtomicUsize>) -> Self {
135        let arena = SharedArena::new();
136        let cfg = Configure::default();
137        let change_store = ChangeStore::new_mem(&arena, cfg.merge_interval_in_s.clone());
138        Self {
139            visible_op_count,
140            history_cache: Mutex::new(ContainerHistoryCache::new(change_store.clone(), None)),
141            dag: AppDag::new(change_store.clone()),
142            change_store,
143            arena,
144            pending_changes: Default::default(),
145            batch_importing: false,
146            configure: cfg,
147            uncommitted_change: None,
148            import_rollback: None,
149        }
150    }
151
152    #[inline]
153    fn calc_visible_op_count(&self) -> usize {
154        let total = self.dag.vv().values().sum::<i32>() as usize;
155        let shallow = self
156            .dag
157            .shallow_since_vv()
158            .iter()
159            .map(|(_, ops)| *ops)
160            .sum::<i32>() as usize;
161        total - shallow
162    }
163
164    #[inline]
165    pub(crate) fn visible_op_count_exact(&self) -> usize {
166        self.calc_visible_op_count()
167    }
168
169    #[inline]
170    pub(crate) fn refresh_visible_op_count(&self) -> usize {
171        let count = self.calc_visible_op_count();
172        self.visible_op_count
173            .store(count, std::sync::atomic::Ordering::Release);
174        count
175    }
176
177    /// Incrementally bump the cached visible op count for newly applied *local*
178    /// ops. Local ops are always visible (never behind the shallow root), so the
179    /// visible count grows by exactly `delta`. This avoids a per-op full
180    /// recompute via [`Self::calc_visible_op_count`], which iterates the version
181    /// vectors and heap-allocates an `im::HashMap` iterator on every call.
182    #[inline]
183    pub(crate) fn inc_visible_op_count(&self, delta: usize) {
184        self.visible_op_count
185            .fetch_add(delta, std::sync::atomic::Ordering::Release);
186    }
187
188    #[cfg(test)]
189    pub(crate) fn cached_visible_op_count(&self) -> usize {
190        self.visible_op_count
191            .load(std::sync::atomic::Ordering::Acquire)
192    }
193
194    #[inline]
195    pub fn dag(&self) -> &AppDag {
196        &self.dag
197    }
198
199    pub fn change_store(&self) -> &ChangeStore {
200        &self.change_store
201    }
202
203    /// Get the change with the given peer and lamport.
204    ///
205    /// If not found, return the change with the greatest lamport that is smaller than the given lamport.
206    pub fn get_change_with_lamport_lte(
207        &self,
208        peer: PeerID,
209        lamport: Lamport,
210    ) -> Option<BlockChangeRef> {
211        let ans = self
212            .change_store
213            .get_change_by_lamport_lte(IdLp::new(peer, lamport))?;
214        debug_assert!(ans.lamport <= lamport);
215        Some(ans)
216    }
217
218    pub fn get_timestamp_of_version(&self, f: &Frontiers) -> Timestamp {
219        let mut timestamp = Timestamp::default();
220        for id in f.iter() {
221            if let Some(change) = self.lookup_change(id) {
222                timestamp = timestamp.max(change.timestamp);
223            }
224        }
225
226        timestamp
227    }
228
229    #[inline]
230    pub fn is_empty(&self) -> bool {
231        self.dag.is_empty() && self.arena.can_import_snapshot()
232    }
233
234    /// This is the **only** place to update the `OpLog.changes`
235    pub(crate) fn insert_new_change(&mut self, change: Change, from_local: bool) {
236        let s = trace_span!(
237            "insert_new_change",
238            id = ?change.id,
239            lamport = change.lamport,
240            deps = ?change.deps
241        );
242        let _enter = s.enter();
243        let rollback_old_vv = self
244            .import_rollback
245            .as_ref()
246            .and_then(|x| (!x.old_vv.is_empty()).then_some(&x.old_vv));
247        self.dag
248            .handle_new_change(&change, from_local, rollback_old_vv);
249        self.history_cache
250            .lock()
251            .insert_by_new_change(&change, true, true);
252        self.register_container_and_parent_link(&change);
253        if let Some(rollback) = self.import_rollback.as_mut() {
254            self.change_store.insert_change_with_rollback(
255                change,
256                true,
257                from_local,
258                &mut rollback.change_store,
259            );
260        } else {
261            self.change_store.insert_change(change, true, from_local);
262        }
263        self.refresh_visible_op_count();
264    }
265
266    pub(crate) fn begin_import_rollback(&mut self) {
267        let arena = self.arena.checkpoint_for_rollback();
268        self.begin_import_rollback_with_arena(arena);
269    }
270
271    pub(crate) fn begin_import_rollback_with_arena(&mut self, arena: SharedArenaRollback) {
272        debug_assert!(self.import_rollback.is_none());
273        let old_vv = self.vv().clone();
274        self.dag.begin_import_rollback();
275        self.import_rollback = Some(ImportRollback {
276            old_vv: old_vv.clone(),
277            arena,
278            change_store: ChangeStoreRollback::new(old_vv),
279            pending: Default::default(),
280        });
281    }
282
283    /// Whether an import rollback scope is currently open.
284    ///
285    /// Scopes cannot nest: [`Self::begin_import_rollback_with_arena`] overwrites the
286    /// journal, and the matching commit/rollback clears it. Callers that may run
287    /// inside another scope (e.g. a blob imported by `LoroDoc::import_batch`) must
288    /// check this first and leave the scope to its owner.
289    pub(crate) fn has_import_rollback(&self) -> bool {
290        self.import_rollback.is_some()
291    }
292
293    pub(crate) fn commit_import_rollback(&mut self) {
294        self.dag.commit_import_rollback();
295        self.import_rollback = None;
296    }
297
298    /// Close an import rollback scope this caller owns: commit it when `keep`,
299    /// roll everything back otherwise. No-op when `owns` is false — the scope
300    /// then belongs to an outer owner such as `import_batch`.
301    pub(crate) fn end_import_rollback(&mut self, owns: bool, keep: bool) {
302        if !owns {
303            return;
304        }
305
306        if keep {
307            self.commit_import_rollback();
308        } else {
309            self.rollback_import();
310        }
311    }
312
313    pub(crate) fn preflight_import_changes(&self, changes: &[Change]) -> ImportChangesPreflight {
314        let mut ans = ImportChangesPreflight::default();
315        for change in changes {
316            if change.ctr_end() <= self.vv().get(&change.id.peer).copied().unwrap_or(0) {
317                continue;
318            }
319
320            if self.dag.import_deps_before_shallow_root(&change.deps) {
321                ans.has_deps_before_shallow_root = true;
322                continue;
323            }
324
325            if self
326                .dag
327                .get_change_lamport_from_deps(&change.deps)
328                .is_none()
329            {
330                continue;
331            }
332
333            ans.applies_to_dag = true;
334            if change.ops.iter().any(|op| {
335                matches!(
336                    op.container.get_type(),
337                    ContainerType::List | ContainerType::Tree
338                )
339            }) {
340                ans.needs_state_apply_rollback = true;
341            }
342        }
343
344        // Any newly applied change can unlock pending changes whose ops are not
345        // visible in `changes`, so include pending in the rollback decision.
346        // Keep this narrow: text/map-only pending changes cannot return a
347        // state-apply error, and forcing rollback there adds lock traffic to
348        // small sync/import workloads.
349        //
350        // Scan pending last, and only when it can still change the answer:
351        // `has_state_apply_rollback_ops` walks every parked change and every op in
352        // it, while a blob that only parks (deps not here yet) leaves
353        // `applies_to_dag` false. Evaluating it eagerly made a batch of N
354        // out-of-order blobs quadratic, since each blob re-scanned the pending set
355        // the earlier blobs had grown.
356        if ans.applies_to_dag
357            && !ans.needs_state_apply_rollback
358            && self.pending_changes.has_state_apply_rollback_ops()
359        {
360            ans.needs_state_apply_rollback = true;
361        }
362
363        #[cfg(test)]
364        if ans.applies_to_dag {
365            ans.needs_state_apply_rollback = true;
366        }
367
368        ans
369    }
370
371    pub(crate) fn rollback_import(&mut self) {
372        let Some(rollback) = self.import_rollback.take() else {
373            return;
374        };
375
376        self.change_store.rollback_import(rollback.change_store);
377        self.dag.rollback_import();
378        rollback.pending.rollback(&mut self.pending_changes);
379        self.history_cache.lock().free_all();
380        self.arena.rollback(rollback.arena);
381        self.refresh_visible_op_count();
382    }
383
384    pub(crate) fn reset_to_empty_for_failed_snapshot_import(
385        &mut self,
386        arena_checkpoint: SharedArenaRollback,
387    ) {
388        let arena = self.arena.clone();
389        let configure = self.configure.clone();
390        arena.rollback(arena_checkpoint);
391        let change_store = ChangeStore::new_mem(&arena, configure.merge_interval_in_s.clone());
392        self.history_cache = Mutex::new(ContainerHistoryCache::new(change_store.clone(), None));
393        self.dag = AppDag::new(change_store.clone());
394        self.change_store = change_store;
395        self.pending_changes = Default::default();
396        self.batch_importing = false;
397        self.configure = configure;
398        self.uncommitted_change = None;
399        self.import_rollback = None;
400        self.visible_op_count
401            .store(0, std::sync::atomic::Ordering::Release);
402    }
403
404    #[inline(always)]
405    pub(crate) fn with_history_cache<F, R>(&self, f: F) -> R
406    where
407        F: FnOnce(&mut ContainerHistoryCache) -> R,
408    {
409        let mut history_cache = self.history_cache.lock();
410        f(&mut history_cache)
411    }
412
413    pub fn has_history_cache(&self) -> bool {
414        self.history_cache.lock().has_cache()
415    }
416
417    pub fn free_history_cache(&self) {
418        let mut history_cache = self.history_cache.lock();
419        history_cache.free();
420    }
421
422    #[cfg(test)]
423    #[allow(dead_code)]
424    pub(crate) fn pending_changes_len(&self) -> usize {
425        self.pending_changes.len()
426    }
427
428    /// Import a change.
429    ///
430    /// Pending changes that haven't been applied to the dag.
431    /// A change can be imported only when all its deps are already imported.
432    /// Key is the ID of the missing dep
433    ///
434    /// # Err
435    ///
436    /// - Return Err(LoroError::UsedOpID) when the change's id is occupied
437    /// - Return Err(LoroError::DecodeError) when the change's deps are missing
438    pub(crate) fn import_local_change(&mut self, change: Change) -> Result<(), LoroError> {
439        self.insert_new_change(change, true);
440        Ok(())
441    }
442
443    /// Trim the known part of change
444    pub(crate) fn trim_the_known_part_of_change(&self, change: Change) -> Option<Change> {
445        let Some(&end) = self.dag.vv().get(&change.id.peer) else {
446            return Some(change);
447        };
448
449        if change.id.counter >= end {
450            return Some(change);
451        }
452
453        if change.ctr_end() <= end {
454            return None;
455        }
456
457        let offset = (end - change.id.counter) as usize;
458        Some(change.slice(offset, change.atom_len()))
459    }
460
461    #[allow(unused)]
462    fn check_id_is_not_duplicated(&self, id: ID) -> Result<(), LoroError> {
463        let cur_end = self.dag.vv().get(&id.peer).cloned().unwrap_or(0);
464        if cur_end > id.counter {
465            return Err(LoroError::UsedOpID { id });
466        }
467
468        Ok(())
469    }
470
471    /// Ensure the new change is greater than the last peer's id and the counter is continuous.
472    ///
473    /// It can be false when users use detached editing mode and use a custom peer id.
474    // This method might be slow and can be optimized if needed in the future.
475    pub(crate) fn check_change_greater_than_last_peer_id(
476        &self,
477        peer: PeerID,
478        counter: Counter,
479        deps: &Frontiers,
480    ) -> Result<(), LoroError> {
481        if counter == 0 {
482            return Ok(());
483        }
484
485        if !self.configure.detached_editing() {
486            return Ok(());
487        }
488
489        let mut max_last_counter = -1;
490        for dep in deps.iter() {
491            let dep_vv = self
492                .dag
493                .get_vv(dep)
494                .ok_or(LoroError::FrontiersNotFound(dep))?;
495            max_last_counter = max_last_counter.max(dep_vv.get(&peer).cloned().unwrap_or(0) - 1);
496        }
497
498        if counter != max_last_counter + 1 {
499            return Err(LoroError::ConcurrentOpsWithSamePeerID {
500                peer,
501                last_counter: max_last_counter,
502                current: counter,
503            });
504        }
505
506        Ok(())
507    }
508
509    pub(crate) fn next_id(&self, peer: PeerID) -> ID {
510        let cnt = self.dag.vv().get(&peer).copied().unwrap_or(0);
511        ID::new(peer, cnt)
512    }
513
514    pub(crate) fn vv(&self) -> &VersionVector {
515        self.dag.vv()
516    }
517
518    pub(crate) fn frontiers(&self) -> &Frontiers {
519        self.dag.frontiers()
520    }
521
522    /// - Ordering::Less means self is less than target or parallel
523    /// - Ordering::Equal means versions equal
524    /// - Ordering::Greater means self's version is greater than target
525    pub fn cmp_with_frontiers(&self, other: &Frontiers) -> Ordering {
526        self.dag.cmp_with_frontiers(other)
527    }
528
529    /// Compare two [Frontiers] causally.
530    ///
531    /// If one of the [Frontiers] are not included, it will return [FrontiersNotIncluded].
532    #[inline]
533    pub fn cmp_frontiers(
534        &self,
535        a: &Frontiers,
536        b: &Frontiers,
537    ) -> Result<Option<Ordering>, FrontiersNotIncluded> {
538        self.dag.cmp_frontiers(a, b)
539    }
540
541    pub(crate) fn get_min_lamport_at(&self, id: ID) -> Lamport {
542        self.get_change_at(id).map(|c| c.lamport).unwrap_or(0)
543    }
544
545    pub(crate) fn get_lamport_at(&self, id: ID) -> Option<Lamport> {
546        self.get_change_at(id)
547            .map(|c| c.lamport + (id.counter - c.id.counter) as Lamport)
548    }
549
550    pub(crate) fn iter_ops(&self, id_span: IdSpan) -> impl Iterator<Item = RichOp<'static>> + '_ {
551        let change_iter = self.change_store.iter_changes(id_span);
552        change_iter.flat_map(move |c| RichOp::new_iter_by_cnt_range(c, id_span.counter))
553    }
554
555    pub(crate) fn iter_changes(
556        &self,
557        id_span: IdSpan,
558    ) -> impl Iterator<Item = BlockChangeRef> + '_ {
559        self.change_store.iter_changes(id_span)
560    }
561
562    pub(crate) fn get_max_lamport_at(&self, id: ID) -> Lamport {
563        self.get_change_at(id)
564            .map(|c| {
565                let change_counter = c.id.counter as u32;
566                c.lamport + c.ops().last().map(|op| op.counter).unwrap_or(0) as u32 - change_counter
567            })
568            .unwrap_or(Lamport::MAX)
569    }
570
571    pub fn get_change_at(&self, id: ID) -> Option<BlockChangeRef> {
572        self.change_store.get_change(id)
573    }
574
575    pub(crate) fn set_uncommitted_change(&mut self, change: Change) {
576        self.uncommitted_change = Some(change);
577    }
578
579    pub(crate) fn get_uncommitted_change_in_span(
580        &self,
581        id_span: IdSpan,
582    ) -> Option<Cow<'_, Change>> {
583        self.uncommitted_change.as_ref().and_then(|c| {
584            if c.id_span() == id_span {
585                Some(Cow::Borrowed(c))
586            } else if let Some((start, end)) = id_span.get_slice_range_on(&c.id_span()) {
587                Some(Cow::Owned(c.slice(start, end)))
588            } else {
589                None
590            }
591        })
592    }
593
594    pub fn get_deps_of(&self, id: ID) -> Option<Frontiers> {
595        self.get_change_at(id).map(|c| {
596            if c.id.counter == id.counter {
597                c.deps.clone()
598            } else {
599                Frontiers::from_id(id.inc(-1))
600            }
601        })
602    }
603
604    pub fn get_remote_change_at(&self, id: ID) -> Option<Change<RemoteOp<'static>>> {
605        let change = self.get_change_at(id)?;
606        Some(convert_change_to_remote(&self.arena, &change))
607    }
608
609    pub(crate) fn import_unknown_lamport_pending_changes(
610        &mut self,
611        remote_changes: Vec<Change>,
612        would_affect: Option<&mut crate::version::VersionRange>,
613    ) -> crate::version::VersionRange {
614        self.extend_pending_changes_with_unknown_lamport(remote_changes, would_affect)
615    }
616
617    /// lookup change by id.
618    ///
619    /// if id does not included in this oplog, return None
620    pub(crate) fn lookup_change(&self, id: ID) -> Option<BlockChangeRef> {
621        self.change_store.get_change(id)
622    }
623
624    #[inline(always)]
625    pub(crate) fn export_change_store_from(&self, vv: &VersionVector, f: &Frontiers) -> Bytes {
626        self.change_store
627            .export_from(vv, f, self.vv(), self.frontiers())
628    }
629
630    #[inline(always)]
631    pub(crate) fn export_change_store_in_range(
632        &self,
633        vv: &VersionVector,
634        f: &Frontiers,
635        to_vv: &VersionVector,
636        to_frontiers: &Frontiers,
637    ) -> Bytes {
638        self.change_store.export_from(vv, f, to_vv, to_frontiers)
639    }
640
641    #[inline(always)]
642    pub(crate) fn export_blocks_from<W: std::io::Write>(&self, vv: &VersionVector, w: &mut W) {
643        self.change_store
644            .export_blocks_from(vv, self.shallow_since_vv(), self.vv(), w)
645    }
646
647    #[inline(always)]
648    pub(crate) fn export_blocks_in_range<W: std::io::Write>(&self, spans: &[IdSpan], w: &mut W) {
649        self.change_store.export_blocks_in_range(spans, w)
650    }
651
652    pub(crate) fn fork_changes_up_to(&self, frontiers: &Frontiers) -> Option<Bytes> {
653        let vv = self.dag.frontiers_to_vv(frontiers)?;
654        Some(
655            self.change_store
656                .fork_changes_up_to(self.dag.shallow_since_vv(), frontiers, &vv),
657        )
658    }
659
660    #[inline(always)]
661    pub(crate) fn decode(&mut self, data: ParsedHeaderAndBody) -> Result<ImportStatus, LoroError> {
662        decode_oplog(self, data)
663    }
664
665    /// Containers that have at least one op inside `spans`.
666    ///
667    /// Only `op.container` is needed, so this scans the borrowed changes
668    /// directly instead of materializing a cloned RichOp per op.
669    pub(crate) fn containers_in_spans(
670        &self,
671        spans: impl Iterator<Item = IdSpan>,
672    ) -> FxHashSet<ContainerIdx> {
673        let mut containers = FxHashSet::default();
674        // Consecutive ops overwhelmingly share a container; skip the hash
675        // probe when it hasn't changed.
676        let mut last_container = None;
677        for span in spans {
678            for change in self.change_store.iter_changes(span) {
679                let start_counter = span.counter.min().max(change.id.counter);
680                let end_counter = span.counter.norm_end();
681                let start = change
682                    .ops
683                    .binary_search_by(|op| op.ctr_last().cmp(&start_counter))
684                    .unwrap_or_else(|e| e);
685                for op in &change.ops.vec()[start..] {
686                    if op.counter >= end_counter {
687                        break;
688                    }
689
690                    if last_container != Some(op.container) {
691                        containers.insert(op.container);
692                        last_container = Some(op.container);
693                    }
694                }
695            }
696        }
697
698        containers
699    }
700
701    /// For `to ⊇ from`: the old-parent frontiers of every entry change of the
702    /// new region `to − from` that does not causally cover all of `from`.
703    ///
704    /// An entry change is a new change (or the new suffix of a change that
705    /// straddles `from`) whose causal parents — explicit deps plus the
706    /// implicit same-peer predecessor — are all old. Every new event is a
707    /// descendant of some entry change, so `⋂ Events(parents)` over the
708    /// uncovered entries is causally before the whole new region and only
709    /// `Events(from)` outside it can be concurrent with a new op.
710    ///
711    /// This is the version-vector form of the DAG's
712    /// `new_region_uncovered_entry_parents` (see
713    /// `docs/critical-version-spec.md` L12): it only scans the new changes
714    /// and answers coverage with cached version vectors, so it costs
715    /// `O(|new changes|)` instead of a lamport-pruned ancestor walk. An empty
716    /// vector means the `ImportGreaterUpdates` contract holds outright;
717    /// `None` means trimmed history got in the way.
718    fn uncovered_entry_parents(
719        &self,
720        from: &VersionVector,
721        from_frontiers: &Frontiers,
722        to: &VersionVector,
723    ) -> Option<Vec<Frontiers>> {
724        let mut uncovered = Vec::new();
725        for (peer, span) in from.diff(to).forward.iter() {
726            let id_span = IdSpan::new(*peer, span.start, span.end);
727            for change in self.change_store.iter_changes(id_span) {
728                let start = change.id.counter.max(span.start);
729                let parents = if start > change.id.counter {
730                    // The change straddles `from`; the new suffix only has
731                    // the implicit predecessor as parent.
732                    Frontiers::from_id(ID::new(*peer, start - 1))
733                } else {
734                    let mut parents = change.deps().clone();
735                    if change.id.counter > 0 {
736                        let prev = ID::new(*peer, change.id.counter - 1);
737                        if !parents.contains(&prev) {
738                            parents.push(prev);
739                        }
740                    }
741                    parents
742                };
743
744                // The everyday shape: the change depends on exactly the
745                // current frontiers. Covered without touching any version vector.
746                if &parents == from_frontiers {
747                    continue;
748                }
749
750                if !parents.iter().all(|id| from.includes_id(id)) {
751                    // Not an entry change; it inherits coverage through its
752                    // new parents.
753                    continue;
754                }
755
756                let parents_vv = self.dag.frontiers_to_vv(&parents)?;
757                if !parents_vv.includes_vv(from) {
758                    uncovered.push(parents);
759                }
760            }
761        }
762
763        Some(uncovered)
764    }
765
766    /// Decide whether an import whose new region `to − from` is concurrent
767    /// with part of `from` can still be replayed from `from`.
768    ///
769    /// `entry_parents` comes from [`OpLog::uncovered_entry_parents`]: the old
770    /// history that is causally before the *whole* new region is
771    /// `⋂ Events(parents)`, so the old ops that may be concurrent with a new op
772    /// are `Events(from) − ⋂ Events(parents)`.
773    ///
774    /// Sequence and tree containers need every concurrent op of the same
775    /// container as context, so a container with ops on both sides forces the
776    /// conservative replay. Map and counter are registers: a counter diff is
777    /// a commutative sum, and a map diff can be resolved per key from the
778    /// history cache without positional context, so concurrency on them is
779    /// harmless. (The map must NOT be resolved by comparing lamports against
780    /// the current state: persisted state drops metadata for deleted roots and
781    /// dead containers, so the diff calculator is told to use the history
782    /// cache for these containers, see `DiffCalculator::calc_diff_internal`.)
783    ///
784    /// Returns the containers touched by the concurrent old history when the
785    /// overlap is register-only, `None` otherwise.
786    fn register_only_concurrency(
787        &self,
788        from: &VersionVector,
789        to: &VersionVector,
790        entry_parents: &[Frontiers],
791    ) -> Option<FxHashSet<ContainerIdx>> {
792        // ⋂ Events(parents) as a version vector: per-peer minimum.
793        let mut causal_past = from.clone();
794        for parents in entry_parents {
795            let parents_vv = self.dag.frontiers_to_vv(parents)?;
796            causal_past.retain(|peer, end| {
797                let bound = parents_vv.get(peer).copied().unwrap_or(0);
798                *end = (*end).min(bound);
799                *end > 0
800            });
801        }
802
803        // A shallow snapshot may retain changes that are concurrent with its
804        // root (independent peer chains, for example), and `from` counts the
805        // trimmed ops below the root. If `concurrent_old` would reach into
806        // that trimmed history we cannot see which containers it touched, so
807        // the decision has to stay with the DAG.
808        if !causal_past.includes_vv(&self.dag.shallow_since_vv().to_vv()) {
809            return None;
810        }
811
812        let concurrent_old = causal_past.diff(from).forward;
813        let old_containers = self.containers_in_spans(
814            concurrent_old
815                .iter()
816                .map(|(peer, span)| IdSpan::new(*peer, span.start, span.end)),
817        );
818        if old_containers.is_empty() {
819            return Some(old_containers);
820        }
821
822        let new_region = from.diff(to).forward;
823        let new_containers = self.containers_in_spans(
824            new_region
825                .iter()
826                .map(|(peer, span)| IdSpan::new(*peer, span.start, span.end)),
827        );
828        let harmless = old_containers
829            .iter()
830            .filter(|idx| new_containers.contains(idx))
831            .all(|idx| match idx.get_type() {
832                ContainerType::Map => true,
833                #[cfg(feature = "counter")]
834                ContainerType::Counter => true,
835                _ => false,
836            });
837        harmless.then_some(old_containers)
838    }
839
840    /// The latest critical version below `from ∩ to`: the greatest causally
841    /// closed `V ⊆ from ∩ to` such that every op in `(from ∪ to) − V` is
842    /// causally after every op in `V`. "Causally after" is measured in the
843    /// ancestry the replay hands the diff calculators — a change's recorded
844    /// deps plus the author's own earlier ops — which is exactly what a
845    /// calculator trusting the base relies on. Unlike
846    /// `dag::latest_single_head_critical_version` the result may be
847    /// multi-head, which is what makes it usable on the criss-cross DAG that
848    /// continuous two-peer sync produces — there every merge point has two
849    /// heads, so the single-head descent walks to genesis and returns nothing.
850    ///
851    /// Method: start at the meet `min(from, to)` — the largest candidate — and
852    /// lower it to a fixpoint. `V` is critical iff for every change of the
853    /// region above it, the context of the change's first op above `V`
854    /// covers `V` (the change's later ops only add to that context). Each
855    /// violating change lowers `V` to the intersection with that context,
856    /// which preserves every critical cut below `V`, so the fixpoint is the
857    /// greatest critical version ≤ the meet (spec L13). Lowering cannot
858    /// change a verdict already reached — a context that covered `V` covers
859    /// the smaller one, and the violator covers the lowered `V` by
860    /// construction — it only exposes the spans between the old and the new
861    /// `V`, which go on a worklist; a change is examined once per lowering
862    /// that moves its peer's cut into it. The scan is thus bounded by the
863    /// replay it enables, which walks the same region and computes the same
864    /// contexts, so it needs no budget of its own.
865    pub(crate) fn latest_critical_version_below_meet(
866        &self,
867        from: &VersionVector,
868        to: &VersionVector,
869        merged: &VersionVector,
870    ) -> CriticalVersionSearch {
871        use CriticalVersionSearch::*;
872
873        // An intersection of two causally closed sets is causally closed, so
874        // `min(from, to)` is a version, and it is the meet of the two.
875        let mut v = from.intersection(to);
876        if v.is_empty() {
877            return NoneBelowMeet;
878        }
879
880        let mut pending: Vec<IdSpan> = v.diff_iter(merged).1.collect();
881        while let Some(span) = pending.pop() {
882            for change in self.change_store.iter_changes(span) {
883                // The context of the change's first op above `v`, as
884                // `iter_from_replay_base_causally` computes it for the
885                // replay: the recorded deps plus the same-peer prefix.
886                let Some(mut ctx) = self.dag.frontiers_to_vv(&change.deps) else {
887                    // The deps reach below trimmed history; nothing can be
888                    // proved from here.
889                    return Unknown;
890                };
891                let cut = v.get(&change.id.peer).copied().unwrap_or(0);
892                let first_above = change.id.counter.max(cut);
893                ctx.extend_to_include_end_id(ID::new(change.id.peer, first_above));
894
895                if !ctx.includes_vv(&v) {
896                    let above = v.clone();
897                    v.intersect_with(&ctx);
898                    if v.is_empty() {
899                        return NoneBelowMeet;
900                    }
901                    pending.extend(v.diff_iter(&above).1);
902                }
903            }
904        }
905
906        Found(v)
907    }
908
909    /// Iterates causally over all changes between the replay base (the meet
910    /// from `find_meet_and_mode` when it is valid for the diff mode, else a
911    /// critical version below it in the Eg-walker sense, see
912    /// `docs/critical-version-spec.md`) and the merged version of `from`/`to`.
913    ///
914    /// Tht iterator will include a version vector when the change is applied
915    ///
916    /// returns: (replay_base, iterator)
917    ///
918    /// Note: the change returned by the iterator may include redundant ops at the beginning, you should trim it by yourself.
919    /// You can trim it by the provided counter value. It should start with the counter.
920    ///
921    /// If frontiers are provided, it will be faster (because we don't need to calculate it from version vector
922    ///
923    /// The third item is `Some(containers)` when part of the new region is
924    /// concurrent with `from` but the oplog proved the concurrency
925    /// register-only (see [`OpLog::register_only_concurrency`]); the replay
926    /// then starts at `from` in `ImportGreaterUpdates` mode. The set lists
927    /// every container with an op in the concurrent old history: the diff
928    /// calculator resolves those from history and treats them as "source not
929    /// in op context", and may trust `from` as the base for everything else.
930    #[allow(clippy::type_complexity)]
931    pub(crate) fn iter_from_replay_base_causally(
932        &self,
933        from: &VersionVector,
934        from_frontiers: &Frontiers,
935        to: &VersionVector,
936        to_frontiers: &Frontiers,
937    ) -> (
938        ReplayBase,
939        impl Iterator<
940                Item = (
941                    BlockChangeRef,
942                    (Counter, Counter),
943                    Rc<RefCell<VersionVector>>,
944                ),
945            > + '_,
946    ) {
947        let mut merged_vv = from.clone();
948        merged_vv.merge(to);
949        loro_common::debug!("to_frontiers={:?} vv={:?}", &to_frontiers, to);
950        let mut concurrent_containers = None;
951        let mut register_only_base = None;
952        if to > from {
953            // `to ⊇ from`, but some new ops may be concurrent with part of
954            // `from`. The DAG walk must then assume the worst and retreat
955            // to a critical version; with container knowledge we can often
956            // prove the concurrency harmless and replay from `from`
957            // without touching the DAG at all.
958            if let Some(entry_parents) = self.uncovered_entry_parents(from, from_frontiers, to) {
959                if !entry_parents.is_empty() {
960                    if let Some(containers) =
961                        self.register_only_concurrency(from, to, &entry_parents)
962                    {
963                        concurrent_containers = Some(containers);
964                        register_only_base = Some(from_frontiers.clone());
965                    }
966                }
967            }
968        }
969
970        let (meet, mut diff_mode) = if let Some(base) = register_only_base {
971            (MeetAsBase::Valid(base), DiffMode::ImportGreaterUpdates)
972        } else {
973            self.dag.find_meet_and_mode(from_frontiers, to_frontiers)
974        };
975        if diff_mode == DiffMode::Checkout && to > from {
976            diff_mode = DiffMode::Import;
977        }
978
979        let shallow_since_vv = self.dag.shallow_since_vv().to_vv();
980        let mut replay_base_is_critical = false;
981        let mut replay_base_frontiers = match meet {
982            MeetAsBase::Valid(meet) => meet,
983            MeetAsBase::NeedsCriticalRetreat => {
984                // Some event above the meet is concurrent with it. Retreat to
985                // a critical version — the latest *multi-head* one when we
986                // can find it, because the single-head descent is both a
987                // whole-DAG walk and, on the criss-cross DAG that continuous
988                // two-peer sync produces, doomed to return the empty version.
989                let descend = || {
990                    #[cfg(test)]
991                    CRITICAL_BASE_FALLBACK_COUNT.with(|c| c.set(c.get() + 1));
992                    self.dag
993                        .latest_single_head_critical_version(from_frontiers, to_frontiers)
994                };
995                match self.latest_critical_version_below_meet(from, to, &merged_vv) {
996                    CriticalVersionSearch::Found(v) => {
997                        // Only replay from the version whose criticality was
998                        // proved. A shallow doc cannot replay from below its
999                        // seed version, and a cut assembled from recorded
1000                        // deps plus the author's own prefix is causally
1001                        // closed only when those deps cover that prefix
1002                        // (spec axiom A6, which imported data is not checked
1003                        // against) — a version that fails to round-trip
1004                        // through frontiers is not one. Both are left to the
1005                        // descent, as before.
1006                        let f = self.dag.vv_to_frontiers(&v);
1007                        let seed = self
1008                            .dag
1009                            .frontiers_to_vv(self.dag.shallow_since_frontiers())
1010                            .unwrap();
1011                        if v.includes_vv(&seed) && self.dag.frontiers_to_vv(&f).as_ref() == Some(&v)
1012                        {
1013                            replay_base_is_critical = true;
1014                            f
1015                        } else {
1016                            descend()
1017                        }
1018                    }
1019                    CriticalVersionSearch::NoneBelowMeet => {
1020                        // ∅ is trivially critical (spec D8b), and the
1021                        // single-head descent provably cannot find a better
1022                        // cut (spec L13(ii); under A6, where its ancestry
1023                        // coincides with the replay's) — skip its whole-DAG
1024                        // walk.
1025                        Frontiers::default()
1026                    }
1027                    CriticalVersionSearch::Unknown => descend(),
1028                }
1029            }
1030        };
1031
1032        let mut replay_base_vv = self.dag.frontiers_to_vv(&replay_base_frontiers).unwrap();
1033        replay_base_is_critical |= replay_base_vv.is_empty();
1034        if !replay_base_vv.includes_vv(&shallow_since_vv) {
1035            // The replay base cannot point before shallow history because those
1036            // ops are no longer available to the causal iterator.
1037            replay_base_frontiers = self.dag.shallow_since_frontiers().clone();
1038            replay_base_vv = self
1039                .dag
1040                .frontiers_to_vv(&replay_base_frontiers)
1041                .unwrap_or(shallow_since_vv);
1042            replay_base_is_critical = false;
1043        }
1044
1045        // go from the replay base to merged_vv
1046        let diff = replay_base_vv.diff(&merged_vv).forward;
1047        let mut iter = self.dag.iter_causal(replay_base_frontiers, diff);
1048        let mut node = iter.next();
1049        let mut cur_cnt = 0;
1050        let vv = Rc::new(RefCell::new(VersionVector::default()));
1051        (
1052            ReplayBase {
1053                vv: replay_base_vv.clone(),
1054                diff_mode,
1055                is_critical: replay_base_is_critical,
1056                concurrent_containers,
1057            },
1058            std::iter::from_fn(move || {
1059                if let Some(inner) = &node {
1060                    let mut inner_vv = vv.borrow_mut();
1061                    // FIXME: PERF: it looks slow for large vv, like 10000+ entries
1062                    inner_vv.clear();
1063                    self.dag.ensure_vv_for(&inner.data);
1064                    inner_vv.extend_to_include_vv(inner.data.vv.get().unwrap().iter());
1065                    let peer = inner.data.peer;
1066                    let cnt = inner
1067                        .data
1068                        .cnt
1069                        .max(cur_cnt)
1070                        .max(replay_base_vv.get(&peer).copied().unwrap_or(0));
1071                    let dag_node_end = (inner.data.cnt + inner.data.len as Counter)
1072                        .min(merged_vv.get(&peer).copied().unwrap_or(0));
1073                    let change = self.change_store.get_change(ID::new(peer, cnt)).unwrap();
1074
1075                    if change.ctr_end() < dag_node_end {
1076                        cur_cnt = change.ctr_end();
1077                    } else {
1078                        node = iter.next();
1079                        cur_cnt = 0;
1080                    }
1081
1082                    inner_vv.extend_to_include_end_id(change.id);
1083
1084                    Some((change, (cnt, dag_node_end), vv.clone()))
1085                } else {
1086                    None
1087                }
1088            }),
1089        )
1090    }
1091
1092    pub fn len_changes(&self) -> usize {
1093        self.change_store.change_num()
1094    }
1095
1096    pub fn diagnose_size(&self) -> SizeInfo {
1097        let mut total_changes = 0;
1098        let mut total_ops = 0;
1099        let mut total_atom_ops = 0;
1100        let total_dag_node = self.dag.total_parsed_dag_node();
1101        self.change_store.visit_all_changes(&mut |change| {
1102            total_changes += 1;
1103            total_ops += change.ops.len();
1104            total_atom_ops += change.atom_len();
1105        });
1106
1107        println!("total changes: {}", total_changes);
1108        println!("total ops: {}", total_ops);
1109        println!("total atom ops: {}", total_atom_ops);
1110        println!("total dag node: {}", total_dag_node);
1111        SizeInfo {
1112            total_changes,
1113            total_ops,
1114            total_atom_ops,
1115            total_dag_node,
1116        }
1117    }
1118
1119    pub(crate) fn iter_changes_peer_by_peer<'a>(
1120        &'a self,
1121        from: &VersionVector,
1122        to: &VersionVector,
1123    ) -> impl Iterator<Item = BlockChangeRef> + 'a {
1124        let spans: Vec<_> = from.diff_iter(to).1.collect();
1125        spans
1126            .into_iter()
1127            .flat_map(move |span| self.change_store.iter_changes(span))
1128    }
1129
1130    #[allow(dead_code)]
1131    pub(crate) fn iter_changes_causally_rev<'a>(
1132        &'a self,
1133        from: &VersionVector,
1134        to: &VersionVector,
1135    ) -> impl Iterator<Item = BlockChangeRef> + 'a {
1136        MergedChangeIter::new_change_iter_rev(self, from, to)
1137    }
1138
1139    pub fn get_timestamp_for_next_txn(&self) -> Timestamp {
1140        if self.configure.record_timestamp() {
1141            get_timestamp_now_txn()
1142        } else {
1143            0
1144        }
1145    }
1146
1147    #[inline(never)]
1148    pub(crate) fn idlp_to_id(&self, id: loro_common::IdLp) -> Option<ID> {
1149        let change = self.change_store.get_change_by_lamport_lte(id)?;
1150
1151        if change.lamport > id.lamport || change.lamport_end() <= id.lamport {
1152            return None;
1153        }
1154
1155        Some(ID::new(
1156            change.id.peer,
1157            (id.lamport - change.lamport) as Counter + change.id.counter,
1158        ))
1159    }
1160
1161    #[allow(unused)]
1162    pub(crate) fn id_to_idlp(&self, id_start: ID) -> IdLp {
1163        let change = self.get_change_at(id_start).unwrap();
1164        let lamport = change.lamport + (id_start.counter - change.id.counter) as Lamport;
1165        let peer = id_start.peer;
1166        loro_common::IdLp { peer, lamport }
1167    }
1168
1169    /// NOTE: This may return a op that includes the given id, not necessarily start with the given id
1170    pub(crate) fn get_op_that_includes(&self, id: ID) -> Option<BlockOpRef> {
1171        let change = self.get_change_at(id)?;
1172        change.get_op_with_counter(id.counter)
1173    }
1174
1175    pub(crate) fn split_span_based_on_deps(&self, id_span: IdSpan) -> Vec<(IdSpan, Frontiers)> {
1176        let peer = id_span.peer;
1177        let mut counter = id_span.counter.min();
1178        let span_end = id_span.counter.norm_end();
1179        let mut ans = Vec::new();
1180
1181        while counter < span_end {
1182            let id = ID::new(peer, counter);
1183            let node = self.dag.get(id).unwrap();
1184
1185            let f = if node.cnt == counter {
1186                node.deps.clone()
1187            } else if counter > 0 {
1188                id.inc(-1).into()
1189            } else {
1190                unreachable!()
1191            };
1192
1193            let cur_end = node.cnt + node.len as Counter;
1194            let len = cur_end.min(span_end) - counter;
1195            ans.push((id.to_span(len as usize), f));
1196            counter += len;
1197        }
1198
1199        ans
1200    }
1201
1202    #[inline]
1203    pub fn compact_change_store(&mut self) {
1204        self.change_store
1205            .flush_and_compact(self.dag.vv(), self.dag.frontiers());
1206    }
1207
1208    #[inline]
1209    pub fn change_store_kv_size(&self) -> usize {
1210        self.change_store.kv_size()
1211    }
1212
1213    pub fn encode_change_store(&self) -> bytes::Bytes {
1214        self.change_store
1215            .encode_all(self.dag.vv(), self.dag.frontiers())
1216    }
1217
1218    pub fn check_dag_correctness(&self) {
1219        self.dag.check_dag_correctness();
1220    }
1221
1222    pub fn shallow_since_vv(&self) -> &ImVersionVector {
1223        self.dag.shallow_since_vv()
1224    }
1225
1226    pub fn shallow_since_frontiers(&self) -> &Frontiers {
1227        self.dag.shallow_since_frontiers()
1228    }
1229
1230    pub fn is_shallow(&self) -> bool {
1231        !self.dag.shallow_since_vv().is_empty()
1232    }
1233
1234    pub fn get_greatest_timestamp(&self, frontiers: &Frontiers) -> Timestamp {
1235        let mut max_timestamp = Timestamp::default();
1236        for id in frontiers.iter() {
1237            let change = self.get_change_at(id).unwrap();
1238            if change.timestamp > max_timestamp {
1239                max_timestamp = change.timestamp;
1240            }
1241        }
1242
1243        max_timestamp
1244    }
1245}
1246
1247#[derive(Debug)]
1248pub struct SizeInfo {
1249    pub total_changes: usize,
1250    pub total_ops: usize,
1251    pub total_atom_ops: usize,
1252    pub total_dag_node: usize,
1253}
1254
1255pub(crate) fn convert_change_to_remote(
1256    arena: &SharedArena,
1257    change: &Change,
1258) -> Change<RemoteOp<'static>> {
1259    let mut ops = RleVec::new();
1260    for op in change.ops.iter() {
1261        for op in local_op_to_remote(arena, op) {
1262            ops.push(op);
1263        }
1264    }
1265
1266    Change {
1267        ops,
1268        id: change.id,
1269        deps: change.deps.clone(),
1270        lamport: change.lamport,
1271        timestamp: change.timestamp,
1272        commit_msg: change.commit_msg.clone(),
1273    }
1274}
1275
1276pub(crate) fn local_op_to_remote(
1277    arena: &SharedArena,
1278    op: &crate::op::Op,
1279) -> SmallVec<[RemoteOp<'static>; 1]> {
1280    let container = arena.get_container_id(op.container).unwrap();
1281    let mut contents: SmallVec<[_; 1]> = SmallVec::new();
1282    match &op.content {
1283        crate::op::InnerContent::List(list) => match list {
1284            list_op::InnerListOp::Insert { slice, pos } => match container.container_type() {
1285                loro_common::ContainerType::Text => {
1286                    let str = arena
1287                        .slice_str_by_unicode_range(slice.0.start as usize..slice.0.end as usize);
1288                    contents.push(RawOpContent::List(list_op::ListOp::Insert {
1289                        slice: ListSlice::RawStr {
1290                            unicode_len: str.chars().count(),
1291                            str: Cow::Owned(str),
1292                        },
1293                        pos: *pos,
1294                    }));
1295                }
1296                loro_common::ContainerType::List | loro_common::ContainerType::MovableList => {
1297                    contents.push(RawOpContent::List(list_op::ListOp::Insert {
1298                        slice: ListSlice::RawData(Cow::Owned(
1299                            arena.get_values(slice.0.start as usize..slice.0.end as usize),
1300                        )),
1301                        pos: *pos,
1302                    }))
1303                }
1304                _ => unreachable!(),
1305            },
1306            list_op::InnerListOp::InsertText {
1307                slice,
1308                unicode_len: len,
1309                unicode_start: _,
1310                pos,
1311            } => match container.container_type() {
1312                loro_common::ContainerType::Text => {
1313                    contents.push(RawOpContent::List(list_op::ListOp::Insert {
1314                        slice: ListSlice::RawStr {
1315                            unicode_len: *len as usize,
1316                            str: Cow::Owned(std::str::from_utf8(slice).unwrap().to_owned()),
1317                        },
1318                        pos: *pos as usize,
1319                    }));
1320                }
1321                _ => unreachable!(),
1322            },
1323            list_op::InnerListOp::Delete(del) => {
1324                contents.push(RawOpContent::List(list_op::ListOp::Delete(*del)))
1325            }
1326            list_op::InnerListOp::StyleStart {
1327                start,
1328                end,
1329                key,
1330                value,
1331                info,
1332            } => contents.push(RawOpContent::List(list_op::ListOp::StyleStart {
1333                start: *start,
1334                end: *end,
1335                key: key.clone(),
1336                value: value.clone(),
1337                info: *info,
1338            })),
1339            list_op::InnerListOp::StyleEnd => {
1340                contents.push(RawOpContent::List(list_op::ListOp::StyleEnd))
1341            }
1342            list_op::InnerListOp::Move {
1343                from,
1344                elem_id: from_id,
1345                to,
1346            } => contents.push(RawOpContent::List(list_op::ListOp::Move {
1347                from: *from,
1348                elem_id: *from_id,
1349                to: *to,
1350            })),
1351            list_op::InnerListOp::Set { elem_id, value } => {
1352                contents.push(RawOpContent::List(list_op::ListOp::Set {
1353                    elem_id: *elem_id,
1354                    value: value.clone(),
1355                }))
1356            }
1357        },
1358        crate::op::InnerContent::Map(map) => {
1359            let value = map.value.clone();
1360            contents.push(RawOpContent::Map(crate::container::map::MapSet {
1361                key: map.key.clone(),
1362                value,
1363            }))
1364        }
1365        crate::op::InnerContent::Tree(tree) => contents.push(RawOpContent::Tree(tree.clone())),
1366        crate::op::InnerContent::Future(f) => match f {
1367            #[cfg(feature = "counter")]
1368            crate::op::FutureInnerContent::Counter(c) => contents.push(RawOpContent::Counter(*c)),
1369            FutureInnerContent::Unknown { prop, value } => {
1370                contents.push(crate::op::RawOpContent::Unknown {
1371                    prop: *prop,
1372                    value: (**value).clone(),
1373                })
1374            }
1375        },
1376    };
1377
1378    let mut ans = SmallVec::with_capacity(contents.len());
1379    for content in contents {
1380        ans.push(RemoteOp {
1381            container: container.clone(),
1382            content,
1383            counter: op.counter,
1384        })
1385    }
1386    ans
1387}
1388
1389pub(crate) fn get_timestamp_now_txn() -> Timestamp {
1390    (get_sys_timestamp() as Timestamp + 500) / 1000
1391}
1392
1393#[cfg(test)]
1394mod visible_op_count_tests {
1395    use crate::{cursor::PosType, loro::ExportMode, LoroDoc};
1396
1397    /// The cached `visible_op_count` (bumped incrementally for local ops, and
1398    /// the only value read in release builds where `can_lock_in_this_thread`
1399    /// returns false) must always equal a from-scratch recompute.
1400    #[test]
1401    fn cached_visible_op_count_matches_exact() {
1402        let doc = LoroDoc::new();
1403        let text = doc.get_text("text");
1404        let mut txn = doc.txn().unwrap();
1405        for i in 0..50 {
1406            text.insert_with_txn(&mut txn, i, "a", PosType::Unicode)
1407                .unwrap();
1408        }
1409        txn.commit().unwrap();
1410        {
1411            let oplog = doc.oplog().lock();
1412            assert_eq!(
1413                oplog.cached_visible_op_count(),
1414                oplog.visible_op_count_exact(),
1415                "after local edits"
1416            );
1417        }
1418
1419        // Import keeps the cached count exact via full refresh; subsequent local
1420        // edits then increment from that exact base.
1421        let doc2 = LoroDoc::new();
1422        doc2.import(&doc.export(ExportMode::all_updates()).unwrap())
1423            .unwrap();
1424        let text2 = doc2.get_text("text");
1425        let mut txn2 = doc2.txn().unwrap();
1426        text2
1427            .insert_with_txn(&mut txn2, 0, "bbb", PosType::Unicode)
1428            .unwrap();
1429        txn2.commit().unwrap();
1430        {
1431            let oplog = doc2.oplog().lock();
1432            assert_eq!(
1433                oplog.cached_visible_op_count(),
1434                oplog.visible_op_count_exact(),
1435                "after import + local edits"
1436            );
1437        }
1438    }
1439}