@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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound sync: a Lexical `EditorState` → the Loro mirror.
|
|
3
|
+
*
|
|
4
|
+
* ── What this must get right ───────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Converging is the easy half. The hard requirement is emitting a MINIMAL,
|
|
7
|
+
* IDENTITY-PRESERVING op set, because in this stack a container that is
|
|
8
|
+
* recreated instead of updated has visible, expensive consequences:
|
|
9
|
+
*
|
|
10
|
+
* - a recreated `LoroText` discards a peer's concurrent insertion into it;
|
|
11
|
+
* - a recreated element `LoroMap` mints a new `ContainerID`, which (via the
|
|
12
|
+
* registry in `mapping.ts`) forces the peer's whole subtree to be rebuilt
|
|
13
|
+
* with fresh `NodeKey`s — tearing down every mounted `LLuiDecoratorNode`
|
|
14
|
+
* sub-app in it (`packages/lexical/src/decorator.ts` disposes on the
|
|
15
|
+
* 'destroyed' mutation) along with selection and IME state.
|
|
16
|
+
*
|
|
17
|
+
* So: a reorder emits ONE `pos` register write per displaced block (never a
|
|
18
|
+
* delete+recreate — see `order.ts` and the schema header), text edits emit a
|
|
19
|
+
* cursor-biased single-region diff, and format changes emit explicit per-format
|
|
20
|
+
* `mark`/`unmark` ops. Every function here returns the number of writes it made,
|
|
21
|
+
* and `syncLexicalToLoro` returns the total — the tests assert on it, which is
|
|
22
|
+
* how a regression in the pruning shows up as a failure rather than as a
|
|
23
|
+
* silently slower, mount-destroying binding.
|
|
24
|
+
*
|
|
25
|
+
* ── Pruning: what is actually sound in Lexical ─────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* loro-prosemirror prunes with reference equality because ProseMirror's tree is
|
|
28
|
+
* PERSISTENT: changing a leaf rebuilds every ancestor, so `parent === parent`
|
|
29
|
+
* really does mean "this whole subtree is unchanged".
|
|
30
|
+
*
|
|
31
|
+
* LEXICAL IS NOT LIKE THAT, and assuming it is produces permanently stale
|
|
32
|
+
* remote documents. `getWritable()` (`LexicalNode.ts`) clones ONLY the node
|
|
33
|
+
* being written; ancestors are left alone and merely recorded in
|
|
34
|
+
* `dirtyElements` by `internalMarkParentElementsAsDirty` (`LexicalUtils.ts`).
|
|
35
|
+
* A paragraph whose text node was just rewritten is therefore the SAME object
|
|
36
|
+
* in both editor states. Reference equality prunes a subtree in ProseMirror; in
|
|
37
|
+
* Lexical it proves only that the node's OWN properties and its OWN child list
|
|
38
|
+
* are unchanged.
|
|
39
|
+
*
|
|
40
|
+
* This module therefore uses two different, individually sound facts:
|
|
41
|
+
*
|
|
42
|
+
* 1. `prevNodeMap.get(key) === node` ⟹ this node's props and the identity and
|
|
43
|
+
* order of its children are unchanged. Used to skip the prop diff and the
|
|
44
|
+
* whole children RECONCILIATION (matching, deletes, moves, inserts) and
|
|
45
|
+
* descend positionally instead. Sound because any child list change goes
|
|
46
|
+
* through the parent's `getWritable()`.
|
|
47
|
+
* 2. `dirtyElements ∪ dirtyLeaves` contains every changed node AND every
|
|
48
|
+
* ancestor of one. Used to decide which children to descend into at all.
|
|
49
|
+
* Sound because `internalMarkNodeAsDirty` walks the parent chain to the
|
|
50
|
+
* root.
|
|
51
|
+
*
|
|
52
|
+
* Fact 2 only holds when a dirty walk actually ran. `editor.setEditorState()`
|
|
53
|
+
* — which Lexical's own history uses for undo/redo, and which the LLui host
|
|
54
|
+
* push path uses — swaps the state wholesale with EMPTY dirty sets while
|
|
55
|
+
* potentially SHARING node objects with the previous state. Trusting empty
|
|
56
|
+
* dirty sets there would silently sync nothing. So when both sets are empty the
|
|
57
|
+
* dirty gate is dropped entirely and the walk falls back to a full structural
|
|
58
|
+
* diff against Loro, which is correct regardless of provenance and costs only
|
|
59
|
+
* on that rare path. Combined, a keystroke costs O(tree depth); a wholesale
|
|
60
|
+
* state swap costs O(document) and still emits only the ops that differ.
|
|
61
|
+
*
|
|
62
|
+
* ── Echo suppression ───────────────────────────────────────────────────────
|
|
63
|
+
*
|
|
64
|
+
* This is layer (b) of the three: an update we ourselves wrote back into
|
|
65
|
+
* Lexical carries `COLLABORATION_TAG` and must not bounce back out. See
|
|
66
|
+
* `index.ts` for the full three-layer contract — in particular that this
|
|
67
|
+
* binding must NEVER emit `PROGRAMMATIC_TAG`.
|
|
68
|
+
*
|
|
69
|
+
* `HISTORIC_TAG` is deliberately NOT suppressed, and must stay that way — even
|
|
70
|
+
* though this binding DOES ship CRDT-aware undo. The reason it can be left
|
|
71
|
+
* unsuppressed is that the Loro undo owner (`undo.ts`) never routes through
|
|
72
|
+
* Lexical's history: `manager.undo()` mutates the shared document directly, and
|
|
73
|
+
* the resulting writeback into the editor carries `COLLABORATION_TAG` (echo
|
|
74
|
+
* layer b), not `HISTORIC_TAG`. So the CRDT undo path emits no historic update
|
|
75
|
+
* for this module to see. `lexicalForeign` also forces `@lexical/history` off
|
|
76
|
+
* whenever `externalUndo` is present, so a shipped app produces no `HISTORIC_TAG`
|
|
77
|
+
* update at all. Suppressing it here would be inert in that configuration and
|
|
78
|
+
* actively WRONG for a host that deliberately runs `@lexical/history` without an
|
|
79
|
+
* `externalUndo` owner (as `test/harden.test.ts` does): there a historic update
|
|
80
|
+
* is a genuine local edit no peer has seen, and dropping it would make that
|
|
81
|
+
* undo invisible to everyone else. This differs from `@lexical/yjs`, where undo
|
|
82
|
+
* IS a historic writeback of the CRDT's own and re-syncing it would echo.
|
|
83
|
+
*/
|
|
84
|
+
import { type EditorState, type UpdateListenerPayload } from 'lexical';
|
|
85
|
+
import { type LoroDoc } from 'loro-crdt';
|
|
86
|
+
import { ContainerNodeMap } from './mapping.js';
|
|
87
|
+
import { type ElementContainer } from './schema.js';
|
|
88
|
+
/** Commit origin stamped on every write this module makes. */
|
|
89
|
+
export declare const OUTBOUND_ORIGIN = "lexical-loro";
|
|
90
|
+
/**
|
|
91
|
+
* Update tags that mean "this update did not originate with the local user, do
|
|
92
|
+
* not mirror it".
|
|
93
|
+
*
|
|
94
|
+
* `COLLABORATION_TAG` is our own inbound writeback (echo layer b);
|
|
95
|
+
* `SKIP_COLLAB_TAG` is Lexical's standard opt-out, which hosts use for local-only
|
|
96
|
+
* decoration. `HISTORIC_TAG` is NOT here — see the file header.
|
|
97
|
+
*/
|
|
98
|
+
export declare const OUTBOUND_SKIP_TAGS: readonly string[];
|
|
99
|
+
/** The Loro side of the binding: the document, its root mirror, and the registry. */
|
|
100
|
+
export interface OutboundTarget {
|
|
101
|
+
readonly doc: LoroDoc;
|
|
102
|
+
/** The root element container, as returned by `initDoc`. */
|
|
103
|
+
readonly root: ElementContainer;
|
|
104
|
+
/** The ContainerID ↔ NodeKey registry, owned by the binding and mutated here. */
|
|
105
|
+
readonly mapping: ContainerNodeMap;
|
|
106
|
+
/** Commit origin. Defaults to {@link OUTBOUND_ORIGIN}. */
|
|
107
|
+
readonly origin?: string;
|
|
108
|
+
/** Tags that suppress the sync. Defaults to {@link OUTBOUND_SKIP_TAGS}. */
|
|
109
|
+
readonly skipTags?: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The slice of `UpdateListenerPayload` this direction consumes. Declared as a
|
|
113
|
+
* `Pick` so an update listener can pass its payload straight through.
|
|
114
|
+
*
|
|
115
|
+
* Register it with a BLOCK body, never an expression body:
|
|
116
|
+
*
|
|
117
|
+
* ```ts
|
|
118
|
+
* editor.registerUpdateListener((payload) => {
|
|
119
|
+
* syncLexicalToLoro(target, payload)
|
|
120
|
+
* })
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* Lexical 0.48 stores whatever an update listener RETURNS and calls it as a
|
|
124
|
+
* cleanup before the next invocation (`triggerListeners` in `LexicalUpdates`),
|
|
125
|
+
* so the concise `(payload) => syncLexicalToLoro(target, payload)` hands it this
|
|
126
|
+
* function's op count and the second update dies with
|
|
127
|
+
* "unregister is not a function".
|
|
128
|
+
*/
|
|
129
|
+
export type OutboundUpdate = Pick<UpdateListenerPayload, 'prevEditorState' | 'editorState' | 'dirtyElements' | 'dirtyLeaves' | 'normalizedNodes' | 'tags'>;
|
|
130
|
+
/**
|
|
131
|
+
* Mirror one Lexical update into the Loro document and commit it.
|
|
132
|
+
*
|
|
133
|
+
* @returns the number of Loro write operations emitted. `0` means the update
|
|
134
|
+
* was a genuine no-op for the shared document — nothing was committed, so no
|
|
135
|
+
* peer sees an event. Tests assert on this to catch pruning regressions.
|
|
136
|
+
*/
|
|
137
|
+
export declare function syncLexicalToLoro(target: OutboundTarget, update: OutboundUpdate): number;
|
|
138
|
+
/**
|
|
139
|
+
* Fill the Loro document from an editor state with no previous state to diff
|
|
140
|
+
* against — the bootstrapping peer's initial seed.
|
|
141
|
+
*
|
|
142
|
+
* Structurally a full-fidelity diff, so it is also idempotent: seeding a
|
|
143
|
+
* document that already matches emits nothing and returns `0`.
|
|
144
|
+
*/
|
|
145
|
+
export declare function seedLoroFromLexical(target: OutboundTarget, editorState: EditorState): number;
|
|
146
|
+
/**
|
|
147
|
+
* The indices of a longest strictly-increasing subsequence of `values`.
|
|
148
|
+
*
|
|
149
|
+
* Patience sorting with a predecessor chain: O(n log n). Exported because it is
|
|
150
|
+
* the part of the reorder planner worth testing in isolation — the number of
|
|
151
|
+
* `pos` writes a drag-reorder costs is exactly `matched.length - lis.length`.
|
|
152
|
+
*/
|
|
153
|
+
export declare function longestIncreasingSubsequence(values: readonly number[]): number[];
|
|
154
|
+
//# sourceMappingURL=to-loro.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-loro.d.ts","sourceRoot":"","sources":["../src/to-loro.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkFG;AAEH,OAAO,EAQL,KAAK,WAAW,EAMhB,KAAK,qBAAqB,EAC3B,MAAM,SAAS,CAAA;AAChB,OAAO,EAA8B,KAAK,OAAO,EAAE,MAAM,WAAW,CAAA;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C,OAAO,EAgBL,KAAK,gBAAgB,EAEtB,MAAM,aAAa,CAAA;AAgBpB,8DAA8D;AAC9D,eAAO,MAAM,eAAe,iBAAiB,CAAA;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAAyC,CAAA;AAEzF,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;IACrB,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAA;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAA;IAClC,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACtC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,qBAAqB,EACrB,iBAAiB,GAAG,aAAa,GAAG,eAAe,GAAG,aAAa,GAAG,iBAAiB,GAAG,MAAM,CACjG,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,GAAG,MAAM,CAoBxF;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,GAAG,MAAM,CAE5F;AAqjBD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CA0BhF"}
|