@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
package/src/order.ts ADDED
@@ -0,0 +1,205 @@
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
+ /**
58
+ * The key alphabet, in ascending code-unit order.
59
+ *
60
+ * Base 62 buys ~5.9 binary subdivisions per character, which is what keeps
61
+ * growth at roughly one character per five same-spot inserts. Every character
62
+ * here must be ASCII and strictly ascending, because the comparator is plain
63
+ * lexicographic `<` on the raw string — the same comparison every peer performs
64
+ * with no locale involved.
65
+ */
66
+ export const DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
67
+
68
+ const BASE = DIGITS.length
69
+
70
+ /**
71
+ * A key strictly between `a` and `b`, where `null` means unbounded.
72
+ *
73
+ * REQUIRES `a < b`. It does not check, and on equal or inverted bounds it
74
+ * returns a key OUTSIDE the interval rather than failing — see constraint 4.
75
+ * Prefer {@link allocateAt}, which cannot be called with a degenerate interval.
76
+ */
77
+ export function between(a: string | null, b: string | null): string {
78
+ let result = ''
79
+ for (let i = 0; ; i++) {
80
+ const low = a !== null && i < a.length ? DIGITS.indexOf(a[i]!) : 0
81
+ const high = b !== null && i < b.length ? DIGITS.indexOf(b[i]!) : BASE
82
+ if (high - low > 1) return result + DIGITS[Math.floor((low + high) / 2)]!
83
+ result += DIGITS[low]!
84
+ }
85
+ }
86
+
87
+ /**
88
+ * `count` strictly increasing keys inside the open interval `(before, after)`.
89
+ *
90
+ * With `count === 1` the jitter is deliberately ignored (constraint 2). With
91
+ * `count > 1` the whole batch hangs off ONE anchor carrying the jitter digit, so
92
+ * two peers' concurrent batches occupy disjoint sub-intervals and cannot
93
+ * interleave (constraint 1).
94
+ *
95
+ * The anchor is a strict extension of a key already strictly below `after`, and
96
+ * every subsequent key extends the anchor further, so the whole batch stays
97
+ * inside the interval and in order.
98
+ */
99
+ export function allocate(
100
+ before: string | null,
101
+ after: string | null,
102
+ count: number,
103
+ jitter: string | null,
104
+ ): string[] {
105
+ if (count <= 0) return []
106
+ if (count === 1 || jitter === null) {
107
+ const keys: string[] = []
108
+ let previous = before
109
+ for (let i = 0; i < count; i++) {
110
+ const key = between(previous, after)
111
+ keys.push(key)
112
+ previous = key
113
+ }
114
+ return keys
115
+ }
116
+
117
+ const anchor = between(before, after) + jitter
118
+ const keys: string[] = [anchor]
119
+ let suffix: string | null = null
120
+ for (let i = 1; i < count; i++) {
121
+ suffix = between(suffix, null)
122
+ keys.push(anchor + suffix)
123
+ }
124
+ return keys
125
+ }
126
+
127
+ /**
128
+ * `count` keys placing new children at rendered index `index` among siblings
129
+ * whose positions are `positions` (ASCENDING — the order the projection
130
+ * renders).
131
+ *
132
+ * This is the only allocation entry point callers should use, because it is the
133
+ * only one that can see, and therefore honour, constraint 4. When the left
134
+ * neighbour's position EQUALS the right neighbour's — reachable whenever two
135
+ * peers insert at the same slot concurrently — there is no key strictly between
136
+ * them. Rather than emit one that breaks the sort invariant, the right bound is
137
+ * widened to the first position STRICTLY greater than the left neighbour's, so
138
+ * the new children land after the whole equal-position group.
139
+ *
140
+ * That is a real, if narrow, loss of fidelity: the block lands one slot later
141
+ * than the user pointed at. It is chosen over the alternatives deliberately —
142
+ * repositioning the neighbour would be a localized rebalance (constraint 3), and
143
+ * emitting an out-of-interval key would corrupt the ordering silently.
144
+ */
145
+ export function allocateAt(
146
+ positions: readonly string[],
147
+ index: number,
148
+ count: number,
149
+ jitter: string | null,
150
+ ): string[] {
151
+ const clamped = Math.max(0, Math.min(index, positions.length))
152
+ const before = clamped === 0 ? null : positions[clamped - 1]!
153
+
154
+ // `positions` is ascending, so the only degenerate case is EQUALITY, and the
155
+ // widened bound is the first strictly-greater position at or after `index`.
156
+ let after: string | null = null
157
+ for (let i = clamped; i < positions.length; i++) {
158
+ const candidate = positions[i]!
159
+ if (before === null || candidate > before) {
160
+ after = candidate
161
+ break
162
+ }
163
+ }
164
+
165
+ return allocate(before, after, count, jitter)
166
+ }
167
+
168
+ /**
169
+ * Digits usable as a per-peer jitter suffix.
170
+ *
171
+ * `DIGITS[0]` is excluded: appending the lowest digit to an anchor produces a
172
+ * key that is effectively the anchor itself for subdivision purposes, wasting
173
+ * the peer-private sub-interval the jitter exists to create.
174
+ */
175
+ const JITTER_DIGITS = DIGITS.slice(1)
176
+
177
+ /**
178
+ * A stable jitter digit for a peer.
179
+ *
180
+ * Takes Loro's own `peerId`, so peers need no coordination to pick distinct
181
+ * digits. Collisions across the {@link JITTER_DIGITS} alphabet only degrade to
182
+ * the un-jittered behaviour for the colliding pair; they are not a correctness
183
+ * problem.
184
+ */
185
+ export function jitterFor(peerId: bigint): string {
186
+ const index = Number(
187
+ ((peerId % BigInt(JITTER_DIGITS.length)) + BigInt(JITTER_DIGITS.length)) %
188
+ BigInt(JITTER_DIGITS.length),
189
+ )
190
+ return JITTER_DIGITS[index]!
191
+ }
192
+
193
+ /**
194
+ * The rendered order of two children: by `pos`, then by `uuid`.
195
+ *
196
+ * The uuid tiebreak is what makes this a TOTAL order even when two peers mint
197
+ * the same `pos`, which is exactly what keeps every peer rendering the same
198
+ * sequence. It resolves rendering only — it does not make the interval between
199
+ * two equal positions usable; see constraint 4.
200
+ */
201
+ export function comparePositions(posA: string, uuidA: string, posB: string, uuidB: string): number {
202
+ if (posA !== posB) return posA < posB ? -1 : 1
203
+ if (uuidA !== uuidB) return uuidA < uuidB ? -1 : 1
204
+ return 0
205
+ }