Sync
Two documents with concurrent edits can be synchronized by just two message exchanges.
Below is an example of synchronization between two documents:
const docA = new LoroDoc();
const docB = new LoroDoc();
const listA: LoroList = docA.getList("list");
listA.insert(0, "A");
listA.insert(1, "B");
listA.insert(2, "C");
// B import the ops from A
const data: Uint8Array = docA.export({ mode: "update" });
// The data can be sent to B through the network
docB.import(data);
expect(docB.toJSON()).toStrictEqual({
list: ["A", "B", "C"],
});
const listB: LoroList = docB.getList("list");
listB.delete(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 missingOps = docB.export({
mode: "update",
from: docA.oplogVersion(),
});
docA.import(missingOps);
expect(docA.toJSON()).toStrictEqual({
list: ["A", "C"],
});
expect(docA.toJSON()).toStrictEqual(docB.toJSON());
Real-time Collaboration
Due to CRDT properties, document consistency is guaranteed when peers receive the same updates, regardless of order or duplicates.
Sync Strategies
-
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 bydoc.export({ mode: "update" })
as shown in the example above. - Example shows basic first sync scenario
-
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:
- Both peers exchange their version information
- Each peer shares their missing updates:
doc2
gets updates it's missing fromdoc1
doc1
gets updates it's missing fromdoc2
- Both peers establish realtime sync to stay connected
const doc1 = new LoroDoc();
doc1.getText("text").insert(0, "Hello");
// Peer2 joins the network
const doc2 = new LoroDoc();
// ... doc2 may import its local snapshot
// 1. Exchange version information
const peer2Version = doc2.oplogVersion();
const peer1Version = doc1.oplogVersion();
// 2. Request missing updates from existing peers
const missingOps = doc1.export({
mode: "update",
from: peer2Version
});
doc2.import(missingOps);
const missingOps2 = doc2.export({
mode: "update",
from: peer1Version,
});
doc1.import(missingOps2);
// 3. Establish realtime sync
doc2.subscribeLocalUpdates((update) => {
// websocket.send(update);
});
doc1.subscribeLocalUpdates((update) => {
// websocket.send(update);
});
// Now both peers are in sync and can collaborate
Understanding the import()
Return Value
The import()
method in Loro's JavaScript/WASM binding returns an object that provides feedback on the import operation. This object, let's call it ImportStatusJS
, has the following structure:
interface ImportStatusJS {
success: PeerVersionRange;
pending?: PeerVersionRange; // Optional: only present if there are pending operations
}
interface PeerVersionRange {
[peerId: string]: {
start: number; // Start counter (inclusive)
end: number; // End counter (exclusive)
};
}
Fields Explained:
-
success
(Object,PeerVersionRange
)- Description: This field is always present and details the ranges of operations (changes) that were successfully imported and applied to the Loro document.
- Structure: It's an object where:
- Each key is a
string
representing aPeerID
(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 fromstart
up to, but not including,end
were processed.
- Each key is a
- 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: // { // "clientA_peerId": { "start": 0, "end": 50 }, // "server_peerId": { "start": 120, "end": 150 } // } // This means operations from clientA (counters 0-49) and // operations from server (counters 120-149) were successfully imported.
-
pending
(Object,PeerVersionRange
, optional)- Description: This field is only present if some operations from the imported data could not be applied because they depend on other operations that Loro has not seen yet (i.e., their causal dependencies are missing). It details these "pending" operation ranges.
- Structure: Identical to the
success
field. An object mappingPeerID
strings to{ start: number, end: number }
counter ranges. - 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) { console.log(importResult.pending); // Example output: // { // "clientA_peerId": { "start": 50, "end": 60 }, // "clientB_peerId": { "start": 10, "end": 25 } // } // This means operations from clientA (counters 50-59) and // operations from clientB (counters 10-24) are pending due to missing dependencies. }
How to Use This Information:
- Check the
success
field to confirm which updates were applied. - If the
pending
field exists and is not empty, it signals that further updates (dependencies) are required to fully integrate all known changes. Your application might need to fetch or request these missing updates from other peers or a central server.