@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,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-write: reconcile an AGENT-AUTHORED full-document rewrite into an existing
|
|
3
|
+
* Loro document without tearing down history.
|
|
4
|
+
*
|
|
5
|
+
* ── The problem ────────────────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* An LLM rewrites a note's WHOLE markdown. The naive route — reparse the new
|
|
8
|
+
* markdown into the live bound editor (`root.clear()` + rebuild) — mints a fresh
|
|
9
|
+
* `NodeKey` for every node, so the outbound sync (`to-loro.ts`) matches nothing
|
|
10
|
+
* through the `NodeKey → ContainerID` registry and recreates EVERY container.
|
|
11
|
+
* A concurrent edit from another window merges into a container this side just
|
|
12
|
+
* deleted and is lost; a mounted `LLuiDecoratorNode` sub-app under any block is
|
|
13
|
+
* torn down. Measured at 0% ContainerID survival even for IDENTICAL markdown.
|
|
14
|
+
*
|
|
15
|
+
* ── The design ─────────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* Reconcile a PARSED TARGET TREE directly against the existing Loro document,
|
|
18
|
+
* matching existing child carriers to target children by CONTENT rather than by
|
|
19
|
+
* NodeKey. Unchanged blocks keep their `ContainerID`s (and therefore their
|
|
20
|
+
* `NodeKey`s and decorator mounts on the inbound bounce); a text-changed block
|
|
21
|
+
* keeps its `LoroText` and diffs the characters; only genuinely new/removed/moved
|
|
22
|
+
* blocks touch the ordering. This is the analog of loro-prosemirror's
|
|
23
|
+
* content-equality match (`eqLoroObjNode`), living where the reconciler already
|
|
24
|
+
* is.
|
|
25
|
+
*
|
|
26
|
+
* This is a SIBLING to the outbound sync ({@link import('./to-loro.js')}), not a
|
|
27
|
+
* replacement: it writes the Loro document directly under {@link
|
|
28
|
+
* AGENT_WRITE_ORIGIN}, and the existing inbound path ({@link
|
|
29
|
+
* import('./to-lexical.js')}) replicates that change into any live editor bound
|
|
30
|
+
* to the same document — preserving `NodeKey`s and decorator mounts on the
|
|
31
|
+
* bounce, and self-healing the `ContainerID ↔ NodeKey` mapping there. For that
|
|
32
|
+
* bounce to happen, {@link AGENT_WRITE_ORIGIN} must be on the inbound target's
|
|
33
|
+
* list of local origins to apply; `binding.ts` wires that.
|
|
34
|
+
*
|
|
35
|
+
* ── What this deliberately does NOT consult ────────────────────────────────
|
|
36
|
+
*
|
|
37
|
+
* The `ContainerNodeMap` registry is intentionally NOT read here — content
|
|
38
|
+
* matching is the whole point, and the mapping self-heals on the inbound bounce.
|
|
39
|
+
* (Contrast `to-loro.ts`, whose whole job is IDENTITY matching through that
|
|
40
|
+
* registry.)
|
|
41
|
+
*
|
|
42
|
+
* ── Where the markdown parse lives ─────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* The markdown → target-tree PARSE is the CALLER's job, because it is
|
|
45
|
+
* caller-specific: the caller owns its custom nodes and its own `@lexical/markdown`
|
|
46
|
+
* transformer set, and that transformer set defines the tree. This module pulls
|
|
47
|
+
* neither `@lexical/markdown` nor `@lexical/headless` (both would otherwise become
|
|
48
|
+
* runtime dependencies of a binding whose core — the reconciler — needs neither).
|
|
49
|
+
* The caller parses markdown headlessly with its own transformers and projects the
|
|
50
|
+
* resulting Lexical tree with {@link projectTarget} (or {@link
|
|
51
|
+
* targetFromEditorState}), then hands the plain, serializable {@link TargetElement}
|
|
52
|
+
* to {@link reconcileTargetIntoLoro}. The reconciler is the reusable core; the
|
|
53
|
+
* markdown parse is not.
|
|
54
|
+
*/
|
|
55
|
+
import { $getRoot, $isElementNode, $isTextNode, } from 'lexical';
|
|
56
|
+
import { allocate, jitterFor } from './order.js';
|
|
57
|
+
import { createElementChild, createTextChild, deleteChild, elementChildren, elementProps, elementType, isTextContainer, newUuid, orderedChildren, setChildPosition, } from './schema.js';
|
|
58
|
+
import { applyMarkOps, applyTextDiff, diffRunFormats, diffText, normalizeRuns, runsFromText, runsText, } from './text.js';
|
|
59
|
+
/** Lexical node props that are structure/bookkeeping, never document data. */
|
|
60
|
+
const NON_PROP_KEYS = new Set(['type', 'version', 'children']);
|
|
61
|
+
/**
|
|
62
|
+
* Normalize an `exportJSON` value into a stored {@link PropValue}, dropping
|
|
63
|
+
* `undefined` exactly as `to-loro.ts`'s `syncProps` does — so a target block's
|
|
64
|
+
* props signature equals what the Loro container already holds.
|
|
65
|
+
*/
|
|
66
|
+
function toProp(value) {
|
|
67
|
+
if (value === null)
|
|
68
|
+
return null;
|
|
69
|
+
switch (typeof value) {
|
|
70
|
+
case 'string':
|
|
71
|
+
case 'number':
|
|
72
|
+
case 'boolean':
|
|
73
|
+
return value;
|
|
74
|
+
case 'object': {
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return value.map(toProp);
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
79
|
+
if (inner === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
out[key] = toProp(inner);
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`lexical-loro: agent-write cannot store a non-JSON prop value (${typeof value})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The document-data props of a Lexical node, mirroring `to-loro.ts`'s `syncProps`. */
|
|
90
|
+
function propsOf(node) {
|
|
91
|
+
const json = node.exportJSON();
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const [key, value] of Object.entries(json)) {
|
|
94
|
+
if (NON_PROP_KEYS.has(key) || value === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
out[key] = toProp(value);
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Project one Lexical element (or element-mirrored leaf) to a {@link TargetElement}.
|
|
102
|
+
*
|
|
103
|
+
* MUST be called inside a Lexical read (`editorState.read(() => …)`), because it
|
|
104
|
+
* reads node content. Uses only `lexical` (a peer dependency) — never
|
|
105
|
+
* `@lexical/markdown`. See {@link targetFromEditorState} for the common wrapper.
|
|
106
|
+
*/
|
|
107
|
+
export function projectTarget(node) {
|
|
108
|
+
const props = propsOf(node);
|
|
109
|
+
if (!$isElementNode(node))
|
|
110
|
+
return { kind: 'element', type: node.getType(), props, children: [] };
|
|
111
|
+
const children = [];
|
|
112
|
+
let run = [];
|
|
113
|
+
const flush = () => {
|
|
114
|
+
if (run.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
children.push({
|
|
117
|
+
kind: 'text',
|
|
118
|
+
runs: normalizeRuns(run.map((n) => ({ text: n.getTextContent(), format: n.getFormat() }))),
|
|
119
|
+
});
|
|
120
|
+
run = [];
|
|
121
|
+
};
|
|
122
|
+
for (const child of node.getChildren()) {
|
|
123
|
+
if ($isTextNode(child)) {
|
|
124
|
+
run.push(child);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
flush();
|
|
128
|
+
children.push(projectTarget(child));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
flush();
|
|
132
|
+
return { kind: 'element', type: node.getType(), props, children };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Project the root of an `EditorState` to a {@link TargetElement}, doing the read
|
|
136
|
+
* for you.
|
|
137
|
+
*
|
|
138
|
+
* The caller owns the markdown → editor-state parse (its own headless editor and
|
|
139
|
+
* `@lexical/markdown` transformer set); this projects the parsed tree into the
|
|
140
|
+
* plain, serializable shape {@link reconcileTargetIntoLoro} consumes. Uses only
|
|
141
|
+
* `lexical`.
|
|
142
|
+
*/
|
|
143
|
+
export function targetFromEditorState(state) {
|
|
144
|
+
let target;
|
|
145
|
+
state.read(() => {
|
|
146
|
+
target = projectTarget($getRoot());
|
|
147
|
+
});
|
|
148
|
+
if (target === undefined)
|
|
149
|
+
throw new Error('lexical-loro: agent-write failed to project editor state');
|
|
150
|
+
return target;
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Content signatures
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
/** Canonical, sort-stable JSON of a prop value, so equal content compares equal. */
|
|
156
|
+
function stableProps(value) {
|
|
157
|
+
if (value === null || typeof value !== 'object')
|
|
158
|
+
return JSON.stringify(value);
|
|
159
|
+
if (Array.isArray(value))
|
|
160
|
+
return `[${value.map(stableProps).join(',')}]`;
|
|
161
|
+
const keys = Object.keys(value).sort();
|
|
162
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableProps(value[k])}`).join(',')}}`;
|
|
163
|
+
}
|
|
164
|
+
function runsSignature(runs) {
|
|
165
|
+
return `T|${JSON.stringify(normalizeRuns(runs).map((r) => [r.text, r.format]))}`;
|
|
166
|
+
}
|
|
167
|
+
/** The content signature of a target child — identical content ⇒ identical string. */
|
|
168
|
+
function targetSignature(child) {
|
|
169
|
+
if (child.kind === 'text')
|
|
170
|
+
return runsSignature(child.runs);
|
|
171
|
+
return `E|${child.type}|${stableProps(child.props)}|[${child.children
|
|
172
|
+
.map(targetSignature)
|
|
173
|
+
.join(',')}]`;
|
|
174
|
+
}
|
|
175
|
+
/** The content signature of an existing carrier — the mirror of {@link targetSignature}. */
|
|
176
|
+
function entrySignature(entry) {
|
|
177
|
+
if (entry.kind === 'text' && isTextContainer(entry.container)) {
|
|
178
|
+
return runsSignature(runsFromText(entry.container));
|
|
179
|
+
}
|
|
180
|
+
const container = entry.container;
|
|
181
|
+
const props = elementProps(container).toJSON();
|
|
182
|
+
return `E|${elementType(container)}|${stableProps(props)}|[${orderedChildren(container)
|
|
183
|
+
.map(entrySignature)
|
|
184
|
+
.join(',')}]`;
|
|
185
|
+
}
|
|
186
|
+
/** The Lexical type a target element maps a carrier to (text runs share `'#text'`). */
|
|
187
|
+
function targetKindKey(child) {
|
|
188
|
+
return child.kind === 'text' ? '#text' : `E:${child.type}`;
|
|
189
|
+
}
|
|
190
|
+
function entryKindKey(entry) {
|
|
191
|
+
return entry.kind === 'text' ? '#text' : `E:${elementType(entry.container)}`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Match desired children to existing carriers by CONTENT, in two passes:
|
|
195
|
+
*
|
|
196
|
+
* 1. EXACT signature — an unchanged subtree keeps its whole identity and needs
|
|
197
|
+
* no descent. Within a group of byte-identical carriers the match is biased
|
|
198
|
+
* toward the SAME POSITION: each target claims the same-signature carrier
|
|
199
|
+
* whose rendered index is nearest the target's own index. This is the
|
|
200
|
+
* duplicate-block mitigation — a still-identical sibling keeps its OWN carrier
|
|
201
|
+
* rather than an arbitrary same-content one, so the block the user CHANGED is
|
|
202
|
+
* the one whose carrier is freed for reuse.
|
|
203
|
+
* 2. SAME-KIND fallback — a leftover carrier of the same type/text-kind is
|
|
204
|
+
* reused for a changed block, so a text edit diffs its `LoroText` and an
|
|
205
|
+
* element edit recurses, rather than deleting and recreating.
|
|
206
|
+
*
|
|
207
|
+
* Whatever is still unmatched is created (desired) or deleted (existing).
|
|
208
|
+
*
|
|
209
|
+
* ── The residual, stated honestly ──────────────────────────────────────────
|
|
210
|
+
*
|
|
211
|
+
* Position bias reduces, but CANNOT eliminate, mis-assignment among TRUE
|
|
212
|
+
* duplicates. When the count of a byte-identical group changes (e.g. one of three
|
|
213
|
+
* identical paragraphs is deleted), content is by definition insufficient to say
|
|
214
|
+
* WHICH carrier the user meant to keep, and position proximity is only a guess;
|
|
215
|
+
* the trailing carrier is dropped regardless of intent. `NodeKey` identity — which
|
|
216
|
+
* the agent path does not have, since the agent hands us markdown, not an edited
|
|
217
|
+
* editor state — is the only complete answer. See the duplicate-block tests.
|
|
218
|
+
*/
|
|
219
|
+
function matchChildren(current, desired) {
|
|
220
|
+
const matched = new Array(desired.length).fill({ entry: null, tag: 'new' });
|
|
221
|
+
const claimed = new Set();
|
|
222
|
+
// Pass 1: exact content signature, biased toward the same rendered position.
|
|
223
|
+
const bySig = new Map();
|
|
224
|
+
for (let index = 0; index < current.length; index++) {
|
|
225
|
+
const entry = current[index];
|
|
226
|
+
const sig = entrySignature(entry);
|
|
227
|
+
const bucket = bySig.get(sig);
|
|
228
|
+
if (bucket === undefined)
|
|
229
|
+
bySig.set(sig, [{ entry, index }]);
|
|
230
|
+
else
|
|
231
|
+
bucket.push({ entry, index });
|
|
232
|
+
}
|
|
233
|
+
for (let i = 0; i < desired.length; i++) {
|
|
234
|
+
const bucket = bySig.get(targetSignature(desired[i]));
|
|
235
|
+
if (bucket === undefined || bucket.length === 0)
|
|
236
|
+
continue;
|
|
237
|
+
// Take the same-content carrier nearest this target's index (position bias),
|
|
238
|
+
// ties resolved toward the earlier carrier.
|
|
239
|
+
let best = 0;
|
|
240
|
+
let bestDistance = Math.abs(bucket[0].index - i);
|
|
241
|
+
for (let b = 1; b < bucket.length; b++) {
|
|
242
|
+
const distance = Math.abs(bucket[b].index - i);
|
|
243
|
+
if (distance < bestDistance) {
|
|
244
|
+
best = b;
|
|
245
|
+
bestDistance = distance;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const [carrier] = bucket.splice(best, 1);
|
|
249
|
+
matched[i] = { entry: carrier.entry, tag: 'exact' };
|
|
250
|
+
claimed.add(carrier.entry);
|
|
251
|
+
}
|
|
252
|
+
// Pass 2: same-kind fallback for changed blocks.
|
|
253
|
+
const byKind = new Map();
|
|
254
|
+
for (const entry of current) {
|
|
255
|
+
if (claimed.has(entry))
|
|
256
|
+
continue;
|
|
257
|
+
const key = entryKindKey(entry);
|
|
258
|
+
const bucket = byKind.get(key);
|
|
259
|
+
if (bucket === undefined)
|
|
260
|
+
byKind.set(key, [entry]);
|
|
261
|
+
else
|
|
262
|
+
bucket.push(entry);
|
|
263
|
+
}
|
|
264
|
+
for (let i = 0; i < desired.length; i++) {
|
|
265
|
+
if (matched[i].entry !== null)
|
|
266
|
+
continue;
|
|
267
|
+
const entry = byKind.get(targetKindKey(desired[i]))?.shift();
|
|
268
|
+
if (entry === undefined)
|
|
269
|
+
continue;
|
|
270
|
+
matched[i] = { entry, tag: 'reuse' };
|
|
271
|
+
claimed.add(entry);
|
|
272
|
+
}
|
|
273
|
+
return matched;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Longest strictly-increasing subsequence indices — the reorder planner, so a
|
|
277
|
+
* move rewrites only genuinely displaced `pos` keys. Mirrors `to-loro.ts`.
|
|
278
|
+
*/
|
|
279
|
+
function longestIncreasingSubsequence(values) {
|
|
280
|
+
if (values.length === 0)
|
|
281
|
+
return [];
|
|
282
|
+
const tails = [];
|
|
283
|
+
const previous = new Array(values.length).fill(-1);
|
|
284
|
+
for (let i = 0; i < values.length; i++) {
|
|
285
|
+
let low = 0;
|
|
286
|
+
let high = tails.length;
|
|
287
|
+
while (low < high) {
|
|
288
|
+
const mid = (low + high) >> 1;
|
|
289
|
+
if (values[tails[mid]] < values[i])
|
|
290
|
+
low = mid + 1;
|
|
291
|
+
else
|
|
292
|
+
high = mid;
|
|
293
|
+
}
|
|
294
|
+
if (low > 0)
|
|
295
|
+
previous[i] = tails[low - 1];
|
|
296
|
+
tails[low] = i;
|
|
297
|
+
}
|
|
298
|
+
const out = new Array(tails.length);
|
|
299
|
+
let cursor = tails[tails.length - 1];
|
|
300
|
+
for (let i = tails.length - 1; i >= 0; i--) {
|
|
301
|
+
out[i] = cursor;
|
|
302
|
+
cursor = previous[cursor];
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
/** Dense ranks so equal positions share a rank and the strict LIS keeps at most one. */
|
|
307
|
+
function positionRanks(positions) {
|
|
308
|
+
const distinct = [...new Set(positions)].sort();
|
|
309
|
+
const rank = new Map(distinct.map((pos, index) => [pos, index]));
|
|
310
|
+
return positions.map((pos) => rank.get(pos));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Assign every desired child a position: keep the already-ordered survivors,
|
|
314
|
+
* re-position the rest, create the ones with no carrier — one maximal gap at a
|
|
315
|
+
* time so a multi-block insertion is allocated as ONE batch (never interleaves a
|
|
316
|
+
* concurrent paste, and never rebalances). Mirrors `to-loro.ts`'s `placeChildren`.
|
|
317
|
+
*/
|
|
318
|
+
function placeChildren(children, desired, matched, context) {
|
|
319
|
+
const survivorIndices = [];
|
|
320
|
+
for (let i = 0; i < desired.length; i++)
|
|
321
|
+
if (matched[i].entry !== null)
|
|
322
|
+
survivorIndices.push(i);
|
|
323
|
+
const ranks = positionRanks(survivorIndices.map((i) => matched[i].entry.pos));
|
|
324
|
+
const keep = new Set();
|
|
325
|
+
for (const index of longestIncreasingSubsequence(ranks))
|
|
326
|
+
keep.add(survivorIndices[index]);
|
|
327
|
+
const placed = new Array(desired.length);
|
|
328
|
+
for (const index of keep)
|
|
329
|
+
placed[index] = matched[index].entry.container;
|
|
330
|
+
let i = 0;
|
|
331
|
+
while (i < desired.length) {
|
|
332
|
+
if (keep.has(i)) {
|
|
333
|
+
i++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let end = i;
|
|
337
|
+
while (end < desired.length && !keep.has(end))
|
|
338
|
+
end++;
|
|
339
|
+
const before = i === 0 ? null : matched[i - 1].entry.pos;
|
|
340
|
+
const after = end === desired.length ? null : matched[end].entry.pos;
|
|
341
|
+
const keys = allocate(before, after, end - i, context.jitter);
|
|
342
|
+
for (let j = i; j < end; j++) {
|
|
343
|
+
const key = keys[j - i];
|
|
344
|
+
const entry = matched[j].entry;
|
|
345
|
+
if (entry === null) {
|
|
346
|
+
placed[j] = createChild(children, desired[j], key, context);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
setChildPosition(entry.carrier, key);
|
|
350
|
+
context.ops++;
|
|
351
|
+
placed[j] = entry.container;
|
|
352
|
+
}
|
|
353
|
+
i = end;
|
|
354
|
+
}
|
|
355
|
+
return placed;
|
|
356
|
+
}
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Reconcile
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
/**
|
|
361
|
+
* Commit origin stamped on the single agent-write commit.
|
|
362
|
+
*
|
|
363
|
+
* Distinct from `to-loro.ts`'s `OUTBOUND_ORIGIN` so the inbound path can tell an
|
|
364
|
+
* agent write apart from an echo of its own outbound write: `binding.ts` lists
|
|
365
|
+
* this origin among the LOCAL batches the inbound path must still APPLY, which is
|
|
366
|
+
* what bounces an agent write into a live editor (preserving `NodeKey`s and
|
|
367
|
+
* decorator mounts).
|
|
368
|
+
*/
|
|
369
|
+
export const AGENT_WRITE_ORIGIN = 'agent-write';
|
|
370
|
+
/**
|
|
371
|
+
* Reconcile a parsed target tree into an existing Loro document, preserving the
|
|
372
|
+
* `ContainerID`s of unchanged and text-edited blocks, and commit under `origin`.
|
|
373
|
+
*
|
|
374
|
+
* A SIBLING to `syncLexicalToLoro` — it writes Loro directly rather than mirroring
|
|
375
|
+
* a Lexical update, matches by CONTENT rather than by `NodeKey`, and does NOT
|
|
376
|
+
* consult the `ContainerNodeMap` (which self-heals on the inbound bounce).
|
|
377
|
+
*
|
|
378
|
+
* @param doc the shared document.
|
|
379
|
+
* @param root its root element container, as returned by `initDoc`.
|
|
380
|
+
* @param target the desired tree, from {@link targetFromEditorState} /
|
|
381
|
+
* {@link projectTarget} (the caller owns the markdown parse).
|
|
382
|
+
* @param origin the commit origin. Defaults to {@link AGENT_WRITE_ORIGIN}; keep
|
|
383
|
+
* it on the inbound target's applied-local-origins list, or a live
|
|
384
|
+
* editor bound to the same doc will not see the change.
|
|
385
|
+
* @returns the number of Loro write ops emitted. `0` means the target already
|
|
386
|
+
* matched the document — nothing committed, no peer sees an event.
|
|
387
|
+
*/
|
|
388
|
+
export function reconcileTargetIntoLoro(doc, root, target, origin = AGENT_WRITE_ORIGIN) {
|
|
389
|
+
const context = { doc, jitter: jitterFor(doc.peerId), ops: 0 };
|
|
390
|
+
reconcileElement(root, target, context);
|
|
391
|
+
if (context.ops > 0)
|
|
392
|
+
doc.commit({ origin });
|
|
393
|
+
return context.ops;
|
|
394
|
+
}
|
|
395
|
+
function reconcileElement(container, target, context) {
|
|
396
|
+
syncProps(container, target.props, context);
|
|
397
|
+
reconcileChildren(container, orderedChildren(container), target.children, context);
|
|
398
|
+
}
|
|
399
|
+
function syncProps(container, target, context) {
|
|
400
|
+
const props = elementProps(container);
|
|
401
|
+
const seen = new Set();
|
|
402
|
+
for (const [key, value] of Object.entries(target)) {
|
|
403
|
+
seen.add(key);
|
|
404
|
+
if (jsonEqual(props.get(key), value))
|
|
405
|
+
continue;
|
|
406
|
+
props.set(key, value);
|
|
407
|
+
context.ops++;
|
|
408
|
+
}
|
|
409
|
+
for (const key of props.keys()) {
|
|
410
|
+
if (seen.has(key))
|
|
411
|
+
continue;
|
|
412
|
+
props.delete(key);
|
|
413
|
+
context.ops++;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function reconcileChildren(container, current, desired, context) {
|
|
417
|
+
const children = elementChildren(container);
|
|
418
|
+
const matched = matchChildren(current, desired);
|
|
419
|
+
const survivors = new Set(matched.map((m) => m.entry).filter((e) => e !== null));
|
|
420
|
+
// Delete carriers no target child reused. A carrier is addressed by uuid, so
|
|
421
|
+
// deleting one cannot shift another.
|
|
422
|
+
for (const entry of current) {
|
|
423
|
+
if (survivors.has(entry))
|
|
424
|
+
continue;
|
|
425
|
+
deleteChild(children, entry.uuid);
|
|
426
|
+
context.ops++;
|
|
427
|
+
}
|
|
428
|
+
const placed = placeChildren(children, desired, matched, context);
|
|
429
|
+
// Descend: exact matches are unchanged (no work); reused carriers get a text
|
|
430
|
+
// diff or a recursive element reconcile.
|
|
431
|
+
for (let i = 0; i < desired.length; i++) {
|
|
432
|
+
const match = matched[i];
|
|
433
|
+
if (match.tag !== 'reuse')
|
|
434
|
+
continue;
|
|
435
|
+
const child = desired[i];
|
|
436
|
+
const containerAt = placed[i];
|
|
437
|
+
if (child.kind === 'text') {
|
|
438
|
+
if (isTextContainer(containerAt))
|
|
439
|
+
syncText(containerAt, child.runs, context);
|
|
440
|
+
}
|
|
441
|
+
else if (!isTextContainer(containerAt)) {
|
|
442
|
+
reconcileElement(containerAt, child, context);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Fill a text run's `LoroText` to `runs`: diff the characters (no caret — an
|
|
448
|
+
* agent edit has none), then replay the resulting formats as `mark`/`unmark`.
|
|
449
|
+
* Mirrors `to-loro.ts`'s `syncText` minus the cursor bias.
|
|
450
|
+
*/
|
|
451
|
+
function syncText(text, runs, context) {
|
|
452
|
+
const target = normalizeRuns(runs.map((r) => ({ text: r.text, format: r.format })));
|
|
453
|
+
const targetString = runsText(target);
|
|
454
|
+
if (text.toString() !== targetString) {
|
|
455
|
+
const diff = diffText(text.toString(), targetString);
|
|
456
|
+
applyTextDiff(text, diff);
|
|
457
|
+
if (diff.remove > 0)
|
|
458
|
+
context.ops++;
|
|
459
|
+
if (diff.insert !== '')
|
|
460
|
+
context.ops++;
|
|
461
|
+
}
|
|
462
|
+
const ops = diffRunFormats(runsFromText(text), target);
|
|
463
|
+
applyMarkOps(text, ops);
|
|
464
|
+
context.ops += ops.length;
|
|
465
|
+
}
|
|
466
|
+
/** Create, attach and fill a brand-new child at position `pos`. */
|
|
467
|
+
function createChild(children, child, pos, context) {
|
|
468
|
+
context.ops++;
|
|
469
|
+
const uuid = newUuid();
|
|
470
|
+
if (child.kind === 'text') {
|
|
471
|
+
const text = createTextChild(children, uuid, pos);
|
|
472
|
+
syncText(text, child.runs, context);
|
|
473
|
+
return text;
|
|
474
|
+
}
|
|
475
|
+
const element = createElementChild(children, uuid, pos, child.type);
|
|
476
|
+
reconcileElement(element, child, context);
|
|
477
|
+
return element;
|
|
478
|
+
}
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
// Shared
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
function jsonEqual(a, b) {
|
|
483
|
+
if (a === b)
|
|
484
|
+
return true;
|
|
485
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object')
|
|
486
|
+
return false;
|
|
487
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
488
|
+
return false;
|
|
489
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
490
|
+
return a.length === b.length && a.every((item, i) => jsonEqual(item, b[i]));
|
|
491
|
+
}
|
|
492
|
+
const left = a;
|
|
493
|
+
const right = b;
|
|
494
|
+
const keys = Object.keys(left);
|
|
495
|
+
if (keys.length !== Object.keys(right).length)
|
|
496
|
+
return false;
|
|
497
|
+
return keys.every((key) => key in right && jsonEqual(left[key], right[key]));
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=agent-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-write.js","sourceRoot":"","sources":["../src/agent-write.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `loroCollab(config)` — the binding that drives both directions.
|
|
3
|
+
*
|
|
4
|
+
* It composes `to-loro.ts` (Lexical → Loro), `to-lexical.ts` (Loro → Lexical)
|
|
5
|
+
* and `seed.ts` (boot) into one `register(editor)` step you hand to
|
|
6
|
+
* `lexicalForeign({ history: false, seedMode: 'deferred', register })`. The
|
|
7
|
+
* returned handle satisfies `@llui/markdown-editor`'s `CollabBinding`
|
|
8
|
+
* structurally, so that package needs no Loro dependency of its own.
|
|
9
|
+
*
|
|
10
|
+
* ── Transport ──────────────────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* There is none, deliberately. `LoroDoc` already exposes the whole wire surface
|
|
13
|
+
* (`subscribeLocalUpdates` / `import` / `export`), so a transport is a dozen
|
|
14
|
+
* lines the consumer writes against their own websocket, WebRTC channel or
|
|
15
|
+
* `loro-websocket` provider — and baking one in would make this package own a
|
|
16
|
+
* connection lifecycle it has no way to test honestly. Pass the same `LoroDoc`
|
|
17
|
+
* to your provider and to `loroCollab`.
|
|
18
|
+
*
|
|
19
|
+
* ── The three echo layers, and where each one lives ────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* All three are required; any one missing produces an infinite loop or, worse,
|
|
22
|
+
* a silent data-loss bug:
|
|
23
|
+
*
|
|
24
|
+
* a. Loro → us. A LOCAL Loro batch is our own outbound write completing its
|
|
25
|
+
* commit. `applyLoroToLexical` drops it (`to-lexical.ts`). Without this the
|
|
26
|
+
* binding re-enters `editor.update` from inside a Lexical update listener.
|
|
27
|
+
* b. us → Lexical → us. Our inbound writeback is tagged `COLLABORATION_TAG`,
|
|
28
|
+
* which `syncLexicalToLoro` skips (`to-loro.ts`). Without this every remote
|
|
29
|
+
* keystroke is echoed straight back to its sender.
|
|
30
|
+
* c. THE SEAM. This binding NEVER emits `PROGRAMMATIC_TAG`.
|
|
31
|
+
* `packages/lexical/src/foreign.ts` treats that tag as "the host pushed new
|
|
32
|
+
* content — cancel pending outbound work and rebase", so a remote writeback
|
|
33
|
+
* carrying it would cancel the local user's in-flight debounced `onChange`
|
|
34
|
+
* and the host's persistence would go dark whenever a peer types. There is
|
|
35
|
+
* no code here to enforce this; there is simply no line that emits it, and
|
|
36
|
+
* `test/to-lexical.test.ts` pins that.
|
|
37
|
+
*
|
|
38
|
+
* ── Undo ───────────────────────────────────────────────────────────────────
|
|
39
|
+
*
|
|
40
|
+
* This binding OWNS undo, via `externalUndo` (`undo.ts`, a Loro `UndoManager`
|
|
41
|
+
* scoped to this peer). Hosts must therefore turn their built-in
|
|
42
|
+
* `@lexical/history` stack off — `lexicalForeign` does that for you the moment
|
|
43
|
+
* `externalUndo` is present, so the two can never both be live.
|
|
44
|
+
*
|
|
45
|
+
* Loro's manager is operation-based and PeerID-scoped: undo reverts exactly the
|
|
46
|
+
* local user's own last change and never a peer's, which snapshot-based history
|
|
47
|
+
* cannot do at any tag setting. See `undo.ts` for why, and for the echo seam the
|
|
48
|
+
* undo batches travel through.
|
|
49
|
+
*/
|
|
50
|
+
import type { LexicalEditor } from 'lexical';
|
|
51
|
+
import { LoroDoc } from 'loro-crdt';
|
|
52
|
+
import { ContainerNodeMap } from './mapping.js';
|
|
53
|
+
import { type BootstrapOutcome } from './seed.js';
|
|
54
|
+
import { type ElementContainer } from './schema.js';
|
|
55
|
+
import { type LoroUndoOptions } from './undo.js';
|
|
56
|
+
export interface LoroCollabConfig {
|
|
57
|
+
/** The shared document. Created if omitted — pass your provider's doc. */
|
|
58
|
+
readonly doc?: LoroDoc;
|
|
59
|
+
/**
|
|
60
|
+
* Whether THIS peer may seed an empty shared document. Default `true`.
|
|
61
|
+
* Set `false` on peers that join rather than create, and on any peer whose
|
|
62
|
+
* transport has not completed its first sync — an unsynced document looks
|
|
63
|
+
* empty, and seeding one races the content about to arrive.
|
|
64
|
+
*/
|
|
65
|
+
readonly shouldBootstrap?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Fill an empty shared document with this peer's default content. Runs once,
|
|
68
|
+
* inside a Lexical update. `@llui/markdown-editor` supplies this as
|
|
69
|
+
* `CollabHooks.seed`, which converts its `defaultValue` markdown.
|
|
70
|
+
*/
|
|
71
|
+
readonly seed?: (editor: LexicalEditor) => void;
|
|
72
|
+
/** Commit origin stamped on this binding's writes. Defaults to `'lexical-loro'`. */
|
|
73
|
+
readonly origin?: string;
|
|
74
|
+
/** Called after boot with what happened — seeded, adopted, or still waiting. */
|
|
75
|
+
readonly onBootstrap?: (outcome: BootstrapOutcome) => void;
|
|
76
|
+
/**
|
|
77
|
+
* Tuning for the peer-scoped undo manager installed by
|
|
78
|
+
* {@link LoroCollab.externalUndo} — merge window, stack depth, excluded
|
|
79
|
+
* origins. Defaults are in `undo.ts`; the `doc` is supplied by the binding.
|
|
80
|
+
*/
|
|
81
|
+
readonly undo?: Omit<LoroUndoOptions, 'doc'>;
|
|
82
|
+
}
|
|
83
|
+
/** Live handle returned by {@link loroCollab}. */
|
|
84
|
+
export interface LoroCollab {
|
|
85
|
+
/**
|
|
86
|
+
* Wire the binding onto an editor; pass as `lexicalForeign({ register })`.
|
|
87
|
+
* Returns a disposer that unsubscribes both directions.
|
|
88
|
+
*
|
|
89
|
+
* Satisfies `@llui/markdown-editor`'s `CollabBinding` structurally.
|
|
90
|
+
*/
|
|
91
|
+
register: (editor: LexicalEditor) => () => void;
|
|
92
|
+
/** The shared document. Hand this to your transport. */
|
|
93
|
+
readonly doc: LoroDoc;
|
|
94
|
+
/** The root element container mirroring Lexical's `RootNode`. */
|
|
95
|
+
readonly root: ElementContainer;
|
|
96
|
+
/** The ContainerID ↔ NodeKey registry. Exposed for tests and diagnostics. */
|
|
97
|
+
readonly mapping: ContainerNodeMap;
|
|
98
|
+
/**
|
|
99
|
+
* Install this binding's CRDT-aware undo/redo on the editor; pass as
|
|
100
|
+
* `lexicalForeign({ externalUndo })`. Returns a disposer.
|
|
101
|
+
*
|
|
102
|
+
* Undo is LOCAL-ONLY: it reverts this peer's own commits and leaves every
|
|
103
|
+
* other peer's concurrent edits standing (`undo.ts`). Registering it forces
|
|
104
|
+
* `lexicalForeign`'s built-in `@lexical/history` stack off, which is the point
|
|
105
|
+
* — a snapshot-based local stack would rewind remote work for everyone.
|
|
106
|
+
*
|
|
107
|
+
* Registration is SEPARATE from {@link LoroCollab.register} so a host that
|
|
108
|
+
* genuinely wants its own undo owner can decline it. Do not register it twice.
|
|
109
|
+
*/
|
|
110
|
+
readonly externalUndo: (editor: LexicalEditor) => () => void;
|
|
111
|
+
/** Re-run the boot decision — call after your transport's first sync. */
|
|
112
|
+
bootstrap: (editor: LexicalEditor) => BootstrapOutcome;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a collaborative-editing binding over a Loro document.
|
|
116
|
+
*
|
|
117
|
+
* The document is configured for this package's schema (`initDoc`) immediately,
|
|
118
|
+
* not at `register` time, so a transport may be attached to `collab.doc` before
|
|
119
|
+
* any editor exists.
|
|
120
|
+
*/
|
|
121
|
+
export declare function loroCollab(config?: LoroCollabConfig): LoroCollab;
|
|
122
|
+
//# sourceMappingURL=binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../src/binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGnC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAA;AACpE,OAAO,EAAW,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAI5D,OAAO,EAAkC,KAAK,eAAe,EAAE,MAAM,WAAW,CAAA;AAEhF,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,CAAA;IAClC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAA;IAC/C,oFAAoF;IACpF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,gFAAgF;IAChF,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC1D;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,CAAA;CAC7C;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,IAAI,CAAA;IAC/C,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,IAAI,CAAA;IAC5D,yEAAyE;IACzE,SAAS,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,gBAAgB,CAAA;CACvD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,GAAE,gBAAqB,GAAG,UAAU,CAuDpE"}
|