Skip to content

Exporting snapshot exports data incorrectly #972

Description

@ioder

Summary

We populate a LoroDoc from a plain JS object using loro-mirror (with defaultMovableList: true). We then try two ways to clone the document:

  • JSON-updates round-trip: doc.exportJsonUpdates()newDoc.importJsonUpdates(...)
  • Snapshot round-trip: doc.export({ mode: 'snapshot' })LoroDoc.fromSnapshot(...)

Path (1) reproduces the state correctly. Path (2) does not — newDoc.toJSON() differs from doc.toJSON(), even though both methods are documented as full-state exports.

Investigating the snapshot itself in Loro Inspector will reveal that the snapshot is already missing data.

Reproduction

https://codesandbox.io/p/devbox/pxtpmw

import { LoroDoc } from 'loro-crdt'
import { Mirror } from 'loro-mirror'
const state = {
  payload: { title: 'hello', count: 42 },
  items: [
    { id: 'a', value: 1 },
    { id: 'b', value: 2 },
  ],
}
const doc = toLoroDoc(state)
console.log('Original document JSON', doc.toJSON())

// (1) clone via JSON updates
const updates = doc.exportJsonUpdates()
const newDoc = new LoroDoc()
newDoc.importJsonUpdates(updates)
console.log('Copy via JSON updates', newDoc.toJSON())   // <-- matches original

// (2) clone via snapshot
const snapshot = doc.export({ mode: 'snapshot' })
const newDoc2 = LoroDoc.fromSnapshot(snapshot)
console.log('Copy via snapshot', newDoc2.toJSON())      // <-- diverges from original

function toLoroDoc(state: any): LoroDoc {
  const doc = new LoroDoc()
  doc.setRecordTimestamp(true)
  const mirror = createLoroMirror({ doc, initialState: state })
  mirror.setState(() => state)
  return doc
}
function createLoroMirror(options: any): any {
  const { initialState, inferOptions } = options
  return new Mirror({
    ...options,
    initialState: emptyRootShape(initialState ?? {}),
    inferOptions: { ...inferOptions, defaultMovableList: true },
    validateUpdates: false,
  })
}
function emptyRootShape(document: any): any {
  return Object.fromEntries(
    Object.entries(document).map(([key, value]) => {
      if (Array.isArray(value)) return [key, []]
      if (!!value && typeof value === 'object') return [key, {}]
      return [key, value]
    }),
  )
}

Expected

After mirror.setState, the doc has a full operation history describing every container creation, map insertion, and list insertion. doc.export({ mode: 'snapshot' }) is documented as it should be exporting all changes, LoroDoc.fromSnapshot(...) should return document with whole state.

Environment

loro-crdt version: 1.12.0
loro-mirror version: 2.1.0
Node version: 22.22.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions