Sync

Two documents with concurrent edits can be synchronized by just two message exchanges.

Below is an example of synchronization between two documents:

const  = new ();
const  = new ();
const :  = .("list");
.(0, "A");
.(1, "B");
.(2, "C");
// B import the ops from A
const :  = .({ : "update" });
// The data can be sent to B through the network
.();
(.()).({
  : ["A", "B", "C"],
});
 
const :  = .("list");
.(1, 1);
 
// `doc.export({mode: "update", from: version})` can encode all the ops from the version to the latest version
// `version` is the version vector of another document
const  = .({
  : "update",
  : .(),
});
.();
 
(.()).({
  : ["A", "C"],
});
(.()).(.());

Real-time Collaboration

Due to CRDT properties, document consistency is guaranteed when peers receive the same updates, regardless of order or duplicates.

Consistency does not remove the need to choose the right child container identity. If two offline peers create regular child containers under the same Map key, sync will converge to one visible Map value and the other child can be hidden by the Map conflict rule. For dynamic child containers that should be shared by logical key, use ensureMergeable* when modeling the document.

Sync Strategies

  1. First Sync (Initial synchronization between peers):

    • New peers can exchange their Version Vectors to determine missing updates
    • Use doc.export({ mode: "update", from: versionVector }) to get updates since the peer’s last known state. You may as well send the whole history by doc.export({ mode: "update" }) as shown in the example above.
    • Example shows basic first sync scenario
  2. Realtime Sync (Continuous updates):

    • Subscribe to local updates
    • Broadcast updates directly to all other peers
    • No need for version comparison after initial sync
    • As long as updates reach all peers, consistency is maintained

Example

Here’s how two peers can establish realtime sync when one comes online with offline changes:

  1. Both peers exchange their version information
  2. Each peer shares their missing updates:
    • doc2 gets updates it’s missing from doc1
    • doc1 gets updates it’s missing from doc2
  3. Both peers establish realtime sync to stay connected
const  = new ();
.("text").(0, "Hello");
// Peer2 joins the network
const  = new ();
// ... doc2 may import its local snapshot
 
// 1. Exchange version information
const  = .();
const  = .();
 
// 2. Request missing updates from existing peers
const  = .({
  : "update",
  : ,
});
.();
const  = .({
  : "update",
  : ,
});
.();
 
// 3. Establish realtime sync
.(() => {
  // websocket.send(update);
});
.(() => {
  // websocket.send(update);
});
 
// Now both peers are in sync and can collaborate

Understanding the import() Return Value

The import() method in both Loro’s WASM binding and pure TypeScript implementation returns an ImportStatus with the following structure:

interface ImportStatus {
  : <, >;
  : <, > | null;
}
 
type  = `${number}`;
type  = {
  : number; // Start counter (inclusive)
  : number; // End counter (exclusive)
};

Fields Explained:

  1. success (Map<PeerID, CounterSpan>)

    • Description: This field is always present and details the ranges of operations integrated into history by this import, including earlier pending operations whose dependencies just arrived.
    • Structure: It’s a JavaScript Map where:
      • Each key is a string representing a PeerID (the unique identifier of a collaborator or a source of changes).
      • Each value is an object { start: number, end: number } defining a continuous range of operation counters for that specific peer.
        • start: The starting counter of the successfully imported range (inclusive).
        • end: The ending counter of the successfully imported range (exclusive). This means operations from start up to, but not including, end were processed.
    • Purpose: Helps understand which parts of the provided update data have been integrated into the local document’s state.
    • Example:
      // Assuming importResult is the return value of doc.import(bytes)
      console.log(importResult.success);
      // Example output:
      // Map(2) {
      //   "1" => { start: 0, end: 50 },
      //   "2" => { start: 120, end: 150 }
      // }
      // This means operations from peer "1" (counters 0-49) and
      // peer "2" (counters 120-149) were successfully imported.
  2. pending (Map<PeerID, CounterSpan> | null)

    • Description: This field is always present. It is a Map of operation ranges waiting for missing causal dependencies, or null when the import reports no pending operations.
    • Structure: When non-null, a JavaScript Map from PeerID strings to { start: number, end: number } counter ranges, like success.
    • Purpose: Informs the application that certain changes are known but are “on hold” awaiting their prerequisites. To apply these pending changes, the missing prerequisite operations must be imported first. This is crucial for maintaining data consistency in collaborative scenarios.
    • Example:
      // Assuming importResult is the return value of doc.import(bytes)
      if (importResult.pending !== null && importResult.pending.size > 0) {
        console.log(importResult.pending);
        // Example output:
        // Map(2) {
        //   "1" => { start: 50, end: 60 },
        //   "2" => { start: 10, end: 25 }
        // }
        // This means operations from peer "1" (counters 50-59) and
        // peer "2" (counters 10-24) are pending due to missing dependencies.
      }

ImportStatus reports an import result, not a complete query of the document’s pending queue; pending === null does not guarantee that no earlier imports remain pending. Use exportPendingUpdates() to export all currently received but unintegrated operations; undefined means that queue is empty, not that the document is synced.

How to Use This Information:

  • Use success.get(peerId) or iterate over success to inspect integrated ranges; use .size to check whether a Map is empty. Object.keys() does not enumerate Map entries.
  • If pending !== null && pending.size > 0, further updates (dependencies) are required to integrate those pending operations. Request these missing updates from other peers or a central server.
  • To preserve pending imports across restarts, see persisting pending imports with the exportPendingUpdates() API. A normal snapshot alone excludes these operations; saving them does not establish that the document is synced.