{"id":47095792,"url":"https://github.com/loro-dev/loro-mirror","last_synced_at":"2026-04-02T13:00:31.111Z","repository":{"id":316501907,"uuid":"939285600","full_name":"loro-dev/loro-mirror","owner":"loro-dev","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-24T08:58:33.000Z","size":808,"stargazers_count":21,"open_issues_count":10,"forks_count":6,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-03-24T09:46:15.057Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loro-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-02-26T09:50:56.000Z","updated_at":"2026-03-24T08:47:10.000Z","dependencies_parsed_at":"2025-12-31T17:07:46.337Z","dependency_job_id":null,"html_url":"https://github.com/loro-dev/loro-mirror","commit_stats":null,"previous_names":["loro-dev/loro-mirror"],"tags_count":48,"template":false,"template_full_name":null,"purl":"pkg:github/loro-dev/loro-mirror","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Floro-mirror","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Floro-mirror/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Floro-mirror/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Floro-mirror/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loro-dev","download_url":"https://codeload.github.com/loro-dev/loro-mirror/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loro-dev%2Floro-mirror/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31306692,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-02T12:59:32.332Z","status":"ssl_error","status_checked_at":"2026-04-02T12:54:48.875Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-03-12T14:17:52.471Z","updated_at":"2026-04-02T13:00:31.095Z","avatar_url":"https://github.com/loro-dev.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Loro Mirror\n\nA TypeScript state management library that syncs application state with [loro-crdt](https://github.com/loro-dev/loro).\n\n## Features\n\n- 🔄 **Bidirectional Sync**: Seamlessly sync between application state and Loro CRDT\n- 📊 **Schema Validation**: Type-safe schema system for validating state\n- 🧩 **Modular Design**: Core package for state management, React package for React integration\n- 🔍 **Selective Updates**: Subscribe to specific parts of your state\n- 🛠️ **Developer Friendly**: Familiar API inspired by popular state management libraries\n- 📱 **React Integration**: Hooks and context providers for React applications\n- 🔀 **Transforms**: Work with rich domain types (Date, BigInt, custom objects) while Loro stores primitives\n\n## Packages\n\n- [`loro-mirror`](./packages/core): Core state management functionality\n- [`loro-mirror-react`](./packages/react): React integration with hooks and context\n- [`loro-mirror-jotai`](./packages/jotai): Jotai integration\n\n## Installation\n\n```bash\nnpm install loro-mirror loro-crdt\n# or\nyarn add loro-mirror loro-crdt\n# or\npnpm add loro-mirror loro-crdt\n```\n\n`loro-mirror-react` and `loro-mirror-jotai` are optional.\n\n## Quick Start\n\n### Usage\n\n```typescript\nimport { LoroDoc } from \"loro-crdt\";\nimport { Mirror, schema } from \"loro-mirror\";\n\n// Define your schema\nconst todoSchema = schema({\n    todos: schema.LoroList(\n        schema.LoroMap({\n            text: schema.String(),\n            completed: schema.Boolean({ defaultValue: false }),\n        }),\n        // Use `$cid` (reuses Loro container id; explained later)\n        (t) =\u003e t.$cid,\n    ),\n});\n\n// Create a Loro document\nconst doc = new LoroDoc();\n// Create a mirror\nconst mirror = new Mirror({\n    doc,\n    schema: todoSchema,\n    initialState: { todos: [] },\n});\n\n// Update the state (immutable update)\nmirror.setState((s) =\u003e ({\n    ...s,\n    todos: [\n        ...s.todos,\n        {\n            text: \"Learn Loro Mirror\",\n            completed: false,\n        },\n    ],\n}));\n\n// Or: draft-style updates (mutate a draft)\nmirror.setState((state) =\u003e {\n    state.todos.push({\n        text: \"Learn Loro Mirror\",\n        completed: false,\n    });\n    // `$cid` is injected automatically for LoroMap items\n    // and reuses the underlying Loro container id (explained later)\n});\n\n// Subscribe to state changes\nmirror.subscribe((state) =\u003e {\n    console.log(\"State updated:\", state);\n});\n```\n\n### Schema Definition\n\nLoro Mirror provides a declarative schema system that enables:\n\n- **Type Inference**: Automatically infer TypeScript types for your application state from the schema\n- **Runtime Validation**: Validate data structure and types during `setState` operations or synchronization\n- **Default Value Generation**: Generate sensible default values based on the schema definition\n\n#### Core Concepts\n\n- **Root Schema**: The root object defined via `schema({...})`, containing only Loro container types (Map/List/Text/MovableList).\n- **Field Schema**: A combination of primitive types (string, number, boolean), ignore fields, and Loro containers.\n- **Schema Options (`SchemaOptions`)**:\n    - **`required?: boolean`**: Whether the field is required (default: `true`).\n    - **`defaultValue?: unknown`**: Default value for the field.\n    - **`description?: string`**: Description of the field.\n    - **`validate?: (value) =\u003e boolean | string`**: Custom validation function. Return `true` for valid values, or a string as error message for invalid ones.\n\n#### Schema Definition API\n\n- **Primitive Types**:\n    - `schema.String\u003cT extends string = string\u003e(options?)` - String type with optional generic constraint\n    - `schema.Number(options?)` - Number type\n    - `schema.Boolean(options?)` - Boolean type\n    - `schema.Any(options?)` - Runtime-inferred value/container type for JSON-like dynamic fields\n    - `schema.Ignore(options?)` - Field that won't sync with Loro, useful for local computed fields\n\n- **Transforms** (on primitive types):\n\n    Primitive schemas support `.transform()` to convert between CRDT primitives and rich domain types:\n\n    ```typescript\n    const dateTransform = {\n        decode: (s: string) =\u003e new Date(s),\n        encode: (d: Date) =\u003e d.toISOString(),\n    };\n\n    const taskSchema = schema({\n        task: schema.LoroMap({\n            title: schema.String(),\n            dueDate: schema.String().transform(dateTransform), // Type infers as Date\n        }),\n    });\n\n    // Now you can use Date objects directly\n    mirror.setState({ task: { title: \"Review PR\", dueDate: new Date() } });\n    mirror.getState().task.dueDate.getTime(); // Already a Date\n    ```\n\n    The transform definition has this shape:\n\n    ```typescript\n    interface TransformDefinition\u003cCRDTType, DomainType\u003e {\n        decode: (value: CRDTType \u0026 {}) =\u003e DomainType \u0026 {};\n        encode: (value: DomainType \u0026 {}) =\u003e CRDTType \u0026 {};\n        validate?: (value: DomainType \u0026 {}) =\u003e boolean | string;\n        isEqual?:\n            | \"reference-equality\"\n            | \"encoded-value-equality\"\n            | \"deep-equality\"\n            | ((a: DomainType, b: DomainType) =\u003e boolean);\n    }\n    ```\n\n    **Note:** The encode and decode functions will never receive `null` or `undefined` values and must not return them. Nullish values are handled before transforms are applied.\n\n    **Important:** `DomainType` should be immutable or [supported by Immer](https://immerjs.github.io/immer/complex-objects/), otherwise changes to transformed values may not be detected and converted to CRDT operations. Never mutate `DomainType` instances outside of Loro Mirror's `setState` function.\n\n    For optional fields, just use the transform directly - the type automatically includes `| undefined`:\n\n`schema.Any` options:\n\n- `defaultLoroText?: boolean` (defaults to `false` for `Any` when omitted)\n- `defaultMovableList?: boolean` (inherits from global inference options)\n\n- **Container Types**:\n    - `schema.LoroMap(definition, options?)` - Object container that can nest arbitrary field schemas\n        - Supports dynamic key-value definition with `catchall`: `schema.LoroMap({...}).catchall(valueSchema)`\n        - Always injects a read-only `$cid` field in mirrored state equal to the underlying Loro container id. Applies uniformly to root maps, nested maps, list items, and tree node `data` maps.\n    - `schema.LoroMapRecord(valueSchema, options?)` - Equivalent to `LoroMap({}).catchall(valueSchema)` for homogeneous maps\n        - Entries’ mirrored state always include `$cid`.\n    - `schema.LoroList(itemSchema, idSelector?, options?)` - Ordered list container\n        - Providing an `idSelector` (e.g., `(item) =\u003e item.id`) enables minimal add/remove/update/move diffs\n    - `schema.LoroMovableList(itemSchema, idSelector, options?)` - List with native move operations, requires an `idSelector`\n    - `schema.LoroText(options?)` - Collaborative text editing container\n\n#### Type Inference\n\nAutomatically derive strongly-typed state from your schema:\n\n```ts\nimport { schema } from \"loro-mirror\";\n\ntype UserId = string \u0026 { __brand: \"userId\" };\nconst appSchema = schema({\n    user: schema.LoroMap({\n        name: schema.String(),\n        age: schema.Number({ required: false }),\n    }),\n    tags: schema.LoroList(schema.String()),\n});\n\n// Inferred state type:\n// type AppState = {\n//   user: { $cid: string; name: string; age: number | undefined };\n//   tags: string[];\n// }\ntype AppState = InferType\u003ctypeof appSchema\u003e;\n```\n\n\u003e **Note**: If you need optional custom string types with generics (e.g., `{ status?: Status }`), explicitly define them as `schema.String\u003cStatus\u003e({ required: false })`.\n\nFor `LoroMap` with dynamic key-value pairs:\n\n```ts\nconst mapWithCatchall = schema\n    .LoroMap({ fixed: schema.Number() })\n    .catchall(schema.String());\n// Type: { fixed: number } \u0026 { [k: string]: string }\n\nconst record = schema.LoroMapRecord(schema.Boolean());\n// Type: { [k: string]: boolean }\n```\n\nWhen a field has `required: false`, the corresponding type becomes optional (union with `undefined`).\n\nLoroMap and LoroMapRecord inferred types always include a `$cid: string` field. For example:\n\n```ts\nconst user = schema.LoroMap({ name: schema.String() });\n// InferType\u003ctypeof user\u003e =\u003e { name: string; $cid: string }\n\n// In lists, `$cid` is handy as a stable idSelector:\nconst users = schema.LoroList(user, (x) =\u003e x.$cid);\n```\n\n#### Default Values \u0026 Creation\n\n- Explicitly specified `defaultValue` takes the highest precedence.\n- Built-in defaults for fields without `defaultValue` and `required: true`:\n    - **String / LoroText** → `\"\"`\n    - **Number** → `0`\n    - **Boolean** → `false`\n    - **LoroList** → `[]`\n    - **LoroMap / Root** → Recursively aggregated defaults from child fields\n\n#### Runtime Validation\n\n`Mirror` validates against the schema when `validateUpdates` is enabled (default: `true`). You can also validate directly:\n\n```ts\nimport { validateSchema } from \"loro-mirror\";\n\nconst result = validateSchema(appSchema, {\n    user: { id: \"u1\", name: \"Alice\", age: 18 },\n    tags: [\"a\", \"b\"],\n});\n// result = { valid: boolean; errors?: string[] }\n```\n\n#### Lists \u0026 Movement\n\n- `LoroList(item, idSelector?)`: Providing an `idSelector` enables more stable add/remove/update/move diffs; otherwise uses index-based comparison.\n- `LoroMovableList(item, idSelector)`: Native move operations (preserves element identity), ideal for drag-and-drop scenarios.\n\n```ts\nconst todoSchema = schema({\n    todos: schema.LoroMovableList(\n        schema.LoroMap({\n            text: schema.String(),\n            completed: schema.Boolean({ defaultValue: false }),\n        }),\n        (t) =\u003e t.$cid, // stable id from Loro container id ($cid)\n    ),\n});\n```\n\n#### Ignored Fields\n\n- Fields defined with `schema.Ignore()` won't sync with Loro, commonly used for derived/cached fields. Runtime validation always passes for these fields.\n\n#### Reserved Field: `$cid`\n\n- `$cid` is a reserved, read-only field injected into mirrored state for all `LoroMap` schemas. It equals the underlying Loro container id, is never written back to Loro, and is ignored by diffs and updates. Use it as a stable identifier where helpful (e.g., list `idSelector`).\n\n### React Usage\n\n```tsx\nimport React, { useMemo, useState } from \"react\";\nimport { LoroDoc } from \"loro-crdt\";\nimport { schema } from \"loro-mirror\";\nimport { createLoroContext } from \"loro-mirror-react\";\n\n// Define your schema (use `$cid` from maps)\nconst todoSchema = schema({\n    todos: schema.LoroList(\n        schema.LoroMap({\n            text: schema.String({ required: true }),\n            completed: schema.Boolean({ defaultValue: false }),\n        }),\n        (t) =\u003e t.$cid, // uses Loro container id; see \"$cid\" section below\n    ),\n});\n\n// Create a context\nconst { LoroProvider, useLoroState, useLoroSelector, useLoroAction } =\n    createLoroContext(todoSchema);\n\n// Root component\nfunction App() {\n    const doc = useMemo(() =\u003e new LoroDoc(), []);\n\n    return (\n        \u003cLoroProvider doc={doc} initialState={{ todos: [] }}\u003e\n            \u003cTodoList /\u003e\n            \u003cAddTodoForm /\u003e\n        \u003c/LoroProvider\u003e\n    );\n}\n\n// Todo list component\nfunction TodoList() {\n    const todos = useLoroSelector((state) =\u003e state.todos);\n    const toggleTodo = useLoroAction((s, cid: string) =\u003e {\n        const i = s.todos.findIndex((t) =\u003e t.$cid === cid);\n        if (i !== -1) s.todos[i].completed = !s.todos[i].completed;\n    }, []);\n\n    return (\n        \u003cul\u003e\n            {todos.map((todo) =\u003e (\n                \u003cli key={todo.$cid}\u003e\n                    \u003cinput\n                        type=\"checkbox\"\n                        checked={todo.completed}\n                        onChange={() =\u003e toggleTodo(todo.$cid)} // `$cid` is the Loro container id\n                    /\u003e\n                    \u003cspan\u003e{todo.text}\u003c/span\u003e\n                \u003c/li\u003e\n            ))}\n        \u003c/ul\u003e\n    );\n}\n\n// Add todo form component\nfunction AddTodoForm() {\n    const [text, setText] = useState(\"\");\n\n    const addTodo = useLoroAction(\n        (state) =\u003e {\n            state.todos.push({\n                text: text.trim(),\n                completed: false,\n            });\n        },\n        [text],\n    );\n\n    const handleSubmit = (e) =\u003e {\n        e.preventDefault();\n        if (text.trim()) {\n            addTodo();\n            setText(\"\");\n        }\n    };\n\n    return (\n        \u003cform onSubmit={handleSubmit}\u003e\n            \u003cinput\n                value={text}\n                onChange={(e) =\u003e setText(e.target.value)}\n                placeholder=\"What needs to be done?\"\n            /\u003e\n            \u003cbutton type=\"submit\"\u003eAdd Todo\u003c/button\u003e\n        \u003c/form\u003e\n    );\n}\n```\n\n## Documentation\n\nFor detailed documentation, see the README files in each package:\n\n- [Loro Mirror Documentation](./packages/core/README.md)\n- [React Documentation](./packages/react/README.md)\n- [Jotai Documentation](./packages/jotai/README.md)\n\n## Schema Overview\n\nLoro Mirror uses a typed schema to map your app state to Loro containers. Common schema constructors:\n\n- `schema.String(options?)`: string (supports `.transform()` for domain types)\n- `schema.Number(options?)`: number (supports `.transform()` for domain types)\n- `schema.Boolean(options?)`: boolean (supports `.transform()` for domain types)\n- `schema.Any(options?)`: JSON-like unknown (runtime-inferred)\n- `schema.Ignore(options?)`: exclude from sync (app-only)\n- `schema.LoroText(options?)`: rich text (`LoroText`)\n- `schema.LoroMap(definition, options?)`: object (`LoroMap`)\n- `schema.LoroList(itemSchema, idSelector?, options?)`: list (`LoroList`)\n- `schema.LoroMovableList(itemSchema, idSelector, options?)`: movable list that emits move ops (requires `idSelector`)\n- `schema.LoroTree(nodeSchema, options?)`: hierarchical tree (`LoroTree`) with per-node `data` map\n\nTree nodes have the shape `{ id?: string; data: T; children: Node\u003cT\u003e[] }`. Define a tree by passing a node `LoroMap` schema:\n\n```ts\nimport { schema } from \"loro-mirror\";\n\nconst node = schema.LoroMap({ title: schema.String({ required: true }) });\nconst mySchema = schema({ outline: schema.LoroTree(node) });\n```\n\n## API Reference (Core Mirror)\n\n### Mirror\n\n- `new Mirror(options)`: Creates a bidirectional sync between app state and a `LoroDoc`.\n    - **`doc`**: `LoroDoc` – required Loro document instance.\n    - **`schema`**: root schema – optional but recommended for strong typing and validation.\n    - **`initialState`**: partial state – merged with schema defaults and current doc JSON.\n    - **`validateUpdates`**: boolean (default `true`) – validate new state against schema.\n    - **`debug`**: boolean (default `false`) – log diffs and applied changes.\n    - **`checkStateConsistency`**: boolean (default `false`) – after non-ephemeral `setState` calls, assert the doc-backed base state equals the normalized `LoroDoc` snapshot.\n    - **`inferOptions`**: `{ defaultLoroText?: boolean; defaultMovableList?: boolean }` – influence container-type inference when inserting containers from plain values.\n\n- `getState(): State`: Returns the current in-memory state view.\n- `setState(updater, options?)`: Update state and sync to Loro. Runs synchronously. When `ephemeralStore` is configured, eligible changes are automatically routed through EphemeralStore. See [Ephemeral Patches](#ephemeral-patches).\n    - **`updater`**: either a partial object to shallow-merge or a function that may mutate a draft (Immer-style) or return a new state object.\n    - **`options`**: `{ tags?: string | string[]; finalizeTimeout?: number }` – tags are delivered to subscribers in metadata; `finalizeTimeout` (default 50 000 ms) controls the debounce delay before ephemeral values auto-commit.\n- `finalizeEphemeralPatches()`: Immediately commit pending ephemeral patches to LoroDoc (e.g. on `mouseup`).\n- `subscribe(callback): () =\u003e void`: Subscribe to state changes. `callback` receives `(state, metadata)` where `metadata` includes:\n    - **`source`**: `UpdateSource` – `LORO` when changes came from the doc, `MIRROR` when a local Mirror API changed state, `EPHEMERAL` when state changed due to an EphemeralStore update.\n    - **`tags`**: `string[] | undefined` – tags provided via `setState`.\n- `dispose()`: Unsubscribe internal listeners and clear subscribers.\n- `checkStateConsistency()`: Manually trigger the consistency assertion described above.\n- `getContainerIds()`: Returns the set of registered Loro container IDs (advanced debugging aid).\n\n#### Notes\n\n- **Lists and IDs**: If your list schema provides an `idSelector`, list updates use minimal add/remove/update/move operations; otherwise index-based diffs are applied.\n- **Container inference**: When schema is missing/ambiguous for a field, the mirror infers container types from values. `inferOptions.defaultLoroText` makes strings become `LoroText`; `inferOptions.defaultMovableList` makes arrays become `LoroMovableList`.\n- **Consistency checks**: Enabling `checkStateConsistency` is useful while developing or writing tests; it auto-checks only non-ephemeral `setState` calls. Calling `checkStateConsistency()` manually compares the doc-backed base state against the normalized document snapshot, so active ephemeral overlay values are intentionally ignored.\n\n### Types\n\n- `UpdateSource`:\n    - `LORO` – applied due to incoming `LoroDoc` changes\n    - `MIRROR` – applied due to a local Mirror API call such as `setState` or `finalizeEphemeralPatches`\n    - `EPHEMERAL` – state recomposed due to an EphemeralStore change\n- `UpdateMetadata`: `{ source: UpdateSource; tags?: string[] }`\n- `SetStateOptions`: `{ tags?: string | string[]; origin?: string; timestamp?: number; message?: string; finalizeTimeout?: number }`\n\n### Example\n\n```ts\nimport { LoroDoc } from \"loro-crdt\";\nimport { Mirror, schema, UpdateSource } from \"loro-mirror\";\n\nconst todoSchema = schema({\n    todos: schema.LoroList(\n        schema.LoroMap({\n            text: schema.String({ required: true }),\n            completed: schema.Boolean({ defaultValue: false }),\n        }),\n        (t) =\u003e t.$cid, // stable id from Loro container id ($cid)\n    ),\n});\n\nconst doc = new LoroDoc();\nconst mirror = new Mirror({ doc, schema: todoSchema, validateUpdates: true });\n\n// Subscribe with metadata\nconst unsubscribe = mirror.subscribe((state, { source, tags }) =\u003e {\n    if (source === UpdateSource.LORO) {\n        console.log(\"Remote update\", tags);\n    } else {\n        console.log(\"Local update\", tags);\n    }\n});\n\n// Update with draft mutation + tags\nmirror.setState(\n    (s) =\u003e {\n        s.todos.push({\n            text: \"Write docs\",\n            completed: false,\n        });\n    },\n    { tags: [\"ui:add\"] },\n);\n```\n\n### Ephemeral Patches\n\nFor high-frequency temporary changes (dragging, resizing, scrubbing), pass an `ephemeralStore` to `MirrorOptions`. This changes how `setState` works: eligible changes (primitive values on existing Map keys) are automatically routed to the EphemeralStore for real-time sync without creating LoroDoc editing history. No separate API is needed — the same `setState` handles both persistent and ephemeral updates.\n\n```ts\nimport { LoroDoc, EphemeralStore } from \"loro-crdt\";\n\nconst doc = new LoroDoc();\nconst eph = new EphemeralStore();\nconst mirror = new Mirror({ doc, schema: mySchema, ephemeralStore: eph });\n\n// Network sync for ephemeral state (your responsibility)\neph.subscribeLocalUpdates((bytes) =\u003e channel.send(bytes));\nchannel.on(\"ephemeral\", (bytes) =\u003e eph.apply(bytes));\n\n// During drag — x/y are primitives on existing keys → EphemeralStore\nmirror.setState(\n    (s) =\u003e { s.items[0].x = e.clientX; s.items[0].y = e.clientY; },\n    { finalizeTimeout: 1_000 }, // auto-commit after 1s of inactivity\n);\n\n// On mouseup — commit ephemeral values to LoroDoc immediately\nmirror.finalizeEphemeralPatches();\n```\n\n**Routing rules** (with `ephemeralStore` configured):\n\n| Change type | Destination |\n|---|---|\n| Primitive value on an existing key of an existing Map | `EphemeralStore` |\n| New Map / new key / container value / List·Text·Tree ops | `LoroDoc` directly |\n\nWithout `ephemeralStore`, all changes go to LoroDoc as usual. See the [core package README](./packages/core/README.md#ephemeral-patches) for full details.\n\n### How `$cid` Works\n\n- Every Loro container has a stable container ID provided by Loro (e.g., a map’s `container.id`).\n- For any `schema.LoroMap(...)`, Mirror injects a read-only `$cid` field into the mirrored state that equals the underlying Loro container ID.\n- `$cid` lives only in the app state and is never written back to the document. Mirror uses it for efficient diffs; you can use it as a stable list selector: `schema.LoroList(item, (x) =\u003e x.$cid)`.\n\n// Cleanup\nunsubscribe();\nmirror.dispose();\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floro-dev%2Floro-mirror","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floro-dev%2Floro-mirror","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floro-dev%2Floro-mirror/lists"}