@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.
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/agent-write.d.ts +123 -0
- package/dist/agent-write.d.ts.map +1 -0
- package/dist/agent-write.js +499 -0
- package/dist/agent-write.js.map +1 -0
- package/dist/binding.d.ts +122 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +114 -0
- package/dist/binding.js.map +1 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/mapping.d.ts +115 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/mapping.js +181 -0
- package/dist/mapping.js.map +1 -0
- package/dist/order.d.ts +124 -0
- package/dist/order.d.ts.map +1 -0
- package/dist/order.js +187 -0
- package/dist/order.js.map +1 -0
- package/dist/schema.d.ts +343 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +363 -0
- package/dist/schema.js.map +1 -0
- package/dist/seed.d.ts +72 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +72 -0
- package/dist/seed.js.map +1 -0
- package/dist/text.d.ts +167 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +289 -0
- package/dist/text.js.map +1 -0
- package/dist/to-lexical.d.ts +119 -0
- package/dist/to-lexical.d.ts.map +1 -0
- package/dist/to-lexical.js +636 -0
- package/dist/to-lexical.js.map +1 -0
- package/dist/to-loro.d.ts +154 -0
- package/dist/to-loro.d.ts.map +1 -0
- package/dist/to-loro.js +718 -0
- package/dist/to-loro.js.map +1 -0
- package/dist/undo.d.ts +94 -0
- package/dist/undo.d.ts.map +1 -0
- package/dist/undo.js +200 -0
- package/dist/undo.js.map +1 -0
- package/package.json +64 -0
- package/src/agent-write.ts +613 -0
- package/src/binding.ts +185 -0
- package/src/index.ts +176 -0
- package/src/mapping.ts +206 -0
- package/src/order.ts +205 -0
- package/src/schema.ts +509 -0
- package/src/seed.ts +112 -0
- package/src/text.ts +357 -0
- package/src/to-lexical.ts +792 -0
- package/src/to-loro.ts +914 -0
- 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
|
+
}
|