@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/dist/order.d.ts
ADDED
|
@@ -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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"}
|
package/dist/schema.d.ts
ADDED
|
@@ -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
|