UNPKG

loro-mirror

Version:

Type-safe state management synchronized with Loro CRDT via a declarative schema and bidirectional mirroring.

338 lines 9.98 kB
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