Readonly
peerPeer ID of the current writer.
Readonly
peerGet peer id in decimal string.
Apply a batch of diff to the document
A diff batch represents a set of changes between two versions of the document.
You can calculate a diff batch using doc.diff()
.
Changes are associated with container IDs. During diff application, if new containers were created in the source document, they will be assigned fresh IDs in the target document. Loro automatically handles remapping these container IDs from their original IDs to the new IDs as the diff is applied.
const doc1 = new LoroDoc();
const doc2 = new LoroDoc();
// Make some changes to doc1
const text = doc1.getText("text");
text.insert(0, "Hello");
// Calculate diff between empty and current state
const diff = doc1.diff([], doc1.frontiers());
// Apply changes to doc2
doc2.applyDiff(diff);
console.log(doc2.getText("text").toString()); // "Hello"
Attach the document state to the latest known version.
The document becomes detached during a
checkout
operation. Beingdetached
implies that theDocState
is not synchronized with the latest version of theOpLog
. In a detached state, the document is not editable, and anyimport
operations will be recorded in theOpLog
without being applied to theDocState
.
This method has the same effect as invoking checkoutToLatest
.
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const frontiers = doc.frontiers();
text.insert(0, "Hello World!");
doc.checkout(frontiers);
// you need call `attach()` or `checkoutToLatest()` before changing the doc.
doc.attach();
text.insert(0, "Hi");
Get the number of changes in the oplog.
Checkout the DocState
to a specific version.
The document becomes detached during a
checkout
operation. Beingdetached
implies that theDocState
is not synchronized with the latest version of theOpLog
. In a detached state, the document is not editable, and anyimport
operations will be recorded in theOpLog
without being applied to theDocState
.
You should call attach
to attach the DocState
to the latest version of OpLog
.
the specific frontiers
Checkout the DocState
to the latest version of OpLog
.
The document becomes detached during a
checkout
operation. Beingdetached
implies that theDocState
is not synchronized with the latest version of theOpLog
. In a detached state, the document is not editable by default, and anyimport
operations will be recorded in theOpLog
without being applied to theDocState
.
This has the same effect as attach
.
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const frontiers = doc.frontiers();
text.insert(0, "Hello World!");
doc.checkout(frontiers);
// you need call `checkoutToLatest()` or `attach()` before changing the doc.
doc.checkoutToLatest();
text.insert(0, "Hi");
Clear the options of the next commit
Compare the ordering of two Frontiers.
It's assumed that both Frontiers are included by the doc. Otherwise, an error will be thrown.
Return value:
Compare the version of the OpLog with the specified frontiers.
This method is useful to compare the version by only a small amount of data.
This method returns an integer indicating the relationship between the version of the OpLog (referred to as 'self') and the provided 'frontiers' parameter:
Frontiers cannot be compared without the history of the OpLog.
Commit the cumulative auto-committed transaction.
You can specify the origin
, timestamp
, and message
of the commit.
origin
is used to mark the eventmessage
works like a git commit message, which will be recorded and synced to peerstimestamp
is the number of seconds that have elapsed since 00:00:00 UTC on January 1, 1970.
It defaults to Date.now() / 1000
when timestamp recording is enabledThe events will be emitted after a transaction is committed. A transaction is committed when:
doc.commit()
is called.doc.export(mode)
is called.doc.import(data)
is called.doc.checkout(version)
is called.NOTE: Timestamps are forced to be in ascending order. If you commit a new change with a timestamp that is less than the existing one, the largest existing timestamp will be used instead.
NOTE: The origin
will not be persisted, but the message
will.
Optional
options: { message?: string; origin?: string; timestamp?: number }Configures the default text style for the document.
This method sets the default text style configuration for the document when using LoroText.
If None
is provided, the default style is reset.
Set the rich text format configuration of the document.
You need to config it if you use rich text mark
method.
Specifically, you need to config the expand
property of each style.
Expand is used to specify the behavior of expanding when new text is inserted at the beginning or end of the style.
You can specify the expand
option to set the behavior when inserting text at the boundary of the range.
after
(default): when inserting text right after the given range, the mark will be expanded to include the inserted textbefore
: when inserting text right before the given range, the mark will be expanded to include the inserted textnone
: the mark will not be expanded to include the inserted text at the boundariesboth
: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted textconst doc = new LoroDoc();
doc.configTextStyle({
bold: { expand: "after" },
link: { expand: "before" }
});
const text = doc.getText("text");
text.insert(0, "Hello World!");
text.mark({ start: 0, end: 5 }, "bold", true);
expect(text.toDelta()).toStrictEqual([
{
insert: "Hello",
attributes: {
bold: true,
},
},
{
insert: " World!",
},
] as Delta<string>[]);
Debug the size of the history
Detach the document state from the latest known version.
After detaching, all import operations will be recorded in the OpLog
without being applied to the DocState
.
When detached
, the document is not editable.
Calculate the differences between two frontiers
The entries in the returned object are sorted by causal order: the creation of a child container will be presented before its use.
The source frontier to diff from. A frontier represents a consistent version of the document.
The target frontier to diff to. A frontier represents a consistent version of the document.
Controls the diff format:
- If true, returns JsonDiff format suitable for JSON serialization
- If false, returns Diff format that shares the same type as LoroEvent
- The default value is true
Export the document based on the specified ExportMode.
The export mode to use. Can be one of:
{ mode: "snapshot" }
: Export a full snapshot of the document.{ mode: "update", from?: VersionVector }
: Export updates from the given version vector.{ mode: "updates-in-range", spans: { id: ID, len: number }[] }
: Export updates within the specified ID spans.{ mode: "shallow-snapshot", frontiers: Frontiers }
: Export a garbage-collected snapshot up to the given frontiers.A byte array containing the exported data.
import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
doc.setPeerId("1");
doc.getText("text").update("Hello World");
// Export a full snapshot
const snapshotBytes = doc.export({ mode: "snapshot" });
// Export updates from a specific version
const vv = doc.oplogVersion();
doc.getText("text").update("Hello Loro");
const updateBytes = doc.export({ mode: "update", from: vv });
// Export a shallow snapshot that only includes the history since the frontiers
const shallowBytes = doc.export({ mode: "shallow-snapshot", frontiers: doc.oplogFrontiers() });
// Export updates within specific ID spans
const spanBytes = doc.export({
mode: "updates-in-range",
spans: [{ id: { peer: "1", counter: 0 }, len: 10 }]
});
Export updates from the specific version to the current version
Optional
version: VersionVector import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// get all updates of the doc
const updates = doc.exportFrom();
const version = doc.oplogVersion();
text.insert(5, " World");
// get updates from specific version to the latest version
const updates2 = doc.exportFrom(version);
Export the readable [Change
]s in the given [IdSpan
].
The peers are not compressed in the returned changes.
The id span to export.
The changes in the given id span.
Export the updates in the given range.
Optional
start: VersionVectorThe start version vector.
Optional
end: VersionVectorThe end version vector.
Optional
withPeerCompression: booleanWhether to compress the peer IDs in the updates. Defaults to true. If you want to process the operations in application code, set this to false.
The updates in the given range.
Find the op id spans that between the from
version and the to
version.
You can combine it with exportJsonInIdSpan
to get the changes between two versions.
You can use it to travel all the changes from from
to to
. from
and to
are frontiers,
and they can be concurrent to each other. You can use it to find all the changes related to an event:
import { LoroDoc } from "loro-crdt";
const docA = new LoroDoc();
docA.setPeerId("1");
const docB = new LoroDoc();
docA.getText("text").update("Hello");
docA.commit();
const snapshot = docA.export({ mode: "snapshot" });
let done = false;
docB.subscribe(e => {
const spans = docB.findIdSpansBetween(e.from, e.to);
const changes = docB.exportJsonInIdSpan(spans.forward[0]);
console.log(changes);
// [{
// id: "0@1",
// timestamp: expect.any(Number),
// deps: [],
// lamport: 0,
// msg: undefined,
// ops: [{
// container: "cid:root-text:Text",
// counter: 0,
// content: {
// type: "insert",
// pos: 0,
// text: "Hello"
// }
// }]
// }]
});
docB.import(snapshot);
Duplicate the document with a different PeerID
The time complexity and space complexity of this operation are both O(n),
When called in detached mode, it will fork at the current state frontiers.
It will have the same effect as forkAt(&self.frontiers())
.
Creates a new LoroDoc at a specified version (Frontiers)
The created doc will only contain the history before the specified frontiers.
Get the frontiers of the current document state.
If you checkout to a specific version, this value will change.
Convert frontiers to a version vector
Learn more about frontiers and version vector here
Get all of changes in the oplog.
Note: this method is expensive when the oplog is large. O(n)
import { LoroDoc, LoroText } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
const changes = doc.getAllChanges();
for (let [peer, c] of changes.entries()){
console.log("peer: ", peer);
for (let change of c){
console.log("change: ", change);
}
}
Get the value or container at the given path
The path can be specified in different ways depending on the container type:
For Tree:
tree/{node_id}/property
tree/0/1/property
For List and MovableList:
list/0
or list/1/property
For Map:
map/key
or map/nested/property
For tree structures, index-based paths follow depth-first traversal order. The indices start from 0 and represent the position of a node among its siblings.
Get the change that contains the specific ID
Get the change of with specific peer_id and lamport <= given lamport
Gets container IDs modified in the given ID range.
NOTE: This method will implicitly commit.
This method identifies which containers were affected by changes in a given range of operations.
It can be used together with doc.travelChangeAncestors()
to analyze the history of changes
and determine which containers were modified by each change.
The starting ID of the change range
The length of the change range to check
An array of container IDs that were modified in the given range
Get a LoroCounter by container id
If the container does not exist, an error will be thrown.
Get deep value of the document with container id
Get a LoroMovableList by container id
The object returned is a new js object each time because it need to cross the WASM boundary.
Get all ops of the change that contains the specific ID
Get the number of operations in the pending transaction.
The pending transaction is the one that is not committed yet. It will be committed
automatically after calling doc.commit()
, doc.export(mode)
or doc.checkout(version)
.
Get the shallow json format of the document state.
Unlike toJSON()
which recursively resolves all containers to their values,
getShallowValue()
returns container IDs as strings for any nested containers.
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
const tree = doc.getTree("tree");
const map = doc.getMap("map");
const shallowValue = doc.getShallowValue();
console.log(shallowValue);
// {
// list: 'cid:root-list:List',
// tree: 'cid:root-tree:Tree',
// map: 'cid:root-map:Map'
// }
// It points to the same container as `list`
const listB = doc.getContainerById(shallowValue.list);
Check if the doc contains the target container.
A root container always exists, while a normal container exists if it has ever been created on the doc.
import { LoroDoc, LoroMap, LoroText, LoroList } from "loro-crdt";
const doc = new LoroDoc();
doc.setPeerId("1");
const text = doc.getMap("map").setContainer("text", new LoroText());
const list = doc.getMap("map").setContainer("list", new LoroList());
expect(doc.isContainerExists("cid:root-map:Map")).toBe(true);
expect(doc.isContainerExists("cid:0@1:Text")).toBe(true);
expect(doc.isContainerExists("cid:1@1:List")).toBe(true);
const doc2 = new LoroDoc();
// Containers exist, as long as the history or the doc state include it
doc.detach();
doc2.import(doc.export({ mode: "update" }));
expect(doc2.isContainerExists("cid:root-map:Map")).toBe(true);
expect(doc2.isContainerExists("cid:0@1:Text")).toBe(true);
expect(doc2.isContainerExists("cid:1@1:List")).toBe(true);
Import snapshot or updates into current doc.
Note:
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
text.insert(0, "Hello");
// get all updates of the doc
const updates = doc.export({ mode: "update" });
const snapshot = doc.export({ mode: "snapshot" });
const doc2 = new LoroDoc();
// import snapshot
doc2.import(snapshot);
// or import updates
doc2.import(updates);
Import a batch of updates or snapshots.
It's more efficient than importing updates one by one.
Import updates from the JSON format.
only supports backward compatibility but not forward compatibility.
Import a batch of updates and snapshots.
It's more efficient than importing updates one by one.
detached
indicates that the DocState
is not synchronized with the latest version of OpLog
.
The document becomes detached during a
checkout
operation. Beingdetached
implies that theDocState
is not synchronized with the latest version of theOpLog
. In a detached state, the document is not editable by default, and anyimport
operations will be recorded in theOpLog
without being applied to theDocState
.
import { LoroDoc } from "loro-crdt";
const doc = new LoroDoc();
const text = doc.getText("text");
const frontiers = doc.frontiers();
text.insert(0, "Hello World!");
console.log(doc.isDetached()); // false
doc.checkout(frontiers);
console.log(doc.isDetached()); // true
doc.attach();
console.log(doc.isDetached()); // false
Whether the editing is enabled in detached mode.
The doc enter detached mode after calling detach
or checking out a non-latest version.
checkout
to apply changes.Check if the doc contains the full history.
Evaluate JSONPath against a LoroDoc
Get the number of ops in the oplog.
Get the frontiers of the latest version in OpLog.
If you checkout to a specific version, this value will not change.
Get the version vector of the latest known version in OpLog.
If you checkout to a specific version, this version vector will not change.
Revert the document to the given frontiers.
The doc will not become detached when using this method. Instead, it will generate a series of operations to revert the document to the given version.
If two continuous local changes are within (<=) the interval(in seconds), they will be merged into one change.
The default value is 1_000 seconds.
By default, we record timestamps in seconds for each change. So if the merge interval is 1, and changes A and B have timestamps of 3 and 4 respectively, then they will be merged into one change
Enables editing in detached mode, which is disabled by default.
The doc enter detached mode after calling detach
or checking out a non-latest version.
checkout
to apply changes.Set the commit message of the next commit
Set the options of the next commit
Set the origin of the next commit
Set the timestamp of the next commit
Set the peer ID of the current writer.
It must be a number, a BigInt, or a decimal string that can be parsed to a unsigned 64-bit integer.
Note: use it with caution. You need to make sure there is not chance that two peers have the same peer ID. Otherwise, we cannot ensure the consistency of the document.
Set whether to record the timestamp of each change. Default is false
.
If enabled, the Unix timestamp (in seconds) will be recorded for each change automatically.
You can also set each timestamp manually when you commit a change. The timestamp manually set will override the automatic one.
NOTE: Timestamps are forced to be in ascending order in the OpLog's history. If you commit a new change with a timestamp that is less than the existing one, the largest existing timestamp will be used instead.
The doc only contains the history since this version
This is empty if the doc is not shallow.
The ops included by the shallow history start frontiers are not in the doc.
The doc only contains the history since this version
This is empty if the doc is not shallow.
The ops included by the shallow history start version vector are not in the doc.
Subscribe to updates from local edits.
This method allows you to listen for local changes made to the document. It's useful for syncing changes with other instances or saving updates.
A callback function that receives a Uint8Array containing the update data.
A function to unsubscribe from the updates.
const loro = new Loro();
const text = loro.getText("text");
const unsubscribe = loro.subscribeLocalUpdates((update) => {
console.log("Local update received:", update);
// You can send this update to other Loro instances
});
text.insert(0, "Hello");
loro.commit();
// Later, when you want to stop listening:
unsubscribe();
Get the json format of the entire document state.
Unlike getShallowValue()
which returns container IDs as strings,
toJSON()
recursively resolves all containers to their actual values.
import { LoroDoc, LoroText, LoroMap } from "loro-crdt";
const doc = new LoroDoc();
const list = doc.getList("list");
list.insert(0, "Hello");
const text = list.insertContainer(0, new LoroText());
text.insert(0, "Hello");
const map = list.insertContainer(1, new LoroMap());
map.set("foo", "bar");
console.log(doc.toJSON());
// {"list": ["Hello", {"foo": "bar"}]}
Convert the document to a JSON value with a custom replacer function.
This method works similarly to JSON.stringify
's replacer parameter.
The replacer function is called for each value in the document and can transform
how values are serialized to JSON.
The JSON representation of the document after applying the replacer function.
Visit all the ancestors of the changes in causal order.
the changes to visit
the callback function, return true
to continue visiting, return false
to stop
Get the version vector of the current document state.
If you checkout to a specific version, the version vector will change.
Convert a version vector to frontiers
Static
from
The CRDTs document. Loro supports different CRDTs include List, RichText, Map and Movable Tree, you could build all kind of applications by these.
Important: Loro is a pure library and does not handle network protocols. It is the responsibility of the user to manage the storage, loading, and synchronization of the bytes exported by Loro in a manner suitable for their specific environment.
Example