@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
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, getType } from 'loro-crdt';
|
|
106
|
+
import { comparePositions } from './order.js';
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Container keys
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/** Root map name on the `LoroDoc`. Mirrors Lexical's `RootNode`. */
|
|
111
|
+
export const ROOT_CONTAINER = 'root';
|
|
112
|
+
/** Key on an element map holding the Lexical node type (`node.getType()`). */
|
|
113
|
+
export const KEY_TYPE = 'type';
|
|
114
|
+
/** Key on an element map holding the scalar-prop sub-map. */
|
|
115
|
+
export const KEY_PROPS = 'props';
|
|
116
|
+
/** Key on an element map holding the child-carrier map. */
|
|
117
|
+
export const KEY_CHILDREN = 'children';
|
|
118
|
+
/**
|
|
119
|
+
* Key on a child carrier holding its own uuid.
|
|
120
|
+
*
|
|
121
|
+
* Duplicated from the `children` map key so a carrier read in isolation still
|
|
122
|
+
* knows its identity, and so the ordering tiebreak needs no parent lookup.
|
|
123
|
+
*/
|
|
124
|
+
export const KEY_UUID = 'uuid';
|
|
125
|
+
/** Key on a child carrier holding its fractional index. See `order.ts`. */
|
|
126
|
+
export const KEY_POS = 'pos';
|
|
127
|
+
/**
|
|
128
|
+
* Key on a child carrier discriminating an element from a text run.
|
|
129
|
+
*
|
|
130
|
+
* Explicit rather than inferred from which other keys are present: a remote
|
|
131
|
+
* update can be applied partially, and a carrier whose `type` has not landed yet
|
|
132
|
+
* must be SKIPPED by the projection, not mistaken for a text run.
|
|
133
|
+
*/
|
|
134
|
+
export const KEY_KIND = 'kind';
|
|
135
|
+
/** Key on a TEXT carrier holding its `LoroText`. */
|
|
136
|
+
export const KEY_TEXT = 'text';
|
|
137
|
+
/**
|
|
138
|
+
* `type` value used for an `LLuiDecoratorNode`. Its identity lives in
|
|
139
|
+
* `props.bridgeType`; its serialized payload in `props.data`.
|
|
140
|
+
*/
|
|
141
|
+
export const DECORATOR_TYPE = 'llui-decorator';
|
|
142
|
+
/** `props` key naming which LLui bridge renders a decorator. */
|
|
143
|
+
export const KEY_BRIDGE_TYPE = 'bridgeType';
|
|
144
|
+
/** `props` key holding a decorator's JSON-serialized payload. */
|
|
145
|
+
export const KEY_DATA = 'data';
|
|
146
|
+
/**
|
|
147
|
+
* The `expand` rule applied UNIFORMLY to every text format.
|
|
148
|
+
*
|
|
149
|
+
* Read `test/expand-semantics.test.ts` before changing this. `expand` is NOT
|
|
150
|
+
* the mechanism that reproduces Lexical's boundary behaviour — a 51-test spike
|
|
151
|
+
* proved no uniform table can, and that no per-format table can either (the
|
|
152
|
+
* divergence set is identical for all 11 formats, because Lexical has no
|
|
153
|
+
* per-format inclusivity: its caret is uniformly left-biased). The Lexical→Loro
|
|
154
|
+
* direction replays RESULTING NODE STATE via explicit mark/unmark ops instead
|
|
155
|
+
* (see `diffRunFormats` in `text.ts`), which makes the local result correct
|
|
156
|
+
* regardless of `expand`.
|
|
157
|
+
*
|
|
158
|
+
* `expand` therefore governs exactly one thing: what happens to text a REMOTE
|
|
159
|
+
* peer inserts CONCURRENTLY at a mark boundary. `'after'` is the closest fit to
|
|
160
|
+
* Lexical's left-biased caret.
|
|
161
|
+
*/
|
|
162
|
+
export const TEXT_MARK_EXPAND = 'after';
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Construction + access
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
/** The Lexical node type of the root. Matches `RootNode.getType()`. */
|
|
167
|
+
export const ROOT_TYPE = 'root';
|
|
168
|
+
/**
|
|
169
|
+
* Configure a `LoroDoc` for this schema and return its root element container,
|
|
170
|
+
* creating the root's schema keys if they are missing.
|
|
171
|
+
*
|
|
172
|
+
* Every peer MUST call this. Two reasons, both load-bearing:
|
|
173
|
+
*
|
|
174
|
+
* 1. `configTextStyle` is LOCAL configuration, not replicated state. A peer that
|
|
175
|
+
* skips it resolves marks under different expand rules and diverges.
|
|
176
|
+
* 2. The root's `props`/`children` are created with `ensureMergeable*`, which
|
|
177
|
+
* derives a DETERMINISTIC ContainerID from the parent and key. Two peers may
|
|
178
|
+
* each initialize an empty document before ever hearing from one another; a
|
|
179
|
+
* plain `setContainer` would mint two different child containers and the map
|
|
180
|
+
* slot's last-writer-wins would silently discard one peer's entire document.
|
|
181
|
+
* `ensureMergeable*` makes both peers land on the same container, so their
|
|
182
|
+
* edits merge. (Only the ROOT needs this — every other element is created
|
|
183
|
+
* whole by a single peer and inserted as one op.)
|
|
184
|
+
*/
|
|
185
|
+
export function initDoc(doc, formats) {
|
|
186
|
+
doc.configTextStyle(Object.fromEntries(formats.map((format) => [format, { expand: TEXT_MARK_EXPAND }])));
|
|
187
|
+
const root = doc.getMap(ROOT_CONTAINER);
|
|
188
|
+
// Write only what is missing. `initDoc` runs on every binding construction —
|
|
189
|
+
// and a shared `LoroDoc` may back more than one — so an unconditional `set`
|
|
190
|
+
// would queue a redundant local op that the next `import`/`commit` flushes to
|
|
191
|
+
// every peer as a spurious update.
|
|
192
|
+
if (root.get(KEY_TYPE) !== ROOT_TYPE)
|
|
193
|
+
root.set(KEY_TYPE, ROOT_TYPE);
|
|
194
|
+
root.ensureMergeableMap(KEY_PROPS);
|
|
195
|
+
root.ensureMergeableMap(KEY_CHILDREN);
|
|
196
|
+
return root;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A fresh child identity.
|
|
200
|
+
*
|
|
201
|
+
* MUST be random. Two peers minting the same uuid would collide on one slot of
|
|
202
|
+
* the `children` map, whose last-writer-wins would silently discard a whole
|
|
203
|
+
* block — the same class of data loss `initDoc`'s `ensureMergeable*` exists to
|
|
204
|
+
* prevent for the root.
|
|
205
|
+
*/
|
|
206
|
+
export function newUuid() {
|
|
207
|
+
return crypto.randomUUID();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Create an element child inside `children` and return its ATTACHED container.
|
|
211
|
+
*
|
|
212
|
+
* The carrier IS the element container: an element needs a `pos` anyway, so
|
|
213
|
+
* wrapping it in a second map would cost an extra container and an extra
|
|
214
|
+
* dereference for nothing. Only the attached handle has a stable `ContainerID`,
|
|
215
|
+
* so always take identity from what this returns.
|
|
216
|
+
*/
|
|
217
|
+
export function createElementChild(children, uuid, pos, type) {
|
|
218
|
+
const element = children.setContainer(uuid, new LoroMap());
|
|
219
|
+
element.set(KEY_UUID, uuid);
|
|
220
|
+
element.set(KEY_POS, pos);
|
|
221
|
+
element.set(KEY_KIND, 'element');
|
|
222
|
+
element.set(KEY_TYPE, type);
|
|
223
|
+
element.setContainer(KEY_PROPS, new LoroMap());
|
|
224
|
+
element.setContainer(KEY_CHILDREN, new LoroMap());
|
|
225
|
+
return element;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Create a text child inside `children` and return its ATTACHED `LoroText`.
|
|
229
|
+
*
|
|
230
|
+
* The `LoroText` is created once, inside its carrier, and never moved or
|
|
231
|
+
* recreated — which is precisely what makes its `ContainerID` invariant across
|
|
232
|
+
* every reorder, and therefore what lets a peer's concurrent insertion into it
|
|
233
|
+
* survive a block move.
|
|
234
|
+
*/
|
|
235
|
+
export function createTextChild(children, uuid, pos) {
|
|
236
|
+
const carrier = children.setContainer(uuid, new LoroMap());
|
|
237
|
+
carrier.set(KEY_UUID, uuid);
|
|
238
|
+
carrier.set(KEY_POS, pos);
|
|
239
|
+
carrier.set(KEY_KIND, 'text');
|
|
240
|
+
return carrier.setContainer(KEY_TEXT, new LoroText());
|
|
241
|
+
}
|
|
242
|
+
/** Re-position a child — the whole cost of a same-parent move. */
|
|
243
|
+
export function setChildPosition(carrier, pos) {
|
|
244
|
+
carrier.set(KEY_POS, pos);
|
|
245
|
+
}
|
|
246
|
+
/** Remove a child from its parent, container and all. */
|
|
247
|
+
export function deleteChild(children, uuid) {
|
|
248
|
+
children.delete(uuid);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* An element's children in RENDERED order: sorted by `(pos, uuid)`.
|
|
252
|
+
*
|
|
253
|
+
* Malformed carriers are SKIPPED rather than thrown on. That is not defensive
|
|
254
|
+
* padding: a remote update can be applied while a carrier's keys are still
|
|
255
|
+
* arriving, and a partially-materialized child must not crash a render — it will
|
|
256
|
+
* appear on the next event, once its `pos` and `kind` have landed.
|
|
257
|
+
*
|
|
258
|
+
* Nothing here consults `isDeleted()`, and nothing may. Projection must depend
|
|
259
|
+
* ONLY on replicated state; a deleted carrier is simply absent from `keys()` on
|
|
260
|
+
* every peer, which is what makes this a pure function of the document.
|
|
261
|
+
*/
|
|
262
|
+
export function orderedChildren(element) {
|
|
263
|
+
const children = elementChildren(element);
|
|
264
|
+
const out = [];
|
|
265
|
+
for (const uuid of children.keys()) {
|
|
266
|
+
// Read as `unknown` and narrow by `instanceof`: the map's declared value
|
|
267
|
+
// type is a PROMISE about well-formed carriers, and this function's whole
|
|
268
|
+
// job is to hold that promise against a document that may not keep it yet.
|
|
269
|
+
const carrier = children.get(uuid);
|
|
270
|
+
if (!(carrier instanceof LoroMap))
|
|
271
|
+
continue;
|
|
272
|
+
const pos = carrier.get(KEY_POS);
|
|
273
|
+
const kind = carrier.get(KEY_KIND);
|
|
274
|
+
if (typeof pos !== 'string')
|
|
275
|
+
continue;
|
|
276
|
+
if (kind === 'text') {
|
|
277
|
+
const text = carrier.get(KEY_TEXT);
|
|
278
|
+
if (!(text instanceof LoroText))
|
|
279
|
+
continue;
|
|
280
|
+
out.push({ uuid, pos, kind, carrier: carrier, container: text });
|
|
281
|
+
}
|
|
282
|
+
else if (kind === 'element') {
|
|
283
|
+
const container = carrier;
|
|
284
|
+
if (typeof container.get(KEY_TYPE) !== 'string')
|
|
285
|
+
continue;
|
|
286
|
+
out.push({ uuid, pos, kind, carrier: carrier, container });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return out.sort((x, y) => comparePositions(x.pos, x.uuid, y.pos, y.uuid));
|
|
290
|
+
}
|
|
291
|
+
/** How many well-formed children an element has. */
|
|
292
|
+
export function childCount(element) {
|
|
293
|
+
return orderedChildren(element).length;
|
|
294
|
+
}
|
|
295
|
+
/** Read an element's Lexical node type. */
|
|
296
|
+
export function elementType(element) {
|
|
297
|
+
const type = element.get(KEY_TYPE);
|
|
298
|
+
if (typeof type !== 'string') {
|
|
299
|
+
throw new Error(`lexical-loro: element ${element.id} has no '${KEY_TYPE}' string`);
|
|
300
|
+
}
|
|
301
|
+
return type;
|
|
302
|
+
}
|
|
303
|
+
/** Read an element's scalar-prop map. */
|
|
304
|
+
export function elementProps(element) {
|
|
305
|
+
const props = element.get(KEY_PROPS);
|
|
306
|
+
if (!(props instanceof LoroMap)) {
|
|
307
|
+
throw new Error(`lexical-loro: element ${element.id} has no '${KEY_PROPS}' map`);
|
|
308
|
+
}
|
|
309
|
+
return props;
|
|
310
|
+
}
|
|
311
|
+
/** Read an element's child-carrier map. UNORDERED — see {@link orderedChildren}. */
|
|
312
|
+
export function elementChildren(element) {
|
|
313
|
+
const children = element.get(KEY_CHILDREN);
|
|
314
|
+
if (!(children instanceof LoroMap)) {
|
|
315
|
+
throw new Error(`lexical-loro: element ${element.id} has no '${KEY_CHILDREN}' map`);
|
|
316
|
+
}
|
|
317
|
+
return children;
|
|
318
|
+
}
|
|
319
|
+
/** Narrow a child slot to an element container. */
|
|
320
|
+
export function isElementContainer(child) {
|
|
321
|
+
return child instanceof LoroMap;
|
|
322
|
+
}
|
|
323
|
+
/** Narrow a child slot to a text run. */
|
|
324
|
+
export function isTextContainer(child) {
|
|
325
|
+
return child instanceof LoroText;
|
|
326
|
+
}
|
|
327
|
+
/** True when this element mirrors an `LLuiDecoratorNode`. */
|
|
328
|
+
export function isDecoratorElement(element) {
|
|
329
|
+
return elementType(element) === DECORATOR_TYPE;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Whether a container still exists in the document.
|
|
333
|
+
*
|
|
334
|
+
* `getContainerById` keeps returning a usable handle for a DELETED container, so
|
|
335
|
+
* `isDeleted()` is the real test. The kind narrowing is not defensive padding:
|
|
336
|
+
* `Container` includes `LoroCounter`, `LoroList` and `LoroTree`, which this
|
|
337
|
+
* schema never uses, and only its two kinds may enter the registry.
|
|
338
|
+
*
|
|
339
|
+
* This is a LOCAL liveness question — "is the registry entry stale?" — not a
|
|
340
|
+
* projection question. See {@link orderedChildren}.
|
|
341
|
+
*/
|
|
342
|
+
export function containerIsLive(doc, id) {
|
|
343
|
+
const container = doc.getContainerById(id);
|
|
344
|
+
if (container instanceof LoroMap)
|
|
345
|
+
return !container.isDeleted();
|
|
346
|
+
if (container instanceof LoroText)
|
|
347
|
+
return !container.isDeleted();
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* The `ContainerID` of an attached container — the STABLE, cross-peer address
|
|
352
|
+
* this binding maps to a per-session `NodeKey`. Throws on a detached container,
|
|
353
|
+
* which has no replicated identity and must never enter the mapping.
|
|
354
|
+
*/
|
|
355
|
+
export function containerId(container) {
|
|
356
|
+
const attached = container.getAttached();
|
|
357
|
+
if (attached === undefined) {
|
|
358
|
+
throw new Error(`lexical-loro: refusing to address a DETACHED ${getType(container)} container — ` +
|
|
359
|
+
'insert it into its parent first and use the handle the insert returns');
|
|
360
|
+
}
|
|
361
|
+
return attached.id;
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuGG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAE7C,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,oEAAoE;AACpE,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAA;AAEpC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE9B,6DAA6D;AAC7D,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAA;AAEhC,2DAA2D;AAC3D,MAAM,CAAC,MAAM,YAAY,GAAG,UAAU,CAAA;AAEtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE9B,2EAA2E;AAC3E,MAAM,CAAC,MAAM,OAAO,GAAG,KAAK,CAAA;AAE5B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE9B,oDAAoD;AACpD,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE9B;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,gBAAgB,CAAA;AAE9C,gEAAgE;AAChE,MAAM,CAAC,MAAM,eAAe,GAAG,YAAY,CAAA;AAE3C,iEAAiE;AACjE,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAA;AAkH9B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAe,OAAO,CAAA;AAEnD,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E,uEAAuE;AACvE,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAA;AAE/B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,OAAO,CAAC,GAAY,EAAE,OAA0B;IAC9D,GAAG,CAAC,eAAe,CACjB,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC,CAAC,CACpF,CAAA;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,cAAc,CAAqB,CAAA;IAC3D,6EAA6E;IAC7E,4EAA4E;IAC5E,8EAA8E;IAC9E,mCAAmC;IACnC,IAAI,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,SAAS;QAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IACnE,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAA;IAClC,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA;IACrC,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO;IACrB,OAAO,MAAM,CAAC,UAAU,EAAE,CAAA;AAC5B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAA2B,EAC3B,IAAY,EACZ,GAAW,EACX,IAAY;IAEZ,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,OAAO,EAAE,CAAqB,CAAA;IAC9E,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3B,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACzB,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;IAChC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3B,OAAO,CAAC,YAAY,CAAC,SAAS,EAAE,IAAI,OAAO,EAAoB,CAAC,CAAA;IAChE,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,IAAI,OAAO,EAAuB,CAAC,CAAA;IACtE,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,QAA2B,EAAE,IAAY,EAAE,GAAW;IACpF,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,OAAO,EAAE,CAAgB,CAAA;IACzE,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC3B,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;IACzB,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IAC7B,OAAO,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,QAAQ,EAAE,CAAC,CAAA;AACvD,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,OAAqB,EAAE,GAAW;IACjE,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AAC3B,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,WAAW,CAAC,QAA2B,EAAE,IAAY;IACnE,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB;IACvD,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAA;IACzC,MAAM,GAAG,GAAiB,EAAE,CAAA;IAC5B,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QACnC,yEAAyE;QACzE,0EAA0E;QAC1E,2EAA2E;QAC3E,MAAM,OAAO,GAAY,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAC3C,IAAI,CAAC,CAAC,OAAO,YAAY,OAAO,CAAC;YAAE,SAAQ;QAC3C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAChC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAClC,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,SAAQ;QACrC,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YAClC,IAAI,CAAC,CAAC,IAAI,YAAY,QAAQ,CAAC;gBAAE,SAAQ;YACzC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,OAAuB,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAClF,CAAC;aAAM,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,SAAS,GAAG,OAA2B,CAAA;YAC7C,IAAI,OAAO,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,QAAQ;gBAAE,SAAQ;YACzD,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,OAAuB,EAAE,SAAS,EAAE,CAAC,CAAA;QAC5E,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;AAC3E,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,UAAU,CAAC,OAAyB;IAClD,OAAO,eAAe,CAAC,OAAO,CAAC,CAAC,MAAM,CAAA;AACxC,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,WAAW,CAAC,OAAyB;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAClC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,CAAC,EAAE,YAAY,QAAQ,UAAU,CAAC,CAAA;IACpF,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,YAAY,CAAC,OAAyB;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACpC,IAAI,CAAC,CAAC,KAAK,YAAY,OAAO,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,CAAC,EAAE,YAAY,SAAS,OAAO,CAAC,CAAA;IAClF,CAAC;IACD,OAAO,KAAuB,CAAA;AAChC,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,eAAe,CAAC,OAAyB;IACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC1C,IAAI,CAAC,CAAC,QAAQ,YAAY,OAAO,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,yBAAyB,OAAO,CAAC,EAAE,YAAY,YAAY,OAAO,CAAC,CAAA;IACrF,CAAC;IACD,OAAO,QAA6B,CAAA;AACtC,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,KAAK,YAAY,OAAO,CAAA;AACjC,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,QAAQ,CAAA;AAClC,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,kBAAkB,CAAC,OAAyB;IAC1D,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,cAAc,CAAA;AAChD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,GAAY,EAAE,EAAe;IAC3D,MAAM,SAAS,GAAG,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAA;IAC1C,IAAI,SAAS,YAAY,OAAO;QAAE,OAAO,CAAC,SAAS,CAAC,SAAS,EAAE,CAAA;IAC/D,IAAI,SAAS,YAAY,QAAQ;QAAE,OAAO,CAAC,SAAS,CAAC,SAAS,EAAE,CAAA;IAChE,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,SAAoB;IAC9C,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,EAAE,CAAA;IACxC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,gDAAgD,OAAO,CAAC,SAAS,CAAC,eAAe;YAC/E,uEAAuE,CAC1E,CAAA;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,EAAE,CAAA;AACpB,CAAC"}
|
package/dist/seed.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot: seed an empty shared document, or adopt a populated one.
|
|
3
|
+
*
|
|
4
|
+
* This is the shortest file in the package and the one with the most damaging
|
|
5
|
+
* failure mode. Getting the branch backwards means the SECOND peer to open a
|
|
6
|
+
* document "seeds" it — writing its local default over content everyone else is
|
|
7
|
+
* already editing. So the decision is made against the SHARED document's own
|
|
8
|
+
* emptiness, never against the local editor's, and both orders are pinned by
|
|
9
|
+
* tests (`test/seed.test.ts`).
|
|
10
|
+
*
|
|
11
|
+
* ── Why the seed suppresses the outbound listener ──────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* The seed runs as a Lexical update, which would ordinarily be mirrored by the
|
|
14
|
+
* registered outbound listener. It is tagged `COLLABORATION_TAG` so that does
|
|
15
|
+
* NOT happen, and `seedLoroFromLexical` writes the shared document explicitly
|
|
16
|
+
* instead. One write path rather than two means the seed cannot half-land if the
|
|
17
|
+
* binding's listener registration order ever changes, and the mapping is
|
|
18
|
+
* populated by the same walk that writes the containers.
|
|
19
|
+
*
|
|
20
|
+
* ── Concurrent seeding ─────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* Two peers can each find the shared document empty before hearing from one
|
|
23
|
+
* another. Nothing can arbitrate that without a coordinator, so the requirement
|
|
24
|
+
* is CONVERGENCE, not deduplication: both seeds merge and every peer ends up
|
|
25
|
+
* with both blocks. `initDoc`'s `ensureMergeable*` root containers are what make
|
|
26
|
+
* that merge possible — with a plain `setContainer` the map slot's
|
|
27
|
+
* last-writer-wins would silently discard one peer's entire document. Apps that
|
|
28
|
+
* need exactly one bootstrapper should elect one and pass `shouldBootstrap`
|
|
29
|
+
* accordingly.
|
|
30
|
+
*/
|
|
31
|
+
import { type LexicalEditor } from 'lexical';
|
|
32
|
+
import type { LoroDoc } from 'loro-crdt';
|
|
33
|
+
import type { ContainerNodeMap } from './mapping.js';
|
|
34
|
+
import { type ElementContainer } from './schema.js';
|
|
35
|
+
/** What {@link bootstrapDocument} did. */
|
|
36
|
+
export type BootstrapOutcome =
|
|
37
|
+
/** The shared document was empty and this peer filled it from `seed`. */
|
|
38
|
+
'seeded'
|
|
39
|
+
/** The shared document had content; the editor now mirrors it. */
|
|
40
|
+
| 'adopted'
|
|
41
|
+
/** Empty document, but this peer is not allowed to bootstrap it. */
|
|
42
|
+
| 'waiting';
|
|
43
|
+
export interface BootstrapTarget {
|
|
44
|
+
readonly doc: LoroDoc;
|
|
45
|
+
/** The root element container, as returned by `initDoc`. */
|
|
46
|
+
readonly root: ElementContainer;
|
|
47
|
+
readonly mapping: ContainerNodeMap;
|
|
48
|
+
readonly editor: LexicalEditor;
|
|
49
|
+
/**
|
|
50
|
+
* Fill an EMPTY editor with this peer's default content. Runs inside a Lexical
|
|
51
|
+
* update, at most once, and only when the shared document is empty.
|
|
52
|
+
*/
|
|
53
|
+
readonly seed?: ((editor: LexicalEditor) => void) | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Whether this peer may bootstrap an empty shared document. Default `true`.
|
|
56
|
+
* Set `false` on peers that join rather than create — with a real transport,
|
|
57
|
+
* "empty" before the first sync is indistinguishable from "genuinely empty",
|
|
58
|
+
* and a joining peer that seeds races the document it was about to receive.
|
|
59
|
+
*/
|
|
60
|
+
readonly shouldBootstrap?: boolean | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Whether the shared document holds any content at all. */
|
|
63
|
+
export declare function isSharedDocumentEmpty(root: ElementContainer): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Bring the editor and the shared document into agreement at boot.
|
|
66
|
+
*
|
|
67
|
+
* Idempotent: calling it again on a populated document adopts (writing nothing
|
|
68
|
+
* and churning no NodeKeys), so a binding may safely call it on every sync
|
|
69
|
+
* event without tracking whether it already ran.
|
|
70
|
+
*/
|
|
71
|
+
export declare function bootstrapDocument(target: BootstrapTarget): BootstrapOutcome;
|
|
72
|
+
//# sourceMappingURL=seed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAkD,KAAK,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAExC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AACpD,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAI/D,0CAA0C;AAC1C,MAAM,MAAM,gBAAgB;AAC1B,yEAAyE;AACvE,QAAQ;AACV,kEAAkE;GAChE,SAAS;AACX,oEAAoE;GAClE,SAAS,CAAA;AAEb,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAC9B;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IAC7D;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/C;AAED,4DAA4D;AAC5D,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAErE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CA+B3E"}
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot: seed an empty shared document, or adopt a populated one.
|
|
3
|
+
*
|
|
4
|
+
* This is the shortest file in the package and the one with the most damaging
|
|
5
|
+
* failure mode. Getting the branch backwards means the SECOND peer to open a
|
|
6
|
+
* document "seeds" it — writing its local default over content everyone else is
|
|
7
|
+
* already editing. So the decision is made against the SHARED document's own
|
|
8
|
+
* emptiness, never against the local editor's, and both orders are pinned by
|
|
9
|
+
* tests (`test/seed.test.ts`).
|
|
10
|
+
*
|
|
11
|
+
* ── Why the seed suppresses the outbound listener ──────────────────────────
|
|
12
|
+
*
|
|
13
|
+
* The seed runs as a Lexical update, which would ordinarily be mirrored by the
|
|
14
|
+
* registered outbound listener. It is tagged `COLLABORATION_TAG` so that does
|
|
15
|
+
* NOT happen, and `seedLoroFromLexical` writes the shared document explicitly
|
|
16
|
+
* instead. One write path rather than two means the seed cannot half-land if the
|
|
17
|
+
* binding's listener registration order ever changes, and the mapping is
|
|
18
|
+
* populated by the same walk that writes the containers.
|
|
19
|
+
*
|
|
20
|
+
* ── Concurrent seeding ─────────────────────────────────────────────────────
|
|
21
|
+
*
|
|
22
|
+
* Two peers can each find the shared document empty before hearing from one
|
|
23
|
+
* another. Nothing can arbitrate that without a coordinator, so the requirement
|
|
24
|
+
* is CONVERGENCE, not deduplication: both seeds merge and every peer ends up
|
|
25
|
+
* with both blocks. `initDoc`'s `ensureMergeable*` root containers are what make
|
|
26
|
+
* that merge possible — with a plain `setContainer` the map slot's
|
|
27
|
+
* last-writer-wins would silently discard one peer's entire document. Apps that
|
|
28
|
+
* need exactly one bootstrapper should elect one and pass `shouldBootstrap`
|
|
29
|
+
* accordingly.
|
|
30
|
+
*/
|
|
31
|
+
import { $getRoot, COLLABORATION_TAG, HISTORY_MERGE_TAG } from 'lexical';
|
|
32
|
+
import { childCount } from './schema.js';
|
|
33
|
+
import { adoptLoroDocument } from './to-lexical.js';
|
|
34
|
+
import { seedLoroFromLexical } from './to-loro.js';
|
|
35
|
+
/** Whether the shared document holds any content at all. */
|
|
36
|
+
export function isSharedDocumentEmpty(root) {
|
|
37
|
+
return childCount(root) === 0;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Bring the editor and the shared document into agreement at boot.
|
|
41
|
+
*
|
|
42
|
+
* Idempotent: calling it again on a populated document adopts (writing nothing
|
|
43
|
+
* and churning no NodeKeys), so a binding may safely call it on every sync
|
|
44
|
+
* event without tracking whether it already ran.
|
|
45
|
+
*/
|
|
46
|
+
export function bootstrapDocument(target) {
|
|
47
|
+
if (!isSharedDocumentEmpty(target.root)) {
|
|
48
|
+
adoptLoroDocument(target);
|
|
49
|
+
return 'adopted';
|
|
50
|
+
}
|
|
51
|
+
if (target.shouldBootstrap === false)
|
|
52
|
+
return 'waiting';
|
|
53
|
+
const seed = target.seed;
|
|
54
|
+
if (seed !== undefined) {
|
|
55
|
+
target.editor.update(() => {
|
|
56
|
+
// Only fill a genuinely empty editor: a remount over a live editor must
|
|
57
|
+
// not have its content replaced by the default.
|
|
58
|
+
if (!$getRoot().isEmpty())
|
|
59
|
+
return;
|
|
60
|
+
seed(target.editor);
|
|
61
|
+
}, {
|
|
62
|
+
// Suppress the outbound listener — the shared document is written below,
|
|
63
|
+
// by one explicit path. `HISTORY_MERGE_TAG` keeps the seed out of the
|
|
64
|
+
// user's undo stack, so the first Cmd+Z cannot empty the document.
|
|
65
|
+
tag: [COLLABORATION_TAG, HISTORY_MERGE_TAG],
|
|
66
|
+
discrete: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
seedLoroFromLexical({ doc: target.doc, root: target.root, mapping: target.mapping }, target.editor.getEditorState());
|
|
70
|
+
return 'seeded';
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=seed.js.map
|
package/dist/seed.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,iBAAiB,EAAsB,MAAM,SAAS,CAAA;AAI5F,OAAO,EAAE,UAAU,EAAyB,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AA+BlD,4DAA4D;AAC5D,MAAM,UAAU,qBAAqB,CAAC,IAAsB;IAC1D,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAuB;IACvD,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,iBAAiB,CAAC,MAAM,CAAC,CAAA;QACzB,OAAO,SAAS,CAAA;IAClB,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,KAAK,KAAK;QAAE,OAAO,SAAS,CAAA;IAEtD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IACxB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,CAAC,MAAM,CAAC,MAAM,CAClB,GAAG,EAAE;YACH,wEAAwE;YACxE,gDAAgD;YAChD,IAAI,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE;gBAAE,OAAM;YACjC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACrB,CAAC,EACD;YACE,yEAAyE;YACzE,sEAAsE;YACtE,mEAAmE;YACnE,GAAG,EAAE,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;YAC3C,QAAQ,EAAE,IAAI;SACf,CACF,CAAA;IACH,CAAC;IAED,mBAAmB,CACjB,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,EAC/D,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAC/B,CAAA;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC"}
|