UNPKG

loro-crdt

Version:

Loro CRDTs is a high-performance CRDT framework that makes your app state synchronized, collaborative and maintainable effortlessly.

1,613 lines (1,546 loc) 117 kB
/* tslint:disable */ /* eslint-disable */ /** * 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; export function decodeFrontiers(bytes: Uint8Array): { peer: PeerID, counter: number }[]; export function encodeFrontiers(frontiers: ({ peer: PeerID, counter: number })[]): Uint8Array; /** * 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; /** * Get the version of Loro */ export function LORO_VERSION(): string; export function run(): void; export function callPendingEvents(): void; /** * Enable debug info of Loro */ export function setDebug(): void; /** * 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}`; export type TextPosType = "unicode" | "utf16" | "utf8"; /** * 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 { /** * * 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]: Value }; retain?: undefined; delete?: undefined; } | { delete: number; attributes?: undefined; retain?: undefined; insert?: undefined; } | { retain: number; attributes?: { [key in string]: Value }; 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 { /** * Convert a position between coordinate systems. */ convertPos(index: number, from: TextPosType, to: TextPosType): number | undefined; /** * 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; /** * Subscribe to changes that may affect a JSONPath query. * Callback may fire false positives and carries no query result. * You can debounce/throttle the callback before running `JSONPath(...)` to optimize heavy reads. */ subscribeJsonpath(path: string, callback: () => void): 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; /** * Clear only the redo stack, preserving the undo stack. * * This is useful when coordinating undo/redo across multiple participants * (e.g., multiple editors) where a new edit in one participant should * invalidate redo in all other participants. */ clearRedo(): void; /** * Clear only the undo stack, preserving the redo stack. */ clearUndo(): void; } interface LoroDoc<T extends Record<string, Container> = Record<string, Container>> { /** * Subscribe to changes that may affect a JSONPath query. * Callback may fire false positives and carries no query result. * You can debounce/throttle the callback before running `JSONPath(...)` to optimize heavy reads. */ subscribeJsonpath(path: string, callback: () => void): Subscription; /** * 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 implicitly commit pending local operations (like `export(...)`) so callers can * observe the latest local edits. When called inside `subscribePreCommit(...)`, it will NOT trigger * an additional implicit commit. * * @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; /** * Convert a position between coordinate systems. */ convertPos(index: number, from: TextPosType, to: TextPosType): number | undefined; /** * 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; /** * 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[]; /** * 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); /** * Get the PeerID of the local peer. */ peer(): PeerID; /** * 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 all the peers */ peers(): PeerID[]; /** * Encodes the state of the given peers. */ encode(peers: Array<any>): Uint8Array; /** * Get the number of peers. */ length(): number; /** * If the state is empty. */ isEmpty(): boolean; /** * Encodes the state of all peers. */ encodeAll(): Uint8Array; } 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; /** * "Cursor" */ kind(): any; /** * Get which side of the character/list item the cursor is on. */ side(): Side; /** * Decode the cursor from a Uint8Array. */ static decode(data: Uint8Array): Cursor; /** * Encode the cursor into a Uint8Array. */ encode(): Uint8Array; } export class EphemeralStoreWasm { free(): void; getAllStates(): any; removeOutdated(): void; get(key: string): any; /** * 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; keys(): string[]; apply(data: Uint8Array): void; delete(key: string): void; encode(key: string): Uint8Array; /** * If the state is empty. */ isEmpty(): boolean; encodeAll(): Uint8Array; } /** * The handler of