loro-mirror
Version:
Type-safe state management synchronized with Loro CRDT via a declarative schema and bidirectional mirroring.
338 lines • 9.98 kB
TypeScript
import { ContainerID, ContainerType, LoroDoc, TreeID } from "loro-crdt";
import { InferInputType, InferType, SchemaType } from "../schema";
/**
* Sync direction for handling updates
*/
export declare enum SyncDirection {
/**
* Changes coming from Loro to application state
*/
FROM_LORO = "FROM_LORO",
/**
* Changes going from application state to Loro
*/
TO_LORO = "TO_LORO",
/**
* Initial sync or manual sync operations
*/
BIDIRECTIONAL = "BIDIRECTIONAL"
}
/**
* Configuration options for the Mirror
*/
export interface MirrorOptions<S extends SchemaType> {
/**
* The Loro document to sync with
*/
doc: LoroDoc;
/**
* The schema definition for the state
*/
schema?: S;
/**
* Initial state (optional)
*/
initialState?: Partial<import("../schema").InferInputType<S>>;
/**
* Whether to validate state updates against the schema
* @default true
*/
validateUpdates?: boolean;
/**
* Whether to throw errors on validation failures
* @default false
*/
throwOnValidationError?: boolean;
/**
* Debug mode - logs operations
* @default false
*/
debug?: boolean;
/**
* When enabled, performs an internal consistency check after setState
* to ensure in-memory state equals the normalized LoroDoc JSON.
* This throws on divergence but does not emit the verbose debug logs.
* @default false
*/
checkStateConsistency?: boolean;
/**
* Default values for new containers
*/
inferOptions?: InferContainerOptions;
}
export type InferContainerOptions = {
defaultLoroText?: boolean;
defaultMovableList?: boolean;
};
export type ChangeKinds = {
set: {
container: ContainerID | "";
key: string | number;
value: unknown;
kind: "set";
childContainerType?: ContainerType;
};
setContainer: {
container: ContainerID | "";
key: string | number;
value: unknown;
kind: "set-container";
childContainerType?: ContainerType;
};
insert: {
container: ContainerID | "";
key: string | number;
value: unknown;
kind: "insert";
};
insertContainer: {
container: ContainerID | "";
key: string | number;
value: unknown;
kind: "insert-container";
childContainerType?: ContainerType;
};
delete: {
container: ContainerID | "";
key: string | number;
value: unknown;
kind: "delete";
};
move: {
container: ContainerID;
key: number;
value: unknown;
kind: "move";
fromIndex: number;
toIndex: number;
childContainerType?: ContainerType;
};
treeCreate: {
container: ContainerID;
kind: "tree-create";
parent?: TreeID;
index: number;
value?: unknown;
onCreate(id: TreeID): void;
};
treeMove: {
container: ContainerID;
kind: "tree-move";
target: TreeID;
parent?: TreeID;
index: number;
};
treeDelete: {
container: ContainerID;
kind: "tree-delete";
target: TreeID;
};
};
export type Change = ChangeKinds[keyof ChangeKinds];
export type MapChangeKinds = ChangeKinds["insert"] | ChangeKinds["insertContainer"] | ChangeKinds["delete"];
export type ListChangeKinds = ChangeKinds["insert"] | ChangeKinds["insertContainer"] | ChangeKinds["delete"];
export type MovableListChangeKinds = ChangeKinds["insert"] | ChangeKinds["insertContainer"] | ChangeKinds["delete"] | ChangeKinds["move"] | ChangeKinds["set"] | ChangeKinds["setContainer"];
export type TreeChangeKinds = ChangeKinds["treeCreate"] | ChangeKinds["treeMove"] | ChangeKinds["treeDelete"];
export type TextChangeKinds = ChangeKinds["insert"] | ChangeKinds["delete"];
/**
* Options for setState and sync operations
*/
export interface SetStateOptions {
/**
* Tags to attach to this state update
* Tags can be used for tracking the source of changes or grouping related changes
*/
tags?: string[] | string;
}
/**
* Additional metadata for state updates
*/
export interface UpdateMetadata {
/**
* Direction of the sync operation
*/
direction: SyncDirection;
/**
* Tags associated with this update, if any
*/
tags?: string[];
}
/**
* Callback type for subscribers
*/
export type SubscriberCallback<T> = (state: T, metadata: UpdateMetadata) => void;
/**
* Mirror class that provides bidirectional sync between application state and Loro
*/
export declare class Mirror<S extends SchemaType> {
private doc;
private schema?;
private state;
private subscribers;
private syncing;
private options;
private containerRegistry;
private subscriptions;
private rootPathById;
/**
* Creates a new Mirror instance
*/
constructor(options: MirrorOptions<S>);
/**
* Ensure root containers exist for keys hinted by initialState.
* Creating root containers is a no-op in Loro (no operations are recorded),
* but it makes them visible in doc JSON, staying consistent with Mirror state.
*/
private ensureRootContainersFromInitialState;
/**
* Initialize containers based on schema
*/
private initializeContainers;
/**
* Register a container with the Mirror
*/
private registerContainer;
/**
* Register nested containers within a container
*/
private registerNestedContainers;
/**
* Handle events from the LoroDoc
*/
private handleLoroEvent;
/**
* Processes container additions/removals from the LoroDoc
* and ensures the containers are reflected in the container registry.
*
* TODO: need to handle removing containers from the registry on import
* right now the Diff Delta only returns the number of items removed
* not the container IDs , of those that were removed.
*/
private registerContainersFromLoroEvent;
/**
* Update Loro based on state changes
*/
private updateLoro;
/**
* Apply a set of changes to the Loro document
*/
private applyChangesToLoro;
/**
* Update root-level fields
*/
private applyRootChanges;
/**
* Apply multiple changes to a container
*/
private applyContainerChanges;
/**
* Update a top-level container directly with a new value
*/
private updateTopLevelContainer;
/**
* Update a Text container
*/
private updateTextContainer;
/**
* Update a List container
*/
private updateListContainer;
/**
* Update a list using ID selector for efficient updates
*/
private updateListWithIdSelector;
/**
* Update a list by index (for lists without an ID selector)
*/
private updateListByIndex;
/**
* Helper to insert an item into a list, handling containers appropriately
*/
private insertItemIntoList;
/**
* Subscribe to state changes
*/
subscribe(callback: SubscriberCallback<InferType<S>>): () => void;
/**
* Notify all subscribers of state change
* @param direction The direction of the sync operation
* @param tags Optional tags associated with this update
*/
private notifySubscribers;
/**
* Clean up resources
*/
dispose(): void;
/**
* Attaches a detached container to a map
*
* If the schema is provided, the container will be registered with the schema
*/
private insertContainerIntoMap;
/**
* Once a container has been created, and attached to its parent
*
* We initialize the inner values using the schema that we previously registered.
*/
private initializeContainer;
/**
* Create a new container based on a given schema.
*
* If the schema is undefined, we infer the container type from the value.
*/
private createContainerFromSchema;
/**
* Attaches a detached container to a list
*
* If the schema is provided, the container will be registered with the schema
*/
private insertContainerIntoList;
/**
* Update a Tree container using existing tree diff to generate precise create/move/delete
* and nested node.data changes, then apply via container change appliers.
*/
private updateTreeContainer;
/**
* Update a Map container
*/
private updateMapContainer;
/**
* Helper to update a single entry in a map
*/
private updateMapEntry;
/**
* Get current state
*/
getState(): InferType<S>;
/**
* Update state and propagate changes to Loro.
*
* - If `updater` is an object, it will shallow-merge into the current state.
* - If `updater` is a function, it may EITHER:
* - mutate a draft (Immer-style), OR
* - return a brand new immutable state object.
*
* This supports both immutable and mutative update styles without surprises.
*/
setState(updater: (state: Readonly<InferInputType<S>>) => InferInputType<S>, options?: SetStateOptions): void;
setState(updater: (state: InferType<S>) => void, options?: SetStateOptions): void;
setState(updater: Partial<InferInputType<S>>, options?: SetStateOptions): void;
checkStateConsistency(): void;
private containerToStateJson;
private buildRootStateSnapshot;
/**
* Register a container schema
*/
private registerContainerWithRegistry;
private getContainerSchema;
private getSchemaForChildContainer;
private getSchemaForChild;
getContainerIds(): ContainerID[];
}
/**
* Export the json of the doc with LoroTree containers normalized
* @param doc
* @returns
*/
export declare function toNormalizedJson(doc: LoroDoc): import("loro-crdt").Value;
//# sourceMappingURL=mirror.d.ts.map