loro-crdt
Version:
Loro CRDTs is a high-performance CRDT framework that makes your app state synchronized, collaborative and maintainable effortlessly.
1,634 lines (1,570 loc) • 114 kB
TypeScript
/* tslint:disable */
/* eslint-disable */
/**
* Get the version of Loro
*/
export function LORO_VERSION(): string;
export function run(): void;
export function encodeFrontiers(frontiers: ({ peer: PeerID, counter: number })[]): Uint8Array;
export function decodeFrontiers(bytes: Uint8Array): { peer: PeerID, counter: number }[];
/**
* Enable debug info of Loro
*/
export function setDebug(): void;
/**
* Decode the metadata of the import blob.
*
* This method is useful to get the following metadata of the import blob:
*
* - startVersionVector
* - endVersionVector
* - startTimestamp
* - endTimestamp
* - mode
* - changeNum
*/
export function decodeImportBlobMeta(blob: Uint8Array, check_checksum: boolean): ImportBlobMetadata;
/**
* Redacts sensitive content in JSON updates within the specified version range.
*
* This function allows you to share document history while removing potentially sensitive content.
* It preserves the document structure and collaboration capabilities while replacing content with
* placeholders according to these redaction rules:
*
* - Preserves delete and move operations
* - Replaces text insertion content with the Unicode replacement character
* - Substitutes list and map insert values with null
* - Maintains structure of child containers
* - Replaces text mark values with null
* - Preserves map keys and text annotation keys
*
* @param {Object|string} jsonUpdates - The JSON updates to redact (object or JSON string)
* @param {Object} versionRange - Version range defining what content to redact,
* format: { peerId: [startCounter, endCounter], ... }
* @returns {Object} The redacted JSON updates
*/
export function redactJsonUpdates(json_updates: string | JsonSchema, version_range: any): JsonSchema;
/**
* Container types supported by loro.
*
* It is most commonly used to specify the type of sub-container to be created.
* @example
* ```ts
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* const text = list.insertContainer(1, new LoroText());
* ```
*/
export type ContainerType = "Text" | "Map" | "List"| "Tree" | "MovableList" | "Counter";
export type PeerID = `${number}`;
/**
* The unique id of each container.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* const containerId = list.id;
* ```
*/
export type ContainerID =
| `cid:root-${string}:${ContainerType}`
| `cid:${number}@${PeerID}:${ContainerType}`;
/**
* The unique id of each tree node.
*/
export type TreeID = `${number}@${PeerID}`;
interface LoroDoc {
/**
* Export updates from the specific version to the current version
*
* @deprecated Use `export({mode: "update", from: version})` instead
*
* @example
* ```ts
* 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);
* ```
*/
exportFrom(version?: VersionVector): Uint8Array;
/**
*
* Get the container corresponding to the container id
*
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* let text = doc.getText("text");
* const textId = text.id;
* text = doc.getContainerById(textId);
* ```
*/
getContainerById(id: ContainerID): Container | undefined;
/**
* 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.
*
* @param f - A callback function that receives a Uint8Array containing the update data.
* @returns A function to unsubscribe from the updates.
*
* @example
* ```ts
* 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();
* ```
*
* @example
* ```ts
* const loro1 = new Loro();
* const loro2 = new Loro();
*
* // Set up two-way sync
* loro1.subscribeLocalUpdates((updates) => {
* loro2.import(updates);
* });
*
* loro2.subscribeLocalUpdates((updates) => {
* loro1.import(updates);
* });
*
* // Now changes in loro1 will be reflected in loro2 and vice versa
* ```
*/
subscribeLocalUpdates(f: (bytes: Uint8Array) => void): () => void
/**
* Subscribe to the first commit from a peer. Operations performed on the `LoroDoc` within this callback
* will be merged into the current commit.
*
* This is useful for managing the relationship between `PeerID` and user information.
* For example, you could store user names in a `LoroMap` using `PeerID` as the key and the `UserID` as the value.
*
* @param f - A callback function that receives a peer id.
*
* @example
* ```ts
* const doc = new LoroDoc();
* doc.setPeerId(0);
* const p = [];
* doc.subscribeFirstCommitFromPeer((peer) => {
* p.push(peer);
* doc.getMap("map").set(e.peer, "user-" + e.peer);
* });
* doc.getList("list").insert(0, 100);
* doc.commit();
* doc.getList("list").insert(0, 200);
* doc.commit();
* doc.setPeerId(1);
* doc.getList("list").insert(0, 300);
* doc.commit();
* expect(p).toEqual(["0", "1"]);
* expect(doc.getMap("map").get("0")).toBe("user-0");
* ```
**/
subscribeFirstCommitFromPeer(f: (e: { peer: PeerID }) => void): () => void
/**
* Subscribe to the pre-commit event.
*
* The callback will be called when the changes are committed but not yet applied to the OpLog.
* You can modify the commit message and timestamp in the callback by `ChangeModifier`.
*
* @example
* ```ts
* const doc = new LoroDoc();
* doc.subscribePreCommit((e) => {
* e.modifier.setMessage("test").setTimestamp(Date.now());
* });
* doc.getList("list").insert(0, 100);
* doc.commit();
* expect(doc.getChangeAt({ peer: "0", counter: 0 }).message).toBe("test");
* ```
*
* ### Advanced Example: Creating a Merkle DAG
*
* By combining `doc.subscribePreCommit` with `doc.exportJsonInIdSpan`, you can implement advanced features like representing Loro's editing history as a Merkle DAG:
*
* ```ts
* const doc = new LoroDoc();
* doc.setPeerId(0);
* doc.subscribePreCommit((e) => {
* const changes = doc.exportJsonInIdSpan(e.changeMeta)
* expect(changes).toHaveLength(1);
* const hash = crypto.createHash('sha256');
* const change = {
* ...changes[0],
* deps: changes[0].deps.map(d => {
* const depChange = doc.getChangeAt(idStrToId(d))
* return depChange.message;
* })
* }
* console.log(change); // The output is shown below
* hash.update(JSON.stringify(change));
* const sha256Hash = hash.digest('hex');
* e.modifier.setMessage(sha256Hash);
* });
*
* doc.getList("list").insert(0, 100);
* doc.commit();
* // Change 0
* // {
* // id: '0@0',
* // timestamp: 0,
* // deps: [],
* // lamport: 0,
* // msg: undefined,
* // ops: [
* // {
* // container: 'cid:root-list:List',
* // content: { type: 'insert', pos: 0, value: [100] },
* // counter: 0
* // }
* // ]
* // }
*
*
* doc.getList("list").insert(0, 200);
* doc.commit();
* // Change 1
* // {
* // id: '1@0',
* // timestamp: 0,
* // deps: [
* // '2af99cf93869173984bcf6b1ce5412610b0413d027a5511a8f720a02a4432853'
* // ],
* // lamport: 1,
* // msg: undefined,
* // ops: [
* // {
* // container: 'cid:root-list:List',
* // content: { type: 'insert', pos: 0, value: [200] },
* // counter: 1
* // }
* // ]
* // }
*
* expect(doc.getChangeAt({ peer: "0", counter: 0 }).message).toBe("2af99cf93869173984bcf6b1ce5412610b0413d027a5511a8f720a02a4432853");
* expect(doc.getChangeAt({ peer: "0", counter: 1 }).message).toBe("aedbb442c554ecf59090e0e8339df1d8febf647f25cc37c67be0c6e27071d37f");
* ```
*
* @param f - A callback function that receives a pre commit event.
*
**/
subscribePreCommit(f: (e: { changeMeta: Change, origin: string, modifier: ChangeModifier }) => void): () => void
/**
* 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.
*
* @param replacer - A function that takes a key and value, and returns how that value
* should be serialized. Similar to JSON.stringify's replacer.
* If return undefined, the value will be skipped.
* @returns The JSON representation of the document after applying the replacer function.
*
* @example
* ```ts
* const doc = new LoroDoc();
* const text = doc.getText("text");
* text.insert(0, "Hello");
* text.mark({ start: 0, end: 2 }, "bold", true);
*
* // Use delta to represent text
* const json = doc.toJsonWithReplacer((key, value) => {
* if (value instanceof LoroText) {
* return value.toDelta();
* }
*
* return value;
* });
* ```
*/
toJsonWithReplacer(replacer: (key: string | index, value: Value | Container) => Value | Container | undefined): Value;
/**
* 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.
*
* @param from - The source frontier to diff from. A frontier represents a consistent version of the document.
* @param to - The target frontier to diff to. A frontier represents a consistent version of the document.
* @param for_json - 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`
*/
diff(from: OpId[], to: OpId[], for_json: false): [ContainerID, Diff][];
diff(from: OpId[], to: OpId[], for_json: true): [ContainerID, JsonDiff][];
diff(from: OpId[], to: OpId[], for_json: undefined): [ContainerID, JsonDiff][];
diff(from: OpId[], to: OpId[], for_json?: boolean): [ContainerID, JsonDiff|Diff][];
}
/**
* Represents a `Delta` type which is a union of different operations that can be performed.
*
* @typeparam T - The data type for the `insert` operation.
*
* The `Delta` type can be one of three distinct shapes:
*
* 1. Insert Operation:
* - `insert`: The item to be inserted, of type T.
* - `attributes`: (Optional) A dictionary of attributes, describing styles in richtext
*
* 2. Delete Operation:
* - `delete`: The number of elements to delete.
*
* 3. Retain Operation:
* - `retain`: The number of elements to retain.
* - `attributes`: (Optional) A dictionary of attributes, describing styles in richtext
*/
export type Delta<T> =
| {
insert: T;
attributes?: { [key in string]: {} };
retain?: undefined;
delete?: undefined;
}
| {
delete: number;
attributes?: undefined;
retain?: undefined;
insert?: undefined;
}
| {
retain: number;
attributes?: { [key in string]: {} };
delete?: undefined;
insert?: undefined;
};
/**
* The unique id of each operation.
*/
export type OpId = { peer: PeerID, counter: number };
/**
* Change is a group of continuous operations
*/
export interface Change {
peer: PeerID,
counter: number,
lamport: number,
length: number,
/**
* The timestamp in seconds.
*
* [Unix time](https://en.wikipedia.org/wiki/Unix_time)
* It is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970.
*/
timestamp: number,
deps: OpId[],
message: string | undefined,
}
/**
* Data types supported by loro
*/
export type Value =
| ContainerID
| string
| number
| boolean
| null
| { [key: string]: Value }
| Uint8Array
| Value[]
| undefined;
export type IdSpan = {
peer: PeerID,
counter: number,
length: number,
}
export type VersionVectorDiff = {
/**
* The spans that the `from` side needs to retreat to reach the `to` side
*
* These spans are included in the `from`, but not in the `to`
*/
retreat: IdSpan[],
/**
* The spans that the `from` side needs to forward to reach the `to` side
*
* These spans are included in the `to`, but not in the `from`
*/
forward: IdSpan[],
}
export type UndoConfig = {
mergeInterval?: number,
maxUndoSteps?: number,
excludeOriginPrefixes?: string[],
onPush?: (isUndo: boolean, counterRange: { start: number, end: number }, event?: LoroEventBatch) => { value: Value, cursors: Cursor[] },
onPop?: (isUndo: boolean, value: { value: Value, cursors: Cursor[] }, counterRange: { start: number, end: number }) => void
};
export type Container = LoroList | LoroMap | LoroText | LoroTree | LoroMovableList | LoroCounter;
export interface ImportBlobMetadata {
/**
* The version vector of the start of the import.
*
* Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
* However, it does not constitute a complete version vector, as it only contains counters
* from peers included within the import blob.
*/
partialStartVersionVector: VersionVector;
/**
* The version vector of the end of the import.
*
* Import blob includes all the ops from `partial_start_vv` to `partial_end_vv`.
* However, it does not constitute a complete version vector, as it only contains counters
* from peers included within the import blob.
*/
partialEndVersionVector: VersionVector;
startFrontiers: OpId[],
startTimestamp: number;
endTimestamp: number;
mode: "outdated-snapshot" | "outdated-update" | "snapshot" | "shallow-snapshot" | "update";
changeNum: number;
}
interface LoroText {
/**
* Get the cursor position at the given pos.
*
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new LoroDoc();
* const text = doc.getText("text");
* text.insert(0, "123");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
getCursor(pos: number, side?: Side): Cursor | undefined;
}
interface LoroList {
/**
* Get the cursor position at the given pos.
*
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new LoroDoc();
* const text = doc.getList("list");
* text.insert(0, "1");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
getCursor(pos: number, side?: Side): Cursor | undefined;
}
export type TreeNodeShallowValue = {
id: TreeID,
parent: TreeID | undefined,
index: number,
fractionalIndex: string,
meta: ContainerID,
children: TreeNodeShallowValue[],
}
export type TreeNodeValue = {
id: TreeID,
parent: TreeID | undefined,
index: number,
fractionalIndex: string,
meta: LoroMap,
children: TreeNodeValue[],
}
export type TreeNodeJSON<T> = Omit<TreeNodeValue, 'meta' | 'children'> & {
meta: T,
children: TreeNodeJSON<T>[],
}
interface LoroMovableList {
/**
* Get the cursor position at the given pos.
*
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new LoroDoc();
* const text = doc.getMovableList("text");
* text.insert(0, "1");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
getCursor(pos: number, side?: Side): Cursor | undefined;
}
export type Side = -1 | 0 | 1;
export type JsonOpID = `${number}@${PeerID}`;
export type JsonContainerID = `🦜:${ContainerID}` ;
export type JsonValue =
| JsonContainerID
| string
| number
| boolean
| null
| { [key: string]: JsonValue }
| Uint8Array
| JsonValue[];
export type JsonSchema = {
schema_version: number;
start_version: Map<string, number>,
peers: PeerID[],
changes: JsonChange[]
};
export type JsonChange = {
id: JsonOpID
/**
* The timestamp in seconds.
*
* [Unix time](https://en.wikipedia.org/wiki/Unix_time)
* It is the number of seconds that have elapsed since 00:00:00 UTC on 1 January 1970.
*/
timestamp: number,
deps: JsonOpID[],
lamport: number,
msg: string | null,
ops: JsonOp[]
}
export interface TextUpdateOptions {
timeoutMs?: number,
useRefinedDiff?: boolean,
}
export type ExportMode = {
mode: "update",
from?: VersionVector,
} | {
mode: "snapshot",
} | {
mode: "shallow-snapshot",
frontiers: Frontiers,
} | {
mode: "updates-in-range",
spans: {
id: OpId,
len: number,
}[],
};
export type JsonOp = {
container: ContainerID,
counter: number,
content: ListOp | TextOp | MapOp | TreeOp | MovableListOp | UnknownOp
}
export type ListOp = {
type: "insert",
pos: number,
value: JsonValue
} | {
type: "delete",
pos: number,
len: number,
start_id: JsonOpID,
};
export type MovableListOp = {
type: "insert",
pos: number,
value: JsonValue
} | {
type: "delete",
pos: number,
len: number,
start_id: JsonOpID,
}| {
type: "move",
from: number,
to: number,
elem_id: JsonOpID,
}|{
type: "set",
elem_id: JsonOpID,
value: JsonValue
}
export type TextOp = {
type: "insert",
pos: number,
text: string
} | {
type: "delete",
pos: number,
len: number,
start_id: JsonOpID,
} | {
type: "mark",
start: number,
end: number,
style_key: string,
style_value: JsonValue,
info: number
}|{
type: "mark_end"
};
export type MapOp = {
type: "insert",
key: string,
value: JsonValue
} | {
type: "delete",
key: string,
};
export type TreeOp = {
type: "create",
target: TreeID,
parent: TreeID | undefined,
fractional_index: string
}|{
type: "move",
target: TreeID,
parent: TreeID | undefined,
fractional_index: string
}|{
type: "delete",
target: TreeID
};
export type UnknownOp = {
type: "unknown"
prop: number,
value_type: "unknown",
value: {
kind: number,
data: Uint8Array
}
};
export type CounterSpan = { start: number, end: number };
export type ImportStatus = {
success: Map<PeerID, CounterSpan>,
pending: Map<PeerID, CounterSpan> | null
}
export type Frontiers = OpId[];
/**
* Represents a path to identify the exact location of an event's target.
* The path is composed of numbers (e.g., indices of a list container) strings
* (e.g., keys of a map container) and TreeID (the node of a tree container),
* indicating the absolute position of the event's source within a loro document.
*/
export type Path = (number | string | TreeID)[];
/**
* A batch of events that created by a single `import`/`transaction`/`checkout`.
*
* @prop by - How the event is triggered.
* @prop origin - (Optional) Provides information about the origin of the event.
* @prop diff - Contains the differential information related to the event.
* @prop target - Identifies the container ID of the event's target.
* @prop path - Specifies the absolute path of the event's emitter, which can be an index of a list container or a key of a map container.
*/
export interface LoroEventBatch {
/**
* How the event is triggered.
*
* - `local`: The event is triggered by a local transaction.
* - `import`: The event is triggered by an import operation.
* - `checkout`: The event is triggered by a checkout operation.
*/
by: "local" | "import" | "checkout";
origin?: string;
/**
* The container ID of the current event receiver.
* It's undefined if the subscriber is on the root document.
*/
currentTarget?: ContainerID;
events: LoroEvent[];
from: Frontiers;
to: Frontiers;
}
/**
* The concrete event of Loro.
*/
export interface LoroEvent {
/**
* The container ID of the event's target.
*/
target: ContainerID;
diff: Diff;
/**
* The absolute path of the event's emitter, which can be an index of a list container or a key of a map container.
*/
path: Path;
}
export type ListDiff = {
type: "list";
diff: Delta<(Value | Container)[]>[];
};
export type ListJsonDiff = {
type: "list";
diff: Delta<(Value | JsonContainerID )[]>[];
};
export type TextDiff = {
type: "text";
diff: Delta<string>[];
};
export type MapDiff = {
type: "map";
updated: Record<string, Value | Container | undefined>;
};
export type MapJsonDiff = {
type: "map";
updated: Record<string, Value | JsonContainerID | undefined>;
};
export type TreeDiffItem =
| {
target: TreeID;
action: "create";
parent: TreeID | undefined;
index: number;
fractionalIndex: string;
}
| {
target: TreeID;
action: "delete";
oldParent: TreeID | undefined;
oldIndex: number;
}
| {
target: TreeID;
action: "move";
parent: TreeID | undefined;
index: number;
fractionalIndex: string;
oldParent: TreeID | undefined;
oldIndex: number;
};
export type TreeDiff = {
type: "tree";
diff: TreeDiffItem[];
};
export type CounterDiff = {
type: "counter";
increment: number;
};
export type Diff = ListDiff | TextDiff | MapDiff | TreeDiff | CounterDiff;
export type JsonDiff = ListJsonDiff | TextDiff | MapJsonDiff | CounterDiff | TreeDiff;
export type Subscription = () => void;
type NonNullableType<T> = Exclude<T, null | undefined>;
export type AwarenessListener = (
arg: { updated: PeerID[]; added: PeerID[]; removed: PeerID[] },
origin: "local" | "timeout" | "remote" | string,
) => void;
interface Listener {
(event: LoroEventBatch): void;
}
interface LoroDoc {
subscribe(listener: Listener): Subscription;
}
interface UndoManager {
/**
* Set the callback function that is called when an undo/redo step is pushed.
* The function can return a meta data value that will be attached to the given stack item.
*
* @param listener - The callback function.
*/
setOnPush(listener?: UndoConfig["onPush"]): void;
/**
* Set the callback function that is called when an undo/redo step is popped.
* The function will have a meta data value that was attached to the given stack item when `onPush` was called.
*
* @param listener - The callback function.
*/
setOnPop(listener?: UndoConfig["onPop"]): void;
/**
* Starts a new grouping of undo operations.
* All changes/commits made after this call will be grouped/merged together.
* to end the group, call `groupEnd`.
*
* If a remote import is received within the group, its possible that the undo item will be
* split and the group will be automatically ended.
*
* Calling `groupStart` within an active group will throw but have no effect.
*
*/
groupStart(): void;
/**
* Ends the current grouping of undo operations.
*/
groupEnd(): void;
}
interface LoroDoc<T extends Record<string, Container> = Record<string, Container>> {
/**
* Get a LoroMap by container id
*
* The object returned is a new js object each time because it need to cross
* the WASM boundary.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* ```
*/
getMap<Key extends keyof T | ContainerID>(name: Key): T[Key] extends LoroMap ? T[Key] : LoroMap;
/**
* Get a LoroList by container id
*
* The object returned is a new js object each time because it need to cross
* the WASM boundary.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* ```
*/
getList<Key extends keyof T | ContainerID>(name: Key): T[Key] extends LoroList ? T[Key] : LoroList;
/**
* Get a LoroMovableList by container id
*
* The object returned is a new js object each time because it need to cross
* the WASM boundary.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* ```
*/
getMovableList<Key extends keyof T | ContainerID>(name: Key): T[Key] extends LoroMovableList ? T[Key] : LoroMovableList;
/**
* Get a LoroTree by container id
*
* The object returned is a new js object each time because it need to cross
* the WASM boundary.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const tree = doc.getTree("tree");
* ```
*/
getTree<Key extends keyof T | ContainerID>(name: Key): T[Key] extends LoroTree ? T[Key] : LoroTree;
getText(key: string | ContainerID): LoroText;
/**
* Export the updates in the given range.
*
* @param start - The start version vector.
* @param end - The end version vector.
* @param withPeerCompression - Whether 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.
* @returns The updates in the given range.
*/
exportJsonUpdates(start?: VersionVector, end?: VersionVector, withPeerCompression?: boolean): JsonSchema;
/**
* Exports changes within the specified ID span to JSON schema format.
*
* The JSON schema format produced by this method is identical to the one generated by `export_json_updates`.
* It ensures deterministic output, making it ideal for hash calculations and integrity checks.
*
* This method can also export pending changes from the uncommitted transaction that have not yet been applied to the OpLog.
*
* This method will NOT trigger a new commit implicitly.
*
* @param idSpan - The id span to export.
* @returns The changes in the given id span.
*/
exportJsonInIdSpan(idSpan: IdSpan): JsonChange[];
}
interface LoroList<T = unknown> {
new(): LoroList<T>;
/**
* Get elements of the list. If the value is a child container, the corresponding
* `Container` will be returned.
*
* @example
* ```ts
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
* list.insert(2, true);
* list.insertContainer(3, new LoroText());
* console.log(list.value); // [100, "foo", true, LoroText];
* ```
*/
toArray(): T[];
/**
* Insert a container at the index.
*
* @example
* ```ts
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* const text = list.insertContainer(1, new LoroText());
* text.insert(0, "Hello");
* console.log(list.toJSON()); // [100, "Hello"];
* ```
*/
insertContainer<C extends Container>(pos: number, child: C): T extends C ? T : C;
/**
* Push a container to the end of the list.
*/
pushContainer<C extends Container>(child: C): T extends C ? T : C;
/**
* Get the value at the index. If the value is a container, the corresponding handler will be returned.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* console.log(list.get(0)); // 100
* console.log(list.get(1)); // undefined
* ```
*/
get(index: number): T;
/**
* Insert a value at index.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
* list.insert(2, true);
* console.log(list.value); // [100, "foo", true];
* ```
*/
insert<V extends T>(pos: number, value: Exclude<V, Container>): void;
delete(pos: number, len: number): void;
push<V extends T>(value: Exclude<V, Container>): void;
subscribe(listener: Listener): Subscription;
getAttached(): undefined | LoroList<T>;
}
interface LoroMovableList<T = unknown> {
new(): LoroMovableList<T>;
/**
* Get elements of the list. If the value is a child container, the corresponding
* `Container` will be returned.
*
* @example
* ```ts
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
* list.insert(2, true);
* list.insertContainer(3, new LoroText());
* console.log(list.value); // [100, "foo", true, LoroText];
* ```
*/
toArray(): T[];
/**
* Insert a container at the index.
*
* @example
* ```ts
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* const text = list.insertContainer(1, new LoroText());
* text.insert(0, "Hello");
* console.log(list.toJSON()); // [100, "Hello"];
* ```
*/
insertContainer<C extends Container>(pos: number, child: C): T extends C ? T : C;
/**
* Push a container to the end of the list.
*/
pushContainer<C extends Container>(child: C): T extends C ? T : C;
/**
* Get the value at the index. If the value is a container, the corresponding handler will be returned.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* console.log(list.get(0)); // 100
* console.log(list.get(1)); // undefined
* ```
*/
get(index: number): T;
/**
* Insert a value at index.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
* list.insert(2, true);
* console.log(list.value); // [100, "foo", true];
* ```
*/
insert<V extends T>(pos: number, value: Exclude<V, Container>): void;
delete(pos: number, len: number): void;
push<V extends T>(value: Exclude<V, Container>): void;
subscribe(listener: Listener): Subscription;
getAttached(): undefined | LoroMovableList<T>;
/**
* Set the value at the given position.
*
* It's different from `delete` + `insert` that it will replace the value at the position.
*
* For example, if you have a list `[1, 2, 3]`, and you call `set(1, 100)`, the list will be `[1, 100, 3]`.
* If concurrently someone call `set(1, 200)`, the list will be `[1, 200, 3]` or `[1, 100, 3]`.
*
* But if you use `delete` + `insert` to simulate the set operation, they may create redundant operations
* and the final result will be `[1, 100, 200, 3]` or `[1, 200, 100, 3]`.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* list.insert(1, "foo");
* list.insert(2, true);
* list.set(1, "bar");
* console.log(list.value); // [100, "bar", true];
* ```
*/
set<V extends T>(pos: number, value: Exclude<V, Container>): void;
/**
* Set a container at the index.
*
* @example
* ```ts
* import { LoroDoc, LoroText } from "loro-crdt";
*
* const doc = new LoroDoc();
* const list = doc.getMovableList("list");
* list.insert(0, 100);
* const text = list.setContainer(0, new LoroText());
* text.insert(0, "Hello");
* console.log(list.toJSON()); // ["Hello"];
* ```
*/
setContainer<C extends Container>(pos: number, child: C): T extends C ? T : C;
}
interface LoroMap<T extends Record<string, unknown> = Record<string, unknown>> {
new(): LoroMap<T>;
/**
* Get the value of the key. If the value is a child container, the corresponding
* `Container` will be returned.
*
* The object returned is a new js object each time because it need to cross
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* const bar = map.get("foo");
* ```
*/
getOrCreateContainer<C extends Container>(key: string, child: C): C;
/**
* Set the key with a container.
*
* @example
* ```ts
* import { LoroDoc, LoroText, LoroList } from "loro-crdt";
*
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* const text = map.setContainer("text", new LoroText());
* const list = map.setContainer("list", new LoroList());
* ```
*/
setContainer<C extends Container, Key extends keyof T>(key: Key, child: C): NonNullableType<T[Key]> extends C ? NonNullableType<T[Key]> : C;
/**
* Get the value of the key. If the value is a child container, the corresponding
* `Container` will be returned.
*
* The object/value returned is a new js object/value each time because it need to cross
* the WASM boundary.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* const bar = map.get("foo");
* ```
*/
get<Key extends keyof T>(key: Key): T[Key];
/**
* Set the key with the value.
*
* If the key already exists, its value will be updated. If the key doesn't exist,
* a new key-value pair will be created.
*
* > **Note**: When calling `map.set(key, value)` on a LoroMap, if `map.get(key)` already returns `value`,
* > the operation will be a no-op (no operation recorded) to avoid unnecessary updates.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const map = doc.getMap("map");
* map.set("foo", "bar");
* map.set("foo", "baz");
* ```
*/
set<Key extends keyof T, V extends T[Key]>(key: Key, value: Exclude<V, Container>): void;
delete(key: string): void;
subscribe(listener: Listener): Subscription;
}
interface LoroText {
new(): LoroText;
insert(pos: number, text: string): void;
delete(pos: number, len: number): void;
subscribe(listener: Listener): Subscription;
/**
* Update the current text to the target text.
*
* It will calculate the minimal difference and apply it to the current text.
* It uses Myers' diff algorithm to compute the optimal difference.
*
* This could take a long time for large texts (e.g. > 50_000 characters).
* In that case, you should use `updateByLine` instead.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const text = doc.getText("text");
* text.insert(0, "Hello");
* text.update("Hello World");
* console.log(text.toString()); // "Hello World"
* ```
*/
update(text: string, options?: TextUpdateOptions): void;
/**
* Update the current text based on the provided text.
* This update calculation is line-based, which will be more efficient but less precise.
*/
updateByLine(text: string, options?: TextUpdateOptions): void;
}
interface LoroTree<T extends Record<string, unknown> = Record<string, unknown>> {
new(): LoroTree<T>;
/**
* Create a new tree node as the child of parent and return a `LoroTreeNode` instance.
* If the parent is undefined, the tree node will be a root node.
*
* If the index is not provided, the new node will be appended to the end.
*
* @example
* ```ts
* import { LoroDoc } from "loro-crdt";
*
* const doc = new LoroDoc();
* const tree = doc.getTree("tree");
* const root = tree.createNode();
* const node = tree.createNode(undefined, 0);
*
* // undefined
* // / \
* // node root
* ```
*/
createNode(parent?: TreeID, index?: number): LoroTreeNode<T>;
move(target: TreeID, parent?: TreeID, index?: number): void;
delete(target: TreeID): void;
has(target: TreeID): boolean;
/**
* Get LoroTreeNode by the TreeID.
*/
getNodeByID(target: TreeID): LoroTreeNode<T> | undefined;
subscribe(listener: Listener): Subscription;
toArray(): TreeNodeValue[];
getNodes(options?: { withDeleted?: boolean } ): LoroTreeNode<T>[];
}
interface LoroTreeNode<T extends Record<string, unknown> = Record<string, unknown>> {
/**
* Get the associated metadata map container of a tree node.
*/
readonly data: LoroMap<T>;
/**
* Create a new node as the child of the current node and
* return an instance of `LoroTreeNode`.
*
* If the index is not provided, the new node will be appended to the end.
*
* @example
* ```typescript
* import { LoroDoc } from "loro-crdt";
*
* let doc = new LoroDoc();
* let tree = doc.getTree("tree");
* let root = tree.createNode();
* let node = root.createNode();
* let node2 = root.createNode(0);
* // root
* // / \
* // node2 node
* ```
*/
createNode(index?: number): LoroTreeNode<T>;
/**
* Move this tree node to be a child of the parent.
* If the parent is undefined, this node will be a root node.
*
* If the index is not provided, the node will be appended to the end.
*
* It's not allowed that the target is an ancestor of the parent.
*
* @example
* ```ts
* const doc = new LoroDoc();
* const tree = doc.getTree("tree");
* const root = tree.createNode();
* const node = root.createNode();
* const node2 = node.createNode();
* node2.move(undefined, 0);
* // node2 root
* // |
* // node
*
* ```
*/
move(parent?: LoroTreeNode<T>, index?: number): void;
/**
* Get the parent node of this node.
*
* - The parent of the root node is `undefined`.
* - The object returned is a new js object each time because it need to cross
* the WASM boundary.
*/
parent(): LoroTreeNode<T> | undefined;
/**
* Get the children of this node.
*
* The objects returned are new js objects each time because they need to cross
* the WASM boundary.
*/
children(): Array<LoroTreeNode<T>> | undefined;
toJSON(): TreeNodeJSON<T>;
}
interface AwarenessWasm<T extends Value = Value> {
getState(peer: PeerID): T | undefined;
getTimestamp(peer: PeerID): number | undefined;
getAllStates(): Record<PeerID, T>;
setLocalState(value: T): void;
removeOutdated(): PeerID[];
}
type EphemeralListener = (event: EphemeralStoreEvent) => void;
type EphemeralLocalListener = (bytes: Uint8Array) => void;
interface EphemeralStoreWasm<T extends Value = Value> {
set(key: string, value: T): void;
get(key: string): T | undefined;
getAllStates(): Record<string, T>;
removeOutdated();
subscribeLocalUpdates(f: EphemeralLocalListener): () => void;
subscribe(f: EphemeralListener): () => void;
}
interface EphemeralStoreEvent {
by: "local" | "import" | "timeout";
added: string[];
updated: string[];
removed: string[];
}
/**
* `Awareness` is a structure that tracks the ephemeral state of peers.
*
* It can be used to synchronize cursor positions, selections, and the names of the peers.
*
* The state of a specific peer is expected to be removed after a specified timeout. Use
* `remove_outdated` to eliminate outdated states.
*/
export class AwarenessWasm {
free(): void;
/**
* Creates a new `Awareness` instance.
*
* The `timeout` parameter specifies the duration in milliseconds.
* A state of a peer is considered outdated, if the last update of the state of the peer
* is older than the `timeout`.
*/
constructor(peer: number | bigint | `${number}`, timeout: number);
/**
* Encodes the state of the given peers.
*/
encode(peers: Array<any>): Uint8Array;
/**
* Encodes the state of all peers.
*/
encodeAll(): Uint8Array;
/**
* Applies the encoded state of peers.
*
* Each peer's deletion countdown will be reset upon update, requiring them to pass through the `timeout`
* interval again before being eligible for deletion.
*/
apply(encoded_peers_info: Uint8Array): { updated: PeerID[], added: PeerID[] };
/**
* Get the PeerID of the local peer.
*/
peer(): PeerID;
/**
* Get the timestamp of the state of a given peer.
*/
getTimestamp(peer: number | bigint | `${number}`): number | undefined;
/**
* Remove the states of outdated peers.
*/
removeOutdated(): PeerID[];
/**
* Get the number of peers.
*/
length(): number;
/**
* If the state is empty.
*/
isEmpty(): boolean;
/**
* Get all the peers
*/
peers(): PeerID[];
}
export class ChangeModifier {
private constructor();
free(): void;
setMessage(message: string): ChangeModifier;
setTimestamp(timestamp: number): ChangeModifier;
}
/**
* Cursor is a stable position representation in the doc.
* When expressing the position of a cursor, using "index" can be unstable
* because the cursor's position may change due to other deletions and insertions,
* requiring updates with each edit. To stably represent a position or range within
* a list structure, we can utilize the ID of each item/character on List CRDT or
* Text CRDT for expression.
*
* Loro optimizes State metadata by not storing the IDs of deleted elements. This
* approach complicates tracking cursors since they rely on these IDs. The solution
* recalculates position by replaying relevant history to update cursors
* accurately. To minimize the performance impact of history replay, the system
* updates cursor info to reference only the IDs of currently present elements,
* thereby reducing the need for replay.
*
* @example
* ```ts
*
* const doc = new LoroDoc();
* const text = doc.getText("text");
* text.insert(0, "123");
* const pos0 = text.getCursor(0, 0);
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(0);
* }
* text.insert(0, "1");
* {
* const ans = doc.getCursorPos(pos0!);
* expect(ans.offset).toBe(1);
* }
* ```
*/
export class Cursor {
private constructor();
free(): void;
/**
* Get the id of the given container.
*/
containerId(): ContainerID;
/**
* Get the ID that represents the position.
*
* It can be undefined if it's not bind into a specific ID.
*/
pos(): { peer: PeerID, counter: number } | undefined;
/**
* Get which side of the character/list item the cursor is on.
*/
side(): Side;
/**
* Encode the cursor into a Uint8Array.
*/
encode(): Uint8Array;
/**
* Decode the cursor from a Uint8Array.
*/
static decode(data: Uint8Array): Cursor;
/**
* "Cursor"
*/
kind(): any;
}
export class EphemeralStoreWasm {
free(): void;
/**
* Creates a new `EphemeralStore` instance.
*
* The `timeout` parameter specifies the duration in milliseconds.
* A state of a peer is considered outdated, if the last update of the state of the peer
* is older than the `timeout`.
*/
constructor(timeout: number);
set(key: string, value: any): void;
delete(key: string): void;
get(key: string): any;
getAllStates(): any;
encode(key: string): Uint8Array;
encodeAll(): Uint8Array;
apply(data: Uint8Array): void;
removeOutdated(): void;
/**
* If the state is empty.
*/
isEmpty(): boolean;
keys(): string[];
}
/**
* The handler of a counter container.
*/
export class LoroCounter {
free(): void;
/**
* Create a new LoroCounter.
*/
constructor();
/**
* "Counter"
*/
kind(): 'Counter';
/**
* Increment the counter by the given value.
*/
increment(value: number): void;
/**
* Decrement the counter by the given value.
*/
decrement(value: number): void;
/**
* Subscribe to the changes of the counter.
*/
subscribe(f: Function): any;
/**
* Get the parent container of the counter container.
*
* - The parent container of the root counter is `undefined`.
* - The object returned is a new js object each time because it need to cross
* the WASM boundary.
*/
parent(): Container | undefined;
/**
* Whether the container is attached to a docuemnt.
*
* If it's detached, the operations on the container will not be persisted.
*/
isAttached(): boolean;
/**
* Get the attached container associated wit