@llui/lexical-loro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +262 -0
  3. package/dist/agent-write.d.ts +123 -0
  4. package/dist/agent-write.d.ts.map +1 -0
  5. package/dist/agent-write.js +499 -0
  6. package/dist/agent-write.js.map +1 -0
  7. package/dist/binding.d.ts +122 -0
  8. package/dist/binding.d.ts.map +1 -0
  9. package/dist/binding.js +114 -0
  10. package/dist/binding.js.map +1 -0
  11. package/dist/index.d.ts +69 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +69 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/mapping.d.ts +115 -0
  16. package/dist/mapping.d.ts.map +1 -0
  17. package/dist/mapping.js +181 -0
  18. package/dist/mapping.js.map +1 -0
  19. package/dist/order.d.ts +124 -0
  20. package/dist/order.d.ts.map +1 -0
  21. package/dist/order.js +187 -0
  22. package/dist/order.js.map +1 -0
  23. package/dist/schema.d.ts +343 -0
  24. package/dist/schema.d.ts.map +1 -0
  25. package/dist/schema.js +363 -0
  26. package/dist/schema.js.map +1 -0
  27. package/dist/seed.d.ts +72 -0
  28. package/dist/seed.d.ts.map +1 -0
  29. package/dist/seed.js +72 -0
  30. package/dist/seed.js.map +1 -0
  31. package/dist/text.d.ts +167 -0
  32. package/dist/text.d.ts.map +1 -0
  33. package/dist/text.js +289 -0
  34. package/dist/text.js.map +1 -0
  35. package/dist/to-lexical.d.ts +119 -0
  36. package/dist/to-lexical.d.ts.map +1 -0
  37. package/dist/to-lexical.js +636 -0
  38. package/dist/to-lexical.js.map +1 -0
  39. package/dist/to-loro.d.ts +154 -0
  40. package/dist/to-loro.d.ts.map +1 -0
  41. package/dist/to-loro.js +718 -0
  42. package/dist/to-loro.js.map +1 -0
  43. package/dist/undo.d.ts +94 -0
  44. package/dist/undo.d.ts.map +1 -0
  45. package/dist/undo.js +200 -0
  46. package/dist/undo.js.map +1 -0
  47. package/package.json +64 -0
  48. package/src/agent-write.ts +613 -0
  49. package/src/binding.ts +185 -0
  50. package/src/index.ts +176 -0
  51. package/src/mapping.ts +206 -0
  52. package/src/order.ts +205 -0
  53. package/src/schema.ts +509 -0
  54. package/src/seed.ts +112 -0
  55. package/src/text.ts +357 -0
  56. package/src/to-lexical.ts +792 -0
  57. package/src/to-loro.ts +914 -0
  58. package/src/undo.ts +269 -0
@@ -0,0 +1 @@
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":"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"}
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
@@ -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"}