@llui/lexical-loro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/agent-write.d.ts +123 -0
  4. package/dist/agent-write.d.ts.map +1 -0
  5. package/dist/agent-write.js +499 -0
  6. package/dist/agent-write.js.map +1 -0
  7. package/dist/binding.d.ts +122 -0
  8. package/dist/binding.d.ts.map +1 -0
  9. package/dist/binding.js +114 -0
  10. package/dist/binding.js.map +1 -0
  11. package/dist/index.d.ts +69 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +69 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mapping.d.ts +115 -0
  16. package/dist/mapping.d.ts.map +1 -0
  17. package/dist/mapping.js +181 -0
  18. package/dist/mapping.js.map +1 -0
  19. package/dist/order.d.ts +124 -0
  20. package/dist/order.d.ts.map +1 -0
  21. package/dist/order.js +187 -0
  22. package/dist/order.js.map +1 -0
  23. package/dist/schema.d.ts +343 -0
  24. package/dist/schema.d.ts.map +1 -0
  25. package/dist/schema.js +363 -0
  26. package/dist/schema.js.map +1 -0
  27. package/dist/seed.d.ts +72 -0
  28. package/dist/seed.d.ts.map +1 -0
  29. package/dist/seed.js +72 -0
  30. package/dist/seed.js.map +1 -0
  31. package/dist/text.d.ts +167 -0
  32. package/dist/text.d.ts.map +1 -0
  33. package/dist/text.js +289 -0
  34. package/dist/text.js.map +1 -0
  35. package/dist/to-lexical.d.ts +119 -0
  36. package/dist/to-lexical.d.ts.map +1 -0
  37. package/dist/to-lexical.js +636 -0
  38. package/dist/to-lexical.js.map +1 -0
  39. package/dist/to-loro.d.ts +154 -0
  40. package/dist/to-loro.d.ts.map +1 -0
  41. package/dist/to-loro.js +718 -0
  42. package/dist/to-loro.js.map +1 -0
  43. package/dist/undo.d.ts +94 -0
  44. package/dist/undo.d.ts.map +1 -0
  45. package/dist/undo.js +200 -0
  46. package/dist/undo.js.map +1 -0
  47. package/package.json +64 -0
  48. package/src/agent-write.ts +613 -0
  49. package/src/binding.ts +185 -0
  50. package/src/index.ts +176 -0
  51. package/src/mapping.ts +206 -0
  52. package/src/order.ts +205 -0
  53. package/src/schema.ts +509 -0
  54. package/src/seed.ts +112 -0
  55. package/src/text.ts +357 -0
  56. package/src/to-lexical.ts +792 -0
  57. package/src/to-loro.ts +914 -0
  58. package/src/undo.ts +269 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `loroCollab(config)` — the binding that drives both directions.
3
+ *
4
+ * It composes `to-loro.ts` (Lexical → Loro), `to-lexical.ts` (Loro → Lexical)
5
+ * and `seed.ts` (boot) into one `register(editor)` step you hand to
6
+ * `lexicalForeign({ history: false, seedMode: 'deferred', register })`. The
7
+ * returned handle satisfies `@llui/markdown-editor`'s `CollabBinding`
8
+ * structurally, so that package needs no Loro dependency of its own.
9
+ *
10
+ * ── Transport ──────────────────────────────────────────────────────────────
11
+ *
12
+ * There is none, deliberately. `LoroDoc` already exposes the whole wire surface
13
+ * (`subscribeLocalUpdates` / `import` / `export`), so a transport is a dozen
14
+ * lines the consumer writes against their own websocket, WebRTC channel or
15
+ * `loro-websocket` provider — and baking one in would make this package own a
16
+ * connection lifecycle it has no way to test honestly. Pass the same `LoroDoc`
17
+ * to your provider and to `loroCollab`.
18
+ *
19
+ * ── The three echo layers, and where each one lives ────────────────────────
20
+ *
21
+ * All three are required; any one missing produces an infinite loop or, worse,
22
+ * a silent data-loss bug:
23
+ *
24
+ * a. Loro → us. A LOCAL Loro batch is our own outbound write completing its
25
+ * commit. `applyLoroToLexical` drops it (`to-lexical.ts`). Without this the
26
+ * binding re-enters `editor.update` from inside a Lexical update listener.
27
+ * b. us → Lexical → us. Our inbound writeback is tagged `COLLABORATION_TAG`,
28
+ * which `syncLexicalToLoro` skips (`to-loro.ts`). Without this every remote
29
+ * keystroke is echoed straight back to its sender.
30
+ * c. THE SEAM. This binding NEVER emits `PROGRAMMATIC_TAG`.
31
+ * `packages/lexical/src/foreign.ts` treats that tag as "the host pushed new
32
+ * content — cancel pending outbound work and rebase", so a remote writeback
33
+ * carrying it would cancel the local user's in-flight debounced `onChange`
34
+ * and the host's persistence would go dark whenever a peer types. There is
35
+ * no code here to enforce this; there is simply no line that emits it, and
36
+ * `test/to-lexical.test.ts` pins that.
37
+ *
38
+ * ── Undo ───────────────────────────────────────────────────────────────────
39
+ *
40
+ * This binding OWNS undo, via `externalUndo` (`undo.ts`, a Loro `UndoManager`
41
+ * scoped to this peer). Hosts must therefore turn their built-in
42
+ * `@lexical/history` stack off — `lexicalForeign` does that for you the moment
43
+ * `externalUndo` is present, so the two can never both be live.
44
+ *
45
+ * Loro's manager is operation-based and PeerID-scoped: undo reverts exactly the
46
+ * local user's own last change and never a peer's, which snapshot-based history
47
+ * cannot do at any tag setting. See `undo.ts` for why, and for the echo seam the
48
+ * undo batches travel through.
49
+ */
50
+ import { LoroDoc } from 'loro-crdt';
51
+ import { AGENT_WRITE_ORIGIN } from './agent-write.js';
52
+ import { ContainerNodeMap } from './mapping.js';
53
+ import { bootstrapDocument } from './seed.js';
54
+ import { initDoc } from './schema.js';
55
+ import { LORO_TEXT_FORMATS } from './text.js';
56
+ import { applyLoroToLexical } from './to-lexical.js';
57
+ import { OUTBOUND_ORIGIN, syncLexicalToLoro } from './to-loro.js';
58
+ import { registerLoroUndo, UNDO_ORIGINS } from './undo.js';
59
+ /**
60
+ * Build a collaborative-editing binding over a Loro document.
61
+ *
62
+ * The document is configured for this package's schema (`initDoc`) immediately,
63
+ * not at `register` time, so a transport may be attached to `collab.doc` before
64
+ * any editor exists.
65
+ */
66
+ export function loroCollab(config = {}) {
67
+ const doc = config.doc ?? new LoroDoc();
68
+ // Must run on EVERY peer: `configTextStyle` is local configuration, not
69
+ // replicated state, and a peer that skips it resolves marks under different
70
+ // expand rules and diverges. Idempotent, so a shared doc may be passed to
71
+ // more than one binding.
72
+ const root = initDoc(doc, LORO_TEXT_FORMATS);
73
+ const mapping = new ContainerNodeMap();
74
+ const origin = config.origin ?? OUTBOUND_ORIGIN;
75
+ const bootstrap = (editor) => {
76
+ const outcome = bootstrapDocument({
77
+ doc,
78
+ root,
79
+ mapping,
80
+ editor,
81
+ seed: config.seed,
82
+ shouldBootstrap: config.shouldBootstrap,
83
+ });
84
+ config.onBootstrap?.(outcome);
85
+ return outcome;
86
+ };
87
+ const register = (editor) => {
88
+ const outboundTarget = { doc, root, mapping, origin };
89
+ // `localOrigins` are the local batches the inbound path must still APPLY
90
+ // rather than drop as an echo (see `to-lexical.ts`). Both are passed
91
+ // unconditionally: only a Loro `UndoManager` produces `UNDO_ORIGINS` and only
92
+ // `reconcileTargetIntoLoro` produces `AGENT_WRITE_ORIGIN`, so listing them is
93
+ // inert until one occurs — and the inbound path stays independent of whether
94
+ // `externalUndo` was registered or an agent write ever happens.
95
+ const localOrigins = [...UNDO_ORIGINS, AGENT_WRITE_ORIGIN];
96
+ const inboundTarget = { doc, root, mapping, editor, localOrigins };
97
+ // Inbound BEFORE outbound: the subscription must be live before the
98
+ // bootstrap writes anything, or a document arriving mid-boot is missed.
99
+ const unsubscribe = doc.subscribe((batch) => {
100
+ applyLoroToLexical(inboundTarget, batch);
101
+ });
102
+ const unregister = editor.registerUpdateListener((payload) => {
103
+ syncLexicalToLoro(outboundTarget, payload);
104
+ });
105
+ bootstrap(editor);
106
+ return () => {
107
+ unregister();
108
+ unsubscribe();
109
+ };
110
+ };
111
+ const externalUndo = (editor) => registerLoroUndo({ doc, ...config.undo }, editor);
112
+ return { register, doc, root, mapping, bootstrap, externalUndo };
113
+ }
114
+ //# sourceMappingURL=binding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding.js","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `@llui/lexical-loro` — Loro CRDT binding for the LLui ↔ Lexical editor.
3
+ *
4
+ * Start at {@link loroCollab}: it composes everything below into a
5
+ * `register(editor)` plus an `externalUndo(editor)` you hand to
6
+ * `lexicalForeign({ seedMode: 'deferred', register, externalUndo })`, and it
7
+ * satisfies `@llui/markdown-editor`'s `CollabBinding` structurally.
8
+ *
9
+ * - `schema.ts` — the Loro container shape mirroring a Lexical EditorState
10
+ * - `order.ts` — fractional indexing: sibling order as a sortable property
11
+ * - `mapping.ts` — the ContainerID ↔ NodeKey registry (the core invariant)
12
+ * - `text.ts` — format-bitmask ↔ named-mark conversion, the run diff, and
13
+ * the cursor-biased text diff
14
+ * - `to-loro.ts` — Lexical → Loro: the update-listener-driven mirror
15
+ * - `to-lexical.ts` — Loro → Lexical: the persistent, in-place mirror
16
+ * - `seed.ts` — boot: seed an empty shared document, or adopt a populated one
17
+ * - `undo.ts` — peer-scoped, CRDT-aware undo/redo over Loro's `UndoManager`
18
+ * - `binding.ts` — `loroCollab`: both directions plus the echo guards
19
+ *
20
+ * ── Scope ──────────────────────────────────────────────────────────────────
21
+ *
22
+ * DOCUMENT SYNC plus LOCAL-ONLY UNDO. Undo is owned by this binding
23
+ * (`LoroCollab.externalUndo`), so a host MUST disable `@lexical/history` —
24
+ * `lexicalForeign` does that automatically once `externalUndo` is passed. Still
25
+ * out of scope: presence and remote cursors (additive, over Loro's
26
+ * `EphemeralStore`), and text-node `style` / `mode` / `detail` — the run model
27
+ * is `{text, format}`.
28
+ *
29
+ * ── Echo suppression ───────────────────────────────────────────────────────
30
+ *
31
+ * Three independent layers are ALL required; see `binding.ts` for where each
32
+ * one lives and what breaks without it. The one with no code to enforce it:
33
+ * this binding NEVER emits `PROGRAMMATIC_TAG`, because
34
+ * `packages/lexical/src/foreign.ts` reads that tag as "the host pushed content —
35
+ * cancel pending outbound work", which would make the host's persistence go dark
36
+ * whenever a peer types.
37
+ *
38
+ * `test/expand-semantics.test.ts` (51 tests) is the specification for the
39
+ * text-format half; read it before touching `text.ts`. `test/network.ts` is the
40
+ * multi-peer convergence harness, driven by `test/convergence.test.ts`.
41
+ */
42
+ export { ROOT_CONTAINER, KEY_TYPE, KEY_PROPS, KEY_CHILDREN, KEY_UUID, KEY_POS, KEY_KIND, KEY_TEXT, DECORATOR_TYPE, KEY_BRIDGE_TYPE, KEY_DATA, ROOT_TYPE, TEXT_MARK_EXPAND, initDoc, newUuid, createElementChild, createTextChild, setChildPosition, deleteChild, orderedChildren, childCount, elementType, elementProps, elementChildren, isElementContainer, isTextContainer, isDecoratorElement, containerId, containerIsLive, type PropValue, type PropsContainer, type ElementContainer, type ElementShape, type ChildrenContainer, type ChildContainer, type ChildCarrier, type CarrierShape, type TextCarrier, type TextCarrierShape, type ChildEntry, type ChildKind, type ExpandType, } from './schema.js';
43
+ export { DIGITS, between, allocate, allocateAt, jitterFor, comparePositions } from './order.js';
44
+ export { ContainerNodeMap, type MappingEntry, type LivenessProbe } from './mapping.js';
45
+ export { LORO_TEXT_FORMATS, FORMAT_BITS, KNOWN_FORMAT_MASK, formatBit, formatsFromBitmask, bitmaskFromFormats, bitmaskFromAttributes, runsText, normalizeRuns, runsFromDelta, runsFromText, diffRunFormats, applyMarkOps, diffTextWithCursor, diffText, applyTextDiff, type LoroTextFormat, type TextRun, type TextDeltaItem, type MarkOp, type TextDiff, } from './text.js';
46
+ export { OUTBOUND_ORIGIN, OUTBOUND_SKIP_TAGS, syncLexicalToLoro, seedLoroFromLexical, longestIncreasingSubsequence, type OutboundTarget, type OutboundUpdate, } from './to-loro.js';
47
+ export { INBOUND_TAGS, applyLoroToLexical, adoptLoroDocument, type InboundTarget, } from './to-lexical.js';
48
+ export { bootstrapDocument, isSharedDocumentEmpty, type BootstrapOutcome, type BootstrapTarget, } from './seed.js';
49
+ export { LORO_UNDO_ORIGIN, UNDO_ORIGINS, DEFAULT_MERGE_INTERVAL, DEFAULT_MAX_UNDO_STEPS, registerLoroUndo, type LoroUndoOptions, } from './undo.js';
50
+ export { loroCollab, type LoroCollab, type LoroCollabConfig } from './binding.js';
51
+ /**
52
+ * Agent-write: apply an LLM's full-document markdown REWRITE to the shared
53
+ * document by CONTENT, so unchanged blocks keep their `ContainerID`s (hence their
54
+ * `NodeKey`s and mounted decorator sub-apps, and any concurrent peer edit into
55
+ * them) instead of being recreated wholesale.
56
+ *
57
+ * `reconcileTargetIntoLoro` is a SIBLING to `syncLexicalToLoro`: it writes Loro
58
+ * directly under {@link AGENT_WRITE_ORIGIN} and does NOT consult the
59
+ * `ContainerNodeMap` (content matching is the point; the mapping self-heals on
60
+ * the inbound bounce into any live editor). The caller owns the markdown → target
61
+ * parse with its own `@lexical/markdown` transformer set — this package never
62
+ * depends on `@lexical/markdown` — and projects the parsed editor state with
63
+ * {@link targetFromEditorState} / {@link projectTarget}.
64
+ *
65
+ * Guarantees and the documented DUPLICATE-BLOCK caveat live on the functions and
66
+ * in the README.
67
+ */
68
+ export { AGENT_WRITE_ORIGIN, reconcileTargetIntoLoro, projectTarget, targetFromEditorState, type TargetElement, type TargetText, type TargetChild, } from './agent-write.js';
69
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EACL,cAAc,EACd,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,eAAe,EACf,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAE/F,OAAO,EAAE,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAEtF,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,aAAa,EACb,aAAa,EACb,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,EACb,KAAK,cAAc,EACnB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,MAAM,EACX,KAAK,QAAQ,GACd,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,EAC5B,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,gBAAgB,EAChB,KAAK,eAAe,GACrB,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEjF;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * `@llui/lexical-loro` — Loro CRDT binding for the LLui ↔ Lexical editor.
3
+ *
4
+ * Start at {@link loroCollab}: it composes everything below into a
5
+ * `register(editor)` plus an `externalUndo(editor)` you hand to
6
+ * `lexicalForeign({ seedMode: 'deferred', register, externalUndo })`, and it
7
+ * satisfies `@llui/markdown-editor`'s `CollabBinding` structurally.
8
+ *
9
+ * - `schema.ts` — the Loro container shape mirroring a Lexical EditorState
10
+ * - `order.ts` — fractional indexing: sibling order as a sortable property
11
+ * - `mapping.ts` — the ContainerID ↔ NodeKey registry (the core invariant)
12
+ * - `text.ts` — format-bitmask ↔ named-mark conversion, the run diff, and
13
+ * the cursor-biased text diff
14
+ * - `to-loro.ts` — Lexical → Loro: the update-listener-driven mirror
15
+ * - `to-lexical.ts` — Loro → Lexical: the persistent, in-place mirror
16
+ * - `seed.ts` — boot: seed an empty shared document, or adopt a populated one
17
+ * - `undo.ts` — peer-scoped, CRDT-aware undo/redo over Loro's `UndoManager`
18
+ * - `binding.ts` — `loroCollab`: both directions plus the echo guards
19
+ *
20
+ * ── Scope ──────────────────────────────────────────────────────────────────
21
+ *
22
+ * DOCUMENT SYNC plus LOCAL-ONLY UNDO. Undo is owned by this binding
23
+ * (`LoroCollab.externalUndo`), so a host MUST disable `@lexical/history` —
24
+ * `lexicalForeign` does that automatically once `externalUndo` is passed. Still
25
+ * out of scope: presence and remote cursors (additive, over Loro's
26
+ * `EphemeralStore`), and text-node `style` / `mode` / `detail` — the run model
27
+ * is `{text, format}`.
28
+ *
29
+ * ── Echo suppression ───────────────────────────────────────────────────────
30
+ *
31
+ * Three independent layers are ALL required; see `binding.ts` for where each
32
+ * one lives and what breaks without it. The one with no code to enforce it:
33
+ * this binding NEVER emits `PROGRAMMATIC_TAG`, because
34
+ * `packages/lexical/src/foreign.ts` reads that tag as "the host pushed content —
35
+ * cancel pending outbound work", which would make the host's persistence go dark
36
+ * whenever a peer types.
37
+ *
38
+ * `test/expand-semantics.test.ts` (51 tests) is the specification for the
39
+ * text-format half; read it before touching `text.ts`. `test/network.ts` is the
40
+ * multi-peer convergence harness, driven by `test/convergence.test.ts`.
41
+ */
42
+ export { ROOT_CONTAINER, KEY_TYPE, KEY_PROPS, KEY_CHILDREN, KEY_UUID, KEY_POS, KEY_KIND, KEY_TEXT, DECORATOR_TYPE, KEY_BRIDGE_TYPE, KEY_DATA, ROOT_TYPE, TEXT_MARK_EXPAND, initDoc, newUuid, createElementChild, createTextChild, setChildPosition, deleteChild, orderedChildren, childCount, elementType, elementProps, elementChildren, isElementContainer, isTextContainer, isDecoratorElement, containerId, containerIsLive, } from './schema.js';
43
+ export { DIGITS, between, allocate, allocateAt, jitterFor, comparePositions } from './order.js';
44
+ export { ContainerNodeMap } from './mapping.js';
45
+ export { LORO_TEXT_FORMATS, FORMAT_BITS, KNOWN_FORMAT_MASK, formatBit, formatsFromBitmask, bitmaskFromFormats, bitmaskFromAttributes, runsText, normalizeRuns, runsFromDelta, runsFromText, diffRunFormats, applyMarkOps, diffTextWithCursor, diffText, applyTextDiff, } from './text.js';
46
+ export { OUTBOUND_ORIGIN, OUTBOUND_SKIP_TAGS, syncLexicalToLoro, seedLoroFromLexical, longestIncreasingSubsequence, } from './to-loro.js';
47
+ export { INBOUND_TAGS, applyLoroToLexical, adoptLoroDocument, } from './to-lexical.js';
48
+ export { bootstrapDocument, isSharedDocumentEmpty, } from './seed.js';
49
+ export { LORO_UNDO_ORIGIN, UNDO_ORIGINS, DEFAULT_MERGE_INTERVAL, DEFAULT_MAX_UNDO_STEPS, registerLoroUndo, } from './undo.js';
50
+ export { loroCollab } from './binding.js';
51
+ /**
52
+ * Agent-write: apply an LLM's full-document markdown REWRITE to the shared
53
+ * document by CONTENT, so unchanged blocks keep their `ContainerID`s (hence their
54
+ * `NodeKey`s and mounted decorator sub-apps, and any concurrent peer edit into
55
+ * them) instead of being recreated wholesale.
56
+ *
57
+ * `reconcileTargetIntoLoro` is a SIBLING to `syncLexicalToLoro`: it writes Loro
58
+ * directly under {@link AGENT_WRITE_ORIGIN} and does NOT consult the
59
+ * `ContainerNodeMap` (content matching is the point; the mapping self-heals on
60
+ * the inbound bounce into any live editor). The caller owns the markdown → target
61
+ * parse with its own `@lexical/markdown` transformer set — this package never
62
+ * depends on `@lexical/markdown` — and projects the parsed editor state with
63
+ * {@link targetFromEditorState} / {@link projectTarget}.
64
+ *
65
+ * Guarantees and the documented DUPLICATE-BLOCK caveat live on the functions and
66
+ * in the README.
67
+ */
68
+ export { AGENT_WRITE_ORIGIN, reconcileTargetIntoLoro, projectTarget, targetFromEditorState, } from './agent-write.js';
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,EACL,cAAc,EACd,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,eAAe,EACf,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,OAAO,EACP,OAAO,EACP,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,WAAW,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,eAAe,GAchB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAE/F,OAAO,EAAE,gBAAgB,EAAyC,MAAM,cAAc,CAAA;AAEtF,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,iBAAiB,EACjB,SAAS,EACT,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,QAAQ,EACR,aAAa,EACb,aAAa,EACb,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,aAAa,GAMd,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAG7B,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,GAElB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,iBAAiB,EACjB,qBAAqB,GAGtB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,gBAAgB,GAEjB,MAAM,WAAW,CAAA;AAElB,OAAO,EAAE,UAAU,EAA0C,MAAM,cAAc,CAAA;AAEjF;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EACL,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,GAItB,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The ContainerID ↔ NodeKey registry — the core invariant of this package.
3
+ *
4
+ * ── Why it exists ──────────────────────────────────────────────────────────
5
+ *
6
+ * Lexical's `NodeKey` is a bare per-session counter
7
+ * (`LexicalUtils.ts`: `return '' + keyCounter++`). It is meaningless to a peer
8
+ * and unstable across reloads. Loro's `ContainerID` is the stable, replicated
9
+ * address. Every sync operation is therefore a translation between the two, and
10
+ * this registry is the only place that translation is allowed to happen.
11
+ *
12
+ * There is also NO stable node-object identity to key off: Lexical CLONES a node
13
+ * on every write, so `node === node` fails across an update. The NodeKey string
14
+ * survives cloning; the object does not. Hence a key↔id map, never a WeakMap.
15
+ *
16
+ * ── THE INVARIANT ──────────────────────────────────────────────────────────
17
+ *
18
+ * The two internal maps are always exact inverses of each other: a BIJECTION.
19
+ *
20
+ * byContainer.get(id) === key ⟺ byNode.get(key) === id
21
+ *
22
+ * No ContainerID ever addresses two NodeKeys, and no NodeKey ever addresses two
23
+ * ContainerIDs. `link()` enforces this by evicting BOTH stale directions before
24
+ * writing, so a re-link can never leave a half-entry behind. A violated
25
+ * bijection is the failure mode that silently corrupts a document: a stale
26
+ * reverse entry makes an inbound remote edit land on the wrong Lexical node.
27
+ *
28
+ * ── When it must be updated ────────────────────────────────────────────────
29
+ *
30
+ * - local create/delete — the user edited; mirror the change outbound
31
+ * - remote create/delete — an inbound event created/removed a container
32
+ * - TEXT NORMALIZATION — Lexical silently merges/splits adjacent TextNodes
33
+ * and reports it via `normalizedNodes`. This is the easiest one to forget and
34
+ * the one that corrupts the document: the surviving node keeps ONE key and the
35
+ * others are discarded, so their entries must be re-linked or dropped. This is
36
+ * also why inbound application runs `discrete: true` — without a synchronous
37
+ * flush the normalization happens after we have already read back keys, and
38
+ * the mapping drifts.
39
+ *
40
+ * Entries are never implicitly expired. Use `sweep()` after a structural change
41
+ * to drop entries whose container or node no longer exists.
42
+ */
43
+ import type { ContainerID } from 'loro-crdt';
44
+ import type { NodeKey } from 'lexical';
45
+ /** One direction of a mapping entry, as reported by iteration and sweeps. */
46
+ export interface MappingEntry {
47
+ readonly id: ContainerID;
48
+ readonly key: NodeKey;
49
+ }
50
+ /** Liveness probes used by {@link ContainerNodeMap.sweep}. */
51
+ export interface LivenessProbe {
52
+ /** True when the container still exists in the Loro document. */
53
+ readonly hasContainer: (id: ContainerID) => boolean;
54
+ /** True when the node still exists in the Lexical editor state. */
55
+ readonly hasNode: (key: NodeKey) => boolean;
56
+ }
57
+ /**
58
+ * A bijective, explicitly-invalidated registry mapping Loro `ContainerID`s to
59
+ * Lexical `NodeKey`s. See the file header for the invariant it maintains.
60
+ */
61
+ export declare class ContainerNodeMap {
62
+ #private;
63
+ /** Number of live entries. Both directions always agree on this. */
64
+ get size(): number;
65
+ /**
66
+ * Link a container to a node, replacing any previous link on EITHER side.
67
+ *
68
+ * Evicting both directions first is what preserves the bijection: linking
69
+ * `id → k2` when `id → k1` existed must also drop `k1 → id`, or `k1` keeps
70
+ * resolving to a container it no longer owns.
71
+ */
72
+ link(id: ContainerID, key: NodeKey): void;
73
+ /** The NodeKey addressed by a container, or `undefined` if unmapped. */
74
+ nodeKey(id: ContainerID): NodeKey | undefined;
75
+ /** The ContainerID addressing a node, or `undefined` if unmapped. */
76
+ containerId(key: NodeKey): ContainerID | undefined;
77
+ /**
78
+ * The NodeKey addressed by a container. Throws when unmapped.
79
+ *
80
+ * Prefer this on paths where a missing entry means the registry is already
81
+ * corrupt — failing loudly beats writing to the wrong node.
82
+ */
83
+ expectNodeKey(id: ContainerID): NodeKey;
84
+ /** The ContainerID addressing a node. Throws when unmapped. */
85
+ expectContainerId(key: NodeKey): ContainerID;
86
+ hasContainer(id: ContainerID): boolean;
87
+ hasNode(key: NodeKey): boolean;
88
+ /**
89
+ * Move a container's link to a different NodeKey, keeping the bijection.
90
+ *
91
+ * This is the TEXT-NORMALIZATION path: Lexical merged our run's node into a
92
+ * sibling, so the container must now address the survivor. Returns false when
93
+ * the container was not mapped (nothing to move).
94
+ */
95
+ rekey(id: ContainerID, key: NodeKey): boolean;
96
+ /** Drop the entry for a container. Returns whether one existed. */
97
+ unlinkContainer(id: ContainerID): boolean;
98
+ /** Drop the entry for a node. Returns whether one existed. */
99
+ unlinkNode(key: NodeKey): boolean;
100
+ /** Every live entry, in insertion order. */
101
+ entries(): MappingEntry[];
102
+ /**
103
+ * Drop every entry whose container or node no longer exists, and return the
104
+ * entries removed.
105
+ *
106
+ * Deleting a subtree removes many containers at once; rather than making every
107
+ * caller walk the removed subtree, sweep after the structural change. An entry
108
+ * is stale if EITHER side is gone — a half-live entry is exactly the corrupt
109
+ * state the invariant forbids.
110
+ */
111
+ sweep(probe: LivenessProbe): MappingEntry[];
112
+ /** Drop every entry. Used when the document is re-seeded from scratch. */
113
+ clear(): void;
114
+ }
115
+ //# sourceMappingURL=mapping.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.d.ts","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAA;IACnD,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAA;CAC5C;AAED;;;GAGG;AACH,qBAAa,gBAAgB;;IAI3B,oEAAoE;IACpE,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI;IASzC,wEAAwE;IACxE,OAAO,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,GAAG,SAAS;IAI7C,qEAAqE;IACrE,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,GAAG,SAAS;IAIlD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO;IAMvC,+DAA+D;IAC/D,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW;IAM5C,YAAY,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO;IAItC,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO;IAI9B;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO;IAM7C,mEAAmE;IACnE,eAAe,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO;IAQzC,8DAA8D;IAC9D,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO;IAQjC,4CAA4C;IAC5C,OAAO,IAAI,YAAY,EAAE;IAIzB;;;;;;;;OAQG;IACH,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,EAAE;IAU3C,0EAA0E;IAC1E,KAAK,IAAI,IAAI;CAuBd"}
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The ContainerID ↔ NodeKey registry — the core invariant of this package.
3
+ *
4
+ * ── Why it exists ──────────────────────────────────────────────────────────
5
+ *
6
+ * Lexical's `NodeKey` is a bare per-session counter
7
+ * (`LexicalUtils.ts`: `return '' + keyCounter++`). It is meaningless to a peer
8
+ * and unstable across reloads. Loro's `ContainerID` is the stable, replicated
9
+ * address. Every sync operation is therefore a translation between the two, and
10
+ * this registry is the only place that translation is allowed to happen.
11
+ *
12
+ * There is also NO stable node-object identity to key off: Lexical CLONES a node
13
+ * on every write, so `node === node` fails across an update. The NodeKey string
14
+ * survives cloning; the object does not. Hence a key↔id map, never a WeakMap.
15
+ *
16
+ * ── THE INVARIANT ──────────────────────────────────────────────────────────
17
+ *
18
+ * The two internal maps are always exact inverses of each other: a BIJECTION.
19
+ *
20
+ * byContainer.get(id) === key ⟺ byNode.get(key) === id
21
+ *
22
+ * No ContainerID ever addresses two NodeKeys, and no NodeKey ever addresses two
23
+ * ContainerIDs. `link()` enforces this by evicting BOTH stale directions before
24
+ * writing, so a re-link can never leave a half-entry behind. A violated
25
+ * bijection is the failure mode that silently corrupts a document: a stale
26
+ * reverse entry makes an inbound remote edit land on the wrong Lexical node.
27
+ *
28
+ * ── When it must be updated ────────────────────────────────────────────────
29
+ *
30
+ * - local create/delete — the user edited; mirror the change outbound
31
+ * - remote create/delete — an inbound event created/removed a container
32
+ * - TEXT NORMALIZATION — Lexical silently merges/splits adjacent TextNodes
33
+ * and reports it via `normalizedNodes`. This is the easiest one to forget and
34
+ * the one that corrupts the document: the surviving node keeps ONE key and the
35
+ * others are discarded, so their entries must be re-linked or dropped. This is
36
+ * also why inbound application runs `discrete: true` — without a synchronous
37
+ * flush the normalization happens after we have already read back keys, and
38
+ * the mapping drifts.
39
+ *
40
+ * Entries are never implicitly expired. Use `sweep()` after a structural change
41
+ * to drop entries whose container or node no longer exists.
42
+ */
43
+ /**
44
+ * A bijective, explicitly-invalidated registry mapping Loro `ContainerID`s to
45
+ * Lexical `NodeKey`s. See the file header for the invariant it maintains.
46
+ */
47
+ export class ContainerNodeMap {
48
+ #byContainer = new Map();
49
+ #byNode = new Map();
50
+ /** Number of live entries. Both directions always agree on this. */
51
+ get size() {
52
+ return this.#byContainer.size;
53
+ }
54
+ /**
55
+ * Link a container to a node, replacing any previous link on EITHER side.
56
+ *
57
+ * Evicting both directions first is what preserves the bijection: linking
58
+ * `id → k2` when `id → k1` existed must also drop `k1 → id`, or `k1` keeps
59
+ * resolving to a container it no longer owns.
60
+ */
61
+ link(id, key) {
62
+ const previousKey = this.#byContainer.get(id);
63
+ if (previousKey !== undefined && previousKey !== key)
64
+ this.#byNode.delete(previousKey);
65
+ const previousId = this.#byNode.get(key);
66
+ if (previousId !== undefined && previousId !== id)
67
+ this.#byContainer.delete(previousId);
68
+ this.#byContainer.set(id, key);
69
+ this.#byNode.set(key, id);
70
+ }
71
+ /** The NodeKey addressed by a container, or `undefined` if unmapped. */
72
+ nodeKey(id) {
73
+ return this.#byContainer.get(id);
74
+ }
75
+ /** The ContainerID addressing a node, or `undefined` if unmapped. */
76
+ containerId(key) {
77
+ return this.#byNode.get(key);
78
+ }
79
+ /**
80
+ * The NodeKey addressed by a container. Throws when unmapped.
81
+ *
82
+ * Prefer this on paths where a missing entry means the registry is already
83
+ * corrupt — failing loudly beats writing to the wrong node.
84
+ */
85
+ expectNodeKey(id) {
86
+ const key = this.#byContainer.get(id);
87
+ if (key === undefined)
88
+ throw new Error(`lexical-loro: no NodeKey mapped for container ${id}`);
89
+ return key;
90
+ }
91
+ /** The ContainerID addressing a node. Throws when unmapped. */
92
+ expectContainerId(key) {
93
+ const id = this.#byNode.get(key);
94
+ if (id === undefined)
95
+ throw new Error(`lexical-loro: no ContainerID mapped for node ${key}`);
96
+ return id;
97
+ }
98
+ hasContainer(id) {
99
+ return this.#byContainer.has(id);
100
+ }
101
+ hasNode(key) {
102
+ return this.#byNode.has(key);
103
+ }
104
+ /**
105
+ * Move a container's link to a different NodeKey, keeping the bijection.
106
+ *
107
+ * This is the TEXT-NORMALIZATION path: Lexical merged our run's node into a
108
+ * sibling, so the container must now address the survivor. Returns false when
109
+ * the container was not mapped (nothing to move).
110
+ */
111
+ rekey(id, key) {
112
+ if (!this.#byContainer.has(id))
113
+ return false;
114
+ this.link(id, key);
115
+ return true;
116
+ }
117
+ /** Drop the entry for a container. Returns whether one existed. */
118
+ unlinkContainer(id) {
119
+ const key = this.#byContainer.get(id);
120
+ if (key === undefined)
121
+ return false;
122
+ this.#byContainer.delete(id);
123
+ this.#byNode.delete(key);
124
+ return true;
125
+ }
126
+ /** Drop the entry for a node. Returns whether one existed. */
127
+ unlinkNode(key) {
128
+ const id = this.#byNode.get(key);
129
+ if (id === undefined)
130
+ return false;
131
+ this.#byNode.delete(key);
132
+ this.#byContainer.delete(id);
133
+ return true;
134
+ }
135
+ /** Every live entry, in insertion order. */
136
+ entries() {
137
+ return [...this.#byContainer].map(([id, key]) => ({ id, key }));
138
+ }
139
+ /**
140
+ * Drop every entry whose container or node no longer exists, and return the
141
+ * entries removed.
142
+ *
143
+ * Deleting a subtree removes many containers at once; rather than making every
144
+ * caller walk the removed subtree, sweep after the structural change. An entry
145
+ * is stale if EITHER side is gone — a half-live entry is exactly the corrupt
146
+ * state the invariant forbids.
147
+ */
148
+ sweep(probe) {
149
+ const removed = [];
150
+ for (const [id, key] of this.#byContainer) {
151
+ if (probe.hasContainer(id) && probe.hasNode(key))
152
+ continue;
153
+ removed.push({ id, key });
154
+ }
155
+ for (const { id } of removed)
156
+ this.unlinkContainer(id);
157
+ return removed;
158
+ }
159
+ /** Drop every entry. Used when the document is re-seeded from scratch. */
160
+ clear() {
161
+ this.#byContainer.clear();
162
+ this.#byNode.clear();
163
+ }
164
+ /**
165
+ * Assert the bijection holds. A debugging/test aid: production paths maintain
166
+ * it structurally, so a failure here means a bug in this class, not a caller.
167
+ *
168
+ * @internal
169
+ */
170
+ assertBijective() {
171
+ if (this.#byContainer.size !== this.#byNode.size) {
172
+ throw new Error(`lexical-loro: mapping is not bijective — ${this.#byContainer.size} containers vs ${this.#byNode.size} nodes`);
173
+ }
174
+ for (const [id, key] of this.#byContainer) {
175
+ if (this.#byNode.get(key) !== id) {
176
+ throw new Error(`lexical-loro: mapping is not bijective at ${id} ↔ ${key}`);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ //# sourceMappingURL=mapping.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapping.js","sourceRoot":"","sources":["../src/mapping.ts"],"names":[],"mappings":"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"}