@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,124 @@
1
+ /**
2
+ * Fractional indexing: the sibling ORDER of a child list, as a sortable
3
+ * property rather than as a list position.
4
+ *
5
+ * Every child carrier holds a `pos` string; the rendered order is
6
+ * `sort by (pos, uuid)` — a PURE function of replicated state, and therefore
7
+ * commutative by construction. A MOVE is one last-writer-wins register write to
8
+ * `pos`: no container is deleted, none is recreated, and nothing in the moved
9
+ * subtree is touched. See `schema.ts` for why that property is load-bearing.
10
+ *
11
+ * ── FOUR CONSTRAINTS, EACH MEASURED, EACH EASY TO BREAK ────────────────────
12
+ *
13
+ * These are not style preferences. Each one was a REAL, reproduced defect in a
14
+ * spike, and each is the kind of thing a later contributor removes because it
15
+ * looks redundant. Do not relax one without re-reading `test/order.test.ts`.
16
+ *
17
+ * 1. BATCH ALLOCATION, OFF ONE ANCHOR. Allocating a multi-block paste one block
18
+ * at a time makes both peers generate the SAME keys in the same gap, so the
19
+ * uuid tiebreak alternates them: two 5-paragraph pastes at the same spot
20
+ * render as a 10-paragraph A/B/A/B interleaving. Convergent, and nonsense.
21
+ * `allocate` therefore takes the WHOLE batch and hangs it off a single anchor
22
+ * carrying a per-peer jitter digit, which confines each peer's paste to a
23
+ * private sub-interval.
24
+ *
25
+ * 2. JITTER PER BATCH, NEVER PER INSERT. Always-on jitter degrades key growth
26
+ * from ~0.2 to ~1.0 characters per insert, because a suffix digit at the far
27
+ * end of the alphabet from the direction of travel consumes the whole
28
+ * remaining interval. And WHICH digit is pathological is direction-dependent
29
+ * — a low digit is worst for repeated left-inserts, a high digit for
30
+ * right-inserts — so no fixed per-peer digit is safe in both. `allocate`
31
+ * consequently IGNORES `jitter` when `count === 1`, which is the overwhelming
32
+ * majority of calls. That branch is not an optimization; deleting it makes
33
+ * single-insert keys grow five times faster.
34
+ *
35
+ * 3. NEVER REBALANCE. Rewriting every `pos` to spread the keys out evenly is the
36
+ * "obvious" fix for key growth. IT IS UNSAFE. A peer that concurrently
37
+ * inserted computed its key against the OLD keys, so after the merge its
38
+ * block lands at an arbitrary position — convergent, and silently wrong about
39
+ * what the user asked for. It is measured: the block ends up at neither of
40
+ * the neighbours it was typed between.
41
+ *
42
+ * There is no need for it anyway. Growth is LINEAR and bounded in practice —
43
+ * 2000 adversarial same-spot inserts reach a 401-character key, and a move
44
+ * carries exactly one such key, keeping even that pathological case under a
45
+ * kilobyte on the wire. Unlike a plain list's delete+recreate, it does not
46
+ * scale with the subtree.
47
+ *
48
+ * 4. EQUAL POSITIONS ARE REACHABLE, and the uuid tiebreak only resolves
49
+ * RENDERING. Two peers inserting at the same slot mint an IDENTICAL `pos`.
50
+ * No key exists strictly between two equal keys, and `between(a, a)` returns
51
+ * a key that sorts AFTER BOTH while claiming to sort between them — silent
52
+ * corruption of the one invariant this module exists to maintain. Callers
53
+ * must go through {@link allocateAt}, which sees the sibling list and widens
54
+ * past the degenerate group rather than emitting an impossible key.
55
+ */
56
+ /**
57
+ * The key alphabet, in ascending code-unit order.
58
+ *
59
+ * Base 62 buys ~5.9 binary subdivisions per character, which is what keeps
60
+ * growth at roughly one character per five same-spot inserts. Every character
61
+ * here must be ASCII and strictly ascending, because the comparator is plain
62
+ * lexicographic `<` on the raw string — the same comparison every peer performs
63
+ * with no locale involved.
64
+ */
65
+ export declare const DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
66
+ /**
67
+ * A key strictly between `a` and `b`, where `null` means unbounded.
68
+ *
69
+ * REQUIRES `a < b`. It does not check, and on equal or inverted bounds it
70
+ * returns a key OUTSIDE the interval rather than failing — see constraint 4.
71
+ * Prefer {@link allocateAt}, which cannot be called with a degenerate interval.
72
+ */
73
+ export declare function between(a: string | null, b: string | null): string;
74
+ /**
75
+ * `count` strictly increasing keys inside the open interval `(before, after)`.
76
+ *
77
+ * With `count === 1` the jitter is deliberately ignored (constraint 2). With
78
+ * `count > 1` the whole batch hangs off ONE anchor carrying the jitter digit, so
79
+ * two peers' concurrent batches occupy disjoint sub-intervals and cannot
80
+ * interleave (constraint 1).
81
+ *
82
+ * The anchor is a strict extension of a key already strictly below `after`, and
83
+ * every subsequent key extends the anchor further, so the whole batch stays
84
+ * inside the interval and in order.
85
+ */
86
+ export declare function allocate(before: string | null, after: string | null, count: number, jitter: string | null): string[];
87
+ /**
88
+ * `count` keys placing new children at rendered index `index` among siblings
89
+ * whose positions are `positions` (ASCENDING — the order the projection
90
+ * renders).
91
+ *
92
+ * This is the only allocation entry point callers should use, because it is the
93
+ * only one that can see, and therefore honour, constraint 4. When the left
94
+ * neighbour's position EQUALS the right neighbour's — reachable whenever two
95
+ * peers insert at the same slot concurrently — there is no key strictly between
96
+ * them. Rather than emit one that breaks the sort invariant, the right bound is
97
+ * widened to the first position STRICTLY greater than the left neighbour's, so
98
+ * the new children land after the whole equal-position group.
99
+ *
100
+ * That is a real, if narrow, loss of fidelity: the block lands one slot later
101
+ * than the user pointed at. It is chosen over the alternatives deliberately —
102
+ * repositioning the neighbour would be a localized rebalance (constraint 3), and
103
+ * emitting an out-of-interval key would corrupt the ordering silently.
104
+ */
105
+ export declare function allocateAt(positions: readonly string[], index: number, count: number, jitter: string | null): string[];
106
+ /**
107
+ * A stable jitter digit for a peer.
108
+ *
109
+ * Takes Loro's own `peerId`, so peers need no coordination to pick distinct
110
+ * digits. Collisions across the {@link JITTER_DIGITS} alphabet only degrade to
111
+ * the un-jittered behaviour for the colliding pair; they are not a correctness
112
+ * problem.
113
+ */
114
+ export declare function jitterFor(peerId: bigint): string;
115
+ /**
116
+ * The rendered order of two children: by `pos`, then by `uuid`.
117
+ *
118
+ * The uuid tiebreak is what makes this a TOTAL order even when two peers mint
119
+ * the same `pos`, which is exactly what keeps every peer rendering the same
120
+ * sequence. It resolves rendering only — it does not make the interval between
121
+ * two equal positions usable; see constraint 4.
122
+ */
123
+ export declare function comparePositions(posA: string, uuidA: string, posB: string, uuidB: string): number;
124
+ //# sourceMappingURL=order.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order.d.ts","sourceRoot":"","sources":["../src/order.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,mEAAmE,CAAA;AAItF;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAQlE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,MAAM,EAAE,CAqBV;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CACxB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,MAAM,EAAE,CAgBV;AAWD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAMhD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjG"}
package/dist/order.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Fractional indexing: the sibling ORDER of a child list, as a sortable
3
+ * property rather than as a list position.
4
+ *
5
+ * Every child carrier holds a `pos` string; the rendered order is
6
+ * `sort by (pos, uuid)` — a PURE function of replicated state, and therefore
7
+ * commutative by construction. A MOVE is one last-writer-wins register write to
8
+ * `pos`: no container is deleted, none is recreated, and nothing in the moved
9
+ * subtree is touched. See `schema.ts` for why that property is load-bearing.
10
+ *
11
+ * ── FOUR CONSTRAINTS, EACH MEASURED, EACH EASY TO BREAK ────────────────────
12
+ *
13
+ * These are not style preferences. Each one was a REAL, reproduced defect in a
14
+ * spike, and each is the kind of thing a later contributor removes because it
15
+ * looks redundant. Do not relax one without re-reading `test/order.test.ts`.
16
+ *
17
+ * 1. BATCH ALLOCATION, OFF ONE ANCHOR. Allocating a multi-block paste one block
18
+ * at a time makes both peers generate the SAME keys in the same gap, so the
19
+ * uuid tiebreak alternates them: two 5-paragraph pastes at the same spot
20
+ * render as a 10-paragraph A/B/A/B interleaving. Convergent, and nonsense.
21
+ * `allocate` therefore takes the WHOLE batch and hangs it off a single anchor
22
+ * carrying a per-peer jitter digit, which confines each peer's paste to a
23
+ * private sub-interval.
24
+ *
25
+ * 2. JITTER PER BATCH, NEVER PER INSERT. Always-on jitter degrades key growth
26
+ * from ~0.2 to ~1.0 characters per insert, because a suffix digit at the far
27
+ * end of the alphabet from the direction of travel consumes the whole
28
+ * remaining interval. And WHICH digit is pathological is direction-dependent
29
+ * — a low digit is worst for repeated left-inserts, a high digit for
30
+ * right-inserts — so no fixed per-peer digit is safe in both. `allocate`
31
+ * consequently IGNORES `jitter` when `count === 1`, which is the overwhelming
32
+ * majority of calls. That branch is not an optimization; deleting it makes
33
+ * single-insert keys grow five times faster.
34
+ *
35
+ * 3. NEVER REBALANCE. Rewriting every `pos` to spread the keys out evenly is the
36
+ * "obvious" fix for key growth. IT IS UNSAFE. A peer that concurrently
37
+ * inserted computed its key against the OLD keys, so after the merge its
38
+ * block lands at an arbitrary position — convergent, and silently wrong about
39
+ * what the user asked for. It is measured: the block ends up at neither of
40
+ * the neighbours it was typed between.
41
+ *
42
+ * There is no need for it anyway. Growth is LINEAR and bounded in practice —
43
+ * 2000 adversarial same-spot inserts reach a 401-character key, and a move
44
+ * carries exactly one such key, keeping even that pathological case under a
45
+ * kilobyte on the wire. Unlike a plain list's delete+recreate, it does not
46
+ * scale with the subtree.
47
+ *
48
+ * 4. EQUAL POSITIONS ARE REACHABLE, and the uuid tiebreak only resolves
49
+ * RENDERING. Two peers inserting at the same slot mint an IDENTICAL `pos`.
50
+ * No key exists strictly between two equal keys, and `between(a, a)` returns
51
+ * a key that sorts AFTER BOTH while claiming to sort between them — silent
52
+ * corruption of the one invariant this module exists to maintain. Callers
53
+ * must go through {@link allocateAt}, which sees the sibling list and widens
54
+ * past the degenerate group rather than emitting an impossible key.
55
+ */
56
+ /**
57
+ * The key alphabet, in ascending code-unit order.
58
+ *
59
+ * Base 62 buys ~5.9 binary subdivisions per character, which is what keeps
60
+ * growth at roughly one character per five same-spot inserts. Every character
61
+ * here must be ASCII and strictly ascending, because the comparator is plain
62
+ * lexicographic `<` on the raw string — the same comparison every peer performs
63
+ * with no locale involved.
64
+ */
65
+ export const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
66
+ const BASE = DIGITS.length;
67
+ /**
68
+ * A key strictly between `a` and `b`, where `null` means unbounded.
69
+ *
70
+ * REQUIRES `a < b`. It does not check, and on equal or inverted bounds it
71
+ * returns a key OUTSIDE the interval rather than failing — see constraint 4.
72
+ * Prefer {@link allocateAt}, which cannot be called with a degenerate interval.
73
+ */
74
+ export function between(a, b) {
75
+ let result = '';
76
+ for (let i = 0;; i++) {
77
+ const low = a !== null && i < a.length ? DIGITS.indexOf(a[i]) : 0;
78
+ const high = b !== null && i < b.length ? DIGITS.indexOf(b[i]) : BASE;
79
+ if (high - low > 1)
80
+ return result + DIGITS[Math.floor((low + high) / 2)];
81
+ result += DIGITS[low];
82
+ }
83
+ }
84
+ /**
85
+ * `count` strictly increasing keys inside the open interval `(before, after)`.
86
+ *
87
+ * With `count === 1` the jitter is deliberately ignored (constraint 2). With
88
+ * `count > 1` the whole batch hangs off ONE anchor carrying the jitter digit, so
89
+ * two peers' concurrent batches occupy disjoint sub-intervals and cannot
90
+ * interleave (constraint 1).
91
+ *
92
+ * The anchor is a strict extension of a key already strictly below `after`, and
93
+ * every subsequent key extends the anchor further, so the whole batch stays
94
+ * inside the interval and in order.
95
+ */
96
+ export function allocate(before, after, count, jitter) {
97
+ if (count <= 0)
98
+ return [];
99
+ if (count === 1 || jitter === null) {
100
+ const keys = [];
101
+ let previous = before;
102
+ for (let i = 0; i < count; i++) {
103
+ const key = between(previous, after);
104
+ keys.push(key);
105
+ previous = key;
106
+ }
107
+ return keys;
108
+ }
109
+ const anchor = between(before, after) + jitter;
110
+ const keys = [anchor];
111
+ let suffix = null;
112
+ for (let i = 1; i < count; i++) {
113
+ suffix = between(suffix, null);
114
+ keys.push(anchor + suffix);
115
+ }
116
+ return keys;
117
+ }
118
+ /**
119
+ * `count` keys placing new children at rendered index `index` among siblings
120
+ * whose positions are `positions` (ASCENDING — the order the projection
121
+ * renders).
122
+ *
123
+ * This is the only allocation entry point callers should use, because it is the
124
+ * only one that can see, and therefore honour, constraint 4. When the left
125
+ * neighbour's position EQUALS the right neighbour's — reachable whenever two
126
+ * peers insert at the same slot concurrently — there is no key strictly between
127
+ * them. Rather than emit one that breaks the sort invariant, the right bound is
128
+ * widened to the first position STRICTLY greater than the left neighbour's, so
129
+ * the new children land after the whole equal-position group.
130
+ *
131
+ * That is a real, if narrow, loss of fidelity: the block lands one slot later
132
+ * than the user pointed at. It is chosen over the alternatives deliberately —
133
+ * repositioning the neighbour would be a localized rebalance (constraint 3), and
134
+ * emitting an out-of-interval key would corrupt the ordering silently.
135
+ */
136
+ export function allocateAt(positions, index, count, jitter) {
137
+ const clamped = Math.max(0, Math.min(index, positions.length));
138
+ const before = clamped === 0 ? null : positions[clamped - 1];
139
+ // `positions` is ascending, so the only degenerate case is EQUALITY, and the
140
+ // widened bound is the first strictly-greater position at or after `index`.
141
+ let after = null;
142
+ for (let i = clamped; i < positions.length; i++) {
143
+ const candidate = positions[i];
144
+ if (before === null || candidate > before) {
145
+ after = candidate;
146
+ break;
147
+ }
148
+ }
149
+ return allocate(before, after, count, jitter);
150
+ }
151
+ /**
152
+ * Digits usable as a per-peer jitter suffix.
153
+ *
154
+ * `DIGITS[0]` is excluded: appending the lowest digit to an anchor produces a
155
+ * key that is effectively the anchor itself for subdivision purposes, wasting
156
+ * the peer-private sub-interval the jitter exists to create.
157
+ */
158
+ const JITTER_DIGITS = DIGITS.slice(1);
159
+ /**
160
+ * A stable jitter digit for a peer.
161
+ *
162
+ * Takes Loro's own `peerId`, so peers need no coordination to pick distinct
163
+ * digits. Collisions across the {@link JITTER_DIGITS} alphabet only degrade to
164
+ * the un-jittered behaviour for the colliding pair; they are not a correctness
165
+ * problem.
166
+ */
167
+ export function jitterFor(peerId) {
168
+ const index = Number(((peerId % BigInt(JITTER_DIGITS.length)) + BigInt(JITTER_DIGITS.length)) %
169
+ BigInt(JITTER_DIGITS.length));
170
+ return JITTER_DIGITS[index];
171
+ }
172
+ /**
173
+ * The rendered order of two children: by `pos`, then by `uuid`.
174
+ *
175
+ * The uuid tiebreak is what makes this a TOTAL order even when two peers mint
176
+ * the same `pos`, which is exactly what keeps every peer rendering the same
177
+ * sequence. It resolves rendering only — it does not make the interval between
178
+ * two equal positions usable; see constraint 4.
179
+ */
180
+ export function comparePositions(posA, uuidA, posB, uuidB) {
181
+ if (posA !== posB)
182
+ return posA < posB ? -1 : 1;
183
+ if (uuidA !== uuidB)
184
+ return uuidA < uuidB ? -1 : 1;
185
+ return 0;
186
+ }
187
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order.js","sourceRoot":"","sources":["../src/order.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,343 @@
1
+ /**
2
+ * The Loro container schema that mirrors a Lexical `EditorState`.
3
+ *
4
+ * ── Shape ──────────────────────────────────────────────────────────────────
5
+ *
6
+ * doc.getMap('root') // ElementContainer
7
+ * 'type' -> string // node.getType()
8
+ * 'props' -> LoroMap<Record<string, PropValue>> // node props, LWW per key
9
+ * 'children' -> LoroMap<uuid, ChildCarrier> // UNORDERED; see below
10
+ *
11
+ * Every child is a CARRIER map, keyed in `children` by its own uuid:
12
+ *
13
+ * ChildCarrier = LoroMap {
14
+ * 'uuid' -> string // its own key, duplicated for reading
15
+ * 'pos' -> string // fractional index; see `order.ts`
16
+ * 'kind' -> 'element' | 'text'
17
+ * // kind === 'element' — the carrier IS the child's ElementContainer:
18
+ * 'type' -> string
19
+ * 'props' -> LoroMap
20
+ * 'children' -> LoroMap<uuid, ChildCarrier>
21
+ * // kind === 'text':
22
+ * 'text' -> LoroText // created ONCE, never recreated
23
+ * }
24
+ *
25
+ * Sibling order is NOT a list position. It is `sort by (pos, uuid)` — a pure
26
+ * function of replicated state, and therefore commutative by construction.
27
+ *
28
+ * ── Why fractional indexing, not a LoroMovableList ─────────────────────────
29
+ *
30
+ * This schema previously held children in a `LoroMovableList`, whose `move` op
31
+ * preserves container identity. That property is load-bearing here: ContainerID
32
+ * is our stable address for a `NodeKey` (see `mapping.ts`), so a child that is
33
+ * deleted and recreated instead of moved forces its whole subtree to be rebuilt
34
+ * with fresh NodeKeys — which DISPOSES every mounted `LLuiDecoratorNode` sub-app
35
+ * in it (`packages/lexical/src/decorator.ts` disposes on the 'destroyed'
36
+ * mutation). Block drag-reorder is a real operation in this editor.
37
+ *
38
+ * `LoroMovableList` was abandoned because loro-crdt 1.13.7 — the LATEST release,
39
+ * with no upgrade path — has TWO defects in it, both pinned by
40
+ * `test/loro-upstream.test.ts`:
41
+ *
42
+ * 1. A WASM PANIC. Uncatchable from JavaScript, and it leaves the document in
43
+ * an unspecified state, so there is no recovery path to write.
44
+ * 2. A SILENT CONVERGENCE FAILURE — two peers accept the same updates and
45
+ * render different documents, with nothing to detect it from.
46
+ *
47
+ * A plain `LoroList` plus uuid identity was evaluated and REJECTED: without a
48
+ * move op, a reorder is delete+recreate, which silently LOSES a peer's
49
+ * concurrent edit into the moved subtree. Convergent and unrepairable — strictly
50
+ * worse than the defects it was meant to route around.
51
+ *
52
+ * Fractional indexing keeps the property that mattered. A same-parent move is
53
+ * ONE last-writer-wins register write to `pos` (~87 bytes regardless of subtree
54
+ * size): no container is deleted, none is created, and every `ContainerID` —
55
+ * including every `LoroText` — is INVARIANT across reorder, text edits, and
56
+ * parent moves. So a concurrent edit into a moved subtree survives, and
57
+ * `mapping.ts` needs no notion of any of this.
58
+ *
59
+ * ── What this deliberately does NOT claim ──────────────────────────────────
60
+ *
61
+ * Three documented limits. Do not read the paragraph above as covering them:
62
+ *
63
+ * - CROSS-PARENT moves are still delete+recreate, and DO lose a concurrent edit
64
+ * into the moved subtree. The "concurrent edit preserved" property is
65
+ * SAME-PARENT ONLY. (This is not a regression: `LoroMovableList#move` is also
66
+ * confined to a single list.)
67
+ * - DELETE BEATS MOVE. A delete concurrent with a move wins and the block
68
+ * vanishes, in both delivery orders. Chosen deliberately: `LoroMovableList`
69
+ * does the opposite — it RESURRECTS a deliberately deleted block — and pays
70
+ * for it with defect 1 above. A tombstone mitigation was tried and REFUTED BY
71
+ * TEST (the delete flag and `pos` are different map keys, so both survive and
72
+ * nothing is resurrected). Do not re-add tombstones.
73
+ * - TWO CONCURRENT SPLITS of the same text run converge on a child COUNT, not
74
+ * on sensible text: ordinal text matching mints a fresh tail container on each
75
+ * peer, so the merged document duplicates a fragment. Pre-existing — the
76
+ * `LoroMovableList` binding produced the same duplication for the same history
77
+ * — and out of scope for the ordering model. Verified against real Lexical;
78
+ * see `test/convergence-attack.test.ts` for the concurrent-text histories.
79
+ *
80
+ * ── Why one LoroText per RUN, not per TextNode ─────────────────────────────
81
+ *
82
+ * A RUN is a MAXIMAL GROUP OF ADJACENT `TextNode`s — not one TextNode. That
83
+ * distinction is the schema's text unit and it is doing real work: Lexical
84
+ * splits and merges adjacent TextNodes freely (normalization), and a node
85
+ * boundary is a rendering detail, not user intent. Mirroring nodes 1:1 would
86
+ * make every normalization a structural CRDT edit and would let two peers'
87
+ * different-but-equivalent splits conflict.
88
+ *
89
+ * The most common "split" is not a structural change at all: bolding a middle
90
+ * sub-range makes Lexical split one TextNode into THREE, but all three are
91
+ * adjacent, so they coalesce back to ONE desired child. The carrier count and
92
+ * the `LoroText` ContainerID are unchanged and the format lands as a mark inside
93
+ * the existing text. Verified against real Lexical 0.48 by the D1 case in
94
+ * `test/to-loro.test.ts` ('run identity under Lexical normalization').
95
+ *
96
+ * ── Index units ────────────────────────────────────────────────────────────
97
+ *
98
+ * loro-crdt's JavaScript binding addresses `LoroText` in UTF-16 code units —
99
+ * the same unit as JavaScript string indices and therefore the same unit as
100
+ * Lexical offsets. NO conversion is required at this seam. (`convertPos` exists
101
+ * for unicode/utf8 interop; we never need it.) This is pinned by a test in
102
+ * `test/schema.test.ts` because it is an assumption the whole binding rests on
103
+ * and it is not stated in loro-crdt's type declarations.
104
+ */
105
+ import { LoroMap, LoroText } from 'loro-crdt';
106
+ import type { Container, ContainerID, LoroDoc } from 'loro-crdt';
107
+ /** Root map name on the `LoroDoc`. Mirrors Lexical's `RootNode`. */
108
+ export declare const ROOT_CONTAINER = "root";
109
+ /** Key on an element map holding the Lexical node type (`node.getType()`). */
110
+ export declare const KEY_TYPE = "type";
111
+ /** Key on an element map holding the scalar-prop sub-map. */
112
+ export declare const KEY_PROPS = "props";
113
+ /** Key on an element map holding the child-carrier map. */
114
+ export declare const KEY_CHILDREN = "children";
115
+ /**
116
+ * Key on a child carrier holding its own uuid.
117
+ *
118
+ * Duplicated from the `children` map key so a carrier read in isolation still
119
+ * knows its identity, and so the ordering tiebreak needs no parent lookup.
120
+ */
121
+ export declare const KEY_UUID = "uuid";
122
+ /** Key on a child carrier holding its fractional index. See `order.ts`. */
123
+ export declare const KEY_POS = "pos";
124
+ /**
125
+ * Key on a child carrier discriminating an element from a text run.
126
+ *
127
+ * Explicit rather than inferred from which other keys are present: a remote
128
+ * update can be applied partially, and a carrier whose `type` has not landed yet
129
+ * must be SKIPPED by the projection, not mistaken for a text run.
130
+ */
131
+ export declare const KEY_KIND = "kind";
132
+ /** Key on a TEXT carrier holding its `LoroText`. */
133
+ export declare const KEY_TEXT = "text";
134
+ /**
135
+ * `type` value used for an `LLuiDecoratorNode`. Its identity lives in
136
+ * `props.bridgeType`; its serialized payload in `props.data`.
137
+ */
138
+ export declare const DECORATOR_TYPE = "llui-decorator";
139
+ /** `props` key naming which LLui bridge renders a decorator. */
140
+ export declare const KEY_BRIDGE_TYPE = "bridgeType";
141
+ /** `props` key holding a decorator's JSON-serialized payload. */
142
+ export declare const KEY_DATA = "data";
143
+ /**
144
+ * A value storable in an element's `props` map: any JSON value.
145
+ *
146
+ * Most Lexical node props are scalars (`tag`, `format`, `indent`, …), but not
147
+ * all — `LLuiDecoratorNode.exportJSON()` emits `data: unknown`, an arbitrary
148
+ * JSON payload, and that payload is precisely what makes a decorator's mounted
149
+ * LLui sub-app reproducible on a peer. Loro stores a JSON value in a map slot as
150
+ * ONE last-writer-wins register, which is the same granularity a scalar gets, so
151
+ * widening the type costs nothing structurally.
152
+ *
153
+ * The LWW granularity is per KEY, not per nested field: two peers editing
154
+ * different fields of the same `data` object do not merge, the later write wins
155
+ * whole. Decorator payloads are small, opaque-to-us blobs, so that is the right
156
+ * trade; a decorator wanting field-level merging should model its state as its
157
+ * own Loro container rather than as a prop.
158
+ */
159
+ export type PropValue = string | number | boolean | null | PropValue[] | {
160
+ [key: string]: PropValue;
161
+ };
162
+ /** An element's prop map. Each key is independently last-writer-wins. */
163
+ export type PropsContainer = LoroMap<Record<string, PropValue>>;
164
+ /**
165
+ * The map mirroring one Lexical `ElementNode` (or a `DecoratorNode` /
166
+ * `LineBreakNode`, which simply carry an empty `children` map).
167
+ *
168
+ * Every element except the ROOT is also a child carrier, so it additionally
169
+ * holds `uuid`, `pos` and `kind`. The root is reached through `doc.getMap` and
170
+ * has no siblings to be ordered among, so those keys are optional.
171
+ */
172
+ export type ElementContainer = LoroMap<ElementShape>;
173
+ /** An element map's key set. */
174
+ export interface ElementShape extends Record<string, unknown> {
175
+ [KEY_TYPE]: string;
176
+ [KEY_PROPS]: PropsContainer;
177
+ [KEY_CHILDREN]: ChildrenContainer;
178
+ }
179
+ /** What a child carrier wraps. */
180
+ export type ChildKind = 'element' | 'text';
181
+ /** A carrier holding one text run's `LoroText`. */
182
+ export type TextCarrier = LoroMap<TextCarrierShape>;
183
+ /** A text carrier's key set. */
184
+ export interface TextCarrierShape extends Record<string, unknown> {
185
+ [KEY_UUID]: string;
186
+ [KEY_POS]: string;
187
+ [KEY_KIND]: 'text';
188
+ [KEY_TEXT]: LoroText;
189
+ }
190
+ /**
191
+ * An element's children, keyed by uuid. UNORDERED — the rendered sequence comes
192
+ * from {@link orderedChildren}, never from iteration order.
193
+ */
194
+ export type ChildrenContainer = LoroMap<Record<string, ChildCarrier>>;
195
+ /**
196
+ * The keys EVERY child carrier holds, whatever it wraps.
197
+ *
198
+ * Typed as its own shape rather than as `ElementContainer | TextCarrier` so that
199
+ * the ordering keys can be written without narrowing: a union of two generic
200
+ * `LoroMap` signatures is not callable, and the position write is the one
201
+ * operation that is genuinely common to both kinds.
202
+ */
203
+ export interface CarrierShape extends Record<string, unknown> {
204
+ [KEY_UUID]: string;
205
+ [KEY_POS]: string;
206
+ [KEY_KIND]: ChildKind;
207
+ }
208
+ /** A carrier sitting in a `children` map, seen through its common keys. */
209
+ export type ChildCarrier = LoroMap<CarrierShape>;
210
+ /**
211
+ * The container a child's IDENTITY is registered under in `mapping.ts`: the
212
+ * `LoroText` for a text run, the element map itself for an element.
213
+ *
214
+ * Both are invariant across every reorder, which is what lets the registry stay
215
+ * ignorant of the ordering model entirely.
216
+ */
217
+ export type ChildContainer = ElementContainer | LoroText;
218
+ /** One child of an element, as the ordering projection sees it. */
219
+ export interface ChildEntry {
220
+ readonly uuid: string;
221
+ readonly pos: string;
222
+ readonly kind: ChildKind;
223
+ /** The carrier map. For an element child this IS its {@link ElementContainer}. */
224
+ readonly carrier: ChildCarrier;
225
+ /** The container this child is addressed by. See {@link ChildContainer}. */
226
+ readonly container: ChildContainer;
227
+ }
228
+ /** Loro's per-mark conflict-resolution rule. */
229
+ export type ExpandType = 'before' | 'after' | 'none' | 'both';
230
+ /**
231
+ * The `expand` rule applied UNIFORMLY to every text format.
232
+ *
233
+ * Read `test/expand-semantics.test.ts` before changing this. `expand` is NOT
234
+ * the mechanism that reproduces Lexical's boundary behaviour — a 51-test spike
235
+ * proved no uniform table can, and that no per-format table can either (the
236
+ * divergence set is identical for all 11 formats, because Lexical has no
237
+ * per-format inclusivity: its caret is uniformly left-biased). The Lexical→Loro
238
+ * direction replays RESULTING NODE STATE via explicit mark/unmark ops instead
239
+ * (see `diffRunFormats` in `text.ts`), which makes the local result correct
240
+ * regardless of `expand`.
241
+ *
242
+ * `expand` therefore governs exactly one thing: what happens to text a REMOTE
243
+ * peer inserts CONCURRENTLY at a mark boundary. `'after'` is the closest fit to
244
+ * Lexical's left-biased caret.
245
+ */
246
+ export declare const TEXT_MARK_EXPAND: ExpandType;
247
+ /** The Lexical node type of the root. Matches `RootNode.getType()`. */
248
+ export declare const ROOT_TYPE = "root";
249
+ /**
250
+ * Configure a `LoroDoc` for this schema and return its root element container,
251
+ * creating the root's schema keys if they are missing.
252
+ *
253
+ * Every peer MUST call this. Two reasons, both load-bearing:
254
+ *
255
+ * 1. `configTextStyle` is LOCAL configuration, not replicated state. A peer that
256
+ * skips it resolves marks under different expand rules and diverges.
257
+ * 2. The root's `props`/`children` are created with `ensureMergeable*`, which
258
+ * derives a DETERMINISTIC ContainerID from the parent and key. Two peers may
259
+ * each initialize an empty document before ever hearing from one another; a
260
+ * plain `setContainer` would mint two different child containers and the map
261
+ * slot's last-writer-wins would silently discard one peer's entire document.
262
+ * `ensureMergeable*` makes both peers land on the same container, so their
263
+ * edits merge. (Only the ROOT needs this — every other element is created
264
+ * whole by a single peer and inserted as one op.)
265
+ */
266
+ export declare function initDoc(doc: LoroDoc, formats: readonly string[]): ElementContainer;
267
+ /**
268
+ * A fresh child identity.
269
+ *
270
+ * MUST be random. Two peers minting the same uuid would collide on one slot of
271
+ * the `children` map, whose last-writer-wins would silently discard a whole
272
+ * block — the same class of data loss `initDoc`'s `ensureMergeable*` exists to
273
+ * prevent for the root.
274
+ */
275
+ export declare function newUuid(): string;
276
+ /**
277
+ * Create an element child inside `children` and return its ATTACHED container.
278
+ *
279
+ * The carrier IS the element container: an element needs a `pos` anyway, so
280
+ * wrapping it in a second map would cost an extra container and an extra
281
+ * dereference for nothing. Only the attached handle has a stable `ContainerID`,
282
+ * so always take identity from what this returns.
283
+ */
284
+ export declare function createElementChild(children: ChildrenContainer, uuid: string, pos: string, type: string): ElementContainer;
285
+ /**
286
+ * Create a text child inside `children` and return its ATTACHED `LoroText`.
287
+ *
288
+ * The `LoroText` is created once, inside its carrier, and never moved or
289
+ * recreated — which is precisely what makes its `ContainerID` invariant across
290
+ * every reorder, and therefore what lets a peer's concurrent insertion into it
291
+ * survive a block move.
292
+ */
293
+ export declare function createTextChild(children: ChildrenContainer, uuid: string, pos: string): LoroText;
294
+ /** Re-position a child — the whole cost of a same-parent move. */
295
+ export declare function setChildPosition(carrier: ChildCarrier, pos: string): void;
296
+ /** Remove a child from its parent, container and all. */
297
+ export declare function deleteChild(children: ChildrenContainer, uuid: string): void;
298
+ /**
299
+ * An element's children in RENDERED order: sorted by `(pos, uuid)`.
300
+ *
301
+ * Malformed carriers are SKIPPED rather than thrown on. That is not defensive
302
+ * padding: a remote update can be applied while a carrier's keys are still
303
+ * arriving, and a partially-materialized child must not crash a render — it will
304
+ * appear on the next event, once its `pos` and `kind` have landed.
305
+ *
306
+ * Nothing here consults `isDeleted()`, and nothing may. Projection must depend
307
+ * ONLY on replicated state; a deleted carrier is simply absent from `keys()` on
308
+ * every peer, which is what makes this a pure function of the document.
309
+ */
310
+ export declare function orderedChildren(element: ElementContainer): ChildEntry[];
311
+ /** How many well-formed children an element has. */
312
+ export declare function childCount(element: ElementContainer): number;
313
+ /** Read an element's Lexical node type. */
314
+ export declare function elementType(element: ElementContainer): string;
315
+ /** Read an element's scalar-prop map. */
316
+ export declare function elementProps(element: ElementContainer): PropsContainer;
317
+ /** Read an element's child-carrier map. UNORDERED — see {@link orderedChildren}. */
318
+ export declare function elementChildren(element: ElementContainer): ChildrenContainer;
319
+ /** Narrow a child slot to an element container. */
320
+ export declare function isElementContainer(child: unknown): child is ElementContainer;
321
+ /** Narrow a child slot to a text run. */
322
+ export declare function isTextContainer(child: unknown): child is LoroText;
323
+ /** True when this element mirrors an `LLuiDecoratorNode`. */
324
+ export declare function isDecoratorElement(element: ElementContainer): boolean;
325
+ /**
326
+ * Whether a container still exists in the document.
327
+ *
328
+ * `getContainerById` keeps returning a usable handle for a DELETED container, so
329
+ * `isDeleted()` is the real test. The kind narrowing is not defensive padding:
330
+ * `Container` includes `LoroCounter`, `LoroList` and `LoroTree`, which this
331
+ * schema never uses, and only its two kinds may enter the registry.
332
+ *
333
+ * This is a LOCAL liveness question — "is the registry entry stale?" — not a
334
+ * projection question. See {@link orderedChildren}.
335
+ */
336
+ export declare function containerIsLive(doc: LoroDoc, id: ContainerID): boolean;
337
+ /**
338
+ * The `ContainerID` of an attached container — the STABLE, cross-peer address
339
+ * this binding maps to a per-session `NodeKey`. Throws on a detached container,
340
+ * which has no replicated identity and must never enter the mapping.
341
+ */
342
+ export declare function containerId(container: Container): ContainerID;
343
+ //# sourceMappingURL=schema.d.ts.map